diff --git a/docs/api/dt/cummax.rst b/docs/api/dt/cummax.rst index c34aed5020..938abd22bd 100644 --- a/docs/api/dt/cummax.rst +++ b/docs/api/dt/cummax.rst @@ -29,7 +29,7 @@ Create a sample datatable frame:: - >>> from datatable import dt, f + >>> from datatable import dt, f, by >>> DT = dt.Frame({"A": [2, None, 5, -1, 0], ... "B": [None, None, None, None, None], ... "C": [5.4, 3, 2.2, 4.323, 3], diff --git a/docs/api/dt/cummin.rst b/docs/api/dt/cummin.rst index 2d9876b347..b17d8afabc 100644 --- a/docs/api/dt/cummin.rst +++ b/docs/api/dt/cummin.rst @@ -29,7 +29,7 @@ Create a sample datatable frame:: - >>> from datatable import dt, f + >>> from datatable import dt, f, by >>> DT = dt.Frame({"A": [2, None, 5, -1, 0], ... "B": [None, None, None, None, None], ... "C": [5.4, 3, 2.2, 4.323, 3], diff --git a/docs/api/dt/cumprod.rst b/docs/api/dt/cumprod.rst new file mode 100644 index 0000000000..3b10177f11 --- /dev/null +++ b/docs/api/dt/cumprod.rst @@ -0,0 +1,89 @@ + +.. xfunction:: datatable.cumprod + :src: src/core/expr/fexpr_cumsumprod.cc pyfn_cumprod + :tests: tests/dt/test-cumprod.py + :cvar: doc_dt_cumprod + :signature: cumprod(cols) + + .. x-version-added:: 1.1.0 + + For each column from `cols` calculate cumulative product. The product of + the missing values is calculated as one. In the presence of :func:`by()`, + the cumulative product is computed within each group. + + Parameters + ---------- + cols: FExpr + Input data for cumulative product calculation. + + return: FExpr + f-expression that converts input columns into the columns filled + with the respective cumulative products. + + except: TypeError + The exception is raised when one of the columns from `cols` + has a non-numeric type. + + + Examples + -------- + + Create a sample datatable frame:: + + >>> from datatable import dt, f, by + >>> DT = dt.Frame({"A": [2, None, 5, -1, 0], + ... "B": [None, None, None, None, None], + ... "C": [5.4, 3, 2.2, 4.323, 3], + ... "D": ['a', 'a', 'b', 'b', 'b']}) + | A B C D + | int32 void float64 str32 + -- + ----- ---- ------- ----- + 0 | 2 NA 5.4 a + 1 | NA NA 3 a + 2 | 5 NA 2.2 b + 3 | -1 NA 4.323 b + 4 | 0 NA 3 b + [5 rows x 4 columns] + + + Calculate cumulative product in a single column:: + + >>> DT[:, dt.cumprod(f.A)] + | A + | int64 + -- + ----- + 0 | 2 + 1 | 2 + 2 | 10 + 3 | -10 + 4 | 0 + [5 rows x 1 column] + + + Calculate cumulative products in multiple columns:: + + >>> DT[:, dt.cumprod(f[:-1])] + | A B C + | int64 int64 float64 + -- + ----- ----- ------- + 0 | 2 1 5.4 + 1 | 2 1 16.2 + 2 | 10 1 35.64 + 3 | -10 1 154.072 + 4 | 0 1 462.215 + [5 rows x 3 columns] + + + In the presence of :func:`by()` calculate cumulative products within each group:: + + >>> DT[:, dt.cumprod(f[:]), by('D')] + | D A B C + | str32 int64 int64 float64 + -- + ----- ----- ----- ------- + 0 | a 2 1 5.4 + 1 | a 2 1 16.2 + 2 | b 5 1 2.2 + 3 | b -5 1 9.5106 + 4 | b 0 1 28.5318 + [5 rows x 4 columns] + diff --git a/docs/api/dt/cumsum.rst b/docs/api/dt/cumsum.rst index 0a85a627b9..d3bbb72863 100644 --- a/docs/api/dt/cumsum.rst +++ b/docs/api/dt/cumsum.rst @@ -1,6 +1,6 @@ .. xfunction:: datatable.cumsum - :src: src/core/expr/fexpr_cumsum.cc pyfn_cumsum + :src: src/core/expr/fexpr_cumsumprod.cc pyfn_cumsum :tests: tests/dt/test-cumsum.py :cvar: doc_dt_cumsum :signature: cumsum(cols) @@ -30,7 +30,7 @@ Create a sample datatable frame:: - >>> from datatable import dt, f + >>> from datatable import dt, f, by >>> DT = dt.Frame({"A": [2, None, 5, -1, 0], ... "B": [None, None, None, None, None], ... "C": [5.4, 3, 2.2, 4.323, 3], diff --git a/docs/api/fexpr.rst b/docs/api/fexpr.rst index 2237c2b513..50b2c261dd 100644 --- a/docs/api/fexpr.rst +++ b/docs/api/fexpr.rst @@ -168,7 +168,10 @@ * - :meth:`.cummax()` - Same as :func:`dt.cummax()`. - + + * - :meth:`.cumprod()` + - Same as :func:`dt.cumprod()`. + * - :meth:`.cumsum()` - Same as :func:`dt.cumsum()`. @@ -297,6 +300,7 @@ .countna() .cummin() .cummax() + .cumprod() .cumsum() .extend() .first() diff --git a/docs/api/fexpr/cumprod.rst b/docs/api/fexpr/cumprod.rst new file mode 100644 index 0000000000..4f5730471f --- /dev/null +++ b/docs/api/fexpr/cumprod.rst @@ -0,0 +1,8 @@ + +.. xmethod:: datatable.FExpr.cumprod + :src: src/core/expr/fexpr.cc PyFExpr::cumprod + :cvar: doc_FExpr_cumprod + :signature: cumprod() + + Equivalent to :func:`dt.cumprod(self)`. + diff --git a/docs/api/index-api.rst b/docs/api/index-api.rst index 8ad197244f..3652e00e5c 100644 --- a/docs/api/index-api.rst +++ b/docs/api/index-api.rst @@ -167,6 +167,8 @@ Functions - Calculate the cumulative maximum of values per column * - :func:`cummin()` - Calculate the cumulative minimum of values per column + * - :func:`cumprod()` + - Calculate the cumulative product of values per column * - :func:`cumsum()` - Calculate the cumulative sum of values per column * - :func:`cov()` @@ -240,6 +242,7 @@ Other cov()
cummax()
cummin()
+ cumprod()
cumsum()
cut()
dt
diff --git a/docs/releases/v1.1.0.rst b/docs/releases/v1.1.0.rst index 8566e16c5d..ac82204063 100644 --- a/docs/releases/v1.1.0.rst +++ b/docs/releases/v1.1.0.rst @@ -78,6 +78,9 @@ the corresponding :meth:`.cummin()` and :meth:`.cummax()` methods, to calculate the cumulative minimum and maximum of values per column. [#3279] + -[new] Added function :func:`dt.cumprod()`, as well as :meth:`.cumprod()` method, + to calculate the cumulative product of values per column. [#3279] + -[enh] Added reducer functions :func:`dt.countna()` and :func:`dt.nunique()`. [#2999] -[new] Class :class:`dt.FExpr` now has method :meth:`.nunique()`, diff --git a/src/core/column/cumsum.h b/src/core/column/cumsumprod.h similarity index 61% rename from src/core/column/cumsum.h rename to src/core/column/cumsumprod.h index 4c9c2474de..effe375863 100644 --- a/src/core/column/cumsum.h +++ b/src/core/column/cumsumprod.h @@ -25,66 +25,69 @@ #include "parallel/api.h" #include "stype.h" -namespace dt { +namespace dt { -template -class Cumsum_ColumnImpl : public Virtual_ColumnImpl { + template + class CumSumProd_ColumnImpl : public Virtual_ColumnImpl { private: Column col_; Groupby gby_; public: - Cumsum_ColumnImpl(Column&& col, const Groupby& gby) - : Virtual_ColumnImpl(col.nrows(), col.stype()), - col_(std::move(col)), - gby_(gby) + CumSumProd_ColumnImpl(Column &&col, const Groupby &gby) + :Virtual_ColumnImpl(col.nrows(), col.stype()), + col_(std::move(col)), + gby_(gby) { xassert(col_.can_be_read_as()); } - - void materialize(Column& col_out, bool) override { + void materialize(Column &col_out, bool) override { Column col = Column::new_data_column(col_.nrows(), col_.stype()); auto data = static_cast(col.get_data_editable()); auto offsets = gby_.offsets_r(); dt::parallel_for_dynamic( - gby_.size(), - [&](size_t gi) { - size_t i1 = size_t(offsets[gi]); - size_t i2 = size_t(offsets[gi + 1]); - - T val; - bool is_valid = col_.get_element(i1, &val); - data[i1] = is_valid? val : 0; - - for (size_t i = i1 + 1; i < i2; ++i) { - is_valid = col_.get_element(i, &val); - data[i] = data[i - 1] + (is_valid? val : 0); - } - - }); + gby_.size(), + [&](size_t gi) { + size_t i1 = size_t(offsets[gi]); + size_t i2 = size_t(offsets[gi + 1]); + + T val; + bool is_valid = col_.get_element(i1, &val); + if (SUM) { + data[i1] = is_valid? val : 0; + } else { + data[i1] = is_valid? val : 1; + } + for (size_t i = i1 + 1; i < i2; ++i) { + is_valid = col_.get_element(i, &val); + if (SUM) { + data[i] = data[i - 1] + (is_valid? val : 0); + } else { + data[i] = data[i - 1] * (is_valid? val : 1); + } + } + }); col_out = std::move(col); } - - ColumnImpl* clone() const override { - return new Cumsum_ColumnImpl(Column(col_), gby_); + ColumnImpl *clone() const override { + return new CumSumProd_ColumnImpl(Column(col_), gby_); } size_t n_children() const noexcept override { return 1; } - const Column& child(size_t i) const override { - xassert(i == 0); (void)i; + const Column &child(size_t i) const override { + xassert(i == 0); + (void)i; return col_; } - -}; - + }; } // namespace dt diff --git a/src/core/documentation.h b/src/core/documentation.h index 8d4a52f391..5633d38731 100644 --- a/src/core/documentation.h +++ b/src/core/documentation.h @@ -32,6 +32,7 @@ extern const char* doc_dt_countna; extern const char* doc_dt_cov; extern const char* doc_dt_cummax; extern const char* doc_dt_cummin; +extern const char* doc_dt_cumprod; extern const char* doc_dt_cumsum; extern const char* doc_dt_cut; extern const char* doc_dt_first; @@ -283,6 +284,7 @@ extern const char* doc_FExpr_count; extern const char* doc_FExpr_countna; extern const char* doc_FExpr_cummax; extern const char* doc_FExpr_cummin; +extern const char* doc_FExpr_cumprod; extern const char* doc_FExpr_cumsum; extern const char* doc_FExpr_extend; extern const char* doc_FExpr_first; diff --git a/src/core/expr/fexpr.cc b/src/core/expr/fexpr.cc index 53e89b049c..33d16e76e5 100644 --- a/src/core/expr/fexpr.cc +++ b/src/core/expr/fexpr.cc @@ -1,5 +1,5 @@ //------------------------------------------------------------------------------ -// Copyright 2020-2021 H2O.ai +// Copyright 2020-2022 H2O.ai // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), @@ -289,8 +289,6 @@ DECLARE_METHOD(&PyFExpr::re_match) - - //------------------------------------------------------------------------------ // Miscellaneous //------------------------------------------------------------------------------ @@ -347,6 +345,15 @@ DECLARE_METHOD(&PyFExpr::cummin) ->name("cummin") ->docs(dt::doc_FExpr_cummin); +oobj PyFExpr::cumprod(const XArgs&) { + auto cumprodFn = oobj::import("datatable", "cumprod"); + return cumprodFn.call({this}); +} + +DECLARE_METHOD(&PyFExpr::cumprod) + ->name("cumprod") + ->docs(dt::doc_FExpr_cumprod); + oobj PyFExpr::cumsum(const XArgs&) { auto cumsumFn = oobj::import("datatable", "cumsum"); diff --git a/src/core/expr/fexpr.h b/src/core/expr/fexpr.h index 80015fb6e0..6fac207f0a 100644 --- a/src/core/expr/fexpr.h +++ b/src/core/expr/fexpr.h @@ -184,6 +184,7 @@ class PyFExpr : public py::XObject { py::oobj countna(const py::XArgs&); py::oobj cummin(const py::XArgs&); py::oobj cummax(const py::XArgs&); + py::oobj cumprod(const py::XArgs&); py::oobj cumsum(const py::XArgs&); py::oobj extend(const py::XArgs&); py::oobj first(const py::XArgs&); diff --git a/src/core/expr/fexpr_cumsum.cc b/src/core/expr/fexpr_cumsum.cc deleted file mode 100644 index b98ba63793..0000000000 --- a/src/core/expr/fexpr_cumsum.cc +++ /dev/null @@ -1,111 +0,0 @@ -//------------------------------------------------------------------------------ -// Copyright 2022 H2O.ai -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files (the "Software"), -// to deal in the Software without restriction, including without limitation -// the rights to use, copy, modify, merge, publish, distribute, sublicense, -// and/or sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -// IN THE SOFTWARE. -//------------------------------------------------------------------------------ -#include "column/const.h" -#include "column/cumsum.h" -#include "column/latent.h" -#include "documentation.h" -#include "expr/fexpr_func.h" -#include "expr/eval_context.h" -#include "expr/workframe.h" -#include "python/xargs.h" -#include "stype.h" - - -namespace dt { -namespace expr { - -class FExpr_cumsum : public FExpr_Func { - private: - ptrExpr arg_; - - public: - FExpr_cumsum(ptrExpr&& arg) - : arg_(std::move(arg)) {} - - std::string repr() const override{ - std::string out = "cumsum("; - out += arg_->repr(); - out += ')'; - return out; - } - - - Workframe evaluate_n(EvalContext& ctx) const override{ - Workframe wf = arg_->evaluate_n(ctx); - Groupby gby = Groupby::single_group(wf.nrows()); - - if (ctx.has_groupby()) { - wf.increase_grouping_mode(Grouping::GtoALL); - gby = ctx.get_groupby(); - } - - for (size_t i = 0; i < wf.ncols(); ++i) { - Column coli = evaluate1(wf.retrieve_column(i), gby); - wf.replace_column(i, std::move(coli)); - } - return wf; - } - - - Column evaluate1(Column&& col, const Groupby& gby) const { - SType stype = col.stype(); - switch (stype) { - case SType::VOID: return Column(new ConstInt_ColumnImpl( - col.nrows(), 0, SType::INT64 - )); - case SType::BOOL: - case SType::INT8: - case SType::INT16: - case SType::INT32: - case SType::INT64: return make(std::move(col), SType::INT64, gby); - case SType::FLOAT32: return make(std::move(col), SType::FLOAT32, gby); - case SType::FLOAT64: return make(std::move(col), SType::FLOAT64, gby); - default: throw TypeError() - << "Invalid column of type `" << stype << "` in " << repr(); - } - } - - - template - Column make(Column&& col, SType stype, const Groupby& gby) const { - col.cast_inplace(stype); - return Column(new Latent_ColumnImpl( - new Cumsum_ColumnImpl(std::move(col), gby) - )); - } -}; - - -static py::oobj pyfn_cumsum(const py::XArgs& args) { - auto cumsum = args[0].to_oobj(); - return PyFExpr::make(new FExpr_cumsum(as_fexpr(cumsum))); -} - - -DECLARE_PYFN(&pyfn_cumsum) - ->name("cumsum") - ->docs(doc_dt_cumsum) - ->arg_names({"cumsum"}) - ->n_positional_args(1) - ->n_required_args(1); - -}} // dt::expr diff --git a/src/core/expr/fexpr_cumsumprod.cc b/src/core/expr/fexpr_cumsumprod.cc new file mode 100644 index 0000000000..ca32c5e9a0 --- /dev/null +++ b/src/core/expr/fexpr_cumsumprod.cc @@ -0,0 +1,132 @@ +//------------------------------------------------------------------------------ +// Copyright 2022 H2O.ai +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +//------------------------------------------------------------------------------ +#include "column/const.h" +#include "column/cumsumprod.h" +#include "column/latent.h" +#include "documentation.h" +#include "expr/fexpr_func.h" +#include "expr/eval_context.h" +#include "expr/workframe.h" +#include "python/xargs.h" +#include "stype.h" + + +namespace dt { + namespace expr { + + template + class FExpr_CumSumProd : public FExpr_Func { + private: + ptrExpr arg_; + + public: + FExpr_CumSumProd(ptrExpr &&arg) + : arg_(std::move(arg)) {} + + std::string repr() const override { + std::string out = SUM? "cumsum" : "cumprod"; + out += '('; + out += arg_->repr(); + out += ')'; + return out; + } + + + Workframe evaluate_n(EvalContext &ctx) const override { + Workframe wf = arg_->evaluate_n(ctx); + Groupby gby = Groupby::single_group(wf.nrows()); + + if (ctx.has_groupby()) { + wf.increase_grouping_mode(Grouping::GtoALL); + gby = ctx.get_groupby(); + } + + for (size_t i = 0; i < wf.ncols(); ++i) { + Column coli = evaluate1(wf.retrieve_column(i), gby); + wf.replace_column(i, std::move(coli)); + } + return wf; + } + + + Column evaluate1(Column &&col, const Groupby &gby) const { + SType stype = col.stype(); + switch (stype) { + case SType::VOID: + if (SUM) { + return Column(new ConstInt_ColumnImpl(col.nrows(), 0, SType::INT64)); + } else { + return Column(new ConstInt_ColumnImpl(col.nrows(), 1, SType::INT64)); + } + case SType::BOOL: + case SType::INT8: + case SType::INT16: + case SType::INT32: + case SType::INT64: + return make(std::move(col), SType::INT64, gby); + case SType::FLOAT32: + return make(std::move(col), SType::FLOAT32, gby); + case SType::FLOAT64: + return make(std::move(col), SType::FLOAT64, gby); + default: + throw TypeError() + << "Invalid column of type `" << stype << "` in " << repr(); + } + } + + + template + Column make(Column &&col, SType stype, const Groupby &gby) const { + col.cast_inplace(stype); + return Column(new Latent_ColumnImpl( + new CumSumProd_ColumnImpl(std::move(col), gby) + )); + } + }; + + + static py::oobj pyfn_cumsum(const py::XArgs &args) { + auto cumsum = args[0].to_oobj(); + return PyFExpr::make(new FExpr_CumSumProd(as_fexpr(cumsum))); + } + + static py::oobj pyfn_cumprod(const py::XArgs &args) { + auto cumprod = args[0].to_oobj(); + return PyFExpr::make(new FExpr_CumSumProd(as_fexpr(cumprod))); + } + + DECLARE_PYFN(&pyfn_cumsum) + ->name("cumsum") + ->docs(doc_dt_cumsum) + ->arg_names({"cumsum"}) + ->n_positional_args(1) + ->n_required_args(1); + + DECLARE_PYFN(&pyfn_cumprod) + ->name("cumprod") + ->docs(doc_dt_cumprod) + ->arg_names({"cumprod"}) + ->n_positional_args(1) + ->n_required_args(1); + + } // namespace dt::expr +} // namespace dt diff --git a/src/datatable/__init__.py b/src/datatable/__init__.py index 459ad6981d..8668dc1377 100644 --- a/src/datatable/__init__.py +++ b/src/datatable/__init__.py @@ -29,6 +29,7 @@ cbind, cummax, cummin, + cumprod, cumsum, cut, fread, @@ -89,6 +90,7 @@ "cov", "cummax", "cummin", + "cumprod", "cumsum", "cut", "dt", diff --git a/tests/dt/test-cumprod.py b/tests/dt/test-cumprod.py new file mode 100644 index 0000000000..a66437194f --- /dev/null +++ b/tests/dt/test-cumprod.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Copyright 2022 H2O.ai +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the "Software"), +# to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Software, and to permit persons to whom the +# Software is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. +#------------------------------------------------------------------------------- +import math +import pytest +from datatable import dt, f, cumprod, FExpr, by +from tests import assert_equals + + +#------------------------------------------------------------------------------- +# Errors +#------------------------------------------------------------------------------- + +def test_cumprod_non_numeric(): + DT = dt.Frame(list('abcde')) + with pytest.raises(TypeError, match = r'Invalid column of type str32 in cumprod'): + DT[:, cumprod(f[0])] + +def test_cumprod_non_numeric_by(): + DT = dt.Frame(list('abcde')) + with pytest.raises(TypeError, match = r'Invalid column of type str32 in cumprod'): + DT[:, cumprod(f[0]), by(f[0])] + +def test_cumprod_no_argument(): + match = r'Function datatable.cumprod\(\) requires exactly 1 positional argument, ' \ + 'but none were given' + with pytest.raises(TypeError, match = match): + dt.cumprod() + + +#------------------------------------------------------------------------------- +# Normal +#------------------------------------------------------------------------------- + +def test_cumprod_str(): + assert str(cumprod(f.A)) == "FExpr" + assert str(cumprod(f.A) + 1) == "FExpr" + assert str(cumprod(f.A + f.B)) == "FExpr" + assert str(cumprod(f.B)) == "FExpr" + assert str(cumprod(f[:2])) == "FExpr" + + +def test_cumprod_empty_frame(): + DT = dt.Frame() + expr_cumprod = cumprod(DT) + assert isinstance(expr_cumprod, FExpr) + assert_equals(DT[:, cumprod(f[:])], DT) + + +def test_cumprod_void(): + DT = dt.Frame([None]*10) + DT_cumprod = DT[:, cumprod(f[:])] + assert_equals(DT_cumprod, dt.Frame([1]*10/dt.int64)) + + +def test_cumprod_trivial(): + DT = dt.Frame([0]/dt.int64) + cumprod_fexpr = cumprod(f[:]) + DT_cumprod = DT[:, cumprod_fexpr] + assert isinstance(cumprod_fexpr, FExpr) + assert_equals(DT, DT_cumprod) + + +def test_cumprod_small(): + DT = dt.Frame([range(5), [-1, 1, None, 2, 5.5]]) + DT_cumprod = DT[:, cumprod(f[:])] + DT_ref = dt.Frame([[0, 0, 0, 0, 0]/dt.int64, [-1, -1, -1, -2, -11]/dt.float64]) + assert_equals(DT_cumprod, DT_ref) + + +def test_cumprod_groupby(): + DT = dt.Frame([[2, 1, 1, 1, 2], [1.5, -1.5, math.inf, 2, 3]]) + DT_cumprod = DT[:, cumprod(f[:]), by(f[0])] + DT_ref = dt.Frame([[1, 1, 1, 2, 2], [-1.5, -math.inf, -math.inf, 1.5, 4.5]/dt.float64]) + assert_equals(DT_cumprod, DT_ref) + + +def test_cumprod_void_grouped_column(): + DT = dt.Frame([None]*10) + DT_cumprod = DT[:, cumprod(f.C0), by(f.C0)] + assert_equals(DT_cumprod, dt.Frame([[None]*10, [1]*10/dt.int64])) + + +def test_cumprod_grouped_column(): + DT = dt.Frame([2, 1, None, 1, 2]) + DT_cumprod = DT[:, cumprod(f[0]), by(f[0])] + DT_ref = dt.Frame([[None, 1, 1, 2, 2], [1, 1, 1, 2, 4]/dt.int64]) + assert_equals(DT_cumprod, DT_ref) + diff --git a/tests/test-f.py b/tests/test-f.py index f26d6c5155..8aad9663b3 100644 --- a/tests/test-f.py +++ b/tests/test-f.py @@ -457,3 +457,9 @@ def test_cummin(): DT = dt.Frame(A = [9, 8, 2, 3, None, None, 3, 0, 5, 5, 8, None, 1]) assert_equals(DT[:, f.A.cummin()], DT[:, dt.cummin(f.A)]) +def test_cumprod(): + assert str(dt.cumprod(f.A)) == str(f.A.cumprod()) + assert str(dt.cumprod(f[:])) == str(f[:].cumprod()) + DT = dt.Frame(A = [9, 8, 2, 3, None, None, 3, 0, 5, 5, 8, None, 1]) + assert_equals(DT[:, f.A.cumprod()], DT[:, dt.cumprod(f.A)]) +