microdf-python 1.2.2__tar.gz → 1.3.0__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.2/microdf_python.egg-info → microdf_python-1.3.0}/PKG-INFO +2 -6
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/microdataframe.py +103 -55
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/microseries.py +257 -83
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/tests/test_microseries_dataframe.py +373 -21
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/tests/test_pandas3_compatibility.py +27 -33
- {microdf_python-1.2.2 → microdf_python-1.3.0/microdf_python.egg-info}/PKG-INFO +2 -6
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf_python.egg-info/requires.txt +1 -5
- {microdf_python-1.2.2 → microdf_python-1.3.0}/pyproject.toml +4 -20
- {microdf_python-1.2.2 → microdf_python-1.3.0}/LICENSE +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/README.md +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/__init__.py +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf/tests/conftest.py +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf_python.egg-info/SOURCES.txt +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf_python.egg-info/dependency_links.txt +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/microdf_python.egg-info/top_level.txt +0 -0
- {microdf_python-1.2.2 → microdf_python-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdf-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
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,12 +11,8 @@ 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"
|
|
@@ -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
|
|
@@ -309,7 +305,11 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
309
305
|
|
|
310
306
|
if isinstance(weights, str):
|
|
311
307
|
self.weights_col = weights
|
|
312
|
-
self.weights = pd.Series(
|
|
308
|
+
self.weights = pd.Series(
|
|
309
|
+
np.asarray(self[weights]),
|
|
310
|
+
index=self.index,
|
|
311
|
+
dtype=float,
|
|
312
|
+
)
|
|
313
313
|
self._link_all_weights()
|
|
314
314
|
elif weights is not None:
|
|
315
315
|
if len(weights) != len(self):
|
|
@@ -318,14 +318,21 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
318
318
|
f"length of DataFrame ({len(self)})."
|
|
319
319
|
)
|
|
320
320
|
self.weights_col = None
|
|
321
|
+
# Align weights to self.index. Without this, weighted ops
|
|
322
|
+
# (self[col].multiply(self.weights) in .sum()) align on
|
|
323
|
+
# label, so any non-default index silently produces all-NaN
|
|
324
|
+
# and aggregations collapse to 0. If a Series is passed in,
|
|
325
|
+
# strip its index so we position-align to self.index.
|
|
326
|
+
if isinstance(weights, pd.Series):
|
|
327
|
+
weights = weights.values
|
|
321
328
|
with warnings.catch_warnings():
|
|
322
329
|
warnings.filterwarnings("ignore", category=UserWarning)
|
|
323
|
-
self.weights = pd.Series(
|
|
330
|
+
self.weights = pd.Series(
|
|
331
|
+
np.asarray(weights), index=self.index, dtype=float
|
|
332
|
+
)
|
|
324
333
|
self._link_all_weights()
|
|
325
334
|
|
|
326
|
-
def set_weight_col(
|
|
327
|
-
self, column: str, preserve_old: Optional[bool] = False
|
|
328
|
-
) -> None:
|
|
335
|
+
def set_weight_col(self, column: str, preserve_old: Optional[bool] = False) -> None:
|
|
329
336
|
"""Sets the weights for the MicroDataFrame by specifying the name of
|
|
330
337
|
the weight column.
|
|
331
338
|
|
|
@@ -422,8 +429,9 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
422
429
|
:return: MicroDataFrame with reset index or None if inplace=True.
|
|
423
430
|
"""
|
|
424
431
|
if inplace:
|
|
425
|
-
|
|
426
|
-
#
|
|
432
|
+
# Snapshot weight *values* positionally — the index is about
|
|
433
|
+
# to change and reset_index preserves row order.
|
|
434
|
+
weight_values = np.asarray(self.weights.values, dtype=float)
|
|
427
435
|
super().reset_index(
|
|
428
436
|
level=level,
|
|
429
437
|
drop=drop,
|
|
@@ -433,7 +441,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
433
441
|
allow_duplicates=allow_duplicates,
|
|
434
442
|
names=names,
|
|
435
443
|
)
|
|
436
|
-
self.weights =
|
|
444
|
+
self.weights = pd.Series(weight_values, index=self.index, dtype=float)
|
|
437
445
|
self._link_all_weights()
|
|
438
446
|
return None
|
|
439
447
|
else:
|
|
@@ -446,13 +454,22 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
446
454
|
allow_duplicates=allow_duplicates,
|
|
447
455
|
names=names,
|
|
448
456
|
)
|
|
449
|
-
|
|
457
|
+
out = MicroDataFrame(res, weights=self.weights.values)
|
|
458
|
+
# Ensure weights align to res.index (reset_index changes the
|
|
459
|
+
# index but preserves row order, so pass values positionally).
|
|
460
|
+
out.weights = pd.Series(
|
|
461
|
+
np.asarray(self.weights.values, dtype=float),
|
|
462
|
+
index=out.index,
|
|
463
|
+
dtype=float,
|
|
464
|
+
)
|
|
465
|
+
return out
|
|
450
466
|
|
|
451
467
|
def copy(self, deep: Optional[bool] = True) -> "MicroDataFrame":
|
|
452
468
|
res = super().copy(deep)
|
|
453
|
-
#
|
|
454
|
-
|
|
455
|
-
|
|
469
|
+
# super().copy() corrupts self's column types to plain Series.
|
|
470
|
+
# Restore them in O(N) instead of O(N²) by calling
|
|
471
|
+
# _link_all_weights once rather than per-column __setitem__.
|
|
472
|
+
self._link_all_weights()
|
|
456
473
|
res = MicroDataFrame(res, weights=self.weights.copy(deep))
|
|
457
474
|
return res
|
|
458
475
|
|
|
@@ -486,8 +503,11 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
486
503
|
dropped.
|
|
487
504
|
:return: MicroDataFrame or None if inplace=True.
|
|
488
505
|
"""
|
|
506
|
+
row_drop = axis in (0, "index") or index is not None
|
|
489
507
|
if inplace:
|
|
490
|
-
|
|
508
|
+
# Snapshot the pre-drop weights keyed by the pre-drop index so
|
|
509
|
+
# we can reindex to the surviving rows after the drop.
|
|
510
|
+
pre_drop_weights = pd.Series(self.weights.values, index=self.index.copy())
|
|
491
511
|
# Perform in-place drop on the parent DataFrame
|
|
492
512
|
super().drop(
|
|
493
513
|
labels=labels,
|
|
@@ -498,7 +518,15 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
498
518
|
inplace=True,
|
|
499
519
|
errors=errors,
|
|
500
520
|
)
|
|
501
|
-
|
|
521
|
+
if row_drop:
|
|
522
|
+
surviving = pre_drop_weights.reindex(self.index)
|
|
523
|
+
self.weights = pd.Series(
|
|
524
|
+
surviving.values, index=self.index, dtype=float
|
|
525
|
+
)
|
|
526
|
+
else:
|
|
527
|
+
self.weights = pd.Series(
|
|
528
|
+
pre_drop_weights.values, index=self.index, dtype=float
|
|
529
|
+
)
|
|
502
530
|
self._link_all_weights()
|
|
503
531
|
return None
|
|
504
532
|
else:
|
|
@@ -511,7 +539,19 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
511
539
|
inplace=False,
|
|
512
540
|
errors=errors,
|
|
513
541
|
)
|
|
514
|
-
|
|
542
|
+
if row_drop:
|
|
543
|
+
# Row drop: keep only the weights for surviving rows,
|
|
544
|
+
# in the order of the resulting DataFrame.
|
|
545
|
+
pre_drop_weights = pd.Series(self.weights.values, index=self.index)
|
|
546
|
+
new_weights = pre_drop_weights.reindex(res.index).values
|
|
547
|
+
else:
|
|
548
|
+
new_weights = self.weights.values
|
|
549
|
+
out = MicroDataFrame(res, weights=new_weights)
|
|
550
|
+
# Guard against the set_weights path building weights with a
|
|
551
|
+
# default RangeIndex, which would misalign against res.index
|
|
552
|
+
# and silently zero weighted aggregations.
|
|
553
|
+
out.weights = pd.Series(new_weights, index=out.index, dtype=float)
|
|
554
|
+
return out
|
|
515
555
|
|
|
516
556
|
def merge(
|
|
517
557
|
self,
|
|
@@ -554,7 +594,17 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
554
594
|
:param validate: If specified, checks if merge is of specified type.
|
|
555
595
|
:return: MicroDataFrame with merged data.
|
|
556
596
|
"""
|
|
557
|
-
|
|
597
|
+
# Attach the left weights as a temporary column so pandas' merge
|
|
598
|
+
# propagates them onto every surviving output row (including
|
|
599
|
+
# many-to-many row duplications, inner-join filtering, and
|
|
600
|
+
# left-with-missing NaNs). We then strip the column back off.
|
|
601
|
+
tmp = "__microdf_weights__"
|
|
602
|
+
# Avoid clobbering if this exact name is already used.
|
|
603
|
+
while tmp in self.columns or tmp in right.columns:
|
|
604
|
+
tmp += "_"
|
|
605
|
+
left_df = pd.DataFrame(self).copy()
|
|
606
|
+
left_df[tmp] = np.asarray(self.weights.values, dtype=float)
|
|
607
|
+
res = left_df.merge(
|
|
558
608
|
right,
|
|
559
609
|
how=how,
|
|
560
610
|
on=on,
|
|
@@ -568,11 +618,17 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
568
618
|
indicator=indicator,
|
|
569
619
|
validate=validate,
|
|
570
620
|
)
|
|
571
|
-
|
|
572
|
-
#
|
|
573
|
-
#
|
|
574
|
-
#
|
|
575
|
-
|
|
621
|
+
# Pull out the propagated weights. Rows with no left match in a
|
|
622
|
+
# right/outer join get NaN weight — fill with 0 so they don't
|
|
623
|
+
# poison later aggregations (a user who needs a different
|
|
624
|
+
# convention can override afterwards).
|
|
625
|
+
merged_weights = res[tmp].fillna(0).to_numpy(dtype=float)
|
|
626
|
+
res = res.drop(columns=[tmp])
|
|
627
|
+
out = MicroDataFrame(res, weights=merged_weights)
|
|
628
|
+
# Ensure the weights Series aligns with res.index regardless of
|
|
629
|
+
# the default-RangeIndex behavior of set_weights.
|
|
630
|
+
out.weights = pd.Series(merged_weights, index=out.index, dtype=float)
|
|
631
|
+
return out
|
|
576
632
|
|
|
577
633
|
def __getattr__(self, name):
|
|
578
634
|
"""Allow accessing columns as attributes (e.g., df.column_name).
|
|
@@ -594,9 +650,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
594
650
|
return equal_values and equal_weights
|
|
595
651
|
|
|
596
652
|
@get_args_as_micro_series()
|
|
597
|
-
def groupby(
|
|
598
|
-
self, by: Union[str, List], *args, **kwargs
|
|
599
|
-
) -> "MicroDataFrameGroupBy":
|
|
653
|
+
def groupby(self, by: Union[str, List], *args, **kwargs) -> "MicroDataFrameGroupBy":
|
|
600
654
|
"""Returns a GroupBy object with MicroSeriesGroupBy objects for each
|
|
601
655
|
column.
|
|
602
656
|
|
|
@@ -606,10 +660,16 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
606
660
|
return: DataFrameGroupBy object with columns using weights
|
|
607
661
|
rtype: DataFrameGroupBy
|
|
608
662
|
"""
|
|
609
|
-
|
|
610
|
-
|
|
663
|
+
# Build the groupby on a *copy* that carries a ``__tmp_weights``
|
|
664
|
+
# column. We used to set this column on ``self`` directly, which
|
|
665
|
+
# permanently leaked the weight column onto the caller's
|
|
666
|
+
# DataFrame — any later ``df.sum()`` or ``list(df.columns)``
|
|
667
|
+
# would then include it.
|
|
668
|
+
staged = pd.DataFrame(self).copy()
|
|
669
|
+
staged["__tmp_weights"] = np.asarray(self.weights.values, dtype=float)
|
|
670
|
+
gb = staged.groupby(by, *args, **kwargs)
|
|
611
671
|
weights = copy.deepcopy(gb["__tmp_weights"])
|
|
612
|
-
for col in
|
|
672
|
+
for col in staged.columns: # df.groupby(...)[col]s use weights
|
|
613
673
|
res = gb[col]
|
|
614
674
|
res.__class__ = MicroSeriesGroupBy
|
|
615
675
|
res._init()
|
|
@@ -757,14 +817,10 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
757
817
|
self.columns.remove("__tmp_weights")
|
|
758
818
|
# Filter to only numeric columns
|
|
759
819
|
self.numeric_columns = [
|
|
760
|
-
col
|
|
761
|
-
for col in self.columns
|
|
762
|
-
if pd.api.types.is_numeric_dtype(self.obj[col])
|
|
820
|
+
col for col in self.columns if pd.api.types.is_numeric_dtype(self.obj[col])
|
|
763
821
|
]
|
|
764
822
|
# Store reference to weights groupby for column selection
|
|
765
|
-
self._weights_groupby = copy.deepcopy(
|
|
766
|
-
super().__getitem__("__tmp_weights")
|
|
767
|
-
)
|
|
823
|
+
self._weights_groupby = copy.deepcopy(super().__getitem__("__tmp_weights"))
|
|
768
824
|
for fn_name in MicroSeries.SCALAR_FUNCTIONS:
|
|
769
825
|
|
|
770
826
|
def get_fn(name):
|
|
@@ -854,18 +910,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
854
910
|
results = {}
|
|
855
911
|
for col in res.numeric_columns:
|
|
856
912
|
try:
|
|
857
|
-
results[col] = getattr(
|
|
858
|
-
|
|
859
|
-
)
|
|
913
|
+
results[col] = getattr(getattr(res, col), name)(
|
|
914
|
+
*args, **kwargs
|
|
915
|
+
)
|
|
860
916
|
except Exception:
|
|
861
917
|
pass
|
|
862
918
|
# Return plain DataFrame - aggregated results don't
|
|
863
919
|
# have per-row weights (weights were already applied)
|
|
864
|
-
return (
|
|
865
|
-
pd.DataFrame(results)
|
|
866
|
-
if results
|
|
867
|
-
else pd.DataFrame()
|
|
868
|
-
)
|
|
920
|
+
return pd.DataFrame(results) if results else pd.DataFrame()
|
|
869
921
|
|
|
870
922
|
return fn
|
|
871
923
|
|
|
@@ -877,18 +929,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
877
929
|
results = {}
|
|
878
930
|
for col in res.numeric_columns:
|
|
879
931
|
try:
|
|
880
|
-
results[col] = getattr(
|
|
881
|
-
|
|
882
|
-
)
|
|
932
|
+
results[col] = getattr(getattr(res, col), name)(
|
|
933
|
+
*args, **kwargs
|
|
934
|
+
)
|
|
883
935
|
except Exception:
|
|
884
936
|
pass
|
|
885
937
|
# Return plain DataFrame - aggregated results don't
|
|
886
938
|
# have per-row weights (weights were already applied)
|
|
887
|
-
return (
|
|
888
|
-
pd.DataFrame(results)
|
|
889
|
-
if results
|
|
890
|
-
else pd.DataFrame()
|
|
891
|
-
)
|
|
939
|
+
return pd.DataFrame(results) if results else pd.DataFrame()
|
|
892
940
|
|
|
893
941
|
return fn
|
|
894
942
|
|