diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index aab0a9b2d49..5fd68bfb26c 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -502,6 +502,7 @@ add_library(
src/reductions/product.cu
src/reductions/reductions.cpp
src/reductions/scan/rank_scan.cu
+ src/reductions/scan/ewm.cu
src/reductions/scan/scan.cpp
src/reductions/scan/scan_exclusive.cu
src/reductions/scan/scan_inclusive.cu
diff --git a/cpp/include/cudf/aggregation.hpp b/cpp/include/cudf/aggregation.hpp
index d458c831f19..3c1023017be 100644
--- a/cpp/include/cudf/aggregation.hpp
+++ b/cpp/include/cudf/aggregation.hpp
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2023, NVIDIA CORPORATION.
+ * Copyright (c) 2019-2024, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -103,6 +103,7 @@ class aggregation {
NUNIQUE, ///< count number of unique elements
NTH_ELEMENT, ///< get the nth element
ROW_NUMBER, ///< get row-number of current index (relative to rolling window)
+ EWMA, ///< get exponential weighted moving average at current index
RANK, ///< get rank of current index
COLLECT_LIST, ///< collect values into a list
COLLECT_SET, ///< collect values into a list without duplicate entries
@@ -250,6 +251,8 @@ class segmented_reduce_aggregation : public virtual aggregation {
enum class udf_type : bool { CUDA, PTX };
/// Type of correlation method.
enum class correlation_type : int32_t { PEARSON, KENDALL, SPEARMAN };
+/// Type of treatment of EWM input values' first value
+enum class ewm_history : int32_t { INFINITE, FINITE };
/// Factory to create a SUM aggregation
/// @return A SUM aggregation object
@@ -411,6 +414,42 @@ std::unique_ptr make_nth_element_aggregation(
template
std::unique_ptr make_row_number_aggregation();
+/**
+ * @brief Factory to create an EWMA aggregation
+ *
+ * `EWMA` returns a non-nullable column with the same type as the input,
+ * whose values are the exponentially weighted moving average of the input
+ * sequence. Let these values be known as the y_i.
+ *
+ * EWMA aggregations are parameterized by a center of mass (`com`) which
+ * affects the contribution of the previous values (y_0 ... y_{i-1}) in
+ * computing the y_i.
+ *
+ * EWMA aggregations are also parameterized by a history `cudf::ewm_history`.
+ * Special considerations have to be given to the mathematical treatment of
+ * the first value of the input sequence. There are two approaches to this,
+ * one which considers the first value of the sequence to be the exponential
+ * weighted moving average of some infinite history of data, and one which
+ * takes the first value to be the only datapoint known. These assumptions
+ * lead to two different formulas for the y_i. `ewm_history` selects which.
+ *
+ * EWMA aggregations have special null handling. Nulls have two effects. The
+ * first is to propagate forward the last valid value as far as it has been
+ * computed. This could be thought of as the nulls not affecting the average
+ * in any way. The second effect changes the way the y_i are computed. Since
+ * a moving average is conceptually designed to weight contributing values by
+ * their recency, nulls ought to count as valid periods even though they do
+ * not change the average. For example, if the input sequence is {1, NULL, 3}
+ * then when computing y_2 one should weigh y_0 as if it occurs two periods
+ * before y_2 rather than just one.
+ *
+ * @param center_of_mass the center of mass.
+ * @param history which assumption to make about the first value
+ * @return A EWM aggregation object
+ */
+template
+std::unique_ptr make_ewma_aggregation(double const center_of_mass, ewm_history history);
+
/**
* @brief Factory to create a RANK aggregation
*
diff --git a/cpp/include/cudf/detail/aggregation/aggregation.hpp b/cpp/include/cudf/detail/aggregation/aggregation.hpp
index edee83783b8..843414817e3 100644
--- a/cpp/include/cudf/detail/aggregation/aggregation.hpp
+++ b/cpp/include/cudf/detail/aggregation/aggregation.hpp
@@ -76,6 +76,8 @@ class simple_aggregations_collector { // Declares the interface for the simple
class nth_element_aggregation const& agg);
virtual std::vector> visit(data_type col_type,
class row_number_aggregation const& agg);
+ virtual std::vector> visit(data_type col_type,
+ class ewma_aggregation const& agg);
virtual std::vector> visit(data_type col_type,
class rank_aggregation const& agg);
virtual std::vector> visit(
@@ -141,6 +143,7 @@ class aggregation_finalizer { // Declares the interface for the finalizer
virtual void visit(class correlation_aggregation const& agg);
virtual void visit(class tdigest_aggregation const& agg);
virtual void visit(class merge_tdigest_aggregation const& agg);
+ virtual void visit(class ewma_aggregation const& agg);
};
/**
@@ -667,6 +670,40 @@ class row_number_aggregation final : public rolling_aggregation {
void finalize(aggregation_finalizer& finalizer) const override { finalizer.visit(*this); }
};
+/**
+ * @brief Derived class for specifying an ewma aggregation
+ */
+class ewma_aggregation final : public scan_aggregation {
+ public:
+ double const center_of_mass;
+ cudf::ewm_history history;
+
+ ewma_aggregation(double const center_of_mass, cudf::ewm_history history)
+ : aggregation{EWMA}, center_of_mass{center_of_mass}, history{history}
+ {
+ }
+
+ std::unique_ptr clone() const override
+ {
+ return std::make_unique(*this);
+ }
+
+ std::vector> get_simple_aggregations(
+ data_type col_type, simple_aggregations_collector& collector) const override
+ {
+ return collector.visit(col_type, *this);
+ }
+
+ bool is_equal(aggregation const& _other) const override
+ {
+ if (!this->aggregation::is_equal(_other)) { return false; }
+ auto const& other = dynamic_cast(_other);
+ return this->center_of_mass == other.center_of_mass and this->history == other.history;
+ }
+
+ void finalize(aggregation_finalizer& finalizer) const override { finalizer.visit(*this); }
+};
+
/**
* @brief Derived class for specifying a rank aggregation
*/
@@ -1336,6 +1373,11 @@ struct target_type_impl {
using type = size_type;
};
+template
+struct target_type_impl {
+ using type = double;
+};
+
// Always use size_type accumulator for RANK
template
struct target_type_impl {
@@ -1536,6 +1578,8 @@ CUDF_HOST_DEVICE inline decltype(auto) aggregation_dispatcher(aggregation::Kind
return f.template operator()(std::forward(args)...);
case aggregation::MERGE_TDIGEST:
return f.template operator()(std::forward(args)...);
+ case aggregation::EWMA:
+ return f.template operator()(std::forward(args)...);
default: {
#ifndef __CUDA_ARCH__
CUDF_FAIL("Unsupported aggregation.");
diff --git a/cpp/src/aggregation/aggregation.cpp b/cpp/src/aggregation/aggregation.cpp
index adee9147740..5422304c5cb 100644
--- a/cpp/src/aggregation/aggregation.cpp
+++ b/cpp/src/aggregation/aggregation.cpp
@@ -154,6 +154,12 @@ std::vector> simple_aggregations_collector::visit(
return visit(col_type, static_cast(agg));
}
+std::vector> simple_aggregations_collector::visit(
+ data_type col_type, ewma_aggregation const& agg)
+{
+ return visit(col_type, static_cast(agg));
+}
+
std::vector> simple_aggregations_collector::visit(
data_type col_type, rank_aggregation const& agg)
{
@@ -333,6 +339,11 @@ void aggregation_finalizer::visit(row_number_aggregation const& agg)
visit(static_cast(agg));
}
+void aggregation_finalizer::visit(ewma_aggregation const& agg)
+{
+ visit(static_cast(agg));
+}
+
void aggregation_finalizer::visit(rank_aggregation const& agg)
{
visit(static_cast(agg));
@@ -665,6 +676,17 @@ std::unique_ptr make_row_number_aggregation()
template std::unique_ptr make_row_number_aggregation();
template std::unique_ptr make_row_number_aggregation();
+/// Factory to create an EWMA aggregation
+template
+std::unique_ptr make_ewma_aggregation(double const com, cudf::ewm_history history)
+{
+ return std::make_unique(com, history);
+}
+template std::unique_ptr make_ewma_aggregation(double const com,
+ cudf::ewm_history history);
+template std::unique_ptr make_ewma_aggregation(
+ double const com, cudf::ewm_history history);
+
/// Factory to create a RANK aggregation
template
std::unique_ptr make_rank_aggregation(rank_method method,
diff --git a/cpp/src/reductions/scan/ewm.cu b/cpp/src/reductions/scan/ewm.cu
new file mode 100644
index 00000000000..3fa2de450ad
--- /dev/null
+++ b/cpp/src/reductions/scan/ewm.cu
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2022-2024, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "scan.cuh"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace cudf {
+namespace detail {
+
+template
+using pair_type = thrust::pair;
+
+/**
+ * @brief functor to be summed over in a prefix sum such that
+ * the recurrence in question is solved. See
+ * G. E. Blelloch. Prefix sums and their applications. Technical Report
+ * CMU-CS-90-190, Nov. 1990. S. 1.4
+ * for details
+ */
+template
+class recurrence_functor {
+ public:
+ __device__ pair_type operator()(pair_type ci, pair_type cj)
+ {
+ return {ci.first * cj.first, ci.second * cj.first + cj.second};
+ }
+};
+
+template
+struct ewma_functor_base {
+ T beta;
+ const pair_type IDENTITY{1.0, 0.0};
+};
+
+template
+struct ewma_adjust_nulls_functor : public ewma_functor_base {
+ __device__ pair_type operator()(thrust::tuple const data)
+ {
+ // Not const to allow for updating the input value
+ auto [valid, exp, input] = data;
+ if (!valid) { return this->IDENTITY; }
+ if constexpr (not is_numerator) { input = 1; }
+
+ // The value is non-null, but nulls preceding it
+ // must adjust the second element of the pair
+ T const beta = this->beta;
+ return {beta * ((exp != 0) ? pow(beta, exp) : 1), input};
+ }
+};
+
+template
+struct ewma_adjust_no_nulls_functor : public ewma_functor_base {
+ __device__ pair_type operator()(T const data)
+ {
+ T const beta = this->beta;
+ if constexpr (is_numerator) {
+ return {beta, data};
+ } else {
+ return {beta, 1.0};
+ }
+ }
+};
+
+template
+struct ewma_noadjust_nulls_functor : public ewma_functor_base {
+ /*
+ In the null case, a denominator actually has to be computed. The formula is
+ y_{i+1} = (1 - alpha)x_{i-1} + alpha x_i, but really there is a "denominator"
+ which is the sum of the weights: alpha + (1 - alpha) == 1. If a null is
+ encountered, that means that the "previous" value is downweighted by a
+ factor (for each missing value). For example with a single null:
+ data = {x_0, NULL, x_1},
+ y_2 = (1 - alpha)**2 x_0 + alpha * x_2 / (alpha + (1-alpha)**2)
+
+ As such, the pairs must be updated before summing like the adjusted case to
+ properly downweight the previous values. But now but we also need to compute
+ the normalization factors and divide the results into them at the end.
+ */
+ __device__ pair_type operator()(thrust::tuple const data)
+ {
+ T const beta = this->beta;
+ auto const [input, index, valid, nullcnt] = data;
+ if (index == 0) {
+ return {beta, input};
+ } else {
+ if (!valid) { return this->IDENTITY; }
+ // preceding value is valid, return normal pair
+ if (nullcnt == 0) { return {beta, (1.0 - beta) * input}; }
+ // one or more preceding values is null, adjust by how many
+ T const factor = (1.0 - beta) + pow(beta, nullcnt + 1);
+ return {(beta * (pow(beta, nullcnt)) / factor), ((1.0 - beta) * input) / factor};
+ }
+ }
+};
+
+template
+struct ewma_noadjust_no_nulls_functor : public ewma_functor_base {
+ __device__ pair_type operator()(thrust::tuple const data)
+ {
+ T const beta = this->beta;
+ auto const [input, index] = data;
+ if (index == 0) {
+ return {beta, input};
+ } else {
+ return {beta, (1.0 - beta) * input};
+ }
+ }
+};
+
+/**
+* @brief Return an array whose values y_i are the number of null entries
+* in between the last valid entry of the input and the current index.
+* Example: {1, NULL, 3, 4, NULL, NULL, 7}
+ -> {0, 0 1, 0, 0, 1, 2}
+*/
+rmm::device_uvector null_roll_up(column_view const& input,
+ rmm::cuda_stream_view stream)
+{
+ rmm::device_uvector output(input.size(), stream);
+
+ auto device_view = column_device_view::create(input);
+ auto invalid_it = thrust::make_transform_iterator(
+ cudf::detail::make_validity_iterator(*device_view),
+ cuda::proclaim_return_type([] __device__(int valid) -> int { return 1 - valid; }));
+
+ // valid mask {1, 0, 1, 0, 0, 1} leads to output array {0, 0, 1, 0, 1, 2}
+ thrust::inclusive_scan_by_key(rmm::exec_policy(stream),
+ invalid_it,
+ invalid_it + input.size() - 1,
+ invalid_it,
+ std::next(output.begin()));
+ return output;
+}
+
+template
+rmm::device_uvector compute_ewma_adjust(column_view const& input,
+ T const beta,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr)
+{
+ rmm::device_uvector output(input.size(), stream);
+ rmm::device_uvector> pairs(input.size(), stream);
+
+ if (input.has_nulls()) {
+ rmm::device_uvector nullcnt = null_roll_up(input, stream);
+ auto device_view = column_device_view::create(input);
+ auto valid_it = cudf::detail::make_validity_iterator(*device_view);
+ auto data =
+ thrust::make_zip_iterator(thrust::make_tuple(valid_it, nullcnt.begin(), input.begin()));
+
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ data,
+ data + input.size(),
+ pairs.begin(),
+ ewma_adjust_nulls_functor{beta},
+ recurrence_functor{});
+ thrust::transform(rmm::exec_policy(stream),
+ pairs.begin(),
+ pairs.end(),
+ output.begin(),
+ [] __device__(pair_type pair) -> T { return pair.second; });
+
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ data,
+ data + input.size(),
+ pairs.begin(),
+ ewma_adjust_nulls_functor{beta},
+ recurrence_functor{});
+
+ } else {
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ input.begin(),
+ input.end(),
+ pairs.begin(),
+ ewma_adjust_no_nulls_functor{beta},
+ recurrence_functor{});
+ thrust::transform(rmm::exec_policy(stream),
+ pairs.begin(),
+ pairs.end(),
+ output.begin(),
+ [] __device__(pair_type pair) -> T { return pair.second; });
+ auto itr = thrust::make_counting_iterator(0);
+
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ itr,
+ itr + input.size(),
+ pairs.begin(),
+ ewma_adjust_no_nulls_functor{beta},
+ recurrence_functor{});
+ }
+
+ thrust::transform(
+ rmm::exec_policy(stream),
+ pairs.begin(),
+ pairs.end(),
+ output.begin(),
+ output.begin(),
+ [] __device__(pair_type pair, T numerator) -> T { return numerator / pair.second; });
+
+ return output;
+}
+
+template
+rmm::device_uvector compute_ewma_noadjust(column_view const& input,
+ T const beta,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr)
+{
+ rmm::device_uvector output(input.size(), stream);
+ rmm::device_uvector> pairs(input.size(), stream);
+ rmm::device_uvector nullcnt =
+ [&input, stream]() -> rmm::device_uvector {
+ if (input.has_nulls()) {
+ return null_roll_up(input, stream);
+ } else {
+ return rmm::device_uvector(input.size(), stream);
+ }
+ }();
+ // denominators are all 1 and do not need to be computed
+ // pairs are all (beta, 1-beta x_i) except for the first one
+
+ if (!input.has_nulls()) {
+ auto data = thrust::make_zip_iterator(
+ thrust::make_tuple(input.begin(), thrust::make_counting_iterator(0)));
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ data,
+ data + input.size(),
+ pairs.begin(),
+ ewma_noadjust_no_nulls_functor{beta},
+ recurrence_functor{});
+
+ } else {
+ auto device_view = column_device_view::create(input);
+ auto valid_it = detail::make_validity_iterator(*device_view);
+
+ auto data = thrust::make_zip_iterator(thrust::make_tuple(
+ input.begin(), thrust::make_counting_iterator(0), valid_it, nullcnt.begin()));
+
+ thrust::transform_inclusive_scan(rmm::exec_policy(stream),
+ data,
+ data + input.size(),
+ pairs.begin(),
+ ewma_noadjust_nulls_functor{beta},
+ recurrence_functor());
+ }
+
+ // copy the second elements to the output for now
+ thrust::transform(rmm::exec_policy(stream),
+ pairs.begin(),
+ pairs.end(),
+ output.begin(),
+ [] __device__(pair_type pair) -> T { return pair.second; });
+ return output;
+}
+
+struct ewma_functor {
+ template ::value)>
+ std::unique_ptr operator()(scan_aggregation const& agg,
+ column_view const& input,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr)
+ {
+ CUDF_FAIL("Unsupported type for EWMA.");
+ }
+
+ template ::value)>
+ std::unique_ptr operator()(scan_aggregation const& agg,
+ column_view const& input,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr)
+ {
+ auto const ewma_agg = dynamic_cast(&agg);
+ auto const history = ewma_agg->history;
+ auto const center_of_mass = ewma_agg->center_of_mass;
+
+ // center of mass is easier for the user, but the recurrences are
+ // better expressed in terms of the derived parameter `beta`
+ T const beta = center_of_mass / (center_of_mass + 1.0);
+
+ auto result = [&]() {
+ if (history == cudf::ewm_history::INFINITE) {
+ return compute_ewma_adjust(input, beta, stream, mr);
+ } else {
+ return compute_ewma_noadjust(input, beta, stream, mr);
+ }
+ }();
+ return std::make_unique(cudf::data_type(cudf::type_to_id()),
+ input.size(),
+ result.release(),
+ rmm::device_buffer{},
+ 0);
+ }
+};
+
+std::unique_ptr exponentially_weighted_moving_average(column_view const& input,
+ scan_aggregation const& agg,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr)
+{
+ return type_dispatcher(input.type(), ewma_functor{}, agg, input, stream, mr);
+}
+
+} // namespace detail
+} // namespace cudf
diff --git a/cpp/src/reductions/scan/scan.cuh b/cpp/src/reductions/scan/scan.cuh
index aeb9e516cd4..6c237741ac3 100644
--- a/cpp/src/reductions/scan/scan.cuh
+++ b/cpp/src/reductions/scan/scan.cuh
@@ -36,6 +36,12 @@ std::pair mask_scan(column_view const& input_view
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr);
+// exponentially weighted moving average of the input
+std::unique_ptr exponentially_weighted_moving_average(column_view const& input,
+ scan_aggregation const& agg,
+ rmm::cuda_stream_view stream,
+ rmm::device_async_resource_ref mr);
+
template typename DispatchFn>
std::unique_ptr scan_agg_dispatch(column_view const& input,
scan_aggregation const& agg,
@@ -59,6 +65,7 @@ std::unique_ptr scan_agg_dispatch(column_view const& input,
if (is_fixed_point(input.type())) CUDF_FAIL("decimal32/64/128 cannot support product scan");
return type_dispatcher(
input.type(), DispatchFn(), input, output_mask, stream, mr);
+ case aggregation::EWMA: return exponentially_weighted_moving_average(input, agg, stream, mr);
default: CUDF_FAIL("Unsupported aggregation operator for scan");
}
}
diff --git a/cpp/src/reductions/scan/scan_inclusive.cu b/cpp/src/reductions/scan/scan_inclusive.cu
index ad2eaa6a471..7c02a8d1b99 100644
--- a/cpp/src/reductions/scan/scan_inclusive.cu
+++ b/cpp/src/reductions/scan/scan_inclusive.cu
@@ -182,7 +182,8 @@ std::unique_ptr scan_inclusive(column_view const& input,
auto output = scan_agg_dispatch(
input, agg, static_cast(mask.data()), stream, mr);
- output->set_null_mask(std::move(mask), null_count);
+ // Use the null mask produced by the op for EWM
+ if (agg.kind != aggregation::EWMA) { output->set_null_mask(std::move(mask), null_count); }
// If the input is a structs column, we also need to push down nulls from the parent output column
// into the children columns.
diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt
index eda470d2309..9f14455f42d 100644
--- a/cpp/tests/CMakeLists.txt
+++ b/cpp/tests/CMakeLists.txt
@@ -205,6 +205,7 @@ ConfigureTest(
ConfigureTest(
REDUCTIONS_TEST
reductions/collect_ops_tests.cpp
+ reductions/ewm_tests.cpp
reductions/rank_tests.cpp
reductions/reduction_tests.cpp
reductions/scan_tests.cpp
diff --git a/cpp/tests/reductions/ewm_tests.cpp b/cpp/tests/reductions/ewm_tests.cpp
new file mode 100644
index 00000000000..09cec688509
--- /dev/null
+++ b/cpp/tests/reductions/ewm_tests.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2021-2024, NVIDIA CORPORATION.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "scan_tests.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+
+template
+struct TypedEwmScanTest : BaseScanTest {
+ inline void test_ungrouped_ewma_scan(cudf::column_view const& input,
+ cudf::column_view const& expect_vals,
+ cudf::scan_aggregation const& agg,
+ cudf::null_policy null_handling)
+ {
+ auto col_out = cudf::scan(input, agg, cudf::scan_type::INCLUSIVE, null_handling);
+ CUDF_TEST_EXPECT_COLUMNS_EQUIVALENT(expect_vals, col_out->view());
+ }
+};
+
+TYPED_TEST_SUITE(TypedEwmScanTest, cudf::test::FloatingPointTypes);
+
+TYPED_TEST(TypedEwmScanTest, Ewm)
+{
+ auto const v = make_vector({1.0, 2.0, 3.0, 4.0, 5.0});
+ auto col = this->make_column(v);
+
+ auto const expected_ewma_vals_adjust = cudf::test::fixed_width_column_wrapper{
+ {1.0, 1.75, 2.61538461538461497469, 3.54999999999999982236, 4.52066115702479365268}};
+
+ auto const expected_ewma_vals_noadjust =
+ cudf::test::fixed_width_column_wrapper{{1.0,
+ 1.66666666666666651864,
+ 2.55555555555555535818,
+ 3.51851851851851815667,
+ 4.50617283950617242283}};
+
+ this->test_ungrouped_ewma_scan(
+ *col,
+ expected_ewma_vals_adjust,
+ *cudf::make_ewma_aggregation(0.5, cudf::ewm_history::INFINITE),
+ cudf::null_policy::INCLUDE);
+ this->test_ungrouped_ewma_scan(
+ *col,
+ expected_ewma_vals_noadjust,
+ *cudf::make_ewma_aggregation(0.5, cudf::ewm_history::FINITE),
+ cudf::null_policy::INCLUDE);
+}
+
+TYPED_TEST(TypedEwmScanTest, EwmWithNulls)
+{
+ auto const v = make_vector({1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0});
+ auto const b = thrust::host_vector(std::vector{1, 0, 1, 0, 0, 1, 1});
+ auto col = this->make_column(v, b);
+
+ auto const expected_ewma_vals_adjust =
+ cudf::test::fixed_width_column_wrapper{{1.0,
+ 1.0,
+ 2.79999999999999982236,
+ 2.79999999999999982236,
+ 2.79999999999999982236,
+ 5.87351778656126466416,
+ 6.70977596741344139986}};
+
+ auto const expected_ewma_vals_noadjust =
+ cudf::test::fixed_width_column_wrapper{{1.0,
+ 1.0,
+ 2.71428571428571441260,
+ 2.71428571428571441260,
+ 2.71428571428571441260,
+ 5.82706766917293172980,
+ 6.60902255639097724327}};
+
+ this->test_ungrouped_ewma_scan(
+ *col,
+ expected_ewma_vals_adjust,
+ *cudf::make_ewma_aggregation(0.5, cudf::ewm_history::INFINITE),
+ cudf::null_policy::INCLUDE);
+ this->test_ungrouped_ewma_scan(
+ *col,
+ expected_ewma_vals_noadjust,
+ *cudf::make_ewma_aggregation(0.5, cudf::ewm_history::FINITE),
+ cudf::null_policy::INCLUDE);
+}
diff --git a/docs/cudf/source/user_guide/api_docs/dataframe.rst b/docs/cudf/source/user_guide/api_docs/dataframe.rst
index 70e4bd060ca..02fd9f7b396 100644
--- a/docs/cudf/source/user_guide/api_docs/dataframe.rst
+++ b/docs/cudf/source/user_guide/api_docs/dataframe.rst
@@ -137,6 +137,7 @@ Computations / descriptive stats
DataFrame.describe
DataFrame.diff
DataFrame.eval
+ DataFrame.ewm
DataFrame.kurt
DataFrame.kurtosis
DataFrame.max
diff --git a/docs/cudf/source/user_guide/api_docs/series.rst b/docs/cudf/source/user_guide/api_docs/series.rst
index 5dc87a97337..48a7dc8ff87 100644
--- a/docs/cudf/source/user_guide/api_docs/series.rst
+++ b/docs/cudf/source/user_guide/api_docs/series.rst
@@ -138,6 +138,7 @@ Computations / descriptive stats
Series.describe
Series.diff
Series.digitize
+ Series.ewm
Series.factorize
Series.kurt
Series.max
diff --git a/python/cudf/cudf/_lib/aggregation.pyx b/python/cudf/cudf/_lib/aggregation.pyx
index 11f801ba772..1616c24eec2 100644
--- a/python/cudf/cudf/_lib/aggregation.pyx
+++ b/python/cudf/cudf/_lib/aggregation.pyx
@@ -58,6 +58,14 @@ class Aggregation:
if dropna else pylibcudf.types.NullPolicy.INCLUDE
))
+ @classmethod
+ def ewma(cls, com=1.0, adjust=True):
+ return cls(pylibcudf.aggregation.ewma(
+ com,
+ pylibcudf.aggregation.EWMHistory.INFINITE
+ if adjust else pylibcudf.aggregation.EWMHistory.FINITE
+ ))
+
@classmethod
def size(cls):
return cls(pylibcudf.aggregation.count(pylibcudf.types.NullPolicy.INCLUDE))
diff --git a/python/cudf/cudf/_lib/pylibcudf/aggregation.pxd b/python/cudf/cudf/_lib/pylibcudf/aggregation.pxd
index 8526728656b..0981d0e855a 100644
--- a/python/cudf/cudf/_lib/pylibcudf/aggregation.pxd
+++ b/python/cudf/cudf/_lib/pylibcudf/aggregation.pxd
@@ -6,6 +6,7 @@ from cudf._lib.pylibcudf.libcudf.aggregation cimport (
Kind as kind_t,
aggregation,
correlation_type,
+ ewm_history,
groupby_aggregation,
groupby_scan_aggregation,
rank_method,
@@ -80,6 +81,8 @@ cpdef Aggregation argmax()
cpdef Aggregation argmin()
+cpdef Aggregation ewma(float center_of_mass, ewm_history history)
+
cpdef Aggregation nunique(null_policy null_handling = *)
cpdef Aggregation nth_element(size_type n, null_policy null_handling = *)
diff --git a/python/cudf/cudf/_lib/pylibcudf/aggregation.pyx b/python/cudf/cudf/_lib/pylibcudf/aggregation.pyx
index 7bb64e32a1b..eed2f6de585 100644
--- a/python/cudf/cudf/_lib/pylibcudf/aggregation.pyx
+++ b/python/cudf/cudf/_lib/pylibcudf/aggregation.pyx
@@ -8,6 +8,7 @@ from libcpp.utility cimport move
from cudf._lib.pylibcudf.libcudf.aggregation cimport (
aggregation,
correlation_type,
+ ewm_history,
groupby_aggregation,
groupby_scan_aggregation,
make_all_aggregation,
@@ -19,6 +20,7 @@ from cudf._lib.pylibcudf.libcudf.aggregation cimport (
make_correlation_aggregation,
make_count_aggregation,
make_covariance_aggregation,
+ make_ewma_aggregation,
make_max_aggregation,
make_mean_aggregation,
make_median_aggregation,
@@ -52,6 +54,8 @@ from cudf._lib.pylibcudf.libcudf.types cimport (
from cudf._lib.pylibcudf.libcudf.aggregation import Kind # no-cython-lint
from cudf._lib.pylibcudf.libcudf.aggregation import \
correlation_type as CorrelationType # no-cython-lint
+from cudf._lib.pylibcudf.libcudf.aggregation import \
+ ewm_history as EWMHistory # no-cython-lint
from cudf._lib.pylibcudf.libcudf.aggregation import \
rank_method as RankMethod # no-cython-lint
from cudf._lib.pylibcudf.libcudf.aggregation import \
@@ -202,6 +206,28 @@ cpdef Aggregation max():
return Aggregation.from_libcudf(move(make_max_aggregation[aggregation]()))
+cpdef Aggregation ewma(float center_of_mass, ewm_history history):
+ """Create a EWMA aggregation.
+
+ For details, see :cpp:func:`make_ewma_aggregation`.
+
+ Parameters
+ ----------
+ center_of_mass : float
+ The decay in terms of the center of mass
+ history : ewm_history
+ Whether or not to treat the history as infinite.
+
+ Returns
+ -------
+ Aggregation
+ The EWMA aggregation.
+ """
+ return Aggregation.from_libcudf(
+ move(make_ewma_aggregation[aggregation](center_of_mass, history))
+ )
+
+
cpdef Aggregation count(null_policy null_handling = null_policy.EXCLUDE):
"""Create a count aggregation.
diff --git a/python/cudf/cudf/_lib/pylibcudf/libcudf/aggregation.pxd b/python/cudf/cudf/_lib/pylibcudf/libcudf/aggregation.pxd
index 8c14bc45723..fe04db52094 100644
--- a/python/cudf/cudf/_lib/pylibcudf/libcudf/aggregation.pxd
+++ b/python/cudf/cudf/_lib/pylibcudf/libcudf/aggregation.pxd
@@ -79,6 +79,10 @@ cdef extern from "cudf/aggregation.hpp" namespace "cudf" nogil:
KENDALL
SPEARMAN
+ cpdef enum class ewm_history(int32_t):
+ INFINITE
+ FINITE
+
cpdef enum class rank_method(int32_t):
FIRST
AVERAGE
@@ -143,6 +147,10 @@ cdef extern from "cudf/aggregation.hpp" namespace "cudf" nogil:
string user_defined_aggregator,
data_type output_type) except +
+ cdef unique_ptr[T] make_ewma_aggregation[T](
+ double com, ewm_history adjust
+ ) except +
+
cdef unique_ptr[T] make_correlation_aggregation[T](
correlation_type type, size_type min_periods) except +
diff --git a/python/cudf/cudf/core/indexed_frame.py b/python/cudf/cudf/core/indexed_frame.py
index f1b74adefed..7515cb2c177 100644
--- a/python/cudf/cudf/core/indexed_frame.py
+++ b/python/cudf/cudf/core/indexed_frame.py
@@ -52,7 +52,7 @@
_post_process_output_col,
_return_arr_from_dtype,
)
-from cudf.core.window import Rolling
+from cudf.core.window import ExponentialMovingWindow, Rolling
from cudf.utils import docutils, ioutils
from cudf.utils._numba import _CUDFNumbaConfig
from cudf.utils.docutils import copy_docstring
@@ -1853,6 +1853,32 @@ def rolling(
win_type=win_type,
)
+ @copy_docstring(ExponentialMovingWindow)
+ def ewm(
+ self,
+ com: float | None = None,
+ span: float | None = None,
+ halflife: float | None = None,
+ alpha: float | None = None,
+ min_periods: int | None = 0,
+ adjust: bool = True,
+ ignore_na: bool = False,
+ axis: int = 0,
+ times: str | np.ndarray | None = None,
+ ):
+ return ExponentialMovingWindow(
+ self,
+ com=com,
+ span=span,
+ halflife=halflife,
+ alpha=alpha,
+ min_periods=min_periods,
+ adjust=adjust,
+ ignore_na=ignore_na,
+ axis=axis,
+ times=times,
+ )
+
@_cudf_nvtx_annotate
def nans_to_nulls(self):
"""
diff --git a/python/cudf/cudf/core/window/__init__.py b/python/cudf/cudf/core/window/__init__.py
index 8ea3eb0179b..23522588d33 100644
--- a/python/cudf/cudf/core/window/__init__.py
+++ b/python/cudf/cudf/core/window/__init__.py
@@ -1,3 +1,3 @@
-# Copyright (c) 2019-2022, NVIDIA CORPORATION
-
+# Copyright (c) 2019-2024, NVIDIA CORPORATION
+from cudf.core.window.ewm import ExponentialMovingWindow
from cudf.core.window.rolling import Rolling
diff --git a/python/cudf/cudf/core/window/ewm.py b/python/cudf/cudf/core/window/ewm.py
new file mode 100644
index 00000000000..21693e106bd
--- /dev/null
+++ b/python/cudf/cudf/core/window/ewm.py
@@ -0,0 +1,200 @@
+# Copyright (c) 2022-2024, NVIDIA CORPORATION.
+
+from __future__ import annotations
+
+import numpy as np
+
+from cudf._lib.reduce import scan
+from cudf.api.types import is_numeric_dtype
+from cudf.core.window.rolling import _RollingBase
+
+
+class ExponentialMovingWindow(_RollingBase):
+ r"""
+ Provide exponential weighted (EW) functions.
+ Available EW functions: ``mean()``
+ Exactly one parameter: ``com``, ``span``, ``halflife``, or ``alpha``
+ must be provided.
+
+ Parameters
+ ----------
+ com : float, optional
+ Specify decay in terms of center of mass,
+ :math:`\alpha = 1 / (1 + com)`, for :math:`com \geq 0`.
+ span : float, optional
+ Specify decay in terms of span,
+ :math:`\alpha = 2 / (span + 1)`, for :math:`span \geq 1`.
+ halflife : float, str, timedelta, optional
+ Specify decay in terms of half-life,
+ :math:`\alpha = 1 - \exp\left(-\ln(2) / halflife\right)`, for
+ :math:`halflife > 0`.
+ alpha : float, optional
+ Specify smoothing factor :math:`\alpha` directly,
+ :math:`0 < \alpha \leq 1`.
+ min_periods : int, default 0
+ Not Supported
+ adjust : bool, default True
+ Controls assumptions about the first value in the sequence.
+ https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html
+ for details.
+ ignore_na : bool, default False
+ Not Supported
+ axis : {0, 1}, default 0
+ Not Supported
+ times : str, np.ndarray, Series, default None
+ Not Supported
+
+ Returns
+ -------
+ ``ExponentialMovingWindow`` object
+
+ Notes
+ -----
+ cuDF input data may contain both nulls and nan values. For the purposes
+ of this method, they are taken to have the same meaning, meaning nulls
+ in cuDF will affect the result the same way that nan values would using
+ the equivalent pandas method.
+
+ .. pandas-compat::
+ **cudf.core.window.ExponentialMovingWindow**
+
+ The parameters ``min_periods``, ``ignore_na``, ``axis``, and ``times``
+ are not yet supported. Behavior is defined only for data that begins
+ with a valid (non-null) element.
+
+ Currently, only ``mean`` is a supported method.
+
+ Examples
+ --------
+ >>> df = cudf.DataFrame({'B': [0, 1, 2, cudf.NA, 4]})
+ >>> df
+ B
+ 0 0
+ 1 1
+ 2 2
+ 3
+ 4 4
+ >>> df.ewm(com=0.5).mean()
+ B
+ 0 0.000000
+ 1 0.750000
+ 2 1.615385
+ 3 1.615385
+ 4 3.670213
+
+ >>> df.ewm(com=0.5, adjust=False).mean()
+ B
+ 0 0.000000
+ 1 0.666667
+ 2 1.555556
+ 3 1.555556
+ 4 3.650794
+ """
+
+ def __init__(
+ self,
+ obj,
+ com: float | None = None,
+ span: float | None = None,
+ halflife: float | None = None,
+ alpha: float | None = None,
+ min_periods: int | None = 0,
+ adjust: bool = True,
+ ignore_na: bool = False,
+ axis: int = 0,
+ times: str | np.ndarray | None = None,
+ ):
+ if (min_periods, ignore_na, axis, times) != (0, False, 0, None):
+ raise NotImplementedError(
+ "The parameters `min_periods`, `ignore_na`, "
+ "`axis`, and `times` are not yet supported."
+ )
+
+ self.obj = obj
+ self.adjust = adjust
+ self.com = get_center_of_mass(com, span, halflife, alpha)
+
+ def mean(self):
+ """
+ Calculate the ewm (exponential weighted moment) mean.
+ """
+ return self._apply_agg("ewma")
+
+ def var(self, bias):
+ raise NotImplementedError("ewmvar not yet supported.")
+
+ def std(self, bias):
+ raise NotImplementedError("ewmstd not yet supported.")
+
+ def corr(self, other):
+ raise NotImplementedError("ewmcorr not yet supported.")
+
+ def cov(self, other):
+ raise NotImplementedError("ewmcov not yet supported.")
+
+ def _apply_agg_series(self, sr, agg_name):
+ if not is_numeric_dtype(sr.dtype):
+ raise TypeError("No numeric types to aggregate")
+
+ # libcudf ewm has special casing for nulls only
+ # and come what may with nans. It treats those nulls like
+ # pandas does nans in the same positions mathematically.
+ # as such we need to convert the nans to nulls before
+ # passing them in.
+ to_libcudf_column = sr._column.astype("float64").nans_to_nulls()
+
+ return self.obj._from_data_like_self(
+ self.obj._data._from_columns_like_self(
+ [
+ scan(
+ agg_name,
+ to_libcudf_column,
+ True,
+ com=self.com,
+ adjust=self.adjust,
+ )
+ ]
+ )
+ )
+
+
+def get_center_of_mass(
+ comass: float | None,
+ span: float | None,
+ halflife: float | None,
+ alpha: float | None,
+) -> float:
+ valid_count = count_not_none(comass, span, halflife, alpha)
+ if valid_count > 1:
+ raise ValueError(
+ "comass, span, halflife, and alpha are mutually exclusive"
+ )
+
+ # Convert to center of mass; domain checks ensure 0 < alpha <= 1
+ if comass is not None:
+ if comass < 0:
+ raise ValueError("comass must satisfy: comass >= 0")
+ elif span is not None:
+ if span < 1:
+ raise ValueError("span must satisfy: span >= 1")
+ comass = (span - 1) / 2
+ elif halflife is not None:
+ if halflife <= 0:
+ raise ValueError("halflife must satisfy: halflife > 0")
+ decay = 1 - np.exp(np.log(0.5) / halflife)
+ comass = 1 / decay - 1
+ elif alpha is not None:
+ if alpha <= 0 or alpha > 1:
+ raise ValueError("alpha must satisfy: 0 < alpha <= 1")
+ comass = (1 - alpha) / alpha
+ else:
+ raise ValueError("Must pass one of comass, span, halflife, or alpha")
+
+ return float(comass)
+
+
+def count_not_none(*args) -> int:
+ """
+ Returns the count of arguments that are not None.
+ """
+ return sum(x is not None for x in args)
diff --git a/python/cudf/cudf/core/window/rolling.py b/python/cudf/cudf/core/window/rolling.py
index 7d140a1ffa5..29391c68471 100644
--- a/python/cudf/cudf/core/window/rolling.py
+++ b/python/cudf/cudf/core/window/rolling.py
@@ -14,7 +14,27 @@
from cudf.utils.utils import GetAttrGetItemMixin
-class Rolling(GetAttrGetItemMixin, Reducible):
+class _RollingBase:
+ """
+ Contains methods common to all kinds of rolling
+ """
+
+ def _apply_agg_dataframe(self, df, agg_name):
+ result_df = cudf.DataFrame({})
+ for i, col_name in enumerate(df.columns):
+ result_col = self._apply_agg_series(df[col_name], agg_name)
+ result_df.insert(i, col_name, result_col)
+ result_df.index = df.index
+ return result_df
+
+ def _apply_agg(self, agg_name):
+ if isinstance(self.obj, cudf.Series):
+ return self._apply_agg_series(self.obj, agg_name)
+ else:
+ return self._apply_agg_dataframe(self.obj, agg_name)
+
+
+class Rolling(GetAttrGetItemMixin, _RollingBase, Reducible):
"""
Rolling window calculations.
diff --git a/python/cudf/cudf/pandas/_wrappers/pandas.py b/python/cudf/cudf/pandas/_wrappers/pandas.py
index 698dd946022..0ba432d6d0e 100644
--- a/python/cudf/cudf/pandas/_wrappers/pandas.py
+++ b/python/cudf/cudf/pandas/_wrappers/pandas.py
@@ -789,7 +789,7 @@ def Index__new__(cls, *args, **kwargs):
ExponentialMovingWindow = make_intermediate_proxy_type(
"ExponentialMovingWindow",
- _Unusable,
+ cudf.core.window.ewm.ExponentialMovingWindow,
pd.core.window.ewm.ExponentialMovingWindow,
)
diff --git a/python/cudf/cudf/tests/test_ewm.py b/python/cudf/cudf/tests/test_ewm.py
new file mode 100644
index 00000000000..0861d2363ce
--- /dev/null
+++ b/python/cudf/cudf/tests/test_ewm.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2022-2024, NVIDIA CORPORATION.
+import pytest
+
+import cudf
+from cudf.testing._utils import assert_eq
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ [1.0, 2.0, 3.0, 4.0, 5.0],
+ [5.0, cudf.NA, 3.0, cudf.NA, 8.5],
+ [5.0, cudf.NA, 3.0, cudf.NA, cudf.NA, 4.5],
+ [5.0, cudf.NA, 3.0, 4.0, cudf.NA, 5.0],
+ ],
+)
+@pytest.mark.parametrize(
+ "params",
+ [
+ {"com": 0.1},
+ {"com": 0.5},
+ {"span": 1.5},
+ {"span": 2.5},
+ {"halflife": 0.5},
+ {"halflife": 1.5},
+ {"alpha": 0.1},
+ {"alpha": 0.5},
+ ],
+)
+@pytest.mark.parametrize("adjust", [True, False])
+def test_ewma(data, params, adjust):
+ """
+ The most basic test asserts that we obtain
+ the same numerical values as pandas for various
+ sets of keyword arguemnts that effect the raw
+ coefficients of the formula
+ """
+ params["adjust"] = adjust
+
+ gsr = cudf.Series(data, dtype="float64")
+ psr = gsr.to_pandas()
+
+ expect = psr.ewm(**params).mean()
+ got = gsr.ewm(**params).mean()
+
+ assert_eq(expect, got)