microdf-python 1.3.0__tar.gz → 1.3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.0
3
+ Version: 1.3.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
@@ -50,13 +50,12 @@ import microdf as mdf
50
50
  import pandas as pd
51
51
 
52
52
  # Create sample data with weights
53
- df = pd.DataFrame({
54
- 'income': [10_000, 20_000, 30_000, 40_000, 50_000],
55
- 'weights': [1, 2, 3, 2, 1]
56
- })
53
+ df = pd.DataFrame(
54
+ {"income": [10_000, 20_000, 30_000, 40_000, 50_000], "weights": [1, 2, 3, 2, 1]}
55
+ )
57
56
 
58
57
  # Create a MicroDataFrame
59
- mdf_df = mdf.MicroDataFrame(df, weights='weights')
58
+ mdf_df = mdf.MicroDataFrame(df, weights="weights")
60
59
 
61
60
  # All operations are weight-aware
62
61
  print(mdf_df.income.mean()) # Weighted mean
@@ -29,13 +29,12 @@ import microdf as mdf
29
29
  import pandas as pd
30
30
 
31
31
  # Create sample data with weights
32
- df = pd.DataFrame({
33
- 'income': [10_000, 20_000, 30_000, 40_000, 50_000],
34
- 'weights': [1, 2, 3, 2, 1]
35
- })
32
+ df = pd.DataFrame(
33
+ {"income": [10_000, 20_000, 30_000, 40_000, 50_000], "weights": [1, 2, 3, 2, 1]}
34
+ )
36
35
 
37
36
  # Create a MicroDataFrame
38
- mdf_df = mdf.MicroDataFrame(df, weights='weights')
37
+ mdf_df = mdf.MicroDataFrame(df, weights="weights")
39
38
 
40
39
  # All operations are weight-aware
41
40
  print(mdf_df.income.mean()) # Weighted mean
@@ -96,8 +96,10 @@ class _MicroILocIndexer:
96
96
 
97
97
  class MicroDataFrame(pd.DataFrame):
98
98
  def __init__(self, *args, weights=None, **kwargs):
99
- """A DataFrame-inheriting class for weighted microdata. Weights can be
100
- provided at initialisation, or using set_weights or set_weight_col.
99
+ """A DataFrame-inheriting class for weighted microdata.
100
+
101
+ Weights can be provided at initialisation, or using set_weights or
102
+ set_weight_col.
101
103
 
102
104
  :param weights: Array of weights.
103
105
  :type weights: np.array
@@ -231,8 +233,9 @@ class MicroDataFrame(pd.DataFrame):
231
233
  return fn
232
234
 
233
235
  def get_args_as_micro_series(*kwarg_names: tuple) -> Callable:
234
- """Decorator for auto-parsing column names into MicroSeries objects. If
235
- given, kwarg_names limits arguments checked to keyword arguments
236
+ """Decorator for auto-parsing column names into MicroSeries objects.
237
+
238
+ If given, kwarg_names limits arguments checked to keyword arguments
236
239
  specified.
237
240
 
238
241
  :param arg_names: argument names to restrict to.
@@ -292,8 +295,10 @@ class MicroDataFrame(pd.DataFrame):
292
295
  weights: Union[np.ndarray, str],
293
296
  preserve_old: Optional[bool] = False,
294
297
  ) -> None:
295
- """Sets the weights for the MicroDataFrame. If a string is received, it
296
- will be assumed to be the column name of the weight column.
298
+ """Sets the weights for the MicroDataFrame.
299
+
300
+ If a string is received, it will be assumed to be the column name of
301
+ the weight column.
297
302
 
298
303
  :param weights: Array of weights.
299
304
  :param preserve_old: If True, keeps the old weights as a column when
@@ -14,10 +14,10 @@ def _weighted_top_share(
14
14
  ) -> float:
15
15
  """Share of the sum held by the top ``top_x_pct`` of weight.
16
16
 
17
- Sort by value ascending, cumulate the weight, pick the slice from
18
- the top that covers exactly ``top_x_pct`` of total weight, and
19
- distribute the tied-at-cutoff row proportionally so constant values
20
- return exactly ``top_x_pct`` rather than 1.0.
17
+ Sort by value ascending, cumulate the weight, pick the slice from the top
18
+ that covers exactly ``top_x_pct`` of total weight, and distribute the tied-
19
+ at-cutoff row proportionally so constant values return exactly
20
+ ``top_x_pct`` rather than 1.0.
21
21
  """
22
22
  if top_x_pct <= 0:
23
23
  return 0.0
