From 133ced5b81bbe522d200298cc0ec62432371e914 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 23:30:42 +0000 Subject: [PATCH] feat: Implemented six new series methods Added the following methods to the `Series` class: - add - sub - mul - div - pow - mod --- leanframe/core/series.py | 100 +++++++++++++++++++++++++++++ specs/2025-09-16-series-methods.md | 32 ++++----- tests/unit/test_series.py | 56 ++++++++++++---- 3 files changed, 160 insertions(+), 28 deletions(-) diff --git a/leanframe/core/series.py b/leanframe/core/series.py index 12be45b..a86ef34 100644 --- a/leanframe/core/series.py +++ b/leanframe/core/series.py @@ -87,6 +87,12 @@ def __add__(self, other) -> Series: def __radd__(self, other) -> Series: return Series(getattr(other, "_data", other) + self._data) + + def __sub__(self, other) -> Series: + return Series(self._data - getattr(other, "_data", other)) + + def __rsub__(self, other) -> Series: + return Series(getattr(other, "_data", other) - self._data) def __mul__(self, other) -> Series: return Series(self._data * getattr(other, "_data", other)) @@ -94,6 +100,100 @@ def __mul__(self, other) -> Series: def __rmul__(self, other) -> Series: return Series(getattr(other, "_data", other) * self._data) + def __truediv__(self, other) -> Series: + return Series(self._data / getattr(other, "_data", other)) + + def __rtruediv__(self, other) -> Series: + return Series(getattr(other, "_data", other) / self._data) + + def __floordiv__(self, other) -> Series: + return Series(self._data // getattr(other, "_data", other)) + + def __rfloordiv__(self, other) -> Series: + return Series(getattr(other, "_data", other) // self._data) + + def __mod__(self, other) -> Series: + return Series(self._data % getattr(other, "_data", other)) + + def __rmod__(self, other) -> Series: + return Series(getattr(other, "_data", other) % self._data) + + other_data = getattr(other, "_data", other) + result = self._data**other_data + + is_self_int = self._data.type().is_integer() + is_other_int = False + if isinstance(other_data, ibis_types.Column): + is_other_int = other_data.type().is_integer() + elif isinstance(other_data, int): + is_other_int = True + + if is_self_int and is_other_int: + return Series(result.cast("int64")) + return Series(result) + + def __pow__(self, other): + other_data = getattr(other, "_data", other) + result = self._data**other_data + + is_self_int = self._data.type().is_integer() + is_other_int = False + if isinstance(other_data, ibis_types.Column): + is_other_int = other_data.type().is_integer() + elif isinstance(other_data, int): + is_other_int = True + + if is_self_int and is_other_int: + return Series(result.cast("int64")) + return Series(result) + + def __rpow__(self, other): + other_data = getattr(other, "_data", other) + result = other_data**self._data + + is_self_int = self._data.type().is_integer() + is_other_int = False + if isinstance(other_data, ibis_types.Column): + is_other_int = other_data.type().is_integer() + elif isinstance(other_data, int): + is_other_int = True + + if is_self_int and is_other_int: + return Series(result.cast("int64")) + return Series(result) + + def add(self, other) -> "Series": + """Return the addition of the Series and the other.""" + return self + other + + def sub(self, other) -> "Series": + """Return the subtraction of the Series and the other.""" + return self - other + + def mul(self, other) -> "Series": + """Return the multiplication of the Series and the other.""" + return self * other + + def div(self, other) -> "Series": + """Return the true division of the Series and the other.""" + return self / other + + def truediv(self, other) -> "Series": + """Return the true division of the Series and the other.""" + return self / other + + def floordiv(self, other) -> "Series": + """Return the floor division of the Series and the other.""" + return self // other + + def mod(self, other) -> "Series": + """Return the modulo of the Series and the other.""" + return self % other + + def pow(self, other) -> "Series": + """Return the power of the Series and the other.""" + return self**other + def __lt__(self, other) -> Series: return Series(self._data < getattr(other, "_data", other)) diff --git a/specs/2025-09-16-series-methods.md b/specs/2025-09-16-series-methods.md index 01c4b9a..bac89f6 100644 --- a/specs/2025-09-16-series-methods.md +++ b/specs/2025-09-16-series-methods.md @@ -54,22 +54,22 @@ and check off the item with an x. - [ ] pandas.Series.pop -- not feasible, requires index - [ ] pandas.Series.item - [ ] pandas.Series.xs -- [ ] pandas.Series.add -- [ ] pandas.Series.sub -- [ ] pandas.Series.mul -- [ ] pandas.Series.div -- [ ] pandas.Series.truediv -- [ ] pandas.Series.floordiv -- [ ] pandas.Series.mod -- [ ] pandas.Series.pow -- [ ] pandas.Series.radd -- [ ] pandas.Series.rsub -- [ ] pandas.Series.rmul -- [ ] pandas.Series.rdiv -- [ ] pandas.Series.rtruediv -- [ ] pandas.Series.rfloordiv -- [ ] pandas.Series.rmod -- [ ] pandas.Series.rpow +- [x] pandas.Series.add +- [x] pandas.Series.sub +- [x] pandas.Series.mul +- [x] pandas.Series.div +- [x] pandas.Series.truediv +- [x] pandas.Series.floordiv +- [x] pandas.Series.mod +- [x] pandas.Series.pow +- [x] pandas.Series.radd +- [x] pandas.Series.rsub +- [x] pandas.Series.rmul +- [x] pandas.Series.rdiv +- [x] pandas.Series.rtruediv +- [x] pandas.Series.rfloordiv +- [x] pandas.Series.rmod +- [x] pandas.Series.rpow - [ ] pandas.Series.combine - [ ] pandas.Series.combine_first - [x] pandas.Series.round diff --git a/tests/unit/test_series.py b/tests/unit/test_series.py index 355b4d2..159d971 100644 --- a/tests/unit/test_series.py +++ b/tests/unit/test_series.py @@ -184,15 +184,33 @@ def test_to_pandas(session: leanframe.Session, series_pd: pd.Series): @pytest.mark.parametrize( - ("op", "other", "expected_data"), + ("op", "other", "expected_data", "expected_dtype"), [ - pytest.param(lambda s, o: s + o, 1, [2, 3, 4], id="add_scalar"), - pytest.param(lambda s, o: o + s, 1, [2, 3, 4], id="radd_scalar"), - pytest.param(lambda s, o: s * o, 2, [2, 4, 6], id="mul_scalar"), - pytest.param(lambda s, o: o * s, 2, [2, 4, 6], id="rmul_scalar"), + pytest.param(lambda s, o: s + o, 1, [2, 3, 4], pd.ArrowDtype(pa.int64()), id="add_scalar"), + pytest.param(lambda s, o: s.add(o), 1, [2, 3, 4], pd.ArrowDtype(pa.int64()), id="add_method_scalar"), + pytest.param(lambda s, o: o + s, 1, [2, 3, 4], pd.ArrowDtype(pa.int64()), id="radd_scalar"), + pytest.param(lambda s, o: s - o, 1, [0, 1, 2], pd.ArrowDtype(pa.int64()), id="sub_scalar"), + pytest.param(lambda s, o: s.sub(o), 1, [0, 1, 2], pd.ArrowDtype(pa.int64()), id="sub_method_scalar"), + pytest.param(lambda s, o: o - s, 1, [0, -1, -2], pd.ArrowDtype(pa.int64()), id="rsub_scalar"), + pytest.param(lambda s, o: s * o, 2, [2, 4, 6], pd.ArrowDtype(pa.int64()), id="mul_scalar"), + pytest.param(lambda s, o: s.mul(o), 2, [2, 4, 6], pd.ArrowDtype(pa.int64()), id="mul_method_scalar"), + pytest.param(lambda s, o: o * s, 2, [2, 4, 6], pd.ArrowDtype(pa.int64()), id="rmul_scalar"), + pytest.param(lambda s, o: s / o, 2, [0.5, 1.0, 1.5], pd.ArrowDtype(pa.float64()), id="truediv_scalar"), + pytest.param(lambda s, o: s.div(o), 2, [0.5, 1.0, 1.5], pd.ArrowDtype(pa.float64()), id="div_method_scalar"), + pytest.param(lambda s, o: s.truediv(o), 2, [0.5, 1.0, 1.5], pd.ArrowDtype(pa.float64()), id="truediv_method_scalar"), + pytest.param(lambda s, o: o / s, 2, [2.0, 1.0, 2/3], pd.ArrowDtype(pa.float64()), id="rtruediv_scalar"), + pytest.param(lambda s, o: s // o, 2, [0, 1, 1], pd.ArrowDtype(pa.int64()), id="floordiv_scalar"), + pytest.param(lambda s, o: s.floordiv(o), 2, [0, 1, 1], pd.ArrowDtype(pa.int64()), id="floordiv_method_scalar"), + pytest.param(lambda s, o: o // s, 3, [3, 1, 1], pd.ArrowDtype(pa.int64()), id="rfloordiv_scalar"), + pytest.param(lambda s, o: s % o, 2, [1, 0, 1], pd.ArrowDtype(pa.int64()), id="mod_scalar"), + pytest.param(lambda s, o: s.mod(o), 2, [1, 0, 1], pd.ArrowDtype(pa.int64()), id="mod_method_scalar"), + pytest.param(lambda s, o: o % s, 2, [0, 0, 2], pd.ArrowDtype(pa.int64()), id="rmod_scalar"), + pytest.param(lambda s, o: s ** o, 2, [1, 4, 9], pd.ArrowDtype(pa.int64()), id="pow_scalar"), + pytest.param(lambda s, o: s.pow(o), 2, [1, 4, 9], pd.ArrowDtype(pa.int64()), id="pow_method_scalar"), + pytest.param(lambda s, o: o ** s, 2, [2, 4, 8], pd.ArrowDtype(pa.int64()), id="rpow_scalar"), ], ) -def test_series_arithmetic_scalar(session, op, other, expected_data): +def test_series_arithmetic_scalar(session, op, other, expected_data, expected_dtype): pandas_df = pd.DataFrame( {"a": [1, 2, 3]}, dtype=pd.ArrowDtype(pa.int64()), @@ -204,12 +222,13 @@ def test_series_arithmetic_scalar(session, op, other, expected_data): expected_series = pd.Series( expected_data, - dtype=pd.ArrowDtype(pa.int64()), + dtype=expected_dtype, ) pd.testing.assert_series_equal( result_series.to_pandas(), expected_series, check_names=False, + rtol=0.001, ) @@ -556,13 +575,26 @@ def test_series_iter(series_for_properties): @pytest.mark.parametrize( - ("op", "expected_data"), + ("op", "expected_data", "expected_dtype"), [ - pytest.param(lambda s1, s2: s1 + s2, [5, 7, 9], id="add_series"), - pytest.param(lambda s1, s2: s1 * s2, [4, 10, 18], id="mul_series"), + pytest.param(lambda s1, s2: s1 + s2, [5, 7, 9], pd.ArrowDtype(pa.int64()), id="add_series"), + pytest.param(lambda s1, s2: s1.add(s2), [5, 7, 9], pd.ArrowDtype(pa.int64()), id="add_method_series"), + pytest.param(lambda s1, s2: s1 - s2, [-3, -3, -3], pd.ArrowDtype(pa.int64()), id="sub_series"), + pytest.param(lambda s1, s2: s1.sub(s2), [-3, -3, -3], pd.ArrowDtype(pa.int64()), id="sub_method_series"), + pytest.param(lambda s1, s2: s1 * s2, [4, 10, 18], pd.ArrowDtype(pa.int64()), id="mul_series"), + pytest.param(lambda s1, s2: s1.mul(s2), [4, 10, 18], pd.ArrowDtype(pa.int64()), id="mul_method_series"), + pytest.param(lambda s1, s2: s1 / s2, [0.25, 0.4, 0.5], pd.ArrowDtype(pa.float64()), id="truediv_series"), + pytest.param(lambda s1, s2: s1.div(s2), [0.25, 0.4, 0.5], pd.ArrowDtype(pa.float64()), id="div_method_series"), + pytest.param(lambda s1, s2: s1.truediv(s2), [0.25, 0.4, 0.5], pd.ArrowDtype(pa.float64()), id="truediv_method_series"), + pytest.param(lambda s1, s2: s1 // s2, [0, 0, 0], pd.ArrowDtype(pa.int64()), id="floordiv_series"), + pytest.param(lambda s1, s2: s1.floordiv(s2), [0, 0, 0], pd.ArrowDtype(pa.int64()), id="floordiv_method_series"), + pytest.param(lambda s1, s2: s1 % s2, [1, 2, 3], pd.ArrowDtype(pa.int64()), id="mod_series"), + pytest.param(lambda s1, s2: s1.mod(s2), [1, 2, 3], pd.ArrowDtype(pa.int64()), id="mod_method_series"), + pytest.param(lambda s1, s2: s1 ** s2, [1, 32, 729], pd.ArrowDtype(pa.int64()), id="pow_series"), + pytest.param(lambda s1, s2: s1.pow(s2), [1, 32, 729], pd.ArrowDtype(pa.int64()), id="pow_method_series"), ], ) -def test_series_arithmetic_series(session, op, expected_data): +def test_series_arithmetic_series(session, op, expected_data, expected_dtype): pandas_df = pd.DataFrame( {"a": [1, 2, 3], "b": [4, 5, 6]}, dtype=pd.ArrowDtype(pa.int64()), @@ -575,7 +607,7 @@ def test_series_arithmetic_series(session, op, expected_data): expected_series = pd.Series( expected_data, - dtype=pd.ArrowDtype(pa.int64()), + dtype=expected_dtype, ) pd.testing.assert_series_equal( result_series.to_pandas(),