Skip to content

Commit 184571a

Browse files
Backup
1 parent 41deb74 commit 184571a

10 files changed

Lines changed: 116 additions & 39 deletions

File tree

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ API Reference
1616
## Highlights
1717

1818
- **STATA-like logging** — wrap a pandas/polars pipeline in `startlog()` / `endlog()`
19-
and get a log line describing how many rows, columns, or cell values changed (and
20-
optionally how long it took).
19+
and get a log line describing how many rows, columns, or cell values changed (plus
20+
the new shape when rows or columns are added/removed, and optionally how long it took).
2121
- **`.docx` export** — write any DataFrame straight to a Word table with `to_docx()`.
2222
- **Frequency & cross tables**`freq()` and `crosstab()` accessors for quick
2323
tabulations.
@@ -79,7 +79,7 @@ df = pd.DataFrame(
7979

8080
# Shape changes are logged automatically
8181
_ = df.raffa.startlog().dropna(subset=["bill_depth_mm"]).raffa.endlog(timeit=False)
82-
# -> Removed 2/6 (33.33%) rows.
82+
# -> Removed 2/6 (33.33%) rows. New shape: (4, 3).
8383

8484
# Pass clone=True to also detect value-level changes when the shape is unchanged
8585
_ = df.raffa.startlog(clone=True).fillna(0).raffa.endlog(timeit=False)
@@ -104,7 +104,7 @@ df = pl.DataFrame(
104104
)
105105

106106
_ = df.raffa.startlog().filter(pl.col("species") == "Adelie").raffa.endlog(timeit=False)
107-
# -> Removed 3/6 (50.00%) rows.
107+
# -> Removed 3/6 (50.00%) rows. New shape: (3, 3).
108108
```
109109

110110
Both backends share the same `startlog(clone=False)` / `endlog(custom_msg=None, timeit=True)`

docs/source/examples.rst

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ change logging to a Series or DataFrame.
1919

2020
The pattern is always the same: call ``startlog()`` before an operation and
2121
``endlog()`` after it. ``endlog`` logs how the data changed — rows or columns
22-
added/removed or, when the shape is unchanged, how many cell values changed.
22+
added/removed (followed by the resulting shape) or, when the shape is unchanged,
23+
how many cell values changed.
2324

2425
``startlog(clone=False)``
2526
Snapshots the current shape (and a timestamp). Pass ``clone=True`` to also
@@ -85,17 +86,17 @@ Removing rows, filtering, or dropping columns changes the shape, which is
8586
logged automatically:
8687

8788
>>> _ = df.raffa.startlog().dropna(subset=["bill_depth_mm"]).raffa.endlog(timeit=False)
88-
Removed 2/10 (20.00%) rows.
89+
Removed 2/10 (20.00%) rows. New shape: (8, 8).
8990
>>> _ = df.raffa.startlog().query("species=='Adelie'").raffa.endlog(timeit=False)
90-
Removed 5/10 (50.00%) rows.
91+
Removed 5/10 (50.00%) rows. New shape: (5, 8).
9192
>>> _ = df.raffa.startlog().drop(["bill_length_mm", "bill_depth_mm"], axis=1).raffa.endlog(timeit=False)
92-
Removed 2/8 (25.00%) columns.
93+
Removed 2/8 (25.00%) columns. New shape: (10, 6).
9394

9495
With the default ``timeit=True``, ``endlog`` appends the elapsed time on a
9596
second line (the duration varies from run to run):
9697

9798
>>> _ = df.raffa.startlog().dropna(subset=["bill_depth_mm"]).raffa.endlog()
98-
Removed 2/10 (20.00%) rows.
99+
Removed 2/10 (20.00%) rows. New shape: (8, 8).
99100
Took: ...
100101

101102
Operations that change values but not the shape need ``clone=True`` so the
@@ -110,7 +111,7 @@ The same accessor is available on a Series:
110111

111112
>>> s = df["bill_length_mm"]
112113
>>> _ = s.raffa.startlog().dropna().raffa.endlog(timeit=False)
113-
Removed 2/10 (20.00%) values.
114+
Removed 2/10 (20.00%) values. New shape: (8,).
114115
>>> _ = s.raffa.startlog(clone=True).fillna(0).raffa.endlog(timeit=False)
115116
Changed 2/10 (20.00%) values.
116117

@@ -264,17 +265,17 @@ Removing rows with nulls, filtering values, or selecting columns changes the
264265
shape, which is logged:
265266

266267
>>> _ = df.raffa.startlog().drop_nulls(subset=["bill_depth_mm"]).raffa.endlog(timeit=False)
267-
Removed 2/10 (20.00%) rows.
268+
Removed 2/10 (20.00%) rows. New shape: (8, 8).
268269
>>> _ = df.raffa.startlog().filter(pl.col("species")=="Adelie").raffa.endlog(timeit=False)
269-
Removed 5/10 (50.00%) rows.
270+
Removed 5/10 (50.00%) rows. New shape: (5, 8).
270271
>>> _ = df.raffa.startlog().select(pl.exclude(["bill_length_mm", "bill_depth_mm"])).raffa.endlog(timeit=False)
271-
Removed 2/8 (25.00%) columns.
272+
Removed 2/8 (25.00%) columns. New shape: (10, 6).
272273

273274
As with pandas, the default ``timeit=True`` appends the elapsed time on a second
274275
line (the duration varies from run to run):
275276

276277
>>> _ = df.raffa.startlog().filter(pl.col("species")=="Adelie").raffa.endlog()
277-
Removed 5/10 (50.00%) rows.
278+
Removed 5/10 (50.00%) rows. New shape: (5, 8).
278279
Took: ...
279280

280281
Operations that change values but not the shape need ``clone=True``:

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ A taste
4747
}
4848
)
4949
df = df.raffa.startlog().dropna(subset=["bill_depth_mm"]).raffa.endlog()
50-
# -> Removed 2/6 (33.33%) rows.
50+
# -> Removed 2/6 (33.33%) rows. New shape: (4, 3).
5151
# -> Took: 0.01 seconds
5252
5353
New here? Start with :doc:`quickstart`, then browse the :doc:`examples` for the

docs/source/quickstart.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ to log how the data changed:
4646
... "body_mass_g": [3750.0, 3800.0, np.nan, 4500.0, 5700.0, 3500.0],
4747
... }
4848
... )
49-
>>> # Row/column count changes are logged automatically
49+
>>> # Row/column count changes are logged automatically, with the resulting shape
5050
>>> _ = df.raffa.startlog().dropna(subset=["bill_depth_mm"]).raffa.endlog(timeit=False)
51-
Removed 2/6 (33.33%) rows.
51+
Removed 2/6 (33.33%) rows. New shape: (4, 3).
5252
>>> # Pass clone=True to also detect value-level changes when the shape is unchanged
5353
>>> _ = df.raffa.startlog(clone=True).fillna(0).raffa.endlog(timeit=False)
5454
Changed 3/18 (16.67%) values.
@@ -65,7 +65,7 @@ The polars accessor works identically:
6565
... }
6666
... )
6767
>>> _ = df.raffa.startlog().filter(pl.col("species") == "Adelie").raffa.endlog(timeit=False)
68-
Removed 3/6 (50.00%) rows.
68+
Removed 3/6 (50.00%) rows. New shape: (3, 3).
6969

7070
Timing each step
7171
----------------
@@ -75,7 +75,7 @@ the default ``timeit=True``, ``endlog`` appends the wall-clock time the step too
7575
on a second line (the duration varies from run to run):
7676

7777
>>> _ = df.raffa.startlog().filter(pl.col("species") == "Adelie").raffa.endlog()
78-
Removed 3/6 (50.00%) rows.
78+
Removed 3/6 (50.00%) rows. New shape: (3, 3).
7979
Took: ...
8080

8181
Next steps

src/raffalib/_logutils.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,61 @@ def count_delta(delta: int, total: int, unit: str) -> str:
7575
return ""
7676

7777

78+
def new_shape(shape: tuple[int, ...]) -> str:
79+
"""
80+
Report the shape a DataFrame or Series has after rows or columns were
81+
added or removed.
82+
83+
:param shape: The shape tuple after the operation (``(n,)`` for a Series,
84+
``(n_rows, n_cols)`` for a DataFrame).
85+
:type shape: tuple[int, ...]
86+
:return: A message like ``"New shape: (8, 6)."``.
87+
:rtype: str
88+
"""
89+
return f"New shape: {shape}."
90+
91+
92+
def series_shape_delta(initial_shape: tuple[int], final_shape: tuple[int]) -> str:
93+
"""
94+
Describe how a Series' length changed and report its new shape.
95+
96+
:param initial_shape: ``(n,)`` before the operation.
97+
:type initial_shape: tuple[int]
98+
:param final_shape: ``(n,)`` after the operation.
99+
:type final_shape: tuple[int]
100+
:return: The value-count change followed by the new shape, or an empty
101+
string when the length did not change.
102+
:rtype: str
103+
"""
104+
delta = count_delta(initial_shape[0] - final_shape[0], initial_shape[0], "values")
105+
if not delta:
106+
return ""
107+
return f"{delta} {new_shape(final_shape)}"
108+
109+
78110
def dataframe_shape_delta(
79111
initial_shape: tuple[int, int], final_shape: tuple[int, int]
80112
) -> str:
81113
"""
82-
Describe how a DataFrame's row and column counts changed.
114+
Describe how a DataFrame's row and column counts changed and report its
115+
new shape.
83116
84117
:param initial_shape: ``(n_rows, n_cols)`` before the operation.
85118
:type initial_shape: tuple[int, int]
86119
:param final_shape: ``(n_rows, n_cols)`` after the operation.
87120
:type final_shape: tuple[int, int]
88-
:return: The concatenated row and column change messages.
121+
:return: The concatenated row and column change messages followed by the
122+
new shape, or an empty string when neither changed.
89123
:rtype: str
90124
"""
91125
nrow0, ncol0 = initial_shape
92126
nrow1, ncol1 = final_shape
93-
return count_delta(nrow0 - nrow1, nrow0, "rows") + count_delta(
127+
delta = count_delta(nrow0 - nrow1, nrow0, "rows") + count_delta(
94128
ncol0 - ncol1, ncol0, "columns"
95129
)
130+
if not delta:
131+
return ""
132+
return f"{delta} {new_shape(final_shape)}"
96133

97134

98135
def changed_cells(nchanged: int, ntotal: int) -> str:

src/raffalib/pandas.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ def endlog(self, custom_msg: str | None = None, timeit: bool = True):
9494
initial_shape = self._series._initial_shape
9595
final_shape = self._series.shape
9696
if final_shape != initial_shape:
97-
dr = initial_shape[0] - final_shape[0]
98-
msg += _logutils.count_delta(dr, initial_shape[0], "values")
97+
msg += _logutils.series_shape_delta(initial_shape, final_shape)
9998
else:
10099
if self._series._initial_series is not None:
101100
nchanged = self._series != self._series._initial_series
@@ -311,9 +310,9 @@ def join(self, df2: pd.DataFrame, *args, keep_row_index: bool = False, **kwargs)
311310
n_both = joined_both.shape[0]
312311
n_left_dups = int(joined_both[left_col].duplicated(keep=False).sum())
313312
n_right_dups = int(joined_both[right_col].duplicated(keep=False).sum())
314-
n_left_only = joined[
315-
joined[left_col].notna() & joined[right_col].isna()
316-
].shape[0]
313+
n_left_only = joined[joined[left_col].notna() & joined[right_col].isna()].shape[
314+
0
315+
]
317316
n_right_only = joined[
318317
joined[left_col].isna() & joined[right_col].notna()
319318
].shape[0]

src/raffalib/polars.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ def endlog(self, custom_msg: str | None = None, timeit: bool = True) -> pl.Serie
109109
new_shape = self._series.shape
110110

111111
if new_shape != old_shape:
112-
dr = old_shape[0] - new_shape[0]
113-
msg += _logutils.count_delta(dr, old_shape[0], "values")
112+
msg += _logutils.series_shape_delta(old_shape, new_shape)
114113
else:
115114
old_series = self._series.config_meta.get_metadata()["old_series"]
116115
if old_series is None:

tests/test_logutils.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,45 @@ def test_count_delta_thousands_separator():
3636

3737
def test_dataframe_shape_delta_rows_and_columns():
3838
msg = _logutils.dataframe_shape_delta((10, 5), (8, 6))
39-
assert msg == "Removed 2/10 (20.00%) rows.Added 1/5 (20.00%) columns."
39+
assert msg == (
40+
"Removed 2/10 (20.00%) rows.Added 1/5 (20.00%) columns. New shape: (8, 6)."
41+
)
4042

4143

4244
def test_dataframe_shape_delta_rows_only():
43-
assert (
44-
_logutils.dataframe_shape_delta((10, 5), (7, 5))
45-
== "Removed 3/10 (30.00%) rows."
45+
assert _logutils.dataframe_shape_delta((10, 5), (7, 5)) == (
46+
"Removed 3/10 (30.00%) rows. New shape: (7, 5)."
4647
)
4748

4849

4950
def test_dataframe_shape_delta_no_change_is_empty():
5051
assert _logutils.dataframe_shape_delta((10, 5), (10, 5)) == ""
5152

5253

54+
def test_new_shape_dataframe():
55+
assert _logutils.new_shape((8, 6)) == "New shape: (8, 6)."
56+
57+
58+
def test_new_shape_series():
59+
assert _logutils.new_shape((8,)) == "New shape: (8,)."
60+
61+
62+
def test_series_shape_delta_removed():
63+
assert _logutils.series_shape_delta((5,), (3,)) == (
64+
"Removed 2/5 (40.00%) values. New shape: (3,)."
65+
)
66+
67+
68+
def test_series_shape_delta_added():
69+
assert _logutils.series_shape_delta((3,), (5,)) == (
70+
"Added 2/3 (66.67%) values. New shape: (5,)."
71+
)
72+
73+
74+
def test_series_shape_delta_no_change_is_empty():
75+
assert _logutils.series_shape_delta((5,), (5,)) == ""
76+
77+
5378
def test_ratio_basic():
5479
assert _logutils.ratio(1, 4) == "1/4 (25.00%)"
5580

tests/test_pandas.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ def test_endlog_logs_removed_rows(caplog):
4747
df.raffa.startlog()
4848
df = df[df["a"] > 1]
4949
df.raffa.endlog()
50-
assert any("Removed 1/4 (25.00%) rows." in r.message for r in caplog.records)
50+
assert any(
51+
"Removed 1/4 (25.00%) rows. New shape: (3, 1)." in r.message
52+
for r in caplog.records
53+
)
5154

5255

5356
def test_endlog_logs_value_changes(caplog):
@@ -73,7 +76,10 @@ def test_series_endlog_removed_values(caplog):
7376
s.raffa.startlog()
7477
s = s[s > 2]
7578
s.raffa.endlog()
76-
assert any("Removed 2/5 (40.00%) values." in r.message for r in caplog.records)
79+
assert any(
80+
"Removed 2/5 (40.00%) values. New shape: (3,)." in r.message
81+
for r in caplog.records
82+
)
7783

7884

7985
def test_join_inner_drops_source_columns(caplog):
@@ -128,7 +134,9 @@ def test_endlog_add_rows_to_empty_frame_is_guarded(caplog):
128134
df.raffa.startlog()
129135
df = df.reindex(range(3))
130136
df.raffa.endlog(timeit=False)
131-
assert any("Added 3/0 (N/A) rows." in r.message for r in caplog.records)
137+
assert any(
138+
"Added 3/0 (N/A) rows. New shape: (3, 1)." in r.message for r in caplog.records
139+
)
132140

133141

134142
def test_join_empty_output_does_not_divide_by_zero(caplog):

tests/test_polars.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ def test_endlog_removed_rows(caplog):
4040
df.raffa.startlog()
4141
df = df.filter(pl.col("a") > 1)
4242
df.raffa.endlog(timeit=False)
43-
assert any("Removed 1/4 (25.00%) rows." in r.message for r in caplog.records)
43+
assert any(
44+
"Removed 1/4 (25.00%) rows. New shape: (3, 1)." in r.message
45+
for r in caplog.records
46+
)
4447

4548

4649
def test_series_endlog_matches_pandas_format(caplog):
@@ -49,7 +52,10 @@ def test_series_endlog_matches_pandas_format(caplog):
4952
s.raffa.startlog()
5053
s = s.filter(s > 2)
5154
s.raffa.endlog(timeit=False)
52-
assert any("Removed 2/5 (40.00%) values." in r.message for r in caplog.records)
55+
assert any(
56+
"Removed 2/5 (40.00%) values. New shape: (3,)." in r.message
57+
for r in caplog.records
58+
)
5359

5460

5561
def test_endlog_timeit_appends_duration(caplog):
@@ -84,7 +90,9 @@ def test_endlog_add_rows_to_empty_frame_is_guarded(caplog):
8490
df.raffa.startlog()
8591
df = df.vstack(pl.DataFrame({"a": [1.0, 2.0, 3.0]}))
8692
df.raffa.endlog(timeit=False)
87-
assert any("Added 3/0 (N/A) rows." in r.message for r in caplog.records)
93+
assert any(
94+
"Added 3/0 (N/A) rows. New shape: (3, 1)." in r.message for r in caplog.records
95+
)
8896

8997

9098
def test_join_empty_output_does_not_divide_by_zero(caplog):

0 commit comments

Comments
 (0)