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.
- {microdf_python-1.3.7/microdf_python.egg-info → microdf_python-1.3.8}/PKG-INFO +1 -1
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/microdataframe.py +31 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/microseries.py +17 -0
- microdf_python-1.3.8/microdf/tests/test_serialization.py +143 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8/microdf_python.egg-info}/PKG-INFO +1 -1
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/SOURCES.txt +1 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/pyproject.toml +1 -1
- {microdf_python-1.3.7 → microdf_python-1.3.8}/LICENSE +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/README.md +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/__init__.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/conftest.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_aggregation_errors.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_dataframe_weight_storage.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_microseries_dataframe.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_nullify_weights_index.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_pandas3_compatibility.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_quantile_missing_values.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_version_metadata.py +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/dependency_links.txt +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/requires.txt +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/microdf_python.egg-info/top_level.txt +0 -0
- {microdf_python-1.3.7 → microdf_python-1.3.8}/setup.cfg +0 -0
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{microdf_python-1.3.7 → microdf_python-1.3.8}/microdf/tests/test_dataframe_weight_storage.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|