microdf-python 1.2.1__tar.gz → 1.2.3__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.1/microdf_python.egg-info → microdf_python-1.2.3}/PKG-INFO +3 -6
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/microdataframe.py +14 -34
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/microseries.py +64 -43
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/tests/test_microseries_dataframe.py +51 -21
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/tests/test_pandas3_compatibility.py +27 -33
- {microdf_python-1.2.1 → microdf_python-1.2.3/microdf_python.egg-info}/PKG-INFO +3 -6
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf_python.egg-info/requires.txt +2 -5
- {microdf_python-1.2.1 → microdf_python-1.2.3}/pyproject.toml +33 -15
- {microdf_python-1.2.1 → microdf_python-1.2.3}/LICENSE +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/README.md +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/__init__.py +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf/tests/conftest.py +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf_python.egg-info/SOURCES.txt +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf_python.egg-info/dependency_links.txt +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/microdf_python.egg-info/top_level.txt +0 -0
- {microdf_python-1.2.1 → microdf_python-1.2.3}/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.3
|
|
4
4
|
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
5
|
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
6
|
License: MIT
|
|
@@ -11,15 +11,12 @@ Requires-Dist: numpy
|
|
|
11
11
|
Requires-Dist: pandas
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: codecov; extra == "dev"
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
16
|
-
Requires-Dist: black; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.9.0; extra == "dev"
|
|
17
15
|
Requires-Dist: docformatter; extra == "dev"
|
|
18
|
-
Requires-Dist: isort; extra == "dev"
|
|
19
|
-
Requires-Dist: linecheck; extra == "dev"
|
|
20
16
|
Requires-Dist: pytest; extra == "dev"
|
|
21
17
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
18
|
Requires-Dist: setuptools; extra == "dev"
|
|
19
|
+
Requires-Dist: towncrier>=24.8.0; extra == "dev"
|
|
23
20
|
Dynamic: license-file
|
|
24
21
|
|
|
25
22
|
[](https://github.com/PolicyEngine/microdf/actions)
|
|
@@ -147,9 +147,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
147
147
|
for col in self.columns:
|
|
148
148
|
if pd.api.types.is_numeric_dtype(self[col]):
|
|
149
149
|
try:
|
|
150
|
-
results[col] = getattr(self[col], name)(
|
|
151
|
-
*args, **kwargs
|
|
152
|
-
)
|
|
150
|
+
results[col] = getattr(self[col], name)(*args, **kwargs)
|
|
153
151
|
except Exception:
|
|
154
152
|
# Skip columns that can't be aggregated
|
|
155
153
|
pass
|
|
@@ -224,9 +222,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
224
222
|
for col in self.columns:
|
|
225
223
|
if pd.api.types.is_numeric_dtype(self[col]):
|
|
226
224
|
try:
|
|
227
|
-
results[col] = getattr(self[col], name)(
|
|
228
|
-
*args, **kwargs
|
|
229
|
-
)
|
|
225
|
+
results[col] = getattr(self[col], name)(*args, **kwargs)
|
|
230
226
|
except Exception:
|
|
231
227
|
# Skip columns that can't be aggregated
|
|
232
228
|
pass
|
|
@@ -323,9 +319,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
323
319
|
self.weights = pd.Series(weights, dtype=float)
|
|
324
320
|
self._link_all_weights()
|
|
325
321
|
|
|
326
|
-
def set_weight_col(
|
|
327
|
-
self, column: str, preserve_old: Optional[bool] = False
|
|
328
|
-
) -> None:
|
|
322
|
+
def set_weight_col(self, column: str, preserve_old: Optional[bool] = False) -> None:
|
|
329
323
|
"""Sets the weights for the MicroDataFrame by specifying the name of
|
|
330
324
|
the weight column.
|
|
331
325
|
|
|
@@ -594,9 +588,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
594
588
|
return equal_values and equal_weights
|
|
595
589
|
|
|
596
590
|
@get_args_as_micro_series()
|
|
597
|
-
def groupby(
|
|
598
|
-
self, by: Union[str, List], *args, **kwargs
|
|
599
|
-
) -> "MicroDataFrameGroupBy":
|
|
591
|
+
def groupby(self, by: Union[str, List], *args, **kwargs) -> "MicroDataFrameGroupBy":
|
|
600
592
|
"""Returns a GroupBy object with MicroSeriesGroupBy objects for each
|
|
601
593
|
column.
|
|
602
594
|
|
|
@@ -757,14 +749,10 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
757
749
|
self.columns.remove("__tmp_weights")
|
|
758
750
|
# Filter to only numeric columns
|
|
759
751
|
self.numeric_columns = [
|
|
760
|
-
col
|
|
761
|
-
for col in self.columns
|
|
762
|
-
if pd.api.types.is_numeric_dtype(self.obj[col])
|
|
752
|
+
col for col in self.columns if pd.api.types.is_numeric_dtype(self.obj[col])
|
|
763
753
|
]
|
|
764
754
|
# Store reference to weights groupby for column selection
|
|
765
|
-
self._weights_groupby = copy.deepcopy(
|
|
766
|
-
super().__getitem__("__tmp_weights")
|
|
767
|
-
)
|
|
755
|
+
self._weights_groupby = copy.deepcopy(super().__getitem__("__tmp_weights"))
|
|
768
756
|
for fn_name in MicroSeries.SCALAR_FUNCTIONS:
|
|
769
757
|
|
|
770
758
|
def get_fn(name):
|
|
@@ -854,18 +842,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
854
842
|
results = {}
|
|
855
843
|
for col in res.numeric_columns:
|
|
856
844
|
try:
|
|
857
|
-
results[col] = getattr(
|
|
858
|
-
|
|
859
|
-
)
|
|
845
|
+
results[col] = getattr(getattr(res, col), name)(
|
|
846
|
+
*args, **kwargs
|
|
847
|
+
)
|
|
860
848
|
except Exception:
|
|
861
849
|
pass
|
|
862
850
|
# Return plain DataFrame - aggregated results don't
|
|
863
851
|
# have per-row weights (weights were already applied)
|
|
864
|
-
return (
|
|
865
|
-
pd.DataFrame(results)
|
|
866
|
-
if results
|
|
867
|
-
else pd.DataFrame()
|
|
868
|
-
)
|
|
852
|
+
return pd.DataFrame(results) if results else pd.DataFrame()
|
|
869
853
|
|
|
870
854
|
return fn
|
|
871
855
|
|
|
@@ -877,18 +861,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
877
861
|
results = {}
|
|
878
862
|
for col in res.numeric_columns:
|
|
879
863
|
try:
|
|
880
|
-
results[col] = getattr(
|
|
881
|
-
|
|
882
|
-
)
|
|
864
|
+
results[col] = getattr(getattr(res, col), name)(
|
|
865
|
+
*args, **kwargs
|
|
866
|
+
)
|
|
883
867
|
except Exception:
|
|
884
868
|
pass
|
|
885
869
|
# Return plain DataFrame - aggregated results don't
|
|
886
870
|
# have per-row weights (weights were already applied)
|
|
887
|
-
return (
|
|
888
|
-
pd.DataFrame(results)
|
|
889
|
-
if results
|
|
890
|
-
else pd.DataFrame()
|
|
891
|
-
)
|
|
871
|
+
return pd.DataFrame(results) if results else pd.DataFrame()
|
|
892
872
|
|
|
893
873
|
return fn
|
|
894
874
|
|
|
@@ -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):
|
|
@@ -51,9 +96,7 @@ class MicroSeries(pd.Series):
|
|
|
51
96
|
"""
|
|
52
97
|
if weights is None:
|
|
53
98
|
if len(self) > 0:
|
|
54
|
-
self.weights = pd.Series(
|
|
55
|
-
np.ones_like(self.values), dtype=float
|
|
56
|
-
)
|
|
99
|
+
self.weights = pd.Series(np.ones_like(self._values), dtype=float)
|
|
57
100
|
else:
|
|
58
101
|
if len(weights) != len(self):
|
|
59
102
|
raise ValueError(
|
|
@@ -110,7 +153,7 @@ class MicroSeries(pd.Series):
|
|
|
110
153
|
:returns: The weighted mean.
|
|
111
154
|
:rtype: float
|
|
112
155
|
"""
|
|
113
|
-
values = self.
|
|
156
|
+
values = self._values
|
|
114
157
|
weights = self.weights
|
|
115
158
|
|
|
116
159
|
if skipna:
|
|
@@ -141,12 +184,12 @@ class MicroSeries(pd.Series):
|
|
|
141
184
|
:return: Weighted quantile value(s).
|
|
142
185
|
:rtype: float or pd.Series
|
|
143
186
|
"""
|
|
144
|
-
values = np.array(self.
|
|
187
|
+
values = np.array(self._values)
|
|
145
188
|
quantiles = np.atleast_1d(q)
|
|
146
189
|
sample_weight = np.array(self.weights)
|
|
147
|
-
assert np.all(quantiles >= 0) and np.all(
|
|
148
|
-
quantiles
|
|
149
|
-
)
|
|
190
|
+
assert np.all(quantiles >= 0) and np.all(quantiles <= 1), (
|
|
191
|
+
"quantiles should be in [0, 1]"
|
|
192
|
+
)
|
|
150
193
|
sorter = np.argsort(values)
|
|
151
194
|
values = values[sorter]
|
|
152
195
|
sample_weight = sample_weight[sorter]
|
|
@@ -154,11 +197,7 @@ class MicroSeries(pd.Series):
|
|
|
154
197
|
cumsum_normalized = cumsum / cumsum[-1]
|
|
155
198
|
result = np.array(
|
|
156
199
|
[
|
|
157
|
-
values[
|
|
158
|
-
min(
|
|
159
|
-
np.searchsorted(cumsum_normalized, qi), len(values) - 1
|
|
160
|
-
)
|
|
161
|
-
]
|
|
200
|
+
values[min(np.searchsorted(cumsum_normalized, qi), len(values) - 1)]
|
|
162
201
|
for qi in quantiles
|
|
163
202
|
]
|
|
164
203
|
)
|
|
@@ -313,7 +352,7 @@ class MicroSeries(pd.Series):
|
|
|
313
352
|
"in division by zero."
|
|
314
353
|
)
|
|
315
354
|
|
|
316
|
-
order = np.argsort(self.
|
|
355
|
+
order = np.argsort(self._values)
|
|
317
356
|
inverse_order = np.argsort(order)
|
|
318
357
|
ranks = np.array(self.weights.values)[order].cumsum()[inverse_order]
|
|
319
358
|
if pct:
|
|
@@ -409,9 +448,7 @@ class MicroSeries(pd.Series):
|
|
|
409
448
|
return MicroSeries(res, weights=self.weights)
|
|
410
449
|
return self
|
|
411
450
|
|
|
412
|
-
def round(
|
|
413
|
-
self, decimals: Optional[int] = 0, *args, **kwargs
|
|
414
|
-
) -> "MicroSeries":
|
|
451
|
+
def round(self, decimals: Optional[int] = 0, *args, **kwargs) -> "MicroSeries":
|
|
415
452
|
res = super().round(decimals=decimals, *args, **kwargs)
|
|
416
453
|
return MicroSeries(res, weights=self.weights)
|
|
417
454
|
|
|
@@ -443,14 +480,10 @@ class MicroSeries(pd.Series):
|
|
|
443
480
|
def __mul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
444
481
|
return MicroSeries(super().__mul__(other), weights=self.weights)
|
|
445
482
|
|
|
446
|
-
def __floordiv__(
|
|
447
|
-
self, other: Union[int, float, pd.Series]
|
|
448
|
-
) -> "MicroSeries":
|
|
483
|
+
def __floordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
449
484
|
return MicroSeries(super().__floordiv__(other), weights=self.weights)
|
|
450
485
|
|
|
451
|
-
def __truediv__(
|
|
452
|
-
self, other: Union[int, float, pd.Series]
|
|
453
|
-
) -> "MicroSeries":
|
|
486
|
+
def __truediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
454
487
|
return MicroSeries(super().__truediv__(other), weights=self.weights)
|
|
455
488
|
|
|
456
489
|
def __mod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
@@ -480,14 +513,10 @@ class MicroSeries(pd.Series):
|
|
|
480
513
|
def __rmul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
481
514
|
return MicroSeries(super().__rmul__(other), weights=self.weights)
|
|
482
515
|
|
|
483
|
-
def __rfloordiv__(
|
|
484
|
-
self, other: Union[int, float, pd.Series]
|
|
485
|
-
) -> "MicroSeries":
|
|
516
|
+
def __rfloordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
486
517
|
return MicroSeries(super().__rfloordiv__(other), weights=self.weights)
|
|
487
518
|
|
|
488
|
-
def __rtruediv__(
|
|
489
|
-
self, other: Union[int, float, pd.Series]
|
|
490
|
-
) -> "MicroSeries":
|
|
519
|
+
def __rtruediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
491
520
|
return MicroSeries(super().__rtruediv__(other), weights=self.weights)
|
|
492
521
|
|
|
493
522
|
def __rmod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
@@ -506,7 +535,7 @@ class MicroSeries(pd.Series):
|
|
|
506
535
|
return MicroSeries(super().__rxor__(other), weights=self.weights)
|
|
507
536
|
|
|
508
537
|
def sqrt(self) -> "MicroSeries":
|
|
509
|
-
sqrt_values = np.sqrt(self.
|
|
538
|
+
sqrt_values = np.sqrt(self._values)
|
|
510
539
|
return MicroSeries(sqrt_values, index=self.index, weights=self.weights)
|
|
511
540
|
|
|
512
541
|
# comparators
|
|
@@ -540,17 +569,13 @@ class MicroSeries(pd.Series):
|
|
|
540
569
|
def __imul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
541
570
|
return MicroSeries(super().__imul__(other), weights=self.weights)
|
|
542
571
|
|
|
543
|
-
def __ifloordiv__(
|
|
544
|
-
self, other: Union[int, float, pd.Series]
|
|
545
|
-
) -> "MicroSeries":
|
|
572
|
+
def __ifloordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
546
573
|
return MicroSeries(super().__ifloordiv__(other), weights=self.weights)
|
|
547
574
|
|
|
548
575
|
def __idiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
549
576
|
return MicroSeries(super().__idiv__(other), weights=self.weights)
|
|
550
577
|
|
|
551
|
-
def __itruediv__(
|
|
552
|
-
self, other: Union[int, float, pd.Series]
|
|
553
|
-
) -> "MicroSeries":
|
|
578
|
+
def __itruediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
554
579
|
return MicroSeries(super().__itruediv__(other), weights=self.weights)
|
|
555
580
|
|
|
556
581
|
def __imod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
@@ -590,7 +615,7 @@ class MicroSeries(pd.Series):
|
|
|
590
615
|
|
|
591
616
|
def __repr__(self) -> str:
|
|
592
617
|
return pd.DataFrame(
|
|
593
|
-
dict(value=self.
|
|
618
|
+
dict(value=self._values, weight=self.weights.values)
|
|
594
619
|
).__repr__()
|
|
595
620
|
|
|
596
621
|
|
|
@@ -621,16 +646,12 @@ class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
|
|
|
621
646
|
def _init(self):
|
|
622
647
|
def _weighted_agg(name) -> Callable:
|
|
623
648
|
def via_micro_series(row, *args, **kwargs):
|
|
624
|
-
return getattr(MicroSeries(row.a, weights=row.w), name)(
|
|
625
|
-
*args, **kwargs
|
|
626
|
-
)
|
|
649
|
+
return getattr(MicroSeries(row.a, weights=row.w), name)(*args, **kwargs)
|
|
627
650
|
|
|
628
651
|
fn = getattr(MicroSeries, name)
|
|
629
652
|
|
|
630
653
|
@wraps(fn)
|
|
631
|
-
def _weighted_agg_fn(
|
|
632
|
-
*args, **kwargs
|
|
633
|
-
) -> Union[pd.Series, pd.DataFrame]:
|
|
654
|
+
def _weighted_agg_fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
634
655
|
arrays = self.apply(np.array)
|
|
635
656
|
weights = self.weights.apply(np.array)
|
|
636
657
|
df = pd.DataFrame(dict(a=arrays, w=weights))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
|
|
@@ -259,9 +261,7 @@ def test_decile_rank() -> None:
|
|
|
259
261
|
|
|
260
262
|
|
|
261
263
|
def test_copy_equals() -> None:
|
|
262
|
-
d = mdf.MicroDataFrame(
|
|
263
|
-
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
264
|
-
)
|
|
264
|
+
d = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8])
|
|
265
265
|
d_copy = d.copy()
|
|
266
266
|
d_copy_diff_weights = d_copy.copy()
|
|
267
267
|
d_copy_diff_weights.weights *= 2
|
|
@@ -273,9 +273,7 @@ def test_copy_equals() -> None:
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
def test_subset() -> None:
|
|
276
|
-
df = mdf.MicroDataFrame(
|
|
277
|
-
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
278
|
-
)
|
|
276
|
+
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8])
|
|
279
277
|
df_no_z = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4]}, weights=[7, 8])
|
|
280
278
|
assert df[["x", "y"]].equals(df_no_z)
|
|
281
279
|
df_no_z_diff_weights = df_no_z.copy()
|
|
@@ -351,9 +349,7 @@ def test_reset_index_inplace() -> None:
|
|
|
351
349
|
# Test 4: Multi-level index
|
|
352
350
|
arrays = [["bar", "bar", "baz", "baz"], ["one", "two", "one", "two"]]
|
|
353
351
|
multi_index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
|
|
354
|
-
df_multi = pd.DataFrame(
|
|
355
|
-
{"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index
|
|
356
|
-
)
|
|
352
|
+
df_multi = pd.DataFrame({"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index)
|
|
357
353
|
mdf_multi = MicroDataFrame(df_multi, weights=weights)
|
|
358
354
|
result = mdf_multi.reset_index(level="first")
|
|
359
355
|
assert "first" in result.columns
|
|
@@ -371,9 +367,7 @@ def test_reset_index_inplace() -> None:
|
|
|
371
367
|
def test_loc_preserves_weights() -> None:
|
|
372
368
|
"""Test that .loc[] returns MicroDataFrame with proper weights (issue
|
|
373
369
|
#265)."""
|
|
374
|
-
df = mdf.MicroDataFrame(
|
|
375
|
-
{"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50]
|
|
376
|
-
)
|
|
370
|
+
df = mdf.MicroDataFrame({"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50])
|
|
377
371
|
|
|
378
372
|
# Filter all rows (should get same weights)
|
|
379
373
|
filtered = df.loc[df.one == 1]
|
|
@@ -381,9 +375,7 @@ def test_loc_preserves_weights() -> None:
|
|
|
381
375
|
assert filtered.one.sum() == 150.0 # Weighted sum
|
|
382
376
|
|
|
383
377
|
# Partial filter
|
|
384
|
-
df2 = mdf.MicroDataFrame(
|
|
385
|
-
{"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
|
|
386
|
-
)
|
|
378
|
+
df2 = mdf.MicroDataFrame({"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50])
|
|
387
379
|
subset = df2.loc[df2.x > 2]
|
|
388
380
|
assert isinstance(subset, MicroDataFrame)
|
|
389
381
|
assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
|
|
@@ -392,9 +384,7 @@ def test_loc_preserves_weights() -> None:
|
|
|
392
384
|
|
|
393
385
|
def test_iloc_preserves_weights() -> None:
|
|
394
386
|
"""Test that .iloc[] returns MicroDataFrame with proper weights."""
|
|
395
|
-
df = mdf.MicroDataFrame(
|
|
396
|
-
{"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
|
|
397
|
-
)
|
|
387
|
+
df = mdf.MicroDataFrame({"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50])
|
|
398
388
|
|
|
399
389
|
# Select rows by position
|
|
400
390
|
subset = df.iloc[2:5]
|
|
@@ -405,9 +395,7 @@ def test_iloc_preserves_weights() -> None:
|
|
|
405
395
|
|
|
406
396
|
def test_groupby_column_selection() -> None:
|
|
407
397
|
"""Test that groupby column selection preserves weights (issue #193)."""
|
|
408
|
-
d = mdf.MicroDataFrame(
|
|
409
|
-
dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6]
|
|
410
|
-
)
|
|
398
|
+
d = mdf.MicroDataFrame(dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6])
|
|
411
399
|
|
|
412
400
|
# Test single column string selection
|
|
413
401
|
result_str = d.groupby("g")["y"].sum()
|
|
@@ -423,3 +411,45 @@ def test_groupby_column_selection() -> None:
|
|
|
423
411
|
result_all = d.groupby("g").sum()
|
|
424
412
|
assert "weight" not in result_all.columns
|
|
425
413
|
assert list(result_all.columns) == ["y"]
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_values_warns() -> None:
|
|
417
|
+
"""Accessing .values on a MicroSeries should emit a UserWarning."""
|
|
418
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
419
|
+
with warnings.catch_warnings(record=True) as w:
|
|
420
|
+
warnings.simplefilter("always")
|
|
421
|
+
_ = ms.values
|
|
422
|
+
assert len(w) == 1
|
|
423
|
+
assert issubclass(w[0].category, UserWarning)
|
|
424
|
+
assert "weights" in str(w[0].message).lower()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_to_numpy_warns() -> None:
|
|
428
|
+
"""Calling .to_numpy() on a MicroSeries should emit a UserWarning."""
|
|
429
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
430
|
+
with warnings.catch_warnings(record=True) as w:
|
|
431
|
+
warnings.simplefilter("always")
|
|
432
|
+
_ = ms.to_numpy()
|
|
433
|
+
assert len(w) == 1
|
|
434
|
+
assert issubclass(w[0].category, UserWarning)
|
|
435
|
+
assert "weights" in str(w[0].message).lower()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def test_mean_no_warning() -> None:
|
|
439
|
+
"""Internal .values usage in .mean() should NOT emit a warning."""
|
|
440
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
441
|
+
with warnings.catch_warnings(record=True) as w:
|
|
442
|
+
warnings.simplefilter("always")
|
|
443
|
+
_ = ms.mean()
|
|
444
|
+
user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
|
|
445
|
+
assert len(user_warnings) == 0
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def test_repr_no_warning() -> None:
|
|
449
|
+
"""Internal .values usage in __repr__ should NOT emit a warning."""
|
|
450
|
+
ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
451
|
+
with warnings.catch_warnings(record=True) as w:
|
|
452
|
+
warnings.simplefilter("always")
|
|
453
|
+
_ = repr(ms)
|
|
454
|
+
user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
|
|
455
|
+
assert len(user_warnings) == 0
|
|
@@ -38,23 +38,23 @@ class TestMicroSeriesSubclassPreservation:
|
|
|
38
38
|
|
|
39
39
|
# Addition
|
|
40
40
|
result = ms + 1
|
|
41
|
-
assert isinstance(
|
|
42
|
-
result
|
|
43
|
-
)
|
|
41
|
+
assert isinstance(result, MicroSeries), (
|
|
42
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
43
|
+
)
|
|
44
44
|
assert hasattr(result, "weights")
|
|
45
45
|
assert hasattr(result, "set_weights")
|
|
46
46
|
|
|
47
47
|
# Multiplication
|
|
48
48
|
result = ms * 2
|
|
49
|
-
assert isinstance(
|
|
50
|
-
result
|
|
51
|
-
)
|
|
49
|
+
assert isinstance(result, MicroSeries), (
|
|
50
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
51
|
+
)
|
|
52
52
|
|
|
53
53
|
# Division
|
|
54
54
|
result = ms / 2
|
|
55
|
-
assert isinstance(
|
|
56
|
-
result
|
|
57
|
-
)
|
|
55
|
+
assert isinstance(result, MicroSeries), (
|
|
56
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
57
|
+
)
|
|
58
58
|
|
|
59
59
|
def test_microseries_preserved_after_comparison(self):
|
|
60
60
|
"""Comparison operations should return MicroSeries, not plain
|
|
@@ -63,35 +63,33 @@ class TestMicroSeriesSubclassPreservation:
|
|
|
63
63
|
|
|
64
64
|
# Greater than
|
|
65
65
|
result = ms > 1
|
|
66
|
-
assert isinstance(
|
|
67
|
-
result
|
|
68
|
-
)
|
|
66
|
+
assert isinstance(result, MicroSeries), (
|
|
67
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
68
|
+
)
|
|
69
69
|
assert hasattr(result, "weights")
|
|
70
70
|
|
|
71
71
|
# Less than
|
|
72
72
|
result = ms < 3
|
|
73
|
-
assert isinstance(
|
|
74
|
-
result
|
|
75
|
-
)
|
|
73
|
+
assert isinstance(result, MicroSeries), (
|
|
74
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
75
|
+
)
|
|
76
76
|
|
|
77
77
|
def test_microseries_preserved_after_indexing(self):
|
|
78
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
|
-
)
|
|
79
|
+
ms = MicroSeries([1, 2, 3, 4, 5], weights=np.array([1.0, 2.0, 3.0, 4.0, 5.0]))
|
|
82
80
|
|
|
83
81
|
# Boolean indexing
|
|
84
82
|
result = ms[ms > 2]
|
|
85
|
-
assert isinstance(
|
|
86
|
-
result
|
|
87
|
-
)
|
|
83
|
+
assert isinstance(result, MicroSeries), (
|
|
84
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
85
|
+
)
|
|
88
86
|
assert hasattr(result, "weights")
|
|
89
87
|
|
|
90
88
|
# Slice indexing
|
|
91
89
|
result = ms[1:3]
|
|
92
|
-
assert isinstance(
|
|
93
|
-
result
|
|
94
|
-
)
|
|
90
|
+
assert isinstance(result, MicroSeries), (
|
|
91
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
92
|
+
)
|
|
95
93
|
|
|
96
94
|
|
|
97
95
|
class TestMicroDataFrameSubclassPreservation:
|
|
@@ -105,9 +103,7 @@ class TestMicroDataFrameSubclassPreservation:
|
|
|
105
103
|
|
|
106
104
|
# Column access
|
|
107
105
|
col = mdf["a"]
|
|
108
|
-
assert isinstance(
|
|
109
|
-
col, MicroSeries
|
|
110
|
-
), f"Got {type(col)} instead of MicroSeries"
|
|
106
|
+
assert isinstance(col, MicroSeries), f"Got {type(col)} instead of MicroSeries"
|
|
111
107
|
assert hasattr(col, "weights")
|
|
112
108
|
assert hasattr(col, "set_weights")
|
|
113
109
|
|
|
@@ -120,9 +116,9 @@ class TestMicroDataFrameSubclassPreservation:
|
|
|
120
116
|
|
|
121
117
|
# Column operations
|
|
122
118
|
result = mdf["a"] + mdf["b"]
|
|
123
|
-
assert isinstance(
|
|
124
|
-
result
|
|
125
|
-
)
|
|
119
|
+
assert isinstance(result, MicroSeries), (
|
|
120
|
+
f"Got {type(result)} instead of MicroSeries"
|
|
121
|
+
)
|
|
126
122
|
assert hasattr(result, "weights")
|
|
127
123
|
|
|
128
124
|
|
|
@@ -186,9 +182,7 @@ class TestCopyOnWriteCompatibility:
|
|
|
186
182
|
|
|
187
183
|
def test_microdataframe_copy_independent(self):
|
|
188
184
|
"""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
|
-
)
|
|
185
|
+
mdf = MicroDataFrame({"a": [1, 2, 3]}, weights=np.array([1.0, 2.0, 3.0]))
|
|
192
186
|
mdf_copy = mdf.copy()
|
|
193
187
|
|
|
194
188
|
# Modify original
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdf-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
5
|
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
6
|
License: MIT
|
|
@@ -11,15 +11,12 @@ Requires-Dist: numpy
|
|
|
11
11
|
Requires-Dist: pandas
|
|
12
12
|
Provides-Extra: dev
|
|
13
13
|
Requires-Dist: codecov; extra == "dev"
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
16
|
-
Requires-Dist: black; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff>=0.9.0; extra == "dev"
|
|
17
15
|
Requires-Dist: docformatter; extra == "dev"
|
|
18
|
-
Requires-Dist: isort; extra == "dev"
|
|
19
|
-
Requires-Dist: linecheck; extra == "dev"
|
|
20
16
|
Requires-Dist: pytest; extra == "dev"
|
|
21
17
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
18
|
Requires-Dist: setuptools; extra == "dev"
|
|
19
|
+
Requires-Dist: towncrier>=24.8.0; extra == "dev"
|
|
23
20
|
Dynamic: license-file
|
|
24
21
|
|
|
25
22
|
[](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.3"
|
|
8
8
|
description = "Weighted pandas DataFrames and Series for survey microdata"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
@@ -20,15 +20,12 @@ dependencies = [
|
|
|
20
20
|
[project.optional-dependencies]
|
|
21
21
|
dev = [
|
|
22
22
|
"codecov",
|
|
23
|
-
"
|
|
24
|
-
"flake8-pyproject",
|
|
25
|
-
"black",
|
|
23
|
+
"ruff>=0.9.0",
|
|
26
24
|
"docformatter",
|
|
27
|
-
"isort",
|
|
28
|
-
"linecheck",
|
|
29
25
|
"pytest",
|
|
30
26
|
"pytest-cov",
|
|
31
27
|
"setuptools",
|
|
28
|
+
"towncrier>=24.8.0",
|
|
32
29
|
]
|
|
33
30
|
# Note: Documentation uses MyST (Jupyter Book 2.0) which is installed via npm
|
|
34
31
|
# Run: cd docs && myst build --html
|
|
@@ -37,14 +34,35 @@ dev = [
|
|
|
37
34
|
where = ["."]
|
|
38
35
|
include = ["microdf*"]
|
|
39
36
|
|
|
40
|
-
[tool.
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
[tool.towncrier]
|
|
38
|
+
package = "microdf_python"
|
|
39
|
+
directory = "changelog.d"
|
|
40
|
+
filename = "CHANGELOG.md"
|
|
41
|
+
title_format = "## [{version}] - {project_date}"
|
|
42
|
+
issue_format = ""
|
|
43
|
+
underlines = ["", "", ""]
|
|
43
44
|
|
|
44
|
-
[tool.
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
[[tool.towncrier.type]]
|
|
46
|
+
directory = "breaking"
|
|
47
|
+
name = "Breaking changes"
|
|
48
|
+
showcontent = true
|
|
47
49
|
|
|
48
|
-
[tool.
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
[[tool.towncrier.type]]
|
|
51
|
+
directory = "added"
|
|
52
|
+
name = "Added"
|
|
53
|
+
showcontent = true
|
|
54
|
+
|
|
55
|
+
[[tool.towncrier.type]]
|
|
56
|
+
directory = "changed"
|
|
57
|
+
name = "Changed"
|
|
58
|
+
showcontent = true
|
|
59
|
+
|
|
60
|
+
[[tool.towncrier.type]]
|
|
61
|
+
directory = "fixed"
|
|
62
|
+
name = "Fixed"
|
|
63
|
+
showcontent = true
|
|
64
|
+
|
|
65
|
+
[[tool.towncrier.type]]
|
|
66
|
+
directory = "removed"
|
|
67
|
+
name = "Removed"
|
|
68
|
+
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
|