Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions leanframe/core/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,113 @@ 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))

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)
Comment on lines +118 to +133

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@google-labs-jules please explain in some comments the logic of rmod. I'm having trouble understanding it at a glance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comments to all the reverse binary operators to clarify their purpose. I also noticed some stray code in __rmod__ and a duplicate __pow__ definition, which I've now fixed. Thanks for catching that!


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)
Comment on lines +135 to +163

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@google-labs-jules likewise, pow and rpow could use further explanation in comments


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))

Expand Down
32 changes: 16 additions & 16 deletions specs/2025-09-16-series-methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 44 additions & 12 deletions tests/unit/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand All @@ -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,
)


Expand Down Expand Up @@ -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()),
Expand All @@ -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(),
Expand Down