microdf-python 1.2.0__tar.gz → 1.2.2__tar.gz
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_python-1.2.0/microdf_python.egg-info → microdf_python-1.2.2}/PKG-INFO +2 -1
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/microdataframe.py +18 -13
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/microseries.py +51 -6
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/tests/test_microseries_dataframe.py +44 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/tests/test_pandas3_compatibility.py +73 -57
- {microdf_python-1.2.0 → microdf_python-1.2.2/microdf_python.egg-info}/PKG-INFO +2 -1
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf_python.egg-info/requires.txt +1 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/pyproject.toml +36 -2
- {microdf_python-1.2.0 → microdf_python-1.2.2}/LICENSE +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/README.md +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/__init__.py +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf/tests/conftest.py +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf_python.egg-info/SOURCES.txt +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf_python.egg-info/dependency_links.txt +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/microdf_python.egg-info/top_level.txt +0 -0
- {microdf_python-1.2.0 → microdf_python-1.2.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdf-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
5
|
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Dist: linecheck; extra == "dev"
|
|
|
20
20
|
Requires-Dist: pytest; extra == "dev"
|
|
21
21
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
22
|
Requires-Dist: setuptools; extra == "dev"
|
|
23
|
+
Requires-Dist: towncrier>=24.8.0; extra == "dev"
|
|
23
24
|
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
[](https://github.com/PolicyEngine/microdf/actions)
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import warnings
|
|
2
3
|
from functools import wraps
|
|
3
4
|
from typing import Callable, List, Optional, Union
|
|
4
5
|
|
|
@@ -19,6 +20,50 @@ class MicroSeries(pd.Series):
|
|
|
19
20
|
super().__init__(*args, **kwargs)
|
|
20
21
|
self.set_weights(weights)
|
|
21
22
|
|
|
23
|
+
@property
|
|
24
|
+
def _values(self):
|
|
25
|
+
"""Internal access to underlying numpy array without warning."""
|
|
26
|
+
return super().values
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def values(self):
|
|
30
|
+
"""Access underlying numpy array.
|
|
31
|
+
|
|
32
|
+
.. warning::
|
|
33
|
+
Returns a plain numpy array without weights. Operations
|
|
34
|
+
like ``.mean()`` on the result will be unweighted. Use
|
|
35
|
+
MicroSeries methods directly for weighted calculations
|
|
36
|
+
(e.g., ``ms.mean()`` instead of ``ms.values.mean()``).
|
|
37
|
+
"""
|
|
38
|
+
warnings.warn(
|
|
39
|
+
"Accessing .values on a MicroSeries returns a plain numpy "
|
|
40
|
+
"array without weights. Operations like .mean() on the "
|
|
41
|
+
"result will be unweighted. Use MicroSeries methods "
|
|
42
|
+
"directly for weighted calculations (e.g., ms.mean() "
|
|
43
|
+
"instead of ms.values.mean()).",
|
|
44
|
+
UserWarning,
|
|
45
|
+
stacklevel=2,
|
|
46
|
+
)
|
|
47
|
+
return super().values
|
|
48
|
+
|
|
49
|
+
def to_numpy(self, *args, **kwargs):
|
|
50
|
+
"""Convert to numpy array.
|
|
51
|
+
|
|
52
|
+
.. warning::
|
|
53
|
+
Returns a plain numpy array without weights. Operations
|
|
54
|
+
like ``.mean()`` on the result will be unweighted. Use
|
|
55
|
+
MicroSeries methods directly for weighted calculations.
|
|
56
|
+
"""
|
|
57
|
+
warnings.warn(
|
|
58
|
+
"Calling .to_numpy() on a MicroSeries returns a plain "
|
|
59
|
+
"numpy array without weights. Operations like .mean() on "
|
|
60
|
+
"the result will be unweighted. Use MicroSeries methods "
|
|
61
|
+
"directly for weighted calculations.",
|
|
62
|
+
UserWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
return super().to_numpy(*args, **kwargs)
|
|
66
|
+
|
|
22
67
|
def weighted_function(fn: Callable) -> Callable:
|
|
23
68
|
@wraps(fn)
|
|
24
69
|
def safe_fn(*args, **kwargs):
|
|
@@ -52,7 +97,7 @@ class MicroSeries(pd.Series):
|
|
|
52
97
|
if weights is None:
|
|
53
98
|
if len(self) > 0:
|
|
54
99
|
self.weights = pd.Series(
|
|
55
|
-
np.ones_like(self.
|
|
100
|
+
np.ones_like(self._values), dtype=float
|
|
56
101
|
)
|
|
57
102
|
else:
|
|
58
103
|
if len(weights) != len(self):
|
|
@@ -110,7 +155,7 @@ class MicroSeries(pd.Series):
|
|
|
110
155
|
:returns: The weighted mean.
|
|
111
156
|
:rtype: float
|
|
112
157
|
"""
|
|
113
|
-
values = self.
|
|
158
|
+
values = self._values
|
|
114
159
|
weights = self.weights
|
|
115
160
|
|
|
116
161
|
if skipna:
|
|
@@ -141,7 +186,7 @@ class MicroSeries(pd.Series):
|
|
|
141
186
|
:return: Weighted quantile value(s).
|
|
142
187
|
:rtype: float or pd.Series
|
|
143
188
|
"""
|
|
144
|
-
values = np.array(self.
|
|
189
|
+
values = np.array(self._values)
|
|
145
190
|
quantiles = np.atleast_1d(q)
|
|
146
191
|
sample_weight = np.array(self.weights)
|
|
147
192
|
assert np.all(quantiles >= 0) and np.all(
|
|
@@ -313,7 +358,7 @@ class MicroSeries(pd.Series):
|
|
|
313
358
|
"in division by zero."
|
|
314
359
|
)
|
|
315
360
|
|
|
316
|
-
order = np.argsort(self.
|
|
361
|
+
order = np.argsort(self._values)
|
|
317
362
|
inverse_order = np.argsort(order)
|
|
318
363
|
ranks = np.array(self.weights.values)[order].cumsum()[inverse_order]
|
|
319
364
|
if pct:
|
|
@@ -506,7 +551,7 @@ class MicroSeries(pd.Series):
|
|
|
506
551
|
return MicroSeries(super().__rxor__(other), weights=self.weights)
|
|
507
552
|
|
|
508
553
|
def sqrt(self) -> "MicroSeries":
|
|
509
|
-
sqrt_values = np.sqrt(self.
|
|
554
|
+
sqrt_values = np.sqrt(self._values)
|
|
510
555
|
return MicroSeries(sqrt_values, index=self.index, weights=self.weights)
|
|
511
556
|
|
|
512
557
|
# comparators
|
|
@@ -590,7 +635,7 @@ class MicroSeries(pd.Series):
|
|
|
590
635
|
|
|
591
636
|
def __repr__(self) -> str:
|
|
592
637
|
return pd.DataFrame(
|
|
593
|
-
dict(value=self.
|
|
638
|
+
dict(value=self._values, weight=self.weights.values)
|
|
594
639
|
).__repr__()
|
|
595
640
|
|
|
596
641
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
|
|
@@ -423,3 +425,45 @@ def test_groupby_column_selection() -> None:
|
|
|
423
425
|
result_all = d.groupby("g").sum()
|
|
424
426
|
assert "weight" not in result_all.columns
|
|
425
427
|
assert list(result_all.columns) == ["y"]
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def test_values_warns() -> None:
|
|
431
|
+
"""Accessing .values on a MicroSeries should emit a UserWarning."""
|
|
432
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
433
|
+
with warnings.catch_warnings(record=True) as w:
|
|
434
|
+
warnings.simplefilter("always")
|
|
435
|
+
_ = ms.values
|
|
436
|
+
assert len(w) == 1
|
|
437
|
+
assert issubclass(w[0].category, UserWarning)
|
|
438
|
+
assert "weights" in str(w[0].message).lower()
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_to_numpy_warns() -> None:
|
|
442
|
+
"""Calling .to_numpy() on a MicroSeries should emit a UserWarning."""
|
|
443
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
444
|
+
with warnings.catch_warnings(record=True) as w:
|
|
445
|
+
warnings.simplefilter("always")
|
|
446
|
+
_ = ms.to_numpy()
|
|
447
|
+
assert len(w) == 1
|
|
448
|
+
assert issubclass(w[0].category, UserWarning)
|
|
449
|
+
assert "weights" in str(w[0].message).lower()
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def test_mean_no_warning() -> None:
|
|
453
|
+
"""Internal .values usage in .mean() should NOT emit a warning."""
|
|
454
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
455
|
+
with warnings.catch_warnings(record=True) as w:
|
|
456
|
+
warnings.simplefilter("always")
|
|
457
|
+
_ = ms.mean()
|
|
458
|
+
user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
|
|
459
|
+
assert len(user_warnings) == 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_repr_no_warning() -> None:
|
|
463
|
+
"""Internal .values usage in __repr__ should NOT emit a warning."""
|
|
464
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
465
|
+
with warnings.catch_warnings(record=True) as w:
|
|
466
|
+
warnings.simplefilter("always")
|
|
467
|
+
_ = repr(ms)
|
|
468
|
+
user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
|
|
469
|
+
assert len(user_warnings) == 0
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for pandas 3.0.0 compatibility in microdf.
|
|
1
|
+
"""Tests for pandas 3.0.0 compatibility in microdf.
|
|
3
2
|
|
|
4
3
|
These tests verify that microdf works correctly with pandas 3.0.0,
|
|
5
4
|
which introduces:
|
|
@@ -10,18 +9,17 @@ which introduces:
|
|
|
10
9
|
|
|
11
10
|
import numpy as np
|
|
12
11
|
import pandas as pd
|
|
13
|
-
import pytest
|
|
14
12
|
|
|
15
|
-
from microdf.microseries import MicroSeries
|
|
16
13
|
from microdf.microdataframe import MicroDataFrame
|
|
14
|
+
from microdf.microseries import MicroSeries
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
class TestMicroSeriesSubclassPreservation:
|
|
20
18
|
"""Test that MicroSeries subclass is preserved across operations."""
|
|
21
19
|
|
|
22
20
|
def test_microseries_set_weights_after_creation(self):
|
|
23
|
-
"""
|
|
24
|
-
|
|
21
|
+
"""Ensure set_weights works on MicroSeries.
|
|
22
|
+
|
|
25
23
|
This is the error reported in pandas 3:
|
|
26
24
|
AttributeError: 'Series' object has no attribute 'set_weights'
|
|
27
25
|
"""
|
|
@@ -34,86 +32,97 @@ class TestMicroSeriesSubclassPreservation:
|
|
|
34
32
|
assert np.allclose(ms.weights, [2.0, 2.0, 2.0])
|
|
35
33
|
|
|
36
34
|
def test_microseries_preserved_after_arithmetic(self):
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
"""
|
|
35
|
+
"""Arithmetic operations should return MicroSeries, not plain
|
|
36
|
+
Series."""
|
|
40
37
|
ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
|
|
41
38
|
|
|
42
39
|
# Addition
|
|
43
40
|
result = ms + 1
|
|
44
|
-
assert isinstance(
|
|
41
|
+
assert isinstance(
|
|
42
|
+
result, MicroSeries
|
|
43
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
45
44
|
assert hasattr(result, "weights")
|
|
46
45
|
assert hasattr(result, "set_weights")
|
|
47
46
|
|
|
48
47
|
# Multiplication
|
|
49
48
|
result = ms * 2
|
|
50
|
-
assert isinstance(
|
|
49
|
+
assert isinstance(
|
|
50
|
+
result, MicroSeries
|
|
51
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
51
52
|
|
|
52
53
|
# Division
|
|
53
54
|
result = ms / 2
|
|
54
|
-
assert isinstance(
|
|
55
|
+
assert isinstance(
|
|
56
|
+
result, MicroSeries
|
|
57
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
55
58
|
|
|
56
59
|
def test_microseries_preserved_after_comparison(self):
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
"""
|
|
60
|
+
"""Comparison operations should return MicroSeries, not plain
|
|
61
|
+
Series."""
|
|
60
62
|
ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
|
|
61
63
|
|
|
62
64
|
# Greater than
|
|
63
65
|
result = ms > 1
|
|
64
|
-
assert isinstance(
|
|
66
|
+
assert isinstance(
|
|
67
|
+
result, MicroSeries
|
|
68
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
65
69
|
assert hasattr(result, "weights")
|
|
66
70
|
|
|
67
71
|
# Less than
|
|
68
72
|
result = ms < 3
|
|
69
|
-
assert isinstance(
|
|
73
|
+
assert isinstance(
|
|
74
|
+
result, MicroSeries
|
|
75
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
70
76
|
|
|
71
77
|
def test_microseries_preserved_after_indexing(self):
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
)
|
|
76
82
|
|
|
77
83
|
# Boolean indexing
|
|
78
84
|
result = ms[ms > 2]
|
|
79
|
-
assert isinstance(
|
|
85
|
+
assert isinstance(
|
|
86
|
+
result, MicroSeries
|
|
87
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
80
88
|
assert hasattr(result, "weights")
|
|
81
89
|
|
|
82
90
|
# Slice indexing
|
|
83
91
|
result = ms[1:3]
|
|
84
|
-
assert isinstance(
|
|
92
|
+
assert isinstance(
|
|
93
|
+
result, MicroSeries
|
|
94
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
85
95
|
|
|
86
96
|
|
|
87
97
|
class TestMicroDataFrameSubclassPreservation:
|
|
88
98
|
"""Test that MicroDataFrame column access returns MicroSeries."""
|
|
89
99
|
|
|
90
100
|
def test_microdataframe_column_returns_microseries(self):
|
|
91
|
-
"""
|
|
92
|
-
Accessing a column from MicroDataFrame should return MicroSeries.
|
|
93
|
-
"""
|
|
101
|
+
"""Accessing a column from MicroDataFrame should return MicroSeries."""
|
|
94
102
|
mdf = MicroDataFrame(
|
|
95
|
-
{"a": [1, 2, 3], "b": [4, 5, 6]},
|
|
96
|
-
weights=np.array([1.0, 2.0, 3.0])
|
|
103
|
+
{"a": [1, 2, 3], "b": [4, 5, 6]}, weights=np.array([1.0, 2.0, 3.0])
|
|
97
104
|
)
|
|
98
105
|
|
|
99
106
|
# Column access
|
|
100
107
|
col = mdf["a"]
|
|
101
|
-
assert isinstance(
|
|
108
|
+
assert isinstance(
|
|
109
|
+
col, MicroSeries
|
|
110
|
+
), f"Got {type(col)} instead of MicroSeries"
|
|
102
111
|
assert hasattr(col, "weights")
|
|
103
112
|
assert hasattr(col, "set_weights")
|
|
104
113
|
|
|
105
114
|
def test_microdataframe_operations_preserve_type(self):
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
"""
|
|
115
|
+
"""Operations on MicroDataFrame columns should preserve MicroSeries
|
|
116
|
+
type."""
|
|
109
117
|
mdf = MicroDataFrame(
|
|
110
|
-
{"a": [1, 2, 3], "b": [4, 5, 6]},
|
|
111
|
-
weights=np.array([1.0, 2.0, 3.0])
|
|
118
|
+
{"a": [1, 2, 3], "b": [4, 5, 6]}, weights=np.array([1.0, 2.0, 3.0])
|
|
112
119
|
)
|
|
113
120
|
|
|
114
121
|
# Column operations
|
|
115
122
|
result = mdf["a"] + mdf["b"]
|
|
116
|
-
assert isinstance(
|
|
123
|
+
assert isinstance(
|
|
124
|
+
result, MicroSeries
|
|
125
|
+
), f"Got {type(result)} instead of MicroSeries"
|
|
117
126
|
assert hasattr(result, "weights")
|
|
118
127
|
|
|
119
128
|
|
|
@@ -121,21 +130,17 @@ class TestStringDtypeHandling:
|
|
|
121
130
|
"""Test that MicroSeries/MicroDataFrame handle pandas 3 string dtypes."""
|
|
122
131
|
|
|
123
132
|
def test_microseries_with_string_data(self):
|
|
124
|
-
"""
|
|
125
|
-
MicroSeries should work with string data in pandas 3.
|
|
126
|
-
"""
|
|
133
|
+
"""MicroSeries should work with string data in pandas 3."""
|
|
127
134
|
# Create with string data
|
|
128
135
|
ms = MicroSeries(["a", "b", "c"], weights=np.array([1.0, 2.0, 3.0]))
|
|
129
136
|
assert len(ms) == 3
|
|
130
137
|
assert hasattr(ms, "weights")
|
|
131
138
|
|
|
132
139
|
def test_microdataframe_with_string_columns(self):
|
|
133
|
-
"""
|
|
134
|
-
MicroDataFrame should work with string columns in pandas 3.
|
|
135
|
-
"""
|
|
140
|
+
"""MicroDataFrame should work with string columns in pandas 3."""
|
|
136
141
|
mdf = MicroDataFrame(
|
|
137
142
|
{"names": ["alice", "bob", "charlie"], "values": [1, 2, 3]},
|
|
138
|
-
weights=np.array([1.0, 2.0, 3.0])
|
|
143
|
+
weights=np.array([1.0, 2.0, 3.0]),
|
|
139
144
|
)
|
|
140
145
|
assert len(mdf) == 3
|
|
141
146
|
|
|
@@ -169,9 +174,7 @@ class TestCopyOnWriteCompatibility:
|
|
|
169
174
|
"""Test compatibility with pandas 3 Copy-on-Write."""
|
|
170
175
|
|
|
171
176
|
def test_microseries_copy_independent(self):
|
|
172
|
-
"""
|
|
173
|
-
Copying a MicroSeries should create an independent copy.
|
|
174
|
-
"""
|
|
177
|
+
"""Copying a MicroSeries should create an independent copy."""
|
|
175
178
|
ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
|
|
176
179
|
ms_copy = ms.copy()
|
|
177
180
|
|
|
@@ -182,12 +185,9 @@ class TestCopyOnWriteCompatibility:
|
|
|
182
185
|
assert np.allclose(ms_copy.weights, [1.0, 2.0, 3.0])
|
|
183
186
|
|
|
184
187
|
def test_microdataframe_copy_independent(self):
|
|
185
|
-
"""
|
|
186
|
-
Copying a MicroDataFrame should create an independent copy.
|
|
187
|
-
"""
|
|
188
|
+
"""Copying a MicroDataFrame should create an independent copy."""
|
|
188
189
|
mdf = MicroDataFrame(
|
|
189
|
-
{"a": [1, 2, 3]},
|
|
190
|
-
weights=np.array([1.0, 2.0, 3.0])
|
|
190
|
+
{"a": [1, 2, 3]}, weights=np.array([1.0, 2.0, 3.0])
|
|
191
191
|
)
|
|
192
192
|
mdf_copy = mdf.copy()
|
|
193
193
|
|
|
@@ -197,14 +197,32 @@ class TestCopyOnWriteCompatibility:
|
|
|
197
197
|
# Copy should be unchanged
|
|
198
198
|
assert np.allclose(mdf_copy.weights, [1.0, 2.0, 3.0])
|
|
199
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
|
+
|
|
200
220
|
|
|
201
221
|
class TestGroupByWithPandas3:
|
|
202
222
|
"""Test groupby operations with pandas 3."""
|
|
203
223
|
|
|
204
224
|
def test_microseries_groupby_preserves_weights(self):
|
|
205
|
-
"""
|
|
206
|
-
GroupBy operations should preserve weights.
|
|
207
|
-
"""
|
|
225
|
+
"""GroupBy operations should preserve weights."""
|
|
208
226
|
ms = MicroSeries([1, 2, 3, 4], weights=np.array([1.0, 2.0, 3.0, 4.0]))
|
|
209
227
|
groups = pd.Series(["a", "a", "b", "b"])
|
|
210
228
|
|
|
@@ -217,12 +235,10 @@ class TestGroupByWithPandas3:
|
|
|
217
235
|
assert result["b"] == 25
|
|
218
236
|
|
|
219
237
|
def test_microdataframe_groupby_preserves_weights(self):
|
|
220
|
-
"""
|
|
221
|
-
MicroDataFrame groupby should preserve weights on columns.
|
|
222
|
-
"""
|
|
238
|
+
"""MicroDataFrame groupby should preserve weights on columns."""
|
|
223
239
|
mdf = MicroDataFrame(
|
|
224
240
|
{"group": ["a", "a", "b", "b"], "value": [1, 2, 3, 4]},
|
|
225
|
-
weights=np.array([1.0, 2.0, 3.0, 4.0])
|
|
241
|
+
weights=np.array([1.0, 2.0, 3.0, 4.0]),
|
|
226
242
|
)
|
|
227
243
|
|
|
228
244
|
gb = mdf.groupby("group")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdf-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
5
|
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,7 @@ Requires-Dist: linecheck; extra == "dev"
|
|
|
20
20
|
Requires-Dist: pytest; extra == "dev"
|
|
21
21
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
22
|
Requires-Dist: setuptools; extra == "dev"
|
|
23
|
+
Requires-Dist: towncrier>=24.8.0; extra == "dev"
|
|
23
24
|
Dynamic: license-file
|
|
24
25
|
|
|
25
26
|
[](https://github.com/PolicyEngine/microdf/actions)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "microdf-python"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.2"
|
|
8
8
|
description = "Weighted pandas DataFrames and Series for survey microdata"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -28,7 +28,8 @@ dev = [
|
|
|
28
28
|
"linecheck",
|
|
29
29
|
"pytest",
|
|
30
30
|
"pytest-cov",
|
|
31
|
-
"setuptools",
|
|
31
|
+
"setuptools", "towncrier>=24.8.0",
|
|
32
|
+
|
|
32
33
|
]
|
|
33
34
|
# Note: Documentation uses MyST (Jupyter Book 2.0) which is installed via npm
|
|
34
35
|
# Run: cd docs && myst build --html
|
|
@@ -48,3 +49,36 @@ target-version = ["py313"]
|
|
|
48
49
|
[tool.flake8]
|
|
49
50
|
max-line-length = 79
|
|
50
51
|
extend-ignore = ["E203", "W503"]
|
|
52
|
+
|
|
53
|
+
[tool.towncrier]
|
|
54
|
+
package = "microdf_python"
|
|
55
|
+
directory = "changelog.d"
|
|
56
|
+
filename = "CHANGELOG.md"
|
|
57
|
+
title_format = "## [{version}] - {project_date}"
|
|
58
|
+
issue_format = ""
|
|
59
|
+
underlines = ["", "", ""]
|
|
60
|
+
|
|
61
|
+
[[tool.towncrier.type]]
|
|
62
|
+
directory = "breaking"
|
|
63
|
+
name = "Breaking changes"
|
|
64
|
+
showcontent = true
|
|
65
|
+
|
|
66
|
+
[[tool.towncrier.type]]
|
|
67
|
+
directory = "added"
|
|
68
|
+
name = "Added"
|
|
69
|
+
showcontent = true
|
|
70
|
+
|
|
71
|
+
[[tool.towncrier.type]]
|
|
72
|
+
directory = "changed"
|
|
73
|
+
name = "Changed"
|
|
74
|
+
showcontent = true
|
|
75
|
+
|
|
76
|
+
[[tool.towncrier.type]]
|
|
77
|
+
directory = "fixed"
|
|
78
|
+
name = "Fixed"
|
|
79
|
+
showcontent = true
|
|
80
|
+
|
|
81
|
+
[[tool.towncrier.type]]
|
|
82
|
+
directory = "removed"
|
|
83
|
+
name = "Removed"
|
|
84
|
+
showcontent = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|