microdf-python 0.4.4__py3-none-any.whl → 0.4.6__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 +6 -6
- microdf/_optional.py +8 -9
- microdf/agg.py +12 -15
- microdf/chart_utils.py +12 -7
- microdf/charts.py +15 -9
- microdf/concat.py +8 -5
- microdf/custom_taxes.py +51 -57
- microdf/generic.py +208 -107
- microdf/income_measures.py +10 -11
- microdf/inequality.py +71 -22
- microdf/io.py +8 -10
- microdf/poverty.py +11 -12
- microdf/style.py +5 -5
- microdf/tax.py +19 -21
- microdf/taxcalc.py +33 -23
- microdf/tests/conftest.py +9 -0
- microdf/tests/test_compare.py +48 -0
- microdf/tests/test_decile_rank.py +91 -0
- microdf/tests/test_generic.py +232 -0
- microdf/tests/test_inequality.py +20 -0
- microdf/tests/test_io.py +10 -0
- microdf/tests/test_optional_dependency.py +54 -0
- microdf/tests/test_poverty.py +73 -0
- microdf/tests/test_quantile_chg.py +18 -0
- microdf/tests/test_tax.py +54 -0
- microdf/tests/test_taxcalc.py +25 -0
- microdf/tests/test_utils.py +39 -0
- microdf/tests/test_weighted.py +74 -0
- microdf/ubi.py +17 -15
- microdf/utils.py +14 -15
- microdf/weighted.py +61 -31
- microdf_python-0.4.6.dist-info/METADATA +54 -0
- microdf_python-0.4.6.dist-info/RECORD +37 -0
- microdf_python-0.4.4.dist-info/METADATA +0 -25
- microdf_python-0.4.4.dist-info/RECORD +0 -24
- {microdf_python-0.4.4.dist-info → microdf_python-0.4.6.dist-info}/WHEEL +0 -0
- {microdf_python-0.4.4.dist-info → microdf_python-0.4.6.dist-info}/licenses/LICENSE +0 -0
- {microdf_python-0.4.4.dist-info → microdf_python-0.4.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
import microdf as mdf
|
|
5
|
+
from microdf.generic import MicroDataFrame, MicroSeries
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_df_init() -> None:
|
|
9
|
+
arr = np.array([0, 1, 1])
|
|
10
|
+
w = np.array([3, 0, 9])
|
|
11
|
+
df = mdf.MicroDataFrame({"a": arr}, weights=w)
|
|
12
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
13
|
+
|
|
14
|
+
df = mdf.MicroDataFrame()
|
|
15
|
+
df["a"] = arr
|
|
16
|
+
df.set_weights(w)
|
|
17
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
18
|
+
|
|
19
|
+
df = mdf.MicroDataFrame()
|
|
20
|
+
df["a"] = arr
|
|
21
|
+
df["w"] = w
|
|
22
|
+
df.set_weight_col("w")
|
|
23
|
+
assert df.a.mean() == np.average(arr, weights=w)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_series_getitem() -> None:
|
|
27
|
+
arr = np.array([0, 1, 1])
|
|
28
|
+
w = np.array([3, 0, 9])
|
|
29
|
+
s = mdf.MicroSeries(arr, weights=w)
|
|
30
|
+
assert s[[1, 2]].sum() == np.sum(arr[[1, 2]] * w[[1, 2]])
|
|
31
|
+
|
|
32
|
+
assert s[1:3].sum() == np.sum(arr[1:3] * w[1:3])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_sum() -> None:
|
|
36
|
+
arr = np.array([0, 1, 1])
|
|
37
|
+
w = np.array([3, 0, 9])
|
|
38
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
39
|
+
assert series.sum() == (arr * w).sum()
|
|
40
|
+
|
|
41
|
+
arr = np.linspace(-20, 100, 100)
|
|
42
|
+
w = np.linspace(1, 3, 100)
|
|
43
|
+
series = mdf.MicroSeries(arr)
|
|
44
|
+
series.set_weights(w)
|
|
45
|
+
assert series.sum() == (arr * w).sum()
|
|
46
|
+
|
|
47
|
+
# Verify that an error is thrown when passing weights of different size
|
|
48
|
+
# from the values.
|
|
49
|
+
w = np.linspace(1, 3, 101)
|
|
50
|
+
series = mdf.MicroSeries(arr)
|
|
51
|
+
try:
|
|
52
|
+
series.set_weights(w)
|
|
53
|
+
assert False
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_mean() -> None:
|
|
59
|
+
arr = np.array([3, 0, 2])
|
|
60
|
+
w = np.array([4, 1, 1])
|
|
61
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
62
|
+
assert series.mean() == np.average(arr, weights=w)
|
|
63
|
+
|
|
64
|
+
arr = np.linspace(-20, 100, 100)
|
|
65
|
+
w = np.linspace(1, 3, 100)
|
|
66
|
+
series = mdf.MicroSeries(arr)
|
|
67
|
+
series.set_weights(w)
|
|
68
|
+
assert series.mean() == np.average(arr, weights=w)
|
|
69
|
+
|
|
70
|
+
w = np.linspace(1, 3, 101)
|
|
71
|
+
series = mdf.MicroSeries(arr)
|
|
72
|
+
try:
|
|
73
|
+
series.set_weights(w)
|
|
74
|
+
assert False
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_poverty_count() -> None:
|
|
80
|
+
arr = np.array([10000, 20000, 50000])
|
|
81
|
+
w = np.array([1123, 1144, 2211])
|
|
82
|
+
df = MicroDataFrame(weights=w)
|
|
83
|
+
df["income"] = arr
|
|
84
|
+
df["threshold"] = 16000
|
|
85
|
+
assert df.poverty_count("income", "threshold") == w[0]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_median() -> None:
|
|
89
|
+
# 1, 2, 3, 4, *4*, 4, 5, 5, 5
|
|
90
|
+
arr = np.array([1, 2, 3, 4, 5])
|
|
91
|
+
w = np.array([1, 1, 1, 3, 3])
|
|
92
|
+
series = mdf.MicroSeries(arr, weights=w)
|
|
93
|
+
assert series.median() == 4
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_unweighted_groupby() -> None:
|
|
97
|
+
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
|
|
98
|
+
assert (df.groupby("x").z.sum().values == np.array([5.0, 6.0])).all()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_multiple_groupby() -> None:
|
|
102
|
+
df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
|
|
103
|
+
assert (df.groupby(["x", "y"]).z.sum() == np.array([5, 6])).all()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_concat() -> None:
|
|
107
|
+
df1 = mdf.MicroDataFrame({"x": [1, 2]}, weights=[3, 4])
|
|
108
|
+
df2 = mdf.MicroDataFrame({"y": [5, 6]}, weights=[7, 8])
|
|
109
|
+
# Verify that pd.concat returns DataFrame (probably no way to fix this).
|
|
110
|
+
pd_long = pd.concat([df1, df2])
|
|
111
|
+
assert isinstance(pd_long, pd.DataFrame)
|
|
112
|
+
assert not isinstance(pd_long, mdf.MicroDataFrame)
|
|
113
|
+
# Verify that mdf.concat works.
|
|
114
|
+
mdf_long = mdf.concat([df1, df2])
|
|
115
|
+
assert isinstance(mdf_long, mdf.MicroDataFrame)
|
|
116
|
+
# Weights should be preserved.
|
|
117
|
+
assert mdf_long.weights.equals(pd.concat([df1.weights, df2.weights]))
|
|
118
|
+
# Verify it works horizontally too (take the first set of weights).
|
|
119
|
+
mdf_wide = mdf.concat([df1, df2], axis=1)
|
|
120
|
+
assert isinstance(mdf_wide, mdf.MicroDataFrame)
|
|
121
|
+
assert mdf_wide.weights.equals(df1.weights)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_set_index() -> None:
|
|
125
|
+
d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
|
|
126
|
+
assert d.x.__class__ == MicroSeries
|
|
127
|
+
d.index = [1, 2, 3]
|
|
128
|
+
assert d.x.__class__ == MicroSeries
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_reset_index() -> None:
|
|
132
|
+
d = mdf.MicroDataFrame(dict(x=[1, 2, 3]), weights=[4, 5, 6])
|
|
133
|
+
assert d.reset_index().__class__ == MicroDataFrame
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_cumsum() -> None:
|
|
137
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
138
|
+
assert np.array_equal(s.cumsum().values, [4, 14, 32])
|
|
139
|
+
|
|
140
|
+
s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
|
|
141
|
+
assert np.array_equal(s.cumsum().values, [10, 14, 32])
|
|
142
|
+
|
|
143
|
+
s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
|
|
144
|
+
assert np.array_equal(s.cumsum().values, [18, 22, 32])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_rank() -> None:
|
|
148
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
149
|
+
assert np.array_equal(s.rank().values, [4, 9, 15])
|
|
150
|
+
|
|
151
|
+
s = mdf.MicroSeries([3, 1, 2], weights=[6, 4, 5])
|
|
152
|
+
assert np.array_equal(s.rank().values, [15, 4, 9])
|
|
153
|
+
|
|
154
|
+
s = mdf.MicroSeries([2, 1, 3], weights=[5, 4, 6])
|
|
155
|
+
assert np.array_equal(s.rank().values, [9, 4, 15])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_percentile_rank() -> None:
|
|
159
|
+
s = mdf.MicroSeries([4, 2, 3, 1], weights=[20, 40, 20, 20])
|
|
160
|
+
assert np.array_equal(s.percentile_rank().values, [100, 60, 80, 20])
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_quartile_rank() -> None:
|
|
164
|
+
s = mdf.MicroSeries([4, 2, 3], weights=[25, 50, 25])
|
|
165
|
+
assert np.array_equal(s.quartile_rank().values, [4, 2, 3])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_quintile_rank() -> None:
|
|
169
|
+
s = mdf.MicroSeries([4, 2, 3], weights=[20, 60, 20])
|
|
170
|
+
assert np.array_equal(s.quintile_rank().values, [5, 3, 4])
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_decile_rank_rank() -> None:
|
|
174
|
+
s = mdf.MicroSeries(
|
|
175
|
+
[5, 4, 3, 2, 1, 6, 7, 8, 9],
|
|
176
|
+
weights=[10, 20, 10, 10, 10, 10, 10, 10, 10, 10],
|
|
177
|
+
)
|
|
178
|
+
assert np.array_equal(s.decile_rank().values, [6, 5, 3, 2, 1, 7, 8, 9, 10])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_copy_equals() -> None:
|
|
182
|
+
d = mdf.MicroDataFrame(
|
|
183
|
+
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
184
|
+
)
|
|
185
|
+
d_copy = d.copy()
|
|
186
|
+
d_copy_diff_weights = d_copy.copy()
|
|
187
|
+
d_copy_diff_weights.weights *= 2
|
|
188
|
+
assert d.equals(d_copy)
|
|
189
|
+
assert not d.equals(d_copy_diff_weights)
|
|
190
|
+
# Same for a MicroSeries.
|
|
191
|
+
assert d.x.equals(d_copy.x)
|
|
192
|
+
assert not d.x.equals(d_copy_diff_weights.x)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_subset() -> None:
|
|
196
|
+
df = mdf.MicroDataFrame(
|
|
197
|
+
{"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
|
|
198
|
+
)
|
|
199
|
+
df_no_z = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4]}, weights=[7, 8])
|
|
200
|
+
assert df[["x", "y"]].equals(df_no_z)
|
|
201
|
+
df_no_z_diff_weights = df_no_z.copy()
|
|
202
|
+
df_no_z_diff_weights.weights += 1
|
|
203
|
+
assert not df[["x", "y"]].equals(df_no_z_diff_weights)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_value_subset() -> None:
|
|
207
|
+
d = mdf.MicroDataFrame({"x": [1, 2, 3], "y": [1, 2, 2]}, weights=[4, 5, 6])
|
|
208
|
+
d2 = d[d.y > 1]
|
|
209
|
+
assert d2.y.shape == d2.weights.shape
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_bitwise_ops_return_microseries() -> None:
|
|
213
|
+
s1 = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
|
|
214
|
+
s2 = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
|
|
215
|
+
and_result = s1 & s2
|
|
216
|
+
or_result = s1 | s2
|
|
217
|
+
assert isinstance(and_result, mdf.MicroSeries)
|
|
218
|
+
assert isinstance(or_result, mdf.MicroSeries)
|
|
219
|
+
expected_and = mdf.MicroSeries([False, False, True], weights=[1, 2, 3])
|
|
220
|
+
expected_or = mdf.MicroSeries([True, False, True], weights=[1, 2, 3])
|
|
221
|
+
assert and_result.equals(expected_and)
|
|
222
|
+
assert or_result.equals(expected_or)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_additional_ops_return_microseries() -> None:
|
|
226
|
+
s = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
|
|
227
|
+
radd = 1 + s
|
|
228
|
+
xor = s ^ mdf.MicroSeries([0, 1, 0], weights=[4, 5, 6])
|
|
229
|
+
inv = ~mdf.MicroSeries([True, False], weights=[1, 1])
|
|
230
|
+
assert isinstance(radd, mdf.MicroSeries)
|
|
231
|
+
assert isinstance(xor, mdf.MicroSeries)
|
|
232
|
+
assert isinstance(inv, mdf.MicroSeries)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import microdf as mdf
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_top_pct() -> None:
|
|
7
|
+
x = list(range(1, 11)) # 1 to 10. Sum = 10 * 11 / 2 = 55.
|
|
8
|
+
df = pd.DataFrame({"x": x})
|
|
9
|
+
ms = mdf.MicroSeries(x)
|
|
10
|
+
RES = 10 / 55
|
|
11
|
+
assert mdf.top_10_pct_share(df, "x") == RES
|
|
12
|
+
assert ms.top_10_pct_share() == RES
|
|
13
|
+
x = list(range(1, 4))
|
|
14
|
+
df = pd.DataFrame({"x": x, "w": x})
|
|
15
|
+
ms = mdf.MicroSeries(x, weights=x)
|
|
16
|
+
# This is equivalent to [1, 2, 2, 3, 3, 3]
|
|
17
|
+
# Sum = 14, top half is 9.
|
|
18
|
+
RES = 9 / 14
|
|
19
|
+
assert mdf.top_50_pct_share(df, "x", "w") == RES
|
|
20
|
+
assert ms.top_50_pct_share() == RES
|
microdf/tests/test_io.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import microdf as mdf
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_read_stata_zip() -> None:
|
|
5
|
+
""""""
|
|
6
|
+
SCF2016 = "https://www.federalreserve.gov/econres/files/scfp2016s.zip"
|
|
7
|
+
COLS = ["wgt", "networth"]
|
|
8
|
+
df = mdf.read_stata_zip(SCF2016, columns=COLS)
|
|
9
|
+
assert df.columns.tolist() == COLS
|
|
10
|
+
assert df.shape[0] > 0
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import types
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from microdf._optional import VERSIONS, import_optional_dependency
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_import_optional() -> None:
|
|
10
|
+
""""""
|
|
11
|
+
match = "Missing .*notapackage.* pip .* conda .* notapackage"
|
|
12
|
+
with pytest.raises(ImportError, match=match):
|
|
13
|
+
import_optional_dependency("notapackage")
|
|
14
|
+
|
|
15
|
+
result = import_optional_dependency("notapackage", raise_on_missing=False)
|
|
16
|
+
assert result is None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_xlrd_version_fallback() -> None:
|
|
20
|
+
""""""
|
|
21
|
+
pytest.importorskip("xlrd")
|
|
22
|
+
import_optional_dependency("xlrd")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_bad_version() -> None:
|
|
26
|
+
""""""
|
|
27
|
+
name = "fakemodule"
|
|
28
|
+
module = types.ModuleType(name)
|
|
29
|
+
module.__version__ = "0.9.0"
|
|
30
|
+
sys.modules[name] = module
|
|
31
|
+
VERSIONS[name] = "1.0.0"
|
|
32
|
+
|
|
33
|
+
match = "microdf requires .*1.0.0.* of .fakemodule.*'0.9.0'"
|
|
34
|
+
with pytest.raises(ImportError, match=match):
|
|
35
|
+
import_optional_dependency("fakemodule")
|
|
36
|
+
|
|
37
|
+
with pytest.warns(UserWarning):
|
|
38
|
+
result = import_optional_dependency("fakemodule", on_version="warn")
|
|
39
|
+
assert result is None
|
|
40
|
+
|
|
41
|
+
module.__version__ = "1.0.0" # exact match is OK
|
|
42
|
+
result = import_optional_dependency("fakemodule")
|
|
43
|
+
assert result is module
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_no_version_raises() -> None:
|
|
47
|
+
""""""
|
|
48
|
+
name = "fakemodule"
|
|
49
|
+
module = types.ModuleType(name)
|
|
50
|
+
sys.modules[name] = module
|
|
51
|
+
VERSIONS[name] = "1.0.0"
|
|
52
|
+
|
|
53
|
+
with pytest.raises(ImportError, match="Can't determine .* fakemodule"):
|
|
54
|
+
import_optional_dependency(name)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
import microdf as mdf
|
|
5
|
+
|
|
6
|
+
df = pd.DataFrame(
|
|
7
|
+
{
|
|
8
|
+
"income": [-10, 0, 10, 20],
|
|
9
|
+
"threshold": [15, 10, 15, 10],
|
|
10
|
+
"weight": [1, 2, 3, 4],
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
md = mdf.MicroDataFrame(df[["income", "threshold"]], weights=df.weight)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_poverty_rate() -> None:
|
|
17
|
+
# Unweighted
|
|
18
|
+
assert np.allclose(mdf.poverty_rate(df, "income", "threshold"), 3 / 4)
|
|
19
|
+
# Weighted
|
|
20
|
+
assert np.allclose(
|
|
21
|
+
mdf.poverty_rate(df, "income", "threshold", "weight"), 6 / 10
|
|
22
|
+
)
|
|
23
|
+
assert np.allclose(md.poverty_rate("income", "threshold"), 6 / 10)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_deep_poverty_rate() -> None:
|
|
27
|
+
# Unweighted
|
|
28
|
+
assert np.allclose(mdf.deep_poverty_rate(df, "income", "threshold"), 2 / 4)
|
|
29
|
+
# Weighted
|
|
30
|
+
assert np.allclose(
|
|
31
|
+
mdf.deep_poverty_rate(df, "income", "threshold", "weight"), 3 / 10
|
|
32
|
+
)
|
|
33
|
+
assert np.allclose(md.deep_poverty_rate("income", "threshold"), 3 / 10)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_poverty_gap() -> None:
|
|
37
|
+
# Unweighted
|
|
38
|
+
assert np.allclose(mdf.poverty_gap(df, "income", "threshold"), 25 + 10 + 5)
|
|
39
|
+
# Weighted
|
|
40
|
+
RES = 25 * 1 + 10 * 2 + 5 * 3
|
|
41
|
+
assert np.allclose(
|
|
42
|
+
mdf.poverty_gap(df, "income", "threshold", "weight"), RES
|
|
43
|
+
)
|
|
44
|
+
assert np.allclose(md.poverty_gap("income", "threshold"), RES)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_squared_poverty_gap() -> None:
|
|
48
|
+
# Unweighted
|
|
49
|
+
assert np.allclose(
|
|
50
|
+
mdf.squared_poverty_gap(df, "income", "threshold"),
|
|
51
|
+
25**2 + 10**2 + 5**2,
|
|
52
|
+
)
|
|
53
|
+
# Weighted
|
|
54
|
+
RES = 1 * (25**2) + 2 * (10**2) + 3 * (5**2)
|
|
55
|
+
assert np.allclose(
|
|
56
|
+
mdf.squared_poverty_gap(df, "income", "threshold", "weight"),
|
|
57
|
+
RES,
|
|
58
|
+
)
|
|
59
|
+
assert np.allclose(md.squared_poverty_gap("income", "threshold"), RES)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_deep_poverty_gap() -> None:
|
|
63
|
+
# Unweighted
|
|
64
|
+
assert np.allclose(
|
|
65
|
+
mdf.deep_poverty_gap(df, "income", "threshold"), 17.5 + 5 + 0 + 0
|
|
66
|
+
)
|
|
67
|
+
# Weighted
|
|
68
|
+
RES = 17.5 * 1 + 5 * 2 + 0 * 3 + 0 * 4
|
|
69
|
+
assert np.allclose(
|
|
70
|
+
mdf.deep_poverty_gap(df, "income", "threshold", "weight"), RES
|
|
71
|
+
)
|
|
72
|
+
# Same in MicroDataFrame.
|
|
73
|
+
assert np.allclose(md.deep_poverty_gap("income", "threshold"), RES)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import microdf as mdf
|
|
4
|
+
|
|
5
|
+
V1 = [1, 2, 3]
|
|
6
|
+
V2 = [4, 5, 6]
|
|
7
|
+
W1 = [7, 8, 9]
|
|
8
|
+
W2 = [10, 11, 12]
|
|
9
|
+
DF1 = pd.DataFrame({"v": V1, "w": W1})
|
|
10
|
+
DF2 = pd.DataFrame({"v": V2, "w": W2})
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_quantile_chg() -> None:
|
|
14
|
+
mdf.quantile_chg(DF1, DF2, "v", "w", "v", "w")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_quantile_pct_chg_plot() -> None:
|
|
18
|
+
mdf.quantile_pct_chg_plot(DF1, DF2, "v", "w", "v", "w")
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
import microdf as mdf
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_tax() -> None:
|
|
9
|
+
""""""
|
|
10
|
+
# Consider a MTR schedule of 0% up to 10,000, then 10% after that.
|
|
11
|
+
BRACKETS = [0, 10e3]
|
|
12
|
+
RATES = [0, 0.1]
|
|
13
|
+
INCOME = [0, 5e3, 10e3, 10e3 + 1, 20e3]
|
|
14
|
+
EXPECTED = [0, 0, 0, 0.1, 1e3]
|
|
15
|
+
res = mdf.tax_from_mtrs(INCOME, BRACKETS, RATES)
|
|
16
|
+
pd.testing.assert_series_equal(res, pd.Series(EXPECTED))
|
|
17
|
+
# Try with 10% avoidance.
|
|
18
|
+
EXPECTED_10PCT_AVOIDANCE = [0, 0, 0, 0, 800.0]
|
|
19
|
+
res_10pct_avoidance = mdf.tax_from_mtrs(INCOME, BRACKETS, RATES, 0.1)
|
|
20
|
+
pd.testing.assert_series_equal(
|
|
21
|
+
res_10pct_avoidance, pd.Series(EXPECTED_10PCT_AVOIDANCE)
|
|
22
|
+
)
|
|
23
|
+
# Try with avoidance elasticity of 2.
|
|
24
|
+
EXPECTED_E2_AVOIDANCE = [
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
0,
|
|
28
|
+
0, # Taxable base becomes (10e3 + 1) * (1 - 2 * 0.1)
|
|
29
|
+
# Taxable base becomes 20e3 * (exp(-2 * 0.1)).
|
|
30
|
+
0.1 * (20e3 * np.exp(-0.2) - 10e3),
|
|
31
|
+
]
|
|
32
|
+
res_e2_avoidance = mdf.tax_from_mtrs(
|
|
33
|
+
INCOME, BRACKETS, RATES, avoidance_elasticity=2
|
|
34
|
+
)
|
|
35
|
+
pd.testing.assert_series_equal(
|
|
36
|
+
res_e2_avoidance, pd.Series(EXPECTED_E2_AVOIDANCE)
|
|
37
|
+
)
|
|
38
|
+
# Try with flat avoidance elasticity of 2.
|
|
39
|
+
EXPECTED_E2_AVOIDANCE_FLAT = [
|
|
40
|
+
0,
|
|
41
|
+
0,
|
|
42
|
+
0,
|
|
43
|
+
0, # Taxable base becomes (10e3 + 1) * (1 - 2 * 0.1)
|
|
44
|
+
600.0,
|
|
45
|
+
] # Taxable base becomes 20e3 * (1 - 2 * 0.1) = 16e3.
|
|
46
|
+
res_e2_avoidance_flat = mdf.tax_from_mtrs(
|
|
47
|
+
INCOME, BRACKETS, RATES, avoidance_elasticity_flat=2
|
|
48
|
+
)
|
|
49
|
+
pd.testing.assert_series_equal(
|
|
50
|
+
res_e2_avoidance_flat, pd.Series(EXPECTED_E2_AVOIDANCE_FLAT)
|
|
51
|
+
)
|
|
52
|
+
# Ensure error when passing both rate and elasticity.
|
|
53
|
+
with pytest.raises(Exception):
|
|
54
|
+
mdf.tax_from_mtrs(INCOME, BRACKETS, RATES, 0.1, 2)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import microdf as mdf
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import taxcalc as tc
|
|
7
|
+
|
|
8
|
+
_HAVE_TAXCALC = True
|
|
9
|
+
except ImportError:
|
|
10
|
+
_HAVE_TAXCALC = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_calc_df() -> None:
|
|
14
|
+
""""""
|
|
15
|
+
if not _HAVE_TAXCALC:
|
|
16
|
+
pytest.skip("taxcalc is not installed")
|
|
17
|
+
mdf.calc_df()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_static_baseline_calc() -> None:
|
|
21
|
+
""""""
|
|
22
|
+
if not _HAVE_TAXCALC:
|
|
23
|
+
pytest.skip("taxcalc is not installed")
|
|
24
|
+
recs = tc.Records.cps_constructor()
|
|
25
|
+
mdf.static_baseline_calc(recs, 2020)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
import microdf as mdf
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_cartesian_product() -> None:
|
|
7
|
+
""""""
|
|
8
|
+
res = mdf.cartesian_product(
|
|
9
|
+
{"a": [1, 2, 3], "b": ["val1", "val2"], "c": [100, 101]}
|
|
10
|
+
)
|
|
11
|
+
EXPECTED = pd.DataFrame(
|
|
12
|
+
{
|
|
13
|
+
"a": [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3],
|
|
14
|
+
"b": [
|
|
15
|
+
"val1",
|
|
16
|
+
"val1",
|
|
17
|
+
"val2",
|
|
18
|
+
"val2",
|
|
19
|
+
"val1",
|
|
20
|
+
"val1",
|
|
21
|
+
"val2",
|
|
22
|
+
"val2",
|
|
23
|
+
"val1",
|
|
24
|
+
"val1",
|
|
25
|
+
"val2",
|
|
26
|
+
"val2",
|
|
27
|
+
],
|
|
28
|
+
"c": [100, 101, 100, 101, 100, 101, 100, 101, 100, 101, 100, 101],
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
pd.testing.assert_frame_equal(res, EXPECTED)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_flatten() -> None:
|
|
35
|
+
""""""
|
|
36
|
+
L = [[[1, 2, 3], [4, 5]], 6]
|
|
37
|
+
res = list(mdf.flatten(L))
|
|
38
|
+
EXPECTED = [1, 2, 3, 4, 5, 6]
|
|
39
|
+
assert res == EXPECTED
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
import microdf as mdf
|
|
5
|
+
|
|
6
|
+
X = [1, 5, 2]
|
|
7
|
+
Y = [0, -6, 3]
|
|
8
|
+
W = [4, 1, 1]
|
|
9
|
+
df = pd.DataFrame({"x": X, "y": Y, "w": W})
|
|
10
|
+
ms = mdf.MicroSeries(X, weights=W)
|
|
11
|
+
md = mdf.MicroDataFrame(df[["x", "y"]], weights=W)
|
|
12
|
+
# Also make a version with groups.
|
|
13
|
+
df2 = df.copy(deep=True)
|
|
14
|
+
df2.x *= 2
|
|
15
|
+
df2.y *= 1.5
|
|
16
|
+
dfg = pd.concat([df, df2])
|
|
17
|
+
dfg["g"] = ["a"] * 3 + ["b"] * 3
|
|
18
|
+
mdg = mdf.MicroDataFrame(dfg[["x", "y", "g"]], weights=W)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_weighted_quantile() -> None:
|
|
22
|
+
Q = [0, 0.5, 1]
|
|
23
|
+
mdf.weighted_quantile(df, "x", "w", Q).tolist()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_weighted_median() -> None:
|
|
27
|
+
assert mdf.weighted_median(df, "x") == 2
|
|
28
|
+
mdf.weighted_median(df, "x", "w")
|
|
29
|
+
# Test with groups.
|
|
30
|
+
mdf.weighted_median(dfg, "x", "w", "g")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_weighted_mean() -> None:
|
|
34
|
+
# Test umweighted.
|
|
35
|
+
assert mdf.weighted_mean(df, "x") == 8 / 3
|
|
36
|
+
# Test weighted.
|
|
37
|
+
assert mdf.weighted_mean(df, "x", "w") == 11 / 6
|
|
38
|
+
# Test weighted with multiple columns.
|
|
39
|
+
assert mdf.weighted_mean(df, ["x", "y"], "w").tolist() == [11 / 6, -3 / 6]
|
|
40
|
+
# Test grouped.
|
|
41
|
+
mdf.weighted_mean(dfg, "x", "w", "g")
|
|
42
|
+
mdf.weighted_mean(dfg, ["x", "y"], "w", "g")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_weighted_sum() -> None:
|
|
46
|
+
# Test unweighted.
|
|
47
|
+
assert mdf.weighted_sum(df, "x") == 8
|
|
48
|
+
# Test weighted.
|
|
49
|
+
assert mdf.weighted_sum(df, "x", "w") == 11
|
|
50
|
+
# Test weighted with multiple columns.
|
|
51
|
+
assert mdf.weighted_sum(df, ["x", "y"], "w").tolist() == [11, -3]
|
|
52
|
+
# Test grouped.
|
|
53
|
+
mdf.weighted_sum(dfg, "x", "w", "g")
|
|
54
|
+
mdf.weighted_sum(dfg, ["x", "y"], "w", "g")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_gini() -> None:
|
|
58
|
+
# Test nothing breaks.
|
|
59
|
+
ms.gini()
|
|
60
|
+
# Unweighted.
|
|
61
|
+
mdf.gini(df, "x")
|
|
62
|
+
# Weighted
|
|
63
|
+
mdf.gini(df, "x", "w")
|
|
64
|
+
# Unweighted, grouped
|
|
65
|
+
mdf.gini(dfg, "x", groupby="g")
|
|
66
|
+
# Weighted, grouped
|
|
67
|
+
mdf.gini(dfg, "x", "w", groupby="g")
|
|
68
|
+
# Test old and new match.
|
|
69
|
+
assert ms.gini() == mdf.gini(df, "x", "w")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_add_weighted_quantiles() -> None:
|
|
73
|
+
with pytest.deprecated_call():
|
|
74
|
+
mdf.add_weighted_quantiles(df, "x", "w")
|
microdf/ubi.py
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
2
5
|
|
|
3
6
|
import microdf as mdf
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def ubi_or_bens(
|
|
7
|
-
df,
|
|
8
|
-
ben_cols,
|
|
9
|
-
max_ubi="max_ubi",
|
|
10
|
-
ubi="ubi",
|
|
11
|
-
bens="bens",
|
|
12
|
-
update_income_measures=None,
|
|
13
|
-
):
|
|
14
|
-
"""Calculates whether a tax unit will take UBI or benefits,
|
|
15
|
-
|
|
10
|
+
df: pd.DataFrame,
|
|
11
|
+
ben_cols: Union[str, List[str]],
|
|
12
|
+
max_ubi: str = "max_ubi",
|
|
13
|
+
ubi: str = "ubi",
|
|
14
|
+
bens: str = "bens",
|
|
15
|
+
update_income_measures: Optional[List[str]] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Calculates whether a tax unit will take UBI or benefits, and adjusts
|
|
18
|
+
values accordingly.
|
|
16
19
|
|
|
17
20
|
:param df: DataFrame.
|
|
18
21
|
:param ben_cols: List of columns for benefits.
|
|
19
|
-
:param max_ubi: Column name of the maximum UBI, before accounting
|
|
20
|
-
|
|
22
|
+
:param max_ubi: Column name of the maximum UBI, before accounting for
|
|
23
|
+
benefits. Defaults to 'max_ubi'.
|
|
21
24
|
:param ubi: Column name to add representing the UBI. Defaults to 'ubi'.
|
|
22
25
|
:param bens: Column name to add representing total benefits (after
|
|
23
|
-
|
|
24
|
-
:param update_income_measures: List of income measures to update.
|
|
25
|
-
|
|
26
|
+
adjustment). Defaults to 'bens'.
|
|
27
|
+
:param update_income_measures: List of income measures to update. Defaults
|
|
28
|
+
to ['expanded_income', 'aftertax_income'].
|
|
26
29
|
:returns: Nothing. Benefits in ben_cols are adjusted, ubi and bens columns
|
|
27
30
|
are added, and expanded_income and aftertax_income are updated
|
|
28
31
|
according to the net difference.
|
|
29
|
-
|
|
30
32
|
"""
|
|
31
33
|
if update_income_measures is None:
|
|
32
34
|
update_income_measures = ["expanded_income", "aftertax_income"]
|