@@ -51,8 +51,9 @@ def _weighted_top_share(
51
51
 
52
52
  class MicroSeries(pd.Series):
53
53
  def __init__(self, *args, weights: np.array = None, **kwargs):
54
- """A Series-inheriting class for weighted microdata. Weights can be
55
- provided at initialisation, or using set_weights.
54
+ """A Series-inheriting class for weighted microdata.
55
+
56
+ Weights can be provided at initialisation, or using set_weights.
56
57
 
57
58
  :param weights: Array of weights.
58
59
  :type weights: np.array
@@ -156,7 +157,11 @@ class MicroSeries(pd.Series):
156
157
  This is useful for comparing weighted and unweighted statistics or when
157
158
  you want to temporarily ignore weights.
158
159
  """
159
- self.weights = pd.Series(np.ones(len(self)), dtype=float)
160
+ # Index the ones against self.index: weighted ops are label-aligned
161
+ # (self.multiply(self.weights) in .sum()/.weight()), so a default
162
+ # RangeIndex here silently produces all-NaN and collapses every
163
+ # aggregation to 0 whenever the caller uses a non-default index.
164
+ self.weights = pd.Series(np.ones(len(self)), index=self.index, dtype=float)
160
165
 
161
166
  @vector_function
162
167
  def weight(self) -> pd.Series:
@@ -275,9 +280,9 @@ class MicroSeries(pd.Series):
275
280
  """Pandas ``cov`` — **unweighted**.
276
281
 
277
282
  MicroSeries does not yet compute weighted covariance. Emits a
278
- ``UserWarning`` so callers aren't silently given an unweighted
279
- number after ``.sum()`` and ``.mean()`` worked as expected. See
280
- issue tracker for a weighted implementation.
283
+ ``UserWarning`` so callers aren't silently given an unweighted number
284
+ after ``.sum()`` and ``.mean()`` worked as expected. See issue tracker
285
+ for a weighted implementation.
281
286
  """
282
287
  warnings.warn(
283
288
  "MicroSeries.cov() falls through to pandas and is "
@@ -293,8 +298,8 @@ class MicroSeries(pd.Series):
293
298
  """Pandas ``corr`` — **unweighted**.
294
299
 
295
300
  MicroSeries does not yet compute weighted correlation. Emits a
296
- ``UserWarning`` so callers aren't silently given an unweighted
297
- number. See issue tracker for a weighted implementation.
301
+ ``UserWarning`` so callers aren't silently given an unweighted number.
302
+ See issue tracker for a weighted implementation.
298
303
  """
299
304
  warnings.warn(
300
305
  "MicroSeries.corr() falls through to pandas and is "
@@ -519,13 +524,13 @@ class MicroSeries(pd.Series):
519
524
  def rank(self, pct: Optional[bool] = False) -> pd.Series:
520
525
  """Weighted rank of each element.
521
526
 
522
- Each element's rank is the cumulative weight of all values that
523
- are less than or equal to it. Tied values therefore share the
524
- same rank, so downstream bucketing (``decile_rank``,
525
- ``quintile_rank``, etc.) lands tied rows in the same bucket.
527
+ Each element's rank is the cumulative weight of all values that are
528
+ less than or equal to it. Tied values therefore share the same rank, so
529
+ downstream bucketing (``decile_rank``, ``quintile_rank``, etc.) lands
530
+ tied rows in the same bucket.
526
531
 
527
- :param pct: If True, divide ranks by the total weight so they
528
- lie in ``(0, 1]``.
532
+ :param pct: If True, divide ranks by the total weight so they lie in
533
+ ``(0, 1]``.
529
534
  :type pct: bool
530
535
  :returns: MicroSeries of ranks aligned to ``self``.
531
536
  :rtype: MicroSeries
@@ -449,11 +449,11 @@ def test_mean_no_warning() -> None:
449
449
  def test_sum_with_non_default_index() -> None:
450
450
  """Weighted sum must not silently return 0 with a non-default index.
451
451
 
452
- Regression test for the bug where ``set_weights`` stored the weights
453
- Series with a default ``RangeIndex`` regardless of ``self.index``.
454
- Element-wise ops like ``self.multiply(self.weights)`` then aligned on
455
- label, producing all-NaN and a silent ``0.0`` from ``.sum()`` while
456
- ``.mean()`` (which uses a positional ndarray) stayed correct.
452
+ Regression test for the bug where ``set_weights`` stored the weights Series
453
+ with a default ``RangeIndex`` regardless of ``self.index``. Element-wise
454
+ ops like ``self.multiply(self.weights)`` then aligned on label, producing
455
+ all-NaN and a silent ``0.0`` from ``.sum()`` while ``.mean()`` (which uses
456
+ a positional ndarray) stayed correct.
457
457
  """
458
458
  # MicroSeries with custom integer index.
459
459
  s = mdf.MicroSeries([1, 2, 3], index=[100, 200, 300], weights=[10, 20, 30])
@@ -541,10 +541,9 @@ def test_merge_preserves_weights_per_surviving_row() -> None:
541
541
  """Regression: merge must propagate weights onto the merged rows.
542
542
 
543
543
  Previously the implementation passed ``self.weights`` straight to the
544
- MicroDataFrame constructor, so any merge that changed row count
545
- (inner filtering, left-with-missing, many-to-many, outer) raised
546
- ``ValueError: Length of weights (N) does not match length of
547
- DataFrame (M)``.
544
+ MicroDataFrame constructor, so any merge that changed row count (inner
545
+ filtering, left-with-missing, many-to-many, outer) raised ``ValueError:
546
+ Length of weights (N) does not match length of DataFrame (M)``.
548
547
  """
549
548
  # Inner join filters rows.
550
549
  left = mdf.MicroDataFrame(
@@ -587,10 +586,10 @@ def test_merge_preserves_weights_per_surviving_row() -> None:
587
586
  def test_groupby_does_not_leak_tmp_weights_column() -> None:
588
587
  """Regression: groupby used to mutate self by adding __tmp_weights.
589
588
 
590
- Previously, ``MicroDataFrame.groupby`` set ``self["__tmp_weights"]``
591
- and never cleaned it up, so ``df.columns`` afterwards included the
592
- weight column and any later ``df.sum()`` or iteration over columns
593
- picked it up as data.
589
+ Previously, ``MicroDataFrame.groupby`` set ``self["__tmp_weights"]`` and
590
+ never cleaned it up, so ``df.columns`` afterwards included the weight
591
+ column and any later ``df.sum()`` or iteration over columns picked it up as
592
+ data.
594
593
  """
595
594
  df = mdf.MicroDataFrame({"g": ["a", "a", "b"], "v": [1, 2, 3]}, weights=[1, 2, 3])
596
595
  original_cols = list(df.columns)
@@ -616,11 +615,10 @@ def test_groupby_does_not_leak_tmp_weights_column() -> None:
616
615
  def test_quantile_skips_zero_weight_rows() -> None:
617
616
  """Regression: quantile(0) shouldn't pick a zero-weight element.
618
617
 
619
- Previously, ``np.searchsorted(cumsum_norm, 0, side='left')`` returned
620
- 0 even when that first sorted element had zero weight, so
621
- ``MicroSeries([10, 20, 30], weights=[0, 1, 1]).quantile(0)`` returned
622
- 10 instead of 20. The fix drops zero-weight rows before computing
623
- the CDF.
618
+ Previously, ``np.searchsorted(cumsum_norm, 0, side='left')`` returned 0
619
+ even when that first sorted element had zero weight, so ``MicroSeries([10,
620
+ 20, 30], weights=[0, 1, 1]).quantile(0)`` returned 10 instead of 20. The
621
+ fix drops zero-weight rows before computing the CDF.
624
622
  """
625
623
  s = mdf.MicroSeries([10, 20, 30], weights=[0, 1, 1])
626
624
  assert s.quantile(0.0) == 20
@@ -681,10 +679,9 @@ def test_top_x_pct_share_handles_ties_and_edges() -> None:
681
679
  def test_gini_negatives_option_applied() -> None:
682
680
  """Regression: gini(negatives=...) was silently ignored.
683
681
 
684
- Both branches of the old implementation sorted ``self`` directly
685
- rather than the local ``x`` that was mutated by the ``negatives``
686
- option, so ``negatives='zero'`` and ``negatives='shift'`` did
687
- nothing.
682
+ Both branches of the old implementation sorted ``self`` directly rather
683
+ than the local ``x`` that was mutated by the ``negatives`` option, so
684
+ ``negatives='zero'`` and ``negatives='shift'`` did nothing.
688
685
  """
689
686
  s = mdf.MicroSeries([-5, 0, 10], weights=[1, 1, 1])
690
687
 
@@ -715,10 +712,9 @@ def test_gini_negatives_option_applied() -> None:
715
712
  def test_std_var_are_weighted() -> None:
716
713
  """Regression: std/var used to silently fall through to pandas.
717
714
 
718
- The old implementation had no override, so a MicroSeries with very
719
- uneven weights returned the unweighted 1.0. Now std and var treat
720
- the weights as frequency counts, matching numpy on the replicated
721
- sample.
715
+ The old implementation had no override, so a MicroSeries with very uneven
716
+ weights returned the unweighted 1.0. Now std and var treat the weights as
717
+ frequency counts, matching numpy on the replicated sample.
722
718
  """
723
719
  s = mdf.MicroSeries([1, 2, 3], weights=[100, 1, 1])
724
720
  # Unweighted would be 1.0. Weighted std pulls toward the heavy row.
@@ -751,8 +747,8 @@ def test_std_var_are_weighted() -> None:
751
747
  def test_cov_corr_warn_when_fallthrough() -> None:
752
748
  """Regression: cov/corr silently returned unweighted pandas values.
753
749
 
754
- They still fall through to pandas (a weighted impl is a separate
755
- issue) but now emit a UserWarning so callers aren't misled.
750
+ They still fall through to pandas (a weighted impl is a separate issue) but
751
+ now emit a UserWarning so callers aren't misled.
756
752
  """
757
753
  s1 = mdf.MicroSeries([1, 2, 3], weights=[1, 1, 1])
758
754
  s2 = mdf.MicroSeries([2, 4, 6], weights=[1, 1, 1])
@@ -773,9 +769,9 @@ def test_cov_corr_warn_when_fallthrough() -> None:
773
769
  def test_count_skips_nan_by_default() -> None:
774
770
  """Regression: ``count()`` included NaN-row weight, contrary to pandas.
775
771
 
776
- Pandas ``Series.count`` skips NaN; MicroSeries returned the full
777
- weight sum regardless. The fix matches pandas semantics and adds a
778
- ``skipna`` kwarg so callers can opt out.
772
+ Pandas ``Series.count`` skips NaN; MicroSeries returned the full weight sum
773
+ regardless. The fix matches pandas semantics and adds a ``skipna`` kwarg so
774
+ callers can opt out.
779
775
  """
780
776
  s = mdf.MicroSeries([1.0, np.nan, 3.0], weights=[10, 20, 30])
781
777
  assert s.count() == 40.0
@@ -794,12 +790,12 @@ def test_count_skips_nan_by_default() -> None:
794
790
  def test_rank_ties_share_bucket() -> None:
795
791
  """Regression: rank used to assign ties to different ranks/buckets.
796
792
 
797
- Previously ``rank`` returned the running cumulative weight in sort
798
- order, so every row — tied or not — got a distinct value. As a
799
- result ``MicroSeries([5]*5, weights=[1]*5).decile_rank()`` returned
800
- ``[2, 4, 6, 8, 10]`` rather than all 10. With max-rank semantics,
801
- tied values share the cumulative weight at the end of their tie
802
- group, so bucketing is stable under ties.
793
+ Previously ``rank`` returned the running cumulative weight in sort order,
794
+ so every row — tied or not — got a distinct value. As a result
795
+ ``MicroSeries([5]*5, weights=[1]*5).decile_rank()`` returned ``[2, 4, 6, 8,
796
+ 10]`` rather than all 10. With max-rank semantics, tied values share the
797
+ cumulative weight at the end of their tie group, so bucketing is stable
798
+ under ties.
803
799
  """
804
800
  # All tied: every element lands in the top decile.
805
801
  s = mdf.MicroSeries([5] * 5, weights=[1] * 5)
@@ -0,0 +1,10 @@
1
+ import microdf as mdf
2
+
3
+
4
+ def test_nullify_weights_non_default_index():
5
+ """nullify_weights must align to the index, not a fresh RangeIndex."""
6
+ s = mdf.MicroSeries([1, 2, 3], index=[10, 11, 12], weights=[1, 2, 3])
7
+ s.nullify_weights()
8
+ assert s.sum() == 6
9
+ assert s.mean() == 2
10
+ assert list(s.weights.index) == [10, 11, 12]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.0
3
+ Version: 1.3.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
@@ -50,13 +50,12 @@ import microdf as mdf
50
50
  import pandas as pd
51
51
 
52
52
  # Create sample data with weights
53
- df = pd.DataFrame({
54
- 'income': [10_000, 20_000, 30_000, 40_000, 50_000],
55
- 'weights': [1, 2, 3, 2, 1]
56
- })
53
+ df = pd.DataFrame(
54
+ {"income": [10_000, 20_000, 30_000, 40_000, 50_000], "weights": [1, 2, 3, 2, 1]}
55
+ )
57
56
 
58
57
  # Create a MicroDataFrame
59
- mdf_df = mdf.MicroDataFrame(df, weights='weights')
58
+ mdf_df = mdf.MicroDataFrame(df, weights="weights")
60
59
 
61
60
  # All operations are weight-aware
62
61
  print(mdf_df.income.mean()) # Weighted mean
@@ -6,6 +6,7 @@ microdf/microdataframe.py
6
6
  microdf/microseries.py
7
7
  microdf/tests/conftest.py
8
8
  microdf/tests/test_microseries_dataframe.py
9
+ microdf/tests/test_nullify_weights_index.py
9
10
  microdf/tests/test_pandas3_compatibility.py
10
11
  microdf_python.egg-info/PKG-INFO
11
12
  microdf_python.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "microdf-python"
7
- version = "1.3.0"
7
+ version = "1.3.3"
8
8
  description = "Weighted pandas DataFrames and Series for survey microdata"
9
9
  readme = "README.md"
10
10
  authors = [
File without changes
File without changes