microdf-python 1.1.1__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/__init__.py +14 -0
- microdf/microdataframe.py +883 -0
- microdf/microseries.py +643 -0
- microdf/tests/conftest.py +9 -0
- microdf/tests/test_microseries_dataframe.py +395 -0
- microdf_python-1.1.1.dist-info/METADATA +73 -0
- microdf_python-1.1.1.dist-info/RECORD +10 -0
- microdf_python-1.1.1.dist-info/WHEEL +5 -0
- microdf_python-1.1.1.dist-info/licenses/LICENSE +21 -0
- microdf_python-1.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
import microdf as mdf
|
|
5
|
+
from microdf.microdataframe import MicroDataFrame
|
|
6
|
+
from microdf.microseries import MicroSeries
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_df_init() -> None:
|
|
10
|
+
arr = np.array([0, 1, 1])
|
|
11
|
+
w = np.array([3, 0, 9])
|
|
12
|
+
df = mdf.MicroDataFrame({"a": arr}, weights=w)
|
|
13
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
14
|
+
|
|
15
|
+
df = mdf.MicroDataFrame()
|
|
16
|
+
df["a"] = arr
|
|
17
|
+
df.set_weights(w)
|
|
18
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
19
|
+
|
|
20
|
+
df = mdf.MicroDataFrame()
|
|
21
|
+
df["a"] = arr
|
|
22
|
+
df["w"] = w
|
|
23
|
+
df.set_weight_col("w")
|
|
24
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
25
|
+
|
|
26
|
+
# Test set_weights with string (column name)
|
|
27
|
+
df2 = mdf.MicroDataFrame()
|
|
28
|
+
df2["a"] = arr
|
|
29
|
+
df2["w"] = w
|
|
30
|
+
df2.set_weights("w") # Using string column name instead of set_weight_col
|
|
31
|
+
assert df2.a.mean() == np.average(arr, weights=w)
|
|
32
|
+
assert np.array_equal(df2.weights.values, w)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_handles_empty_index() -> None:
|
|
36
|
+
arr = np.array([0, 1, 1])
|
|
37
|
+
w = np.array([3, 0, 9])
|
|
38
|
+
df = mdf.MicroDataFrame({"a": arr}, weights=w)
|
|
39
|
+
|
|
40
|
+
empty_index = pd.Index([])
|
|
41
|
+
df[empty_index] # Implicit assert; checking for ValueError
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_series_getitem() -> None:
|
|
45
|
+
arr = np.array([0, 1, 1])
|
|
46
|
+
w = np.array([3, 0, 9])
|
|
47
|
+
s = mdf.MicroSeries(arr, weights=w)
|
|
48
|
+
assert s[[1, 2]].sum() == np.sum(arr[[1, 2]] * w[[1, 2]])
|
|
49
|
+
|
|
50
|
+
assert s[1:3].sum() == np.sum(arr[1:3] * w[1:3])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_sum() -> None:
|
|
54
|
+
arr = np.array([0, 1, 1])
|
|
55
|
+
w = np.array([3, 0, 9])
|
|
56
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
57
|
+
assert series.sum() == (arr * w).sum()
|
|
58
|
+
|
|
59
|
+
arr = np.linspace(-20, 100, 100)
|
|
60
|
+
w = np.linspace(1, 3, 100)
|
|
61
|
+
series = mdf.MicroSeries(arr)
|
|
62
|
+
series.set_weights(w)
|
|
63
|
+
assert series.sum() == (arr * w).sum()
|
|
64
|
+
|
|
65
|
+
# Verify that an error is thrown when passing weights of different size
|
|
66
|
+
# from the values.
|
|
67
|
+
w = np.linspace(1, 3, 101)
|
|
68
|
+
series = mdf.MicroSeries(arr)
|
|
69
|
+
try:
|
|
70
|
+
series.set_weights(w)
|
|
71
|
+
assert False
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_mean() -> None:
|
|
77
|
+
arr = np.array([3, 0, 2])
|
|
78
|
+
w = np.array([4, 1, 1])
|
|
79
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
80
|
+
assert series.mean() == np.average(arr, weights=w)
|
|
81
|
+
|
|
82
|
+
arr = np.linspace(-20, 100, 100)
|
|
83
|
+
w = np.linspace(1, 3, 100)
|
|
84
|
+
series = mdf.MicroSeries(arr)
|
|
85
|
+
series.set_weights(w)
|
|
86
|
+
assert series.mean() == np.average(arr, weights=w)
|
|
87
|
+
|
|
88
|
+
w = np.linspace(1, 3, 101)
|
|
89
|
+
series = mdf.MicroSeries(arr)
|
|
90
|
+
try:
|
|
91
|
+
series.set_weights(w)
|
|
92
|
+
assert False
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_poverty_count() -> None:
|
|
98
|
+
arr = np.array([10000, 20000, 50000])
|
|
99
|
+
w = np.array([1123, 1144, 2211])
|
|
100
|
+
df = pd.DataFrame()
|
|
101
|
+
df["income"] = arr
|
|
102
|
+
df["threshold"] = 16000
|
|
103
|
+
df = MicroDataFrame(df, weights=w)
|
|
104
|
+
assert df.poverty_count("income", "threshold") == w[0]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_median() -> None:
|
|
108
|
+
# 1, 2, 3, 4, *4*, 4, 5, 5, 5
|
|
109
|
+
arr = np.array([1, 2, 3, 4, 5])
|
|
110
|
+
w = np.array([1, 1, 1, 3, 3])
|
|
111
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
112
|
+
assert series.median() == 4
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_weighted_quantile_skewed() -> None:
|
|
116
|
+
# 99% of the population has 0 income, 1% has 1M
|
|
117
|
+
# The median should be 0, not an interpolated value
|
|
118
|
+
series = mdf.MicroSeries([0, 1_000_000], weights=[99, 1])
|
|
119
|
+
assert series.median() == 0
|
|
120
|
+
assert series.quantile(0.5) == 0
|
|
121
|
+
# 99th percentile is still 0 since exactly 99% have 0
|
|
122
|
+
assert series.quantile(0.99) == 0
|
|
123
|
+
# Only quantile > 0.99 gives 1M
|
|
124
|
+
assert series.quantile(1.0) == 1_000_000
|
|
125
|
+
# Test multiple quantiles
|
|
126
|
+
result = series.quantile([0.1, 0.5, 0.99, 1.0])
|
|
127
|
+
assert result[0.1] == 0
|
|
128
|
+
assert result[0.5] == 0
|
|
129
|
+
assert result[0.99] == 0
|
|
130
|
+
assert result[1.0] == 1_000_000
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_weighted_quantile_boundaries() -> None:
|
|
134
|
+
# Test q=0 returns minimum, q=1 returns maximum
|
|
135
|
+
series = mdf.MicroSeries([10, 20, 30], weights=[1, 1, 1])
|
|
136
|
+
assert series.quantile(0.0) == 10
|
|
137
|
+
assert series.quantile(1.0) == 30
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_weighted_quantile_equal_weights() -> None:
|
|
141
|
+
# With equal weights, should match "replicated" interpretation
|
|
142
|
+
# Values: 1, 2, 3 each with weight 2 -> like [1,1,2,2,3,3]
|
|
143
|
+
series = mdf.MicroSeries([1, 2, 3], weights=[2, 2, 2])
|
|
144
|
+
# cumsum_normalized = [2/6, 4/6, 6/6] = [0.333, 0.667, 1.0]
|
|
145
|
+
# median (0.5): smallest where cumsum >= 0.5 -> index 1 -> value 2
|
|
146
|
+
assert series.median() == 2
|
|
147
|
+
# 0.25 quantile: smallest where cumsum >= 0.25 -> index 0 -> value 1
|
|
148
|
+
assert series.quantile(0.25) == 1
|
|
149
|
+
# 0.75 quantile: smallest where cumsum >= 0.75 -> index 2 -> value 3
|
|
150
|
+
assert series.quantile(0.75) == 3
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def test_weighted_quantile_unsorted_input() -> None:
|
|
154
|
+
# Ensure sorting works correctly
|
|
155
|
+
series = mdf.MicroSeries([30, 10, 20], weights=[1, 2, 1])
|
|
156
|
+
# Sorted: values [10, 20, 30], weights [2, 1, 1]
|
|
157
|
+
# cumsum_normalized = [0.5, 0.75, 1.0]
|
|
158
|
+
assert series.quantile(0.0) == 10
|
|
159
|
+
assert series.quantile(0.5) == 10 # cumsum[0]=0.5 >= 0.5
|
|
160
|
+
assert series.quantile(0.6) == 20 # cumsum[1]=0.75 >= 0.6
|
|
161
|
+
assert series.quantile(1.0) == 30
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_unweighted_groupby() -> None:
|
|
165
|
+
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
|
|
166
|
+
assert (df.groupby("x").z.sum().values == np.array([5.0, 6.0])).all()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_multiple_groupby() -> None:
|
|
170
|
+
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
|
|
171
|
+
assert (df.groupby(["x", "y"]).z.sum() == np.array([5, 6])).all()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_set_index() -> None:
|
|
175
|
+
d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
|
|
176
|
+
assert d.x.__class__ == MicroSeries
|
|
177
|
+
d.index = [1, 2, 3]
|
|
178
|
+
assert d.x.__class__ == MicroSeries
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_reset_index() -> None:
|
|
182
|
+
d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
|
|
183
|
+
assert d.reset_index().__class__ == MicroDataFrame
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_cumsum() -> None:
|
|
187
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
188
|
+
assert np.array_equal(s.cumsum().values, [4, 14, 32])
|
|
189
|
+
|
|
190
|
+
s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
|
|
191
|
+
assert np.array_equal(s.cumsum().values, [10, 14, 32])
|
|
192
|
+
|
|
193
|
+
s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
|
|
194
|
+
assert np.array_equal(s.cumsum().values, [18, 22, 32])
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_rank() -> None:
|
|
198
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
199
|
+
assert np.array_equal(s.rank().values, [4, 9, 15])
|
|
200
|
+
|
|
201
|
+
s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
|
|
202
|
+
assert np.array_equal(s.rank().values, [15, 4, 9])
|
|
203
|
+
|
|
204
|
+
s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
|
|
205
|
+
assert np.array_equal(s.rank().values, [9, 4, 15])
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_percentile_rank() -> None:
|
|
209
|
+
s = mdf.MicroSeries([4, 2, 3, 1], weights=[20, 40, 20, 20])
|
|
210
|
+
assert np.array_equal(s.percentile_rank().values, [100, 60, 80, 20])
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_quartile_rank() -> None:
|
|
214
|
+
s = mdf.MicroSeries([4, 2, 3], weights=[25, 50, 25])
|
|
215
|
+
assert np.array_equal(s.quartile_rank().values, [4, 2, 3])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_quintile_rank() -> None:
|
|
219
|
+
s = mdf.MicroSeries([4, 2, 3], weights=[20, 60, 20])
|
|
220
|
+
assert np.array_equal(s.quintile_rank().values, [5, 3, 4])
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_decile_rank() -> None:
|
|
224
|
+
s = mdf.MicroSeries(
|
|
225
|
+
[5, 4, 3, 2, 1, 6, 7, 8, 9],
|
|
226
|
+
weights=[10, 20, 10, 10, 10, 10, 10, 10, 10],
|
|
227
|
+
)
|
|
228
|
+
assert np.array_equal(s.decile_rank().values, [6, 5, 3, 2, 1, 7, 8, 9, 10])
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_copy_equals() -> None:
|
|
232
|
+
d = mdf.MicroDataFrame(
|
|
233
|
+
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
234
|
+
)
|
|
235
|
+
d_copy = d.copy()
|
|
236
|
+
d_copy_diff_weights = d_copy.copy()
|
|
237
|
+
d_copy_diff_weights.weights *= 2
|
|
238
|
+
assert d.equals(d_copy)
|
|
239
|
+
assert not d.equals(d_copy_diff_weights)
|
|
240
|
+
# Same for a MicroSeries.
|
|
241
|
+
assert d.x.equals(d_copy.x)
|
|
242
|
+
assert not d.x.equals(d_copy_diff_weights.x)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_subset() -> None:
|
|
246
|
+
df = mdf.MicroDataFrame(
|
|
247
|
+
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
248
|
+
)
|
|
249
|
+
df_no_z = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4]}, weights=[7, 8])
|
|
250
|
+
assert df[["x", "y"]].equals(df_no_z)
|
|
251
|
+
df_no_z_diff_weights = df_no_z.copy()
|
|
252
|
+
df_no_z_diff_weights.weights += 1
|
|
253
|
+
assert not df[["x", "y"]].equals(df_no_z_diff_weights)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_value_subset() -> None:
|
|
257
|
+
d = mdf.MicroDataFrame({"x": [1, 2, 3], "y": [1, 2, 2]}, weights=[4, 5, 6])
|
|
258
|
+
d2 = d[d.y > 1]
|
|
259
|
+
assert d2.y.shape == d2.weights.shape
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_bitwise_ops_return_microseries() -> None:
|
|
263
|
+
s1 = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
|
|
264
|
+
s2 = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
|
|
265
|
+
and_result = s1 & s2
|
|
266
|
+
or_result = s1 | s2
|
|
267
|
+
assert isinstance(and_result, mdf.MicroSeries)
|
|
268
|
+
assert isinstance(or_result, mdf.MicroSeries)
|
|
269
|
+
expected_and = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
|
|
270
|
+
expected_or = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
|
|
271
|
+
assert and_result.equals(expected_and)
|
|
272
|
+
assert or_result.equals(expected_or)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_additional_ops_return_microseries() -> None:
|
|
276
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
277
|
+
radd = 1 + s
|
|
278
|
+
xor = s ^ mdf.MicroSeries([0, 1, 0], weights=[4, 5, 6])
|
|
279
|
+
inv = ~mdf.MicroSeries([True, False], weights=[1, 1])
|
|
280
|
+
assert isinstance(radd, mdf.MicroSeries)
|
|
281
|
+
assert isinstance(xor, mdf.MicroSeries)
|
|
282
|
+
assert isinstance(inv, mdf.MicroSeries)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_reset_index_inplace() -> None:
|
|
286
|
+
df = pd.DataFrame(
|
|
287
|
+
{"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=["a", "b", "c", "d"]
|
|
288
|
+
)
|
|
289
|
+
weights = np.array([0.1, 0.2, 0.3, 0.4])
|
|
290
|
+
mdf = MicroDataFrame(df, weights=weights)
|
|
291
|
+
|
|
292
|
+
# Test 1: reset_index with inplace=False (default)
|
|
293
|
+
mdf_copy = mdf.copy()
|
|
294
|
+
result = mdf_copy.reset_index()
|
|
295
|
+
assert list(mdf_copy.index) == ["a", "b", "c", "d"]
|
|
296
|
+
assert list(result.index) == [0, 1, 2, 3]
|
|
297
|
+
assert "index" in result.columns
|
|
298
|
+
assert list(result["index"]) == ["a", "b", "c", "d"]
|
|
299
|
+
np.testing.assert_array_equal(result.weights.values, weights)
|
|
300
|
+
|
|
301
|
+
# Test 2: reset_index with inplace=True
|
|
302
|
+
mdf_copy = mdf.copy()
|
|
303
|
+
result = mdf_copy.reset_index(inplace=True)
|
|
304
|
+
assert result is None
|
|
305
|
+
assert list(mdf_copy.index) == [0, 1, 2, 3]
|
|
306
|
+
assert "index" in mdf_copy.columns
|
|
307
|
+
assert list(mdf_copy["index"]) == ["a", "b", "c", "d"]
|
|
308
|
+
np.testing.assert_array_equal(mdf_copy.weights.values, weights)
|
|
309
|
+
assert isinstance(mdf_copy["A"], MicroSeries)
|
|
310
|
+
assert isinstance(mdf_copy["B"], MicroSeries)
|
|
311
|
+
assert isinstance(mdf_copy["index"], MicroSeries)
|
|
312
|
+
|
|
313
|
+
# Test 3: reset_index with drop=True
|
|
314
|
+
mdf_copy = mdf.copy()
|
|
315
|
+
mdf_copy.reset_index(drop=True, inplace=True)
|
|
316
|
+
assert list(mdf_copy.index) == [0, 1, 2, 3]
|
|
317
|
+
assert "index" not in mdf_copy.columns
|
|
318
|
+
assert list(mdf_copy.columns) == ["A", "B"]
|
|
319
|
+
np.testing.assert_array_equal(mdf_copy.weights.values, weights)
|
|
320
|
+
|
|
321
|
+
# Test 4: Multi-level index
|
|
322
|
+
arrays = [["bar", "bar", "baz", "baz"], ["one", "two", "one", "two"]]
|
|
323
|
+
multi_index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
|
|
324
|
+
df_multi = pd.DataFrame(
|
|
325
|
+
{"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index
|
|
326
|
+
)
|
|
327
|
+
mdf_multi = MicroDataFrame(df_multi, weights=weights)
|
|
328
|
+
result = mdf_multi.reset_index(level="first")
|
|
329
|
+
assert "first" in result.columns
|
|
330
|
+
assert result.index.name == "second"
|
|
331
|
+
np.testing.assert_array_equal(result.weights.values, weights)
|
|
332
|
+
|
|
333
|
+
# Reset all levels in place
|
|
334
|
+
mdf_multi.reset_index(inplace=True)
|
|
335
|
+
assert "first" in mdf_multi.columns
|
|
336
|
+
assert "second" in mdf_multi.columns
|
|
337
|
+
assert list(mdf_multi.index) == [0, 1, 2, 3]
|
|
338
|
+
np.testing.assert_array_equal(mdf_multi.weights.values, weights)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_loc_preserves_weights() -> None:
|
|
342
|
+
"""Test that .loc[] returns MicroDataFrame with proper weights (issue
|
|
343
|
+
#265)."""
|
|
344
|
+
df = mdf.MicroDataFrame(
|
|
345
|
+
{"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50]
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Filter all rows (should get same weights)
|
|
349
|
+
filtered = df.loc[df.one == 1]
|
|
350
|
+
assert isinstance(filtered, MicroDataFrame)
|
|
351
|
+
assert filtered.one.sum() == 150.0 # Weighted sum
|
|
352
|
+
|
|
353
|
+
# Partial filter
|
|
354
|
+
df2 = mdf.MicroDataFrame(
|
|
355
|
+
{"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
|
|
356
|
+
)
|
|
357
|
+
subset = df2.loc[df2.x > 2]
|
|
358
|
+
assert isinstance(subset, MicroDataFrame)
|
|
359
|
+
assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
|
|
360
|
+
np.testing.assert_array_equal(subset.weights.values, [30.0, 40.0, 50.0])
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_iloc_preserves_weights() -> None:
|
|
364
|
+
"""Test that .iloc[] returns MicroDataFrame with proper weights."""
|
|
365
|
+
df = mdf.MicroDataFrame(
|
|
366
|
+
{"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Select rows by position
|
|
370
|
+
subset = df.iloc[2:5]
|
|
371
|
+
assert isinstance(subset, MicroDataFrame)
|
|
372
|
+
assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
|
|
373
|
+
np.testing.assert_array_equal(subset.weights.values, [30.0, 40.0, 50.0])
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_groupby_column_selection() -> None:
|
|
377
|
+
"""Test that groupby column selection preserves weights (issue #193)."""
|
|
378
|
+
d = mdf.MicroDataFrame(
|
|
379
|
+
dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6]
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# Test single column string selection
|
|
383
|
+
result_str = d.groupby("g")["y"].sum()
|
|
384
|
+
assert result_str["a"] == 14.0 # 1*4 + 2*5 = 14
|
|
385
|
+
assert result_str["b"] == 18.0 # 3*6 = 18
|
|
386
|
+
|
|
387
|
+
# Test list column selection
|
|
388
|
+
result_list = d.groupby("g")[["y"]].sum()
|
|
389
|
+
assert result_list.loc["a", "y"] == 14.0
|
|
390
|
+
assert result_list.loc["b", "y"] == 18.0
|
|
391
|
+
|
|
392
|
+
# Aggregated results should be plain DataFrame (no spurious weight column)
|
|
393
|
+
result_all = d.groupby("g").sum()
|
|
394
|
+
assert "weight" not in result_all.columns
|
|
395
|
+
assert list(result_all.columns) == ["y"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: microdf-python
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
|
+
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: codecov; extra == "dev"
|
|
14
|
+
Requires-Dist: flake8; extra == "dev"
|
|
15
|
+
Requires-Dist: flake8-pyproject; extra == "dev"
|
|
16
|
+
Requires-Dist: black; extra == "dev"
|
|
17
|
+
Requires-Dist: docformatter; extra == "dev"
|
|
18
|
+
Requires-Dist: isort; extra == "dev"
|
|
19
|
+
Requires-Dist: linecheck; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
22
|
+
Requires-Dist: setuptools; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
[](https://github.com/PolicyEngine/microdf/actions)
|
|
26
|
+
[](https://codecov.io/gh/PolicyEngine/microdf)
|
|
27
|
+
|
|
28
|
+
# microdf
|
|
29
|
+
Weighted pandas DataFrames and Series for survey microdata analysis.
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
microdf provides `MicroDataFrame` and `MicroSeries` classes that extend pandas functionality with integrated weighting support, essential for accurate survey data analysis.
|
|
33
|
+
|
|
34
|
+
## Key Features
|
|
35
|
+
- **MicroDataFrame**: A pandas DataFrame with an integrated weight column
|
|
36
|
+
- **MicroSeries**: A pandas Series with integrated weights
|
|
37
|
+
- **Weighted operations**: All aggregations (sum, mean, median, etc.) automatically use weights
|
|
38
|
+
- **Inequality metrics**: Built-in Gini coefficient calculation
|
|
39
|
+
- **Poverty analysis**: Integrated poverty rate and gap calculations
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
Install with:
|
|
43
|
+
|
|
44
|
+
pip install microdf-python
|
|
45
|
+
|
|
46
|
+
Or for development:
|
|
47
|
+
|
|
48
|
+
pip install git+https://github.com/PolicyEngine/microdf.git
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
```python
|
|
52
|
+
import microdf as mdf
|
|
53
|
+
import pandas as pd
|
|
54
|
+
|
|
55
|
+
# Create sample data with weights
|
|
56
|
+
df = pd.DataFrame({
|
|
57
|
+
'income': [10_000, 20_000, 30_000, 40_000, 50_000],
|
|
58
|
+
'weights': [1, 2, 3, 2, 1]
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
# Create a MicroDataFrame
|
|
62
|
+
mdf_df = mdf.MicroDataFrame(df, weights='weights')
|
|
63
|
+
|
|
64
|
+
# All operations are weight-aware
|
|
65
|
+
print(mdf_df.income.mean()) # Weighted mean
|
|
66
|
+
print(mdf_df.income.gini()) # Gini coefficient
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Questions
|
|
70
|
+
Contact the maintainer, Max Ghenis (max@policyengine.org).
|
|
71
|
+
|
|
72
|
+
## Citation
|
|
73
|
+
You may cite the source of your analysis as "microdf release #.#.#, author's calculations."
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
|
|
2
|
+
microdf/microdataframe.py,sha256=X91fUvDS9xMzj2FFAoa6g9EhR_mJv60DXtvK4iIlMrM,33237
|
|
3
|
+
microdf/microseries.py,sha256=Z6ElPGlKikM3MwQo1aNbprsJW7ZqFZBRYk39C--XdJk,22719
|
|
4
|
+
microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
|
|
5
|
+
microdf/tests/test_microseries_dataframe.py,sha256=alhJOVhPQIwNPaZNQDFCpIzRbljWgY-o2UWhGZ2OJvM,13388
|
|
6
|
+
microdf_python-1.1.1.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
|
|
7
|
+
microdf_python-1.1.1.dist-info/METADATA,sha256=y0QTwJLvGE57pl3iF94_7R6CzMlwEgPQYV-KosS44y4,2420
|
|
8
|
+
microdf_python-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
microdf_python-1.1.1.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
|
|
10
|
+
microdf_python-1.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Max Ghenis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
microdf
|