microdf-python 1.1.0__py3-none-any.whl → 1.1.2__py3-none-any.whl
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/microdataframe.py +8 -0
- microdf/microseries.py +43 -14
- microdf/tests/test_microseries_dataframe.py +79 -0
- {microdf_python-1.1.0.dist-info → microdf_python-1.1.2.dist-info}/METADATA +1 -1
- microdf_python-1.1.2.dist-info/RECORD +10 -0
- microdf_python-1.1.0.dist-info/RECORD +0 -10
- {microdf_python-1.1.0.dist-info → microdf_python-1.1.2.dist-info}/WHEEL +0 -0
- {microdf_python-1.1.0.dist-info → microdf_python-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {microdf_python-1.1.0.dist-info → microdf_python-1.1.2.dist-info}/top_level.txt +0 -0
microdf/microdataframe.py
CHANGED
|
@@ -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):
|
microdf/microseries.py
CHANGED
|
@@ -101,28 +101,48 @@ 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
|
-
|
|
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.
|
|
114
133
|
|
|
115
|
-
|
|
116
|
-
|
|
134
|
+
Uses the inverse CDF method: the q-th quantile is the smallest
|
|
135
|
+
value where the cumulative weight proportion >= q. This matches
|
|
136
|
+
the default behavior of R's survey::svyquantile.
|
|
117
137
|
|
|
118
|
-
:param q:
|
|
119
|
-
:type q: np.array
|
|
138
|
+
:param q: Quantile(s) to calculate, must be in [0, 1].
|
|
139
|
+
:type q: float or np.array
|
|
120
140
|
|
|
121
|
-
:return:
|
|
122
|
-
:rtype: pd.Series
|
|
141
|
+
:return: Weighted quantile value(s).
|
|
142
|
+
:rtype: float or pd.Series
|
|
123
143
|
"""
|
|
124
144
|
values = np.array(self.values)
|
|
125
|
-
quantiles = np.
|
|
145
|
+
quantiles = np.atleast_1d(q)
|
|
126
146
|
sample_weight = np.array(self.weights)
|
|
127
147
|
assert np.all(quantiles >= 0) and np.all(
|
|
128
148
|
quantiles <= 1
|
|
@@ -130,11 +150,20 @@ class MicroSeries(pd.Series):
|
|
|
130
150
|
sorter = np.argsort(values)
|
|
131
151
|
values = values[sorter]
|
|
132
152
|
sample_weight = sample_weight[sorter]
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
result = np.
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
cumsum = np.cumsum(sample_weight)
|
|
154
|
+
cumsum_normalized = cumsum / cumsum[-1]
|
|
155
|
+
result = np.array(
|
|
156
|
+
[
|
|
157
|
+
values[
|
|
158
|
+
min(
|
|
159
|
+
np.searchsorted(cumsum_normalized, qi), len(values) - 1
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
for qi in quantiles
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
if np.array(q).shape == ():
|
|
166
|
+
return result[0]
|
|
138
167
|
return pd.Series(result, index=quantiles)
|
|
139
168
|
|
|
140
169
|
@scalar_function
|
|
@@ -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])
|
|
@@ -112,6 +142,55 @@ def test_median() -> None:
|
|
|
112
142
|
assert series.median() == 4
|
|
113
143
|
|
|
114
144
|
|
|
145
|
+
def test_weighted_quantile_skewed() -> None:
|
|
146
|
+
# 99% of the population has 0 income, 1% has 1M
|
|
147
|
+
# The median should be 0, not an interpolated value
|
|
148
|
+
series = mdf.MicroSeries([0, 1_000_000], weights=[99, 1])
|
|
149
|
+
assert series.median() == 0
|
|
150
|
+
assert series.quantile(0.5) == 0
|
|
151
|
+
# 99th percentile is still 0 since exactly 99% have 0
|
|
152
|
+
assert series.quantile(0.99) == 0
|
|
153
|
+
# Only quantile > 0.99 gives 1M
|
|
154
|
+
assert series.quantile(1.0) == 1_000_000
|
|
155
|
+
# Test multiple quantiles
|
|
156
|
+
result = series.quantile([0.1, 0.5, 0.99, 1.0])
|
|
157
|
+
assert result[0.1] == 0
|
|
158
|
+
assert result[0.5] == 0
|
|
159
|
+
assert result[0.99] == 0
|
|
160
|
+
assert result[1.0] == 1_000_000
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_weighted_quantile_boundaries() -> None:
|
|
164
|
+
# Test q=0 returns minimum, q=1 returns maximum
|
|
165
|
+
series = mdf.MicroSeries([10, 20, 30], weights=[1, 1, 1])
|
|
166
|
+
assert series.quantile(0.0) == 10
|
|
167
|
+
assert series.quantile(1.0) == 30
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_weighted_quantile_equal_weights() -> None:
|
|
171
|
+
# With equal weights, should match "replicated" interpretation
|
|
172
|
+
# Values: 1, 2, 3 each with weight 2 -> like [1,1,2,2,3,3]
|
|
173
|
+
series = mdf.MicroSeries([1, 2, 3], weights=[2, 2, 2])
|
|
174
|
+
# cumsum_normalized = [2/6, 4/6, 6/6] = [0.333, 0.667, 1.0]
|
|
175
|
+
# median (0.5): smallest where cumsum >= 0.5 -> index 1 -> value 2
|
|
176
|
+
assert series.median() == 2
|
|
177
|
+
# 0.25 quantile: smallest where cumsum >= 0.25 -> index 0 -> value 1
|
|
178
|
+
assert series.quantile(0.25) == 1
|
|
179
|
+
# 0.75 quantile: smallest where cumsum >= 0.75 -> index 2 -> value 3
|
|
180
|
+
assert series.quantile(0.75) == 3
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_weighted_quantile_unsorted_input() -> None:
|
|
184
|
+
# Ensure sorting works correctly
|
|
185
|
+
series = mdf.MicroSeries([30, 10, 20], weights=[1, 2, 1])
|
|
186
|
+
# Sorted: values [10, 20, 30], weights [2, 1, 1]
|
|
187
|
+
# cumsum_normalized = [0.5, 0.75, 1.0]
|
|
188
|
+
assert series.quantile(0.0) == 10
|
|
189
|
+
assert series.quantile(0.5) == 10 # cumsum[0]=0.5 >= 0.5
|
|
190
|
+
assert series.quantile(0.6) == 20 # cumsum[1]=0.75 >= 0.6
|
|
191
|
+
assert series.quantile(1.0) == 30
|
|
192
|
+
|
|
193
|
+
|
|
115
194
|
def test_unweighted_groupby() -> None:
|
|
116
195
|
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
|
|
117
196
|
assert (df.groupby("x").z.sum().values == np.array([5.0, 6.0])).all()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
|
|
2
|
+
microdf/microdataframe.py,sha256=h0vIuCACMo1BuV3VKDIEfhc_UwMUV5kUsnlQe3bHfgM,33539
|
|
3
|
+
microdf/microseries.py,sha256=YRnSHVm3mGGd1_-D29BAUDaqWoIiwGjJPKYUrnWbinY,23376
|
|
4
|
+
microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
|
|
5
|
+
microdf/tests/test_microseries_dataframe.py,sha256=lG2_3lJqTdGekbbJRFl0Vp0b4DEQwTk6scXr-ncOHgs,14647
|
|
6
|
+
microdf_python-1.1.2.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
|
|
7
|
+
microdf_python-1.1.2.dist-info/METADATA,sha256=MNlbpTktwqMoFlf-CImA-Xsi-ix7Zv1XAYZ09W1YRcY,2420
|
|
8
|
+
microdf_python-1.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
microdf_python-1.1.2.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
|
|
10
|
+
microdf_python-1.1.2.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
|
|
2
|
-
microdf/microdataframe.py,sha256=X91fUvDS9xMzj2FFAoa6g9EhR_mJv60DXtvK4iIlMrM,33237
|
|
3
|
-
microdf/microseries.py,sha256=MFBStp1IaNVABaf3_Ap_VwXSC3bA8V7rXD4TEG_091g,22452
|
|
4
|
-
microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
|
|
5
|
-
microdf/tests/test_microseries_dataframe.py,sha256=U33uBNW6uiF8c7ygDJjwYX_tJJc_i7OKHi-O746XQLY,11398
|
|
6
|
-
microdf_python-1.1.0.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
|
|
7
|
-
microdf_python-1.1.0.dist-info/METADATA,sha256=vPKXByn0atLs1zxTwc6XqESm7iE1wQ4lNtVIIAPBfQA,2420
|
|
8
|
-
microdf_python-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
microdf_python-1.1.0.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
|
|
10
|
-
microdf_python-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|