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 +18 -13
- microdf/tests/test_pandas3_compatibility.py +248 -0
- {microdf_python-1.1.2.dist-info → microdf_python-1.2.1.dist-info}/METADATA +1 -1
- microdf_python-1.2.1.dist-info/RECORD +11 -0
- {microdf_python-1.1.2.dist-info → microdf_python-1.2.1.dist-info}/WHEEL +1 -1
- microdf_python-1.1.2.dist-info/RECORD +0 -10
- {microdf_python-1.1.2.dist-info → microdf_python-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {microdf_python-1.1.2.dist-info → microdf_python-1.2.1.dist-info}/top_level.txt +0 -0
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
|
-
#
|
|
282
|
-
#
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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[
|
|
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
|
-
#
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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,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,,
|
|
File without changes
|
|
File without changes
|