microdf-python 1.3.7__tar.gz → 1.3.8__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.
Files changed (22) hide show
  1. {microdf_python-1.3.7/microdf_python.egg-info → microdf_python-1.3.8}/PKG-INFO +1 -1
  2. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/microdataframe.py +31 -0
  3. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/microseries.py +17 -0
  4. microdf_python-1.3.8/microdf/tests/test_serialization.py +143 -0
  5. {microdf_python-1.3.7 → microdf_python-1.3.8/microdf_python.egg-info}/PKG-INFO +1 -1
  6. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/SOURCES.txt +1 -0
  7. {microdf_python-1.3.7 → microdf_python-1.3.8}/pyproject.toml +1 -1
  8. {microdf_python-1.3.7 → microdf_python-1.3.8}/LICENSE +0 -0
  9. {microdf_python-1.3.7 → microdf_python-1.3.8}/README.md +0 -0
  10. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/__init__.py +0 -0
  11. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/conftest.py +0 -0
  12. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_aggregation_errors.py +0 -0
  13. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_dataframe_weight_storage.py +0 -0
  14. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_microseries_dataframe.py +0 -0
  15. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_nullify_weights_index.py +0 -0
  16. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_pandas3_compatibility.py +0 -0
  17. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_quantile_missing_values.py +0 -0
  18. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_version_metadata.py +0 -0
  19. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/dependency_links.txt +0 -0
  20. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/requires.txt +0 -0
  21. {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/top_level.txt +0 -0
  22. {microdf_python-1.3.7 → microdf_python-1.3.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.7
3
+ Version: 1.3.8
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -95,6 +95,13 @@ class _MicroILocIndexer:
95
95
 
96
96
 
97
97
  class MicroDataFrame(pd.DataFrame):
98
+ # Declare weight state as pandas metadata. pandas includes
99
+ # _metadata attributes in the pickle state, so weights now survive
100
+ # pickling, to_pickle/read_pickle and copy.deepcopy instead of
101
+ # vanishing and leaving an AttributeError on the next aggregation.
102
+ # Retain the column name for set_weights(..., preserve_old=True).
103
+ _metadata = pd.DataFrame._metadata + ["weights", "weights_col"]
104
+
98
105
  def __init__(self, *args, weights=None, **kwargs):
99
106
  """A DataFrame-inheriting class for weighted microdata.
100
107
 
@@ -110,6 +117,30 @@ class MicroDataFrame(pd.DataFrame):
110
117
  self._link_all_weights()
111
118
  self.override_df_functions()
112
119
 
120
+ def __finalize__(self, other, method=None, **kwargs) -> "MicroDataFrame":
121
+ """Retain copied weights when pandas finalizes a renamed result."""
122
+ copied_weights = getattr(self, "weights", None) if method == "rename" else None
123
+ super().__finalize__(other, method=method, **kwargs)
124
+ if copied_weights is not None:
125
+ # rename already called copy(); metadata propagation must not
126
+ # replace those weights with the source's mutable Series.
127
+ self.weights = copied_weights
128
+ return self
129
+
130
+ def __setstate__(self, state) -> None:
131
+ """Restore a pickled MicroDataFrame.
132
+
133
+ The weighted aggregations are installed as per-instance closures by
134
+ ``override_df_functions``, which only runs in ``__init__`` — a path
135
+ unpickling skips. Without reinstalling them, ``mdf.sum()`` on an
136
+ unpickled frame silently fell through to the unweighted pandas
137
+ implementation.
138
+ """
139
+ super().__setstate__(state)
140
+ if getattr(self, "weights", None) is None:
141
+ self._link_all_weights()
142
+ self.override_df_functions()
143
+
113
144
  @property
114
145
  def loc(self) -> _MicroLocIndexer:
115
146
  """Label-based indexer that preserves MicroDataFrame type and weights.
@@ -50,6 +50,13 @@ def _weighted_top_share(
50
50
 
51
51
 
52
52
  class MicroSeries(pd.Series):
53
+ # Declare ``weights`` as pandas metadata. pandas includes
54
+ # _metadata attributes in the pickle state, so weights now survive
55
+ # pickling, to_pickle/read_pickle and copy.deepcopy instead of
56
+ # vanishing and leaving an AttributeError on the next aggregation.
57
+ # Keep pandas' own metadata, including the Series name.
58
+ _metadata = pd.Series._metadata + ["weights"]
59
+
53
60
  def __init__(self, *args, weights: np.array = None, **kwargs):
54
61
  """A Series-inheriting class for weighted microdata.
55
62
 
@@ -61,6 +68,16 @@ class MicroSeries(pd.Series):
61
68
  super().__init__(*args, **kwargs)
62
69
  self.set_weights(weights)
63
70
 
71
+ def __finalize__(self, other, method=None, **kwargs) -> "MicroSeries":
72
+ """Retain copied weights when pandas finalizes a renamed result."""
73
+ copied_weights = getattr(self, "weights", None) if method == "rename" else None
74
+ super().__finalize__(other, method=method, **kwargs)
75
+ if copied_weights is not None:
76
+ # rename already called copy(); metadata propagation must not
77
+ # replace those weights with the source's mutable Series.
78
+ self.weights = copied_weights
79
+ return self
80
+
64
81
  @property
65
82
  def _values(self):
66
83
  """Internal access to underlying numpy array without warning."""
@@ -0,0 +1,143 @@
1
+ import copy
2
+ import io
3
+ import pickle
4
+
5
+ import pandas as pd
6
+ import pytest
7
+
8
+ import microdf as mdf
9
+
10
+
11
+ def test_microseries_survives_pickling():
12
+ """Weights must survive a pickle round-trip."""
13
+ import pickle
14
+
15
+ s = mdf.MicroSeries([1, 2, 3], index=[7, 8, 9], weights=[1, 2, 3])
16
+ restored = pickle.loads(pickle.dumps(s))
17
+ assert isinstance(restored, mdf.MicroSeries)
18
+ assert restored.sum() == 14
19
+ assert list(restored.weights) == [1.0, 2.0, 3.0]
20
+
21
+
22
+ def test_microdataframe_survives_pickling():
23
+ """Weights and the weighted aggregations must survive a round-trip."""
24
+ import pickle
25
+
26
+ df = mdf.MicroDataFrame(
27
+ pd.DataFrame({"x": [1, 2, 3]}, index=[7, 8, 9]), weights=[1, 2, 3]
28
+ )
29
+ restored = pickle.loads(pickle.dumps(df))
30
+ assert isinstance(restored, mdf.MicroDataFrame)
31
+ assert isinstance(restored.weights, pd.Series)
32
+ # Would be 6 (unweighted) if the aggregation overrides were not
33
+ # reinstalled after unpickling.
34
+ assert restored.sum()["x"] == 14
35
+
36
+
37
+ def test_deepcopy_preserves_weights():
38
+ df = mdf.MicroDataFrame(pd.DataFrame({"x": [1, 2, 3]}), weights=[1, 2, 3])
39
+ assert copy.deepcopy(df).sum()["x"] == 14
40
+ s = mdf.MicroSeries([1, 2, 3], weights=[1, 2, 3])
41
+ assert copy.deepcopy(s).sum() == 14
42
+
43
+
44
+ @pytest.mark.parametrize("use_weight_column", [False, True])
45
+ @pytest.mark.parametrize("use_pandas_pickle", [False, True])
46
+ def test_serialization_preserves_weight_column_state(
47
+ use_weight_column, use_pandas_pickle
48
+ ):
49
+ """Replacing restored weights can preserve the original weight column."""
50
+ frame = mdf.MicroDataFrame(
51
+ pd.DataFrame({"x": [1, 2, 3], "w": [1, 2, 3]}, index=[7, 8, 9]),
52
+ weights="w" if use_weight_column else [1, 2, 3],
53
+ )
54
+ if use_pandas_pickle:
55
+ buffer = io.BytesIO()
56
+ frame.to_pickle(buffer)
57
+ buffer.seek(0)
58
+ restored = pd.read_pickle(buffer)
59
+ else:
60
+ restored = pickle.loads(pickle.dumps(frame))
61
+
62
+ assert restored.weights_col == ("w" if use_weight_column else None)
63
+ restored.set_weights([3, 2, 1], preserve_old=True)
64
+ assert restored.sum()["x"] == 10
65
+ assert restored.index.equals(frame.index)
66
+ if use_weight_column:
67
+ assert restored["old_w"].tolist() == [1, 2, 3]
68
+ else:
69
+ assert "old_w" not in restored.columns
70
+
71
+
72
+ @pytest.mark.parametrize("operation", ["pickle", "pandas_pickle", "deepcopy"])
73
+ def test_named_microseries_preserves_name_and_weights(operation):
74
+ """Serialization retains pandas metadata as well as survey weights."""
75
+ series = mdf.MicroSeries(
76
+ [1, 2, 3], index=[7, 8, 9], name="group", weights=[1, 2, 3]
77
+ )
78
+ if operation == "deepcopy":
79
+ restored = copy.deepcopy(series)
80
+ elif operation == "pandas_pickle":
81
+ buffer = io.BytesIO()
82
+ series.to_pickle(buffer)
83
+ buffer.seek(0)
84
+ restored = pd.read_pickle(buffer)
85
+ else:
86
+ restored = pickle.loads(pickle.dumps(series))
87
+
88
+ assert restored.name == "group"
89
+ assert restored.index.equals(series.index)
90
+ pd.testing.assert_series_equal(restored.weights, series.weights)
91
+ assert restored.sum() == 14
92
+
93
+
94
+ @pytest.mark.parametrize("selected", [False, True])
95
+ def test_grouped_aggregation_retains_index_name(selected):
96
+ """Copying internal grouped weights must retain the grouping label."""
97
+ frame = mdf.MicroDataFrame(
98
+ {"group": ["a", "a", "b", "b"], "value": [1, 2, 3, 4]},
99
+ weights=[1, 2, 3, 4],
100
+ )
101
+ grouped = frame.groupby("group")
102
+ if selected:
103
+ grouped = grouped[["value"]]
104
+ expected = pd.DataFrame(
105
+ {"value": [5.0, 25.0]}, index=pd.Index(["a", "b"], name="group")
106
+ )
107
+ pd.testing.assert_frame_equal(grouped.sum(), expected)
108
+
109
+
110
+ @pytest.mark.parametrize("kind", ["frame_columns", "frame_index", "series_index"])
111
+ def test_renamed_weights_are_independent(kind):
112
+ """Pandas finalization must retain the renamed result's copied weights."""
113
+ if kind == "series_index":
114
+ original = mdf.MicroSeries(
115
+ [10, 20], index=[7, 8], name="income", weights=[1, 2]
116
+ )
117
+ renamed = original.rename(index={7: 70})
118
+ assert renamed.name == "income"
119
+ else:
120
+ original = mdf.MicroDataFrame(
121
+ {"x": [10, 20], "w": [1, 2]}, index=[7, 8], weights="w"
122
+ )
123
+ renamed = (
124
+ original.rename(columns={"x": "income"})
125
+ if kind == "frame_columns"
126
+ else original.rename(index={7: 70})
127
+ )
128
+ assert renamed.weights_col == "w"
129
+
130
+ renamed.weights.iloc[0] = 100
131
+ pd.testing.assert_series_equal(
132
+ original.weights, pd.Series([1.0, 2.0], index=[7, 8])
133
+ )
134
+ original_total = original.sum() if kind == "series_index" else original.sum()["x"]
135
+ assert original_total == 10 * 1 + 20 * 2
136
+
137
+ original.weights.iloc[1] = 9
138
+ assert renamed.weights.iloc[1] == 2
139
+ if kind == "frame_columns":
140
+ # The renamed frame must still use weighted aggregation after pickle.
141
+ restored = pickle.loads(pickle.dumps(renamed))
142
+ assert restored.weights_col == "w"
143
+ assert restored.sum()["income"] == 10 * 100 + 20 * 2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.7
3
+ Version: 1.3.8
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,6 +11,7 @@ microdf/tests/test_microseries_dataframe.py
11
11
  microdf/tests/test_nullify_weights_index.py
12
12
  microdf/tests/test_pandas3_compatibility.py
13
13
  microdf/tests/test_quantile_missing_values.py
14
+ microdf/tests/test_serialization.py
14
15
  microdf/tests/test_version_metadata.py
15
16
  microdf_python.egg-info/PKG-INFO
16
17
  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.7"
7
+ version = "1.3.8"
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
File without changes