microdf-python 1.1.2__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
microdf/microdataframe.py CHANGED
@@ -278,18 +278,18 @@ class MicroDataFrame(pd.DataFrame):
278
278
  self._link_all_weights()
279
279
 
280
280
  def _link_weights(self, column) -> None:
281
- # self[column] = ... triggers __setitem__, which forces pd.Series
282
- # this workaround avoids that
283
- self[column].__class__ = MicroSeries
284
- self[column].set_weights(self.weights)
281
+ # In pandas 3.0+, we can't modify column classes in-place due to CoW.
282
+ # Instead, we rely on __getitem__ to wrap columns as MicroSeries on
283
+ # access. This method is kept for backward compatibility but is now
284
+ # a no-op.
285
+ pass
285
286
 
286
287
  def _link_all_weights(self) -> None:
287
288
  if self.weights is None:
288
289
  if len(self) > 0:
289
290
  self.set_weights(np.ones((len(self))))
290
- for column in self.columns:
291
- if column != self.weights_col:
292
- self._link_weights(column)
291
+ # In pandas 3.0+, columns are wrapped as MicroSeries on access via
292
+ # __getitem__, not stored as MicroSeries internally.
293
293
 
294
294
  def set_weights(
295
295
  self,
@@ -365,7 +365,7 @@ class MicroDataFrame(pd.DataFrame):
365
365
 
366
366
  def __getitem__(
367
367
  self, key: Union[str, List]
368
- ) -> Union[pd.Series, pd.DataFrame]:
368
+ ) -> Union[MicroSeries, "MicroDataFrame"]:
369
369
  # Let pandas handle the initial slicing
370
370
  result = super().__getitem__(key)
371
371
 
@@ -374,17 +374,22 @@ class MicroDataFrame(pd.DataFrame):
374
374
  new_weights = self.weights.reindex(result.index)
375
375
  return MicroDataFrame(result, weights=new_weights)
376
376
 
377
- # Otherwise, the result is a Series or a scalar, so just return it
377
+ # If the result is a Series (single column), wrap as MicroSeries
378
+ if isinstance(result, pd.Series):
379
+ return MicroSeries(result, weights=self.weights)
380
+
381
+ # Otherwise, the result is a scalar, so just return it
378
382
  return result
379
383
 
380
384
  def catch_series_relapse(self) -> None:
381
- for col in self.columns:
382
- if self[col].__class__ == pd.Series:
383
- self._link_weights(col)
385
+ # In pandas 3.0+, we don't need to track series class changes since
386
+ # __getitem__ always wraps columns as MicroSeries on access.
387
+ pass
384
388
 
385
389
  def __setattr__(self, key, value) -> None:
386
390
  super().__setattr__(key, value)
387
- self.catch_series_relapse()
391
+ # No need to call catch_series_relapse in pandas 3.0+ since we wrap
392
+ # on access rather than store MicroSeries internally.
388
393
 
