microdf-python 1.1.1__tar.gz → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.1.1
3
+ Version: 1.2.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
@@ -44,6 +44,10 @@ class _MicroLocIndexer:
44
44
  self._parent_loc[key] = value
45
45
  self._mdf._link_all_weights()
46
46
 
47
+ def __getattr__(self, name):
48
+ """Delegate unknown attributes to the parent loc indexer."""
49
+ return getattr(self._parent_loc, name)
50
+
47
51
 
48
52
  class _MicroILocIndexer:
49
53
  """Custom iloc indexer that returns MicroDataFrame with proper weights."""
@@ -85,6 +89,10 @@ class _MicroILocIndexer:
85
89
  self._parent_iloc[key] = value
86
90
  self._mdf._link_all_weights()
87
91
 
92
+ def __getattr__(self, name):
93
+ """Delegate unknown attributes to the parent iloc indexer."""
94
+ return getattr(self._parent_iloc, name)
95
+
88
96
 
89
97
  class MicroDataFrame(pd.DataFrame):
90
98
  def __init__(self, *args, weights=None, **kwargs):
@@ -101,13 +101,32 @@ class MicroSeries(pd.Series):
101
101
  return self.weights.sum()
102
102
 
103
103
  @scalar_function
104
- def mean(self) -> float:
104
+ def mean(self, skipna: bool = True) -> float:
105
105
  """Calculates the weighted mean of the MicroSeries.
106
106
 
107
+ :param skipna: Exclude NA/null values. If True (default), NaN values
108
+ are excluded. If False, returns NaN if any value is NaN.
109
+ :type skipna: bool
107
110
  :returns: The weighted mean.
108
111
  :rtype: float
109
112
  """
110
- return np.average(self.values, weights=self.weights)
113
+ values = self.values
114
+ weights = self.weights
115
+
116
+ if skipna:
117
+ # Create mask for non-NaN values
118
+ mask = ~pd.isna(values)
119
+ if not mask.any():
120
+ # All values are NaN
121
+ return np.nan
122
+ values = values[mask]
123
+ weights = weights[mask]
124
+
125
+ # If skipna=False and there are any NaN values, return NaN
126
+ if not skipna and pd.isna(values).any():
127
+ return np.nan
128
+
129
+ return np.average(values, weights=weights)
111
130
 
112
131
  def quantile(self, q: np.array) -> pd.Series:
