From 8a3870c6150e4784d59555b9d659632f10e821ce Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Tue, 16 Apr 2019 23:02:10 -0400 Subject: [PATCH 01/13] python series cumulative scan ops and single cumsum test --- python/cudf/dataframe/numerical.py | 6 +++++ python/cudf/dataframe/series.py | 34 +++++++++++++++++++++++++++++ python/cudf/tests/test_prefixsum.py | 27 +++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/python/cudf/dataframe/numerical.py b/python/cudf/dataframe/numerical.py index 19274c12463..bfaf6ffa132 100644 --- a/python/cudf/dataframe/numerical.py +++ b/python/cudf/dataframe/numerical.py @@ -101,6 +101,12 @@ def unordered_compare(self, cmpop, rhs): def ordered_compare(self, cmpop, rhs): return numeric_column_compare(self, rhs, op=cmpop) + def _apply_scan_op(self, op): + out_col = columnops.column_empty_like(self, dtype=self.dtype, + masked=self.has_null_mask) + cpp_reduce.apply_scan(self, out_col, op, inclusive=True) + return out_col + def normalize_binop_value(self, other): other_dtype = np.min_scalar_type(other) if other_dtype.kind in 'biuf': diff --git a/python/cudf/dataframe/series.py b/python/cudf/dataframe/series.py index 8e2cf756ffc..7e9e637f216 100644 --- a/python/cudf/dataframe/series.py +++ b/python/cudf/dataframe/series.py @@ -1113,6 +1113,40 @@ def product(self, axis=None, skipna=True, dtype=None): assert axis in (None, 0) and skipna is True return self._column.product(dtype=dtype) + def cummin(self, axis=0, skipna=True): + """Compute the cumulative minimum of the series""" + assert axis in (None, 0) and skipna is True + return Series(self._column._apply_scan_op('min'), name=self.name) + + def cummax(self, axis=0, skipna=True): + """Compute the cumulative maximum of the series""" + assert axis in (None, 0) and skipna is True + return Series(self._column._apply_scan_op('max'), name=self.name) + + def cumsum(self, axis=0, skipna=True): + """Compute the cumulative sum of the series""" + assert axis in (None, 0) and skipna is True + + # pandas always returns int64 dtype if original dtype is int + if np.issubdtype(self.dtype, np.integer): + return Series(self.astype(np.int64)._column._apply_scan_op('sum'), + name=self.name) + else: + return Series(self._column._apply_scan_op('sum'), name=self.name) + + def cumprod(self, axis=0, skipna=True): + """Compute the cumulative sum of the series""" + assert axis in (None, 0) and skipna is True + + # pandas always returns int64 dtype if original dtype is int + if np.issubdtype(self.dtype, np.integer): + return Series( + self.astype(np.int64)._column._apply_scan_op('product'), + name=self.name) + else: + return Series(self._column._apply_scan_op('product'), + name=self.name) + def mean(self, axis=None, skipna=True, dtype=None): """Compute the mean of the series """ diff --git a/python/cudf/tests/test_prefixsum.py b/python/cudf/tests/test_prefixsum.py index 85c3b3d7cf1..be54b0462c5 100644 --- a/python/cudf/tests/test_prefixsum.py +++ b/python/cudf/tests/test_prefixsum.py @@ -2,11 +2,13 @@ import pytest import numpy as np +import pandas as pd import cudf.bindings.reduce as cpp_reduce from itertools import product from cudf.dataframe.buffer import Buffer from cudf.dataframe.numerical import NumericalColumn +from cudf.dataframe.dataframe import Series, DataFrame from cudf.tests import utils from cudf.tests.utils import gen_rand @@ -110,3 +112,28 @@ def test_prefixsum_masked(dtype, nelem): decimal = 4 if dtype == np.float32 else 6 np.testing.assert_array_almost_equal(expect, got, decimal=decimal) + + +@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) +def test_cumsum(dtype, nelem): + if dtype == np.int8: + # to keep data in range + data = gen_rand(dtype, nelem, low=-2, high=2) + else: + data = gen_rand(dtype, nelem) + + decimal = 4 if dtype == np.float32 else 6 + + # series + gs = Series(data) + ps = pd.Series(data) + np.testing.assert_array_almost_equal(gs.cumsum(), ps.cumsum(), + decimal=decimal) + + # dataframe series (named series) + gdf = DataFrame() + gdf['a'] = Series(data) + pdf = pd.DataFrame() + pdf['a'] = pd.Series(data) + np.testing.assert_array_almost_equal(gdf.a.cumsum(), pdf.a.cumsum(), + decimal=decimal) From 89318904b718aa05f8a6da9755628d83806f447a Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Tue, 16 Apr 2019 23:03:28 -0400 Subject: [PATCH 02/13] rename test_prefixsum to test_scan --- .../tests/{test_prefixsum.py => test_scan.py} | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) rename python/cudf/tests/{test_prefixsum.py => test_scan.py} (94%) diff --git a/python/cudf/tests/test_prefixsum.py b/python/cudf/tests/test_scan.py similarity index 94% rename from python/cudf/tests/test_prefixsum.py rename to python/cudf/tests/test_scan.py index be54b0462c5..4a3f4fbb7b7 100644 --- a/python/cudf/tests/test_prefixsum.py +++ b/python/cudf/tests/test_scan.py @@ -137,3 +137,26 @@ def test_cumsum(dtype, nelem): pdf['a'] = pd.Series(data) np.testing.assert_array_almost_equal(gdf.a.cumsum(), pdf.a.cumsum(), decimal=decimal) + +def test_cumsum_masked(): + pass + + +def test_cumsum_masked(): + pass + + +def test_cumsum_masked(): + pass + + +def test_cumsum_masked(): + pass + + +def test_cumsum_masked(): + pass + + +def test_cumsum_masked(): + pass \ No newline at end of file From a76e4fc83e3f5a74339d738fc6c7160afeb79f7c Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Tue, 16 Apr 2019 23:06:13 -0400 Subject: [PATCH 03/13] mocked cumulative ops tests --- python/cudf/tests/test_scan.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 4a3f4fbb7b7..cc62cfd2e9c 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -142,21 +142,25 @@ def test_cumsum_masked(): pass -def test_cumsum_masked(): +def test_cummin(): pass -def test_cumsum_masked(): +def test_cummin_masked(): pass -def test_cumsum_masked(): +def test_cummax(): pass -def test_cumsum_masked(): +def test_cummax_masked(): pass -def test_cumsum_masked(): - pass \ No newline at end of file +def test_cumprod(): + pass + + +def test_cumprod_masked(): + pass From 26339b124b1c59aad5c58194f652bdd539799581 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 08:26:57 -0400 Subject: [PATCH 04/13] spacing --- python/cudf/tests/test_scan.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index cc62cfd2e9c..356130b3367 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -138,6 +138,7 @@ def test_cumsum(dtype, nelem): np.testing.assert_array_almost_equal(gdf.a.cumsum(), pdf.a.cumsum(), decimal=decimal) + def test_cumsum_masked(): pass From e16cfef803936cfa3e6908975ed761355bb46838 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 10:41:32 -0400 Subject: [PATCH 05/13] update apply_scan to enforce the same nullmask --- python/cudf/dataframe/numerical.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/cudf/dataframe/numerical.py b/python/cudf/dataframe/numerical.py index bfaf6ffa132..0a032bd7dc5 100644 --- a/python/cudf/dataframe/numerical.py +++ b/python/cudf/dataframe/numerical.py @@ -102,8 +102,7 @@ def ordered_compare(self, cmpop, rhs): return numeric_column_compare(self, rhs, op=cmpop) def _apply_scan_op(self, op): - out_col = columnops.column_empty_like(self, dtype=self.dtype, - masked=self.has_null_mask) + out_col = columnops.column_empty_like_same_mask(self, dtype=self.dtype) cpp_reduce.apply_scan(self, out_col, op, inclusive=True) return out_col From 8cd0167e91bc64a10c7a35313eb660a8c02d9193 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:14:16 -0400 Subject: [PATCH 06/13] standard and masked tests for cumprod --- python/cudf/tests/test_scan.py | 55 ++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 356130b3367..1c2c1688262 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -10,7 +10,7 @@ from cudf.dataframe.numerical import NumericalColumn from cudf.dataframe.dataframe import Series, DataFrame from cudf.tests import utils -from cudf.tests.utils import gen_rand +from cudf.tests.utils import gen_rand, assert_eq from librmm_cffi import librmm as rmm @@ -140,7 +140,19 @@ def test_cumsum(dtype, nelem): def test_cumsum_masked(): - pass + data = [1, 2, None, 4, 5] + float_types = ['float32', 'float64'] + int_types = ['int8', 'int16', 'int32', 'int64'] + + for type_ in float_types: + gs = Series(data).astype(type_) + ps = pd.Series(data).astype(type_) + assert_eq(gs.cumsum(), ps.cumsum()) + + for type_ in int_types: + expected = pd.Series([1, 3, -1, 7, 12]).astype('int64') + gs = Series(data).astype(type_) + assert_eq(gs.cumsum(), expected) def test_cummin(): @@ -159,9 +171,42 @@ def test_cummax_masked(): pass -def test_cumprod(): - pass +@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) +def test_cumprod(dtype, nelem): + if dtype == np.int8: + # to keep data in range + data = gen_rand(dtype, nelem, low=-2, high=2) + else: + data = gen_rand(dtype, nelem) + + decimal = 4 if dtype == np.float32 else 6 + + # series + gs = Series(data) + ps = pd.Series(data) + np.testing.assert_array_almost_equal(gs.cumprod(), ps.cumprod(), + decimal=decimal) + + # dataframe series (named series) + gdf = DataFrame() + gdf['a'] = Series(data) + pdf = pd.DataFrame() + pdf['a'] = pd.Series(data) + np.testing.assert_array_almost_equal(gdf.a.cumprod(), pdf.a.cumprod(), + decimal=decimal) def test_cumprod_masked(): - pass + data = [1, 2, None, 4, 5] + float_types = ['float32', 'float64'] + int_types = ['int8', 'int16', 'int32', 'int64'] + + for type_ in float_types: + gs = Series(data).astype(type_) + ps = pd.Series(data).astype(type_) + assert_eq(gs.cumprod(), ps.cumprod()) + + for type_ in int_types: + expected = pd.Series([1, 2, -1, 8, 40]).astype('int64') + gs = Series(data).astype(type_) + assert_eq(gs.cumprod(), expected) From af3c6189b8d6d14c638c21746e77a7564ae95aa7 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:16:02 -0400 Subject: [PATCH 07/13] standard tests for cummin and cummax --- python/cudf/tests/test_scan.py | 50 +++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 1c2c1688262..35946b99e50 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -155,16 +155,58 @@ def test_cumsum_masked(): assert_eq(gs.cumsum(), expected) -def test_cummin(): - pass +@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) +def test_cummin(dtype, nelem): + if dtype == np.int8: + # to keep data in range + data = gen_rand(dtype, nelem, low=-2, high=2) + else: + data = gen_rand(dtype, nelem) + + decimal = 4 if dtype == np.float32 else 6 + + # series + gs = Series(data) + ps = pd.Series(data) + np.testing.assert_array_almost_equal(gs.cummin(), ps.cummin(), + decimal=decimal) + + # dataframe series (named series) + gdf = DataFrame() + gdf['a'] = Series(data) + pdf = pd.DataFrame() + pdf['a'] = pd.Series(data) + np.testing.assert_array_almost_equal(gdf.a.cummin(), pdf.a.cummin(), + decimal=decimal) def test_cummin_masked(): pass -def test_cummax(): - pass +@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) +def test_cummax(dtype, nelem): + if dtype == np.int8: + # to keep data in range + data = gen_rand(dtype, nelem, low=-2, high=2) + else: + data = gen_rand(dtype, nelem) + + decimal = 4 if dtype == np.float32 else 6 + + # series + gs = Series(data) + ps = pd.Series(data) + np.testing.assert_array_almost_equal(gs.cummax(), ps.cummax(), + decimal=decimal) + + # dataframe series (named series) + gdf = DataFrame() + gdf['a'] = Series(data) + pdf = pd.DataFrame() + pdf['a'] = pd.Series(data) + np.testing.assert_array_almost_equal(gdf.a.cummax(), pdf.a.cummax(), + decimal=decimal) def test_cummax_masked(): From 551b3cdec3b1a36a88375eea3706ebed0e5dc804 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:23:07 -0400 Subject: [PATCH 08/13] masked tests for cummin and cummax --- python/cudf/tests/test_scan.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 35946b99e50..9193b0fdc8e 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -181,7 +181,19 @@ def test_cummin(dtype, nelem): def test_cummin_masked(): - pass + data = [1, 2, None, 4, 5] + float_types = ['float32', 'float64'] + int_types = ['int8', 'int16', 'int32', 'int64'] + + for type_ in float_types: + gs = Series(data).astype(type_) + ps = pd.Series(data).astype(type_) + assert_eq(gs.cummin(), ps.cummin()) + + for type_ in int_types: + expected = pd.Series([1, 1, -1, 1, 1]).astype(type_) + gs = Series(data).astype(type_) + assert_eq(gs.cummin(), expected) @pytest.mark.parametrize('dtype,nelem', list(_gen_params())) @@ -210,7 +222,19 @@ def test_cummax(dtype, nelem): def test_cummax_masked(): - pass + data = [1, 2, None, 4, 5] + float_types = ['float32', 'float64'] + int_types = ['int8', 'int16', 'int32', 'int64'] + + for type_ in float_types: + gs = Series(data).astype(type_) + ps = pd.Series(data).astype(type_) + assert_eq(gs.cummax(), ps.cummax()) + + for type_ in int_types: + expected = pd.Series([1, 2, -1, 4, 5]).astype(type_) + gs = Series(data).astype(type_) + assert_eq(gs.cummax(), expected) @pytest.mark.parametrize('dtype,nelem', list(_gen_params())) From 776b7d577fdb5d16cddcb72137fc2f483d7d5920 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:30:53 -0400 Subject: [PATCH 09/13] keep consistent indices in output column --- python/cudf/dataframe/series.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/python/cudf/dataframe/series.py b/python/cudf/dataframe/series.py index 7e9e637f216..f422cbd7f62 100644 --- a/python/cudf/dataframe/series.py +++ b/python/cudf/dataframe/series.py @@ -1116,12 +1116,14 @@ def product(self, axis=None, skipna=True, dtype=None): def cummin(self, axis=0, skipna=True): """Compute the cumulative minimum of the series""" assert axis in (None, 0) and skipna is True - return Series(self._column._apply_scan_op('min'), name=self.name) + return Series(self._column._apply_scan_op('min'), name=self.name, + index=self.index) def cummax(self, axis=0, skipna=True): """Compute the cumulative maximum of the series""" assert axis in (None, 0) and skipna is True - return Series(self._column._apply_scan_op('max'), name=self.name) + return Series(self._column._apply_scan_op('max'), name=self.name, + index=self.index) def cumsum(self, axis=0, skipna=True): """Compute the cumulative sum of the series""" @@ -1130,9 +1132,10 @@ def cumsum(self, axis=0, skipna=True): # pandas always returns int64 dtype if original dtype is int if np.issubdtype(self.dtype, np.integer): return Series(self.astype(np.int64)._column._apply_scan_op('sum'), - name=self.name) + name=self.name, index=self.index) else: - return Series(self._column._apply_scan_op('sum'), name=self.name) + return Series(self._column._apply_scan_op('sum'), name=self.name, + index=self.index) def cumprod(self, axis=0, skipna=True): """Compute the cumulative sum of the series""" @@ -1142,10 +1145,10 @@ def cumprod(self, axis=0, skipna=True): if np.issubdtype(self.dtype, np.integer): return Series( self.astype(np.int64)._column._apply_scan_op('product'), - name=self.name) + name=self.name, index=self.index) else: return Series(self._column._apply_scan_op('product'), - name=self.name) + name=self.name, index=self.index) def mean(self, axis=None, skipna=True, dtype=None): """Compute the mean of the series From 0912bfc9e430643a13d9e9aab51ccf1b8a5d0637 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:31:59 -0400 Subject: [PATCH 10/13] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97fd58dc61e..ff628594d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - PR #1396 Add DataFrame.drop method - PR #1413 Add DataFrame.melt method - PR #1412 Add DataFrame.pop() +- PR #1441 Add Series level cumulative ops (cumsum, cummin, cummax, cumprod) ## Improvements From e69c9496d4983606913ab9a5667b6797e09f09c9 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 11:33:50 -0400 Subject: [PATCH 11/13] remove NumericalColumn prefixsum test (subsumed by Series test) --- python/cudf/tests/test_scan.py | 79 ---------------------------------- 1 file changed, 79 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 9193b0fdc8e..85d3b3fb1a3 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -35,85 +35,6 @@ def _gen_params(): yield t, n -@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) -def test_prefixsum(dtype, nelem): - if dtype == np.int8: - # to keep data in range - data = gen_rand(dtype, nelem, low=-2, high=2) - else: - data = gen_rand(dtype, nelem) - - d_data = rmm.to_device(data) - - # Allocate output - d_result = rmm.device_array(shape=data.size, dtype=dtype) - - # construct numerical columns - in_col = NumericalColumn(data=Buffer(d_data), mask=None, - null_count=0, dtype=dtype) - out_col = NumericalColumn(data=Buffer(d_result), mask=None, - null_count=0, dtype=dtype) - - # compute scan - inclusive = True - cpp_reduce.apply_scan(in_col, out_col, 'sum', inclusive=inclusive) - - expect = np.cumsum(d_data.copy_to_host()) - - got = d_result.copy_to_host() - if not inclusive: - expect = expect[:-1] - assert got[0] == 0 - got = got[1:] - - decimal = 4 if dtype == np.float32 else 6 - np.testing.assert_array_almost_equal(expect, got, decimal=decimal) - - -@pytest.mark.parametrize('dtype,nelem', list(_gen_params())) -def test_prefixsum_masked(dtype, nelem): - if dtype == np.int8: - data = gen_rand(dtype, nelem, low=-2, high=2) - else: - data = gen_rand(dtype, nelem) - - mask = utils.random_bitmask(nelem) - bitmask = utils.expand_bits_to_bytes(mask)[:nelem] - null_count = utils.count_zero(bitmask) - - result_mask = utils.random_bitmask(nelem) - - d_data = rmm.to_device(data) - d_mask = rmm.to_device(mask) - - d_result = rmm.device_array(d_data.size, dtype=d_data.dtype) - d_result_mask = rmm.to_device(result_mask) - - # construct numerical columns - in_col = NumericalColumn(data=Buffer(d_data), mask=Buffer(d_mask), - null_count=null_count, dtype=dtype) - out_col = NumericalColumn(data=Buffer(d_result), - mask=Buffer(d_result_mask), - null_count=null_count, dtype=dtype) - - # compute scan - inclusive = True - cpp_reduce.apply_scan(in_col, out_col, 'sum', inclusive=inclusive) - - res_mask = np.asarray(bitmask, dtype=np.bool_)[:data.size] - - expect = np.cumsum(data[res_mask]) - got = d_result.copy_to_host()[res_mask] - - if not inclusive: - expect = expect[:-1] - assert got[0] == 0 - got = got[1:] - - decimal = 4 if dtype == np.float32 else 6 - np.testing.assert_array_almost_equal(expect, got, decimal=decimal) - - @pytest.mark.parametrize('dtype,nelem', list(_gen_params())) def test_cumsum(dtype, nelem): if dtype == np.int8: From 88fe14500fb6a37bf322ae5b5fff8bf245b8efa1 Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 12:20:18 -0400 Subject: [PATCH 12/13] remove unused imports --- python/cudf/tests/test_scan.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/python/cudf/tests/test_scan.py b/python/cudf/tests/test_scan.py index 85d3b3fb1a3..193dbf87b23 100644 --- a/python/cudf/tests/test_scan.py +++ b/python/cudf/tests/test_scan.py @@ -1,19 +1,12 @@ -from __future__ import division +from itertools import product import pytest import numpy as np import pandas as pd -import cudf.bindings.reduce as cpp_reduce -from itertools import product -from cudf.dataframe.buffer import Buffer -from cudf.dataframe.numerical import NumericalColumn from cudf.dataframe.dataframe import Series, DataFrame -from cudf.tests import utils from cudf.tests.utils import gen_rand, assert_eq -from librmm_cffi import librmm as rmm - params_dtype = [ np.int8, From e6bb4d1df5f6dcea46057f159d3a349056c6ab6f Mon Sep 17 00:00:00 2001 From: Nick Becker Date: Wed, 17 Apr 2019 13:35:45 -0400 Subject: [PATCH 13/13] fix comprod docstring --- python/cudf/dataframe/series.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/cudf/dataframe/series.py b/python/cudf/dataframe/series.py index f422cbd7f62..4a47bf6038a 100644 --- a/python/cudf/dataframe/series.py +++ b/python/cudf/dataframe/series.py @@ -1138,7 +1138,7 @@ def cumsum(self, axis=0, skipna=True): index=self.index) def cumprod(self, axis=0, skipna=True): - """Compute the cumulative sum of the series""" + """Compute the cumulative product of the series""" assert axis in (None, 0) and skipna is True # pandas always returns int64 dtype if original dtype is int