389
394
  def reset_index(
390
395
  self,
@@ -0,0 +1,248 @@
1
+ """Tests for pandas 3.0.0 compatibility in microdf.
2
+
3
+ These tests verify that microdf works correctly with pandas 3.0.0,
4
+ which introduces:
5
+ 1. PyArrow-backed strings as default (StringDtype)
6
+ 2. Copy-on-Write by default
7
+ 3. Changes to how Series subclasses are handled
8
+ """
9
+
10
+ import numpy as np
11
+ import pandas as pd
12
+
13
+ from microdf.microdataframe import MicroDataFrame
14
+ from microdf.microseries import MicroSeries
15
+
16
+
17
+ class TestMicroSeriesSubclassPreservation:
18
+ """Test that MicroSeries subclass is preserved across operations."""
19
+
20
+ def test_microseries_set_weights_after_creation(self):
21
+ """Ensure set_weights works on MicroSeries.
22
+
23
+ This is the error reported in pandas 3:
24
+ AttributeError: 'Series' object has no attribute 'set_weights'
25
+ """
26
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 1.0, 1.0]))
27
+ assert hasattr(ms, "set_weights")
28
+ assert hasattr(ms, "weights")
29
+
30
+ # Should be able to call set_weights
31
+ ms.set_weights(np.array([2.0, 2.0, 2.0]))
32
+ assert np.allclose(ms.weights, [2.0, 2.0, 2.0])
33
+
34
+ def test_microseries_preserved_after_arithmetic(self):
35
+ """Arithmetic operations should return MicroSeries, not plain
36
+ Series."""
37
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
38
+
39
+ # Addition
40
+ result = ms + 1
41
+ assert isinstance(
42
+ result, MicroSeries
43
+ ), f"Got {type(result)} instead of MicroSeries"
44
+ assert hasattr(result, "weights")
45
+ assert hasattr(result, "set_weights")
46
+
47
+ # Multiplication
48
+ result = ms * 2
49
+ assert isinstance(
50
+ result, MicroSeries
51
+ ), f"Got {type(result)} instead of MicroSeries"
52
+
53
+ # Division
54
+ result = ms / 2
55
+ assert isinstance(
56
+ result, MicroSeries
57
+ ), f"Got {type(result)} instead of MicroSeries"
58
+
59
+ def test_microseries_preserved_after_comparison(self):
60
+ """Comparison operations should return MicroSeries, not plain
61
+ Series."""
62
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
63
+
64
+ # Greater than
65
+ result = ms > 1
66
+ assert isinstance(
67
+ result, MicroSeries
68
+ ), f"Got {type(result)} instead of MicroSeries"
69
+ assert hasattr(result, "weights")
70
+
71
+ # Less than
72
+ result = ms < 3
73
+ assert isinstance(
74
+ result, MicroSeries
75
+ ), f"Got {type(result)} instead of MicroSeries"
76
+
77
+ def test_microseries_preserved_after_indexing(self):
78
+ """Indexing operations should return MicroSeries, not plain Series."""
79
+ ms = MicroSeries(
80
+ [1, 2, 3, 4, 5], weights=np.array([1.0, 2.0, 3.0, 4.0, 5.0])
81
+ )
82
+
83
+ # Boolean indexing
84
+ result = ms[ms > 2]
85
+ assert isinstance(
86
+ result, MicroSeries
87
+ ), f"Got {type(result)} instead of MicroSeries"
88
+ assert hasattr(result, "weights")
89
+
90
+ # Slice indexing
91
+ result = ms[1:3]
92
+ assert isinstance(
93
+ result, MicroSeries
94
+ ), f"Got {type(result)} instead of MicroSeries"
95
+
96
+
97
+ class TestMicroDataFrameSubclassPreservation:
98
+ """Test that MicroDataFrame column access returns MicroSeries."""
99
+
100
+ def test_microdataframe_column_returns_microseries(self):
101
+ """Accessing a column from MicroDataFrame should return MicroSeries."""
102
+ mdf = MicroDataFrame(
103
+ {"a": [1, 2, 3], "b": [4, 5, 6]}, weights=np.array([1.0, 2.0, 3.0])
104
+ )
105
+
106
+ # Column access
107
+ col = mdf["a"]
108
+ assert isinstance(
109
+ col, MicroSeries
110
+ ), f"Got {type(col)} instead of MicroSeries"
111
+ assert hasattr(col, "weights")
112
+ assert hasattr(col, "set_weights")
113
+
114
+ def test_microdataframe_operations_preserve_type(self):
115
+ """Operations on MicroDataFrame columns should preserve MicroSeries
116
+ type."""
117
+ mdf = MicroDataFrame(
118
+ {"a": [1, 2, 3], "b": [4, 5, 6]}, weights=np.array([1.0, 2.0, 3.0])
119
+ )
120
+
121
+ # Column operations
122
+ result = mdf["a"] + mdf["b"]
123
+ assert isinstance(
124
+ result, MicroSeries
125
+ ), f"Got {type(result)} instead of MicroSeries"
126
+ assert hasattr(result, "weights")
127
+
128
+
129
+ class TestStringDtypeHandling:
130
+ """Test that MicroSeries/MicroDataFrame handle pandas 3 string dtypes."""
131
+
132
+ def test_microseries_with_string_data(self):
133
+ """MicroSeries should work with string data in pandas 3."""
134
+ # Create with string data
135
+ ms = MicroSeries(["a", "b", "c"], weights=np.array([1.0, 2.0, 3.0]))
136
+ assert len(ms) == 3
137
+ assert hasattr(ms, "weights")
138
+
139
+ def test_microdataframe_with_string_columns(self):
140
+ """MicroDataFrame should work with string columns in pandas 3."""
141
+ mdf = MicroDataFrame(
142
+ {"names": ["alice", "bob", "charlie"], "values": [1, 2, 3]},
143
+ weights=np.array([1.0, 2.0, 3.0]),
144
+ )
145
+ assert len(mdf) == 3
146
+
147
+ # String column access should still work
148
+ names = mdf["names"]
149
+ assert len(names) == 3
150
+
151
+
152
+ class TestWeightedOperationsWithPandas3:
153
+ """Test that weighted operations work correctly with pandas 3."""
154
+
155
+ def test_weighted_sum(self):
156
+ """Weighted sum should work correctly."""
157
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
158
+ # Weighted sum: 1*1 + 2*2 + 3*3 = 1 + 4 + 9 = 14
159
+ assert ms.sum() == 14
160
+
161
+ def test_weighted_mean(self):
162
+ """Weighted mean should work correctly."""
163
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
164
+ # Weighted mean: (1*1 + 2*2 + 3*3) / (1 + 2 + 3) = 14 / 6 ≈ 2.333
165
+ assert np.isclose(ms.mean(), 14 / 6)
166
+
167
+ def test_weighted_count(self):
168
+ """Weighted count should return sum of weights."""
169
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
170
+ assert ms.count() == 6.0
171
+
172
+
173
+ class TestCopyOnWriteCompatibility:
174
+ """Test compatibility with pandas 3 Copy-on-Write."""
175
+
176
+ def test_microseries_copy_independent(self):
177
+ """Copying a MicroSeries should create an independent copy."""
178
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
179
+ ms_copy = ms.copy()
180
+
181
+ # Modify original
182
+ ms.set_weights(np.array([4.0, 5.0, 6.0]))
183
+
184
+ # Copy should be unchanged
185
+ assert np.allclose(ms_copy.weights, [1.0, 2.0, 3.0])
186
+
187
+ def test_microdataframe_copy_independent(self):
188
+ """Copying a MicroDataFrame should create an independent copy."""
189
+ mdf = MicroDataFrame(
190
+ {"a": [1, 2, 3]}, weights=np.array([1.0, 2.0, 3.0])
191
+ )
192
+ mdf_copy = mdf.copy()
193
+
194
+ # Modify original
195
+ mdf.set_weights(np.array([4.0, 5.0, 6.0]))
196
+
197
+ # Copy should be unchanged
198
+ assert np.allclose(mdf_copy.weights, [1.0, 2.0, 3.0])
199
+
200
+ def test_column_set_weights_after_access_regression(self):
201
+ """Regression test for pandas 3.0 CoW compatibility.
202
+
203
+ In pandas 3.0 with Copy-on-Write, modifying column.__class__ doesn't
204
+ persist because each access returns a copy. This test verifies the fix
205
+ that wraps columns as MicroSeries on access in __getitem__.
206
+ """
207
+ mdf = MicroDataFrame(
208
+ {"income": [10000, 20000, 30000]},
209
+ weights=np.array([1.0, 2.0, 3.0]),
210
+ )
211
+
212
+ # This was the exact error that occurred:
213
+ # AttributeError: 'Series' object has no attribute 'set_weights'
214
+ col = mdf["income"]
215
+ col.set_weights(np.array([4.0, 5.0, 6.0])) # Would fail before fix
216
+
217
+ # Verify the new weights took effect
218
+ assert np.allclose(col.weights, [4.0, 5.0, 6.0])
219
+
220
+
221
+ class TestGroupByWithPandas3:
222
+ """Test groupby operations with pandas 3."""
223
+
224
+ def test_microseries_groupby_preserves_weights(self):
225
+ """GroupBy operations should preserve weights."""
226
+ ms = MicroSeries([1, 2, 3, 4], weights=np.array([1.0, 2.0, 3.0, 4.0]))
227
+ groups = pd.Series(["a", "a", "b", "b"])
228
+
229
+ gb = ms.groupby(groups)
230
+ # Should be able to call weighted operations
231
+ result = gb.sum()
232
+ # Group a: 1*1 + 2*2 = 5
233
+ # Group b: 3*3 + 4*4 = 25
234
+ assert result["a"] == 5
235
+ assert result["b"] == 25
236
+
237
+ def test_microdataframe_groupby_preserves_weights(self):
238
+ """MicroDataFrame groupby should preserve weights on columns."""
239
+ mdf = MicroDataFrame(
240
+ {"group": ["a", "a", "b", "b"], "value": [1, 2, 3, 4]},
241
+ weights=np.array([1.0, 2.0, 3.0, 4.0]),
242
+ )
243
+
244
+ gb = mdf.groupby("group")
245
+ result = gb.sum()
246
+
247
+ # Check that weighted sum was computed
248
+ assert "value" in result.columns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.1.2
3
+ Version: 1.2.1
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -0,0 +1,11 @@
1
+ microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
2
+ microdf/microdataframe.py,sha256=2lZU3FAtVCgKiVD-iD_3xu3NMEEl-7Tbn-T-aOR_ihc,33920
3
+ microdf/microseries.py,sha256=YRnSHVm3mGGd1_-D29BAUDaqWoIiwGjJPKYUrnWbinY,23376
4
+ microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
5
+ microdf/tests/test_microseries_dataframe.py,sha256=lG2_3lJqTdGekbbJRFl0Vp0b4DEQwTk6scXr-ncOHgs,14647
6
+ microdf/tests/test_pandas3_compatibility.py,sha256=p4SZoW59REA5GV84HH9InfCdKQ1nEBGmatnRK84GiGY,8623
7
+ microdf_python-1.2.1.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
8
+ microdf_python-1.2.1.dist-info/METADATA,sha256=_2sK8uH_um7Pir-OpT5eT3Utfv9jFnYNYYV-p4aYNbE,2420
9
+ microdf_python-1.2.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
10
+ microdf_python-1.2.1.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
11
+ microdf_python-1.2.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,10 +0,0 @@
1
- microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
2
- microdf/microdataframe.py,sha256=h0vIuCACMo1BuV3VKDIEfhc_UwMUV5kUsnlQe3bHfgM,33539
3
- microdf/microseries.py,sha256=YRnSHVm3mGGd1_-D29BAUDaqWoIiwGjJPKYUrnWbinY,23376
4
- microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
5
- microdf/tests/test_microseries_dataframe.py,sha256=lG2_3lJqTdGekbbJRFl0Vp0b4DEQwTk6scXr-ncOHgs,14647
6
- microdf_python-1.1.2.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
7
- microdf_python-1.1.2.dist-info/METADATA,sha256=MNlbpTktwqMoFlf-CImA-Xsi-ix7Zv1XAYZ09W1YRcY,2420
8
- microdf_python-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- microdf_python-1.1.2.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
10
- microdf_python-1.1.2.dist-info/RECORD,,