113
132
  """Calculates weighted quantiles of the MicroSeries.
@@ -94,6 +94,36 @@ def test_mean() -> None:
94
94
  pass
95
95
 
96
96
 
97
+ def test_mean_skipna() -> None:
98
+ # Test skipna=True (default) - should skip NaN values
99
+ arr = np.array([3.0, np.nan, 2.0])
100
+ w = np.array([4.0, 1.0, 1.0])
101
+ series = mdf.MicroSeries(arr, weights=w)
102
+
103
+ # skipna=True should exclude NaN and its weight
104
+ expected = np.average([3.0, 2.0], weights=[4.0, 1.0])
105
+ assert series.mean(skipna=True) == expected
106
+ assert series.mean() == expected # Default should be skipna=True
107
+
108
+ # Test skipna=False - should return NaN if any value is NaN
109
+ assert np.isnan(series.mean(skipna=False))
110
+
111
+ # Test with all NaN values
112
+ arr_all_nan = np.array([np.nan, np.nan, np.nan])
113
+ w_all_nan = np.array([1.0, 2.0, 3.0])
114
+ series_all_nan = mdf.MicroSeries(arr_all_nan, weights=w_all_nan)
115
+ assert np.isnan(series_all_nan.mean(skipna=True))
116
+ assert np.isnan(series_all_nan.mean(skipna=False))
117
+
118
+ # Test with no NaN values - skipna should not affect result
119
+ arr_no_nan = np.array([3.0, 5.0, 2.0])
120
+ w_no_nan = np.array([4.0, 1.0, 1.0])
121
+ series_no_nan = mdf.MicroSeries(arr_no_nan, weights=w_no_nan)
122
+ expected_no_nan = np.average(arr_no_nan, weights=w_no_nan)
123
+ assert series_no_nan.mean(skipna=True) == expected_no_nan
124
+ assert series_no_nan.mean(skipna=False) == expected_no_nan
125
+
126
+
97
127
  def test_poverty_count() -> None:
98
128
  arr = np.array([10000, 20000, 50000])
99
129
  w = np.array([1123, 1144, 2211])
@@ -0,0 +1,232 @@
1
+ """
2
+ Tests for pandas 3.0.0 compatibility in microdf.
3
+
4
+ These tests verify that microdf works correctly with pandas 3.0.0,
5
+ which introduces:
6
+ 1. PyArrow-backed strings as default (StringDtype)
7
+ 2. Copy-on-Write by default
8
+ 3. Changes to how Series subclasses are handled
9
+ """
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ import pytest
14
+
15
+ from microdf.microseries import MicroSeries
16
+ from microdf.microdataframe import MicroDataFrame
17
+
18
+
19
+ class TestMicroSeriesSubclassPreservation:
20
+ """Test that MicroSeries subclass is preserved across operations."""
21
+
22
+ def test_microseries_set_weights_after_creation(self):
23
+ """
24
+ Ensure set_weights works on MicroSeries.
25
+ This is the error reported in pandas 3:
26
+ AttributeError: 'Series' object has no attribute 'set_weights'
27
+ """
28
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 1.0, 1.0]))
29
+ assert hasattr(ms, "set_weights")
30
+ assert hasattr(ms, "weights")
31
+
32
+ # Should be able to call set_weights
33
+ ms.set_weights(np.array([2.0, 2.0, 2.0]))
34
+ assert np.allclose(ms.weights, [2.0, 2.0, 2.0])
35
+
36
+ def test_microseries_preserved_after_arithmetic(self):
37
+ """
38
+ Arithmetic operations should return MicroSeries, not plain Series.
39
+ """
40
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
41
+
42
+ # Addition
43
+ result = ms + 1
44
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
45
+ assert hasattr(result, "weights")
46
+ assert hasattr(result, "set_weights")
47
+
48
+ # Multiplication
49
+ result = ms * 2
50
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
51
+
52
+ # Division
53
+ result = ms / 2
54
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
55
+
56
+ def test_microseries_preserved_after_comparison(self):
57
+ """
58
+ Comparison operations should return MicroSeries, not plain Series.
59
+ """
60
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
61
+
62
+ # Greater than
63
+ result = ms > 1
64
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
65
+ assert hasattr(result, "weights")
66
+
67
+ # Less than
68
+ result = ms < 3
69
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
70
+
71
+ def test_microseries_preserved_after_indexing(self):
72
+ """
73
+ Indexing operations should return MicroSeries, not plain Series.
74
+ """
75
+ ms = MicroSeries([1, 2, 3, 4, 5], weights=np.array([1.0, 2.0, 3.0, 4.0, 5.0]))
76
+
77
+ # Boolean indexing
78
+ result = ms[ms > 2]
79
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
80
+ assert hasattr(result, "weights")
81
+
82
+ # Slice indexing
83
+ result = ms[1:3]
84
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
85
+
86
+
87
+ class TestMicroDataFrameSubclassPreservation:
88
+ """Test that MicroDataFrame column access returns MicroSeries."""
89
+
90
+ def test_microdataframe_column_returns_microseries(self):
91
+ """
92
+ Accessing a column from MicroDataFrame should return MicroSeries.
93
+ """
94
+ mdf = MicroDataFrame(
95
+ {"a": [1, 2, 3], "b": [4, 5, 6]},
96
+ weights=np.array([1.0, 2.0, 3.0])
97
+ )
98
+
99
+ # Column access
100
+ col = mdf["a"]
101
+ assert isinstance(col, MicroSeries), f"Got {type(col)} instead of MicroSeries"
102
+ assert hasattr(col, "weights")
103
+ assert hasattr(col, "set_weights")
104
+
105
+ def test_microdataframe_operations_preserve_type(self):
106
+ """
107
+ Operations on MicroDataFrame columns should preserve MicroSeries type.
108
+ """
109
+ mdf = MicroDataFrame(
110
+ {"a": [1, 2, 3], "b": [4, 5, 6]},
111
+ weights=np.array([1.0, 2.0, 3.0])
112
+ )
113
+
114
+ # Column operations
115
+ result = mdf["a"] + mdf["b"]
116
+ assert isinstance(result, MicroSeries), f"Got {type(result)} instead of MicroSeries"
117
+ assert hasattr(result, "weights")
118
+
119
+
120
+ class TestStringDtypeHandling:
121
+ """Test that MicroSeries/MicroDataFrame handle pandas 3 string dtypes."""
122
+
123
+ def test_microseries_with_string_data(self):
124
+ """
125
+ MicroSeries should work with string data in pandas 3.
126
+ """
127
+ # Create with string data
128
+ ms = MicroSeries(["a", "b", "c"], weights=np.array([1.0, 2.0, 3.0]))
129
+ assert len(ms) == 3
130
+ assert hasattr(ms, "weights")
131
+
132
+ def test_microdataframe_with_string_columns(self):
133
+ """
134
+ MicroDataFrame should work with string columns in pandas 3.
135
+ """
136
+ mdf = MicroDataFrame(
137
+ {"names": ["alice", "bob", "charlie"], "values": [1, 2, 3]},
138
+ weights=np.array([1.0, 2.0, 3.0])
139
+ )
140
+ assert len(mdf) == 3
141
+
142
+ # String column access should still work
143
+ names = mdf["names"]
144
+ assert len(names) == 3
145
+
146
+
147
+ class TestWeightedOperationsWithPandas3:
148
+ """Test that weighted operations work correctly with pandas 3."""
149
+
150
+ def test_weighted_sum(self):
151
+ """Weighted sum should work correctly."""
152
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
153
+ # Weighted sum: 1*1 + 2*2 + 3*3 = 1 + 4 + 9 = 14
154
+ assert ms.sum() == 14
155
+
156
+ def test_weighted_mean(self):
157
+ """Weighted mean should work correctly."""
158
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
159
+ # Weighted mean: (1*1 + 2*2 + 3*3) / (1 + 2 + 3) = 14 / 6 ≈ 2.333
160
+ assert np.isclose(ms.mean(), 14 / 6)
161
+
162
+ def test_weighted_count(self):
163
+ """Weighted count should return sum of weights."""
164
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
165
+ assert ms.count() == 6.0
166
+
167
+
168
+ class TestCopyOnWriteCompatibility:
169
+ """Test compatibility with pandas 3 Copy-on-Write."""
170
+
171
+ def test_microseries_copy_independent(self):
172
+ """
173
+ Copying a MicroSeries should create an independent copy.
174
+ """
175
+ ms = MicroSeries([1, 2, 3], weights=np.array([1.0, 2.0, 3.0]))
176
+ ms_copy = ms.copy()
177
+
178
+ # Modify original
179
+ ms.set_weights(np.array([4.0, 5.0, 6.0]))
180
+
181
+ # Copy should be unchanged
182
+ assert np.allclose(ms_copy.weights, [1.0, 2.0, 3.0])
183
+
184
+ def test_microdataframe_copy_independent(self):
185
+ """
186
+ Copying a MicroDataFrame should create an independent copy.
187
+ """
188
+ mdf = MicroDataFrame(
189
+ {"a": [1, 2, 3]},
190
+ weights=np.array([1.0, 2.0, 3.0])
191
+ )
192
+ mdf_copy = mdf.copy()
193
+
194
+ # Modify original
195
+ mdf.set_weights(np.array([4.0, 5.0, 6.0]))
196
+
197
+ # Copy should be unchanged
198
+ assert np.allclose(mdf_copy.weights, [1.0, 2.0, 3.0])
199
+
200
+
201
+ class TestGroupByWithPandas3:
202
+ """Test groupby operations with pandas 3."""
203
+
204
+ def test_microseries_groupby_preserves_weights(self):
205
+ """
206
+ GroupBy operations should preserve weights.
207
+ """
208
+ ms = MicroSeries([1, 2, 3, 4], weights=np.array([1.0, 2.0, 3.0, 4.0]))
209
+ groups = pd.Series(["a", "a", "b", "b"])
210
+
211
+ gb = ms.groupby(groups)
212
+ # Should be able to call weighted operations
213
+ result = gb.sum()
214
+ # Group a: 1*1 + 2*2 = 5
215
+ # Group b: 3*3 + 4*4 = 25
216
+ assert result["a"] == 5
217
+ assert result["b"] == 25
218
+
219
+ def test_microdataframe_groupby_preserves_weights(self):
220
+ """
221
+ MicroDataFrame groupby should preserve weights on columns.
222
+ """
223
+ mdf = MicroDataFrame(
224
+ {"group": ["a", "a", "b", "b"], "value": [1, 2, 3, 4]},
225
+ weights=np.array([1.0, 2.0, 3.0, 4.0])
226
+ )
227
+
228
+ gb = mdf.groupby("group")
229
+ result = gb.sum()
230
+
231
+ # Check that weighted sum was computed
232
+ assert "value" in result.columns
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.1.1
3
+ Version: 1.2.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
@@ -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_pandas3_compatibility.py
9
10
  microdf_python.egg-info/PKG-INFO
10
11
  microdf_python.egg-info/SOURCES.txt
11
12
  microdf_python.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "microdf-python"
7
- version = "1.1.1"
7
+ version = "1.2.0"
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