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
microdf/inequality.py
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
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
|
-
def gini(
|
|
9
|
+
def gini(
|
|
10
|
+
df: pd.DataFrame,
|
|
11
|
+
col: str,
|
|
12
|
+
w: Optional[str] = None,
|
|
13
|
+
negatives: Optional[str] = None,
|
|
14
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
15
|
+
) -> float:
|
|
7
16
|
"""Calculates Gini index.
|
|
8
17
|
|
|
9
18
|
:param df: DataFrame.
|
|
@@ -18,10 +27,14 @@ def gini(df, col, w=None, negatives=None, groupby=None):
|
|
|
18
27
|
Defaults to None, which leaves negative values as they are.
|
|
19
28
|
:param groupby: Column, or list of columns, to group by.
|
|
20
29
|
:returns: A float, the Gini index.
|
|
21
|
-
|
|
22
30
|
"""
|
|
23
31
|
|
|
24
|
-
def _gini(
|
|
32
|
+
def _gini(
|
|
33
|
+
df: pd.DataFrame,
|
|
34
|
+
col: str,
|
|
35
|
+
w: Optional[str] = None,
|
|
36
|
+
negatives: Optional[str] = None,
|
|
37
|
+
) -> float:
|
|
25
38
|
# Requires float numpy arrays (not pandas Series or lists) to work.
|
|
26
39
|
x = np.array(df[col]).astype("float")
|
|
27
40
|
if negatives == "zero":
|
|
@@ -50,7 +63,13 @@ def gini(df, col, w=None, negatives=None, groupby=None):
|
|
|
50
63
|
return df.groupby(groupby).apply(lambda x: _gini(x, col, w, negatives))
|
|
51
64
|
|
|
52
65
|
|
|
53
|
-
def top_x_pct_share(
|
|
66
|
+
def top_x_pct_share(
|
|
67
|
+
df: pd.DataFrame,
|
|
68
|
+
col: str,
|
|
69
|
+
top_x_pct: float,
|
|
70
|
+
w: Optional[str] = None,
|
|
71
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
72
|
+
) -> float:
|
|
54
73
|
"""Calculates top x% share.
|
|
55
74
|
|
|
56
75
|
:param df: DataFrame.
|
|
@@ -59,10 +78,11 @@ def top_x_pct_share(df, col, top_x_pct, w=None, groupby=None):
|
|
|
59
78
|
:param w: Column representing weight in df.
|
|
60
79
|
:param groupby: Column, or list of columns, to group by.
|
|
61
80
|
:returns: The share of w-weighted val held by the top x%.
|
|
62
|
-
|
|
63
81
|
"""
|
|
64
82
|
|
|
65
|
-
def _top_x_pct_share(
|
|
83
|
+
def _top_x_pct_share(
|
|
84
|
+
df: pd.DataFrame, col: str, top_x_pct: float, w: Optional[str] = None
|
|
85
|
+
) -> float:
|
|
66
86
|
threshold = mdf.weighted_quantile(df, col, w, 1 - top_x_pct)
|
|
67
87
|
top_x_pct_sum = mdf.weighted_sum(df[df[col] >= threshold], col, w)
|
|
68
88
|
total_sum = mdf.weighted_sum(df, col, w)
|
|
@@ -75,7 +95,13 @@ def top_x_pct_share(df, col, top_x_pct, w=None, groupby=None):
|
|
|
75
95
|
)
|
|
76
96
|
|
|
77
97
|
|
|
78
|
-
def bottom_x_pct_share(
|
|
98
|
+
def bottom_x_pct_share(
|
|
99
|
+
df: pd.DataFrame,
|
|
100
|
+
col: str,
|
|
101
|
+
bottom_x_pct: float,
|
|
102
|
+
w: Optional[str] = None,
|
|
103
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
104
|
+
) -> float:
|
|
79
105
|
"""Calculates bottom x% share.
|
|
80
106
|
|
|
81
107
|
:param df: DataFrame.
|
|
@@ -84,12 +110,16 @@ def bottom_x_pct_share(df, col, bottom_x_pct, w=None, groupby=None):
|
|
|
84
110
|
:param w: Column representing weight in df.
|
|
85
111
|
:param groupby: Column, or list of columns, to group by.
|
|
86
112
|
:returns: The share of w-weighted val held by the bottom x%.
|
|
87
|
-
|
|
88
113
|
"""
|
|
89
114
|
return 1 - top_x_pct_share(df, col, 1 - bottom_x_pct, w, groupby)
|
|
90
115
|
|
|
91
116
|
|
|
92
|
-
def bottom_50_pct_share(
|
|
117
|
+
def bottom_50_pct_share(
|
|
118
|
+
df: pd.DataFrame,
|
|
119
|
+
col: str,
|
|
120
|
+
w: Optional[str] = None,
|
|
121
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
122
|
+
) -> float:
|
|
93
123
|
"""Calculates bottom 50% share.
|
|
94
124
|
|
|
95
125
|
:param df: DataFrame.
|
|
@@ -97,12 +127,16 @@ def bottom_50_pct_share(df, col, w=None, groupby=None):
|
|
|
97
127
|
:param w: Column representing weight in df.
|
|
98
128
|
:param groupby: Column, or list of columns, to group by.
|
|
99
129
|
:returns: The share of w-weighted val held by the bottom 50%.
|
|
100
|
-
|
|
101
130
|
"""
|
|
102
131
|
return bottom_x_pct_share(df, col, 0.5, w, groupby)
|
|
103
132
|
|
|
104
133
|
|
|
105
|
-
def top_50_pct_share(
|
|
134
|
+
def top_50_pct_share(
|
|
135
|
+
df: pd.DataFrame,
|
|
136
|
+
col: str,
|
|
137
|
+
w: Optional[str] = None,
|
|
138
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
139
|
+
) -> float:
|
|
106
140
|
"""Calculates top 50% share.
|
|
107
141
|
|
|
108
142
|
:param df: DataFrame.
|
|
@@ -110,12 +144,16 @@ def top_50_pct_share(df, col, w=None, groupby=None):
|
|
|
110
144
|
:param w: Column representing weight in df.
|
|
111
145
|
:param groupby: Column, or list of columns, to group by.
|
|
112
146
|
:returns: The share of w-weighted val held by the top 50%.
|
|
113
|
-
|
|
114
147
|
"""
|
|
115
148
|
return top_x_pct_share(df, col, 0.5, w, groupby)
|
|
116
149
|
|
|
117
150
|
|
|
118
|
-
def top_10_pct_share(
|
|
151
|
+
def top_10_pct_share(
|
|
152
|
+
df: pd.DataFrame,
|
|
153
|
+
col: str,
|
|
154
|
+
w: Optional[str] = None,
|
|
155
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
156
|
+
) -> float:
|
|
119
157
|
"""Calculates top 10% share.
|
|
120
158
|
|
|
121
159
|
:param df: DataFrame.
|
|
@@ -123,12 +161,16 @@ def top_10_pct_share(df, col, w=None, groupby=None):
|
|
|
123
161
|
:param w: Column representing weight in df.
|
|
124
162
|
:param groupby: Column, or list of columns, to group by.
|
|
125
163
|
:returns: The share of w-weighted val held by the top 10%.
|
|
126
|
-
|
|
127
164
|
"""
|
|
128
165
|
return top_x_pct_share(df, col, 0.1, w, groupby)
|
|
129
166
|
|
|
130
167
|
|
|
131
|
-
def top_1_pct_share(
|
|
168
|
+
def top_1_pct_share(
|
|
169
|
+
df: pd.DataFrame,
|
|
170
|
+
col: str,
|
|
171
|
+
w: Optional[str] = None,
|
|
172
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
173
|
+
) -> float:
|
|
132
174
|
"""Calculates top 1% share.
|
|
133
175
|
|
|
134
176
|
:param df: DataFrame.
|
|
@@ -136,12 +178,16 @@ def top_1_pct_share(df, col, w=None, groupby=None):
|
|
|
136
178
|
:param w: Column representing weight in df.
|
|
137
179
|
:param groupby: Column, or list of columns, to group by.
|
|
138
180
|
:returns: The share of w-weighted val held by the top 1%.
|
|
139
|
-
|
|
140
181
|
"""
|
|
141
182
|
return top_x_pct_share(df, col, 0.01, w, groupby)
|
|
142
183
|
|
|
143
184
|
|
|
144
|
-
def top_0_1_pct_share(
|
|
185
|
+
def top_0_1_pct_share(
|
|
186
|
+
df: pd.DataFrame,
|
|
187
|
+
col: str,
|
|
188
|
+
w: Optional[str] = None,
|
|
189
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
190
|
+
) -> float:
|
|
145
191
|
"""Calculates top 0.1% share.
|
|
146
192
|
|
|
147
193
|
:param df: DataFrame.
|
|
@@ -149,21 +195,24 @@ def top_0_1_pct_share(df, col, w=None, groupby=None):
|
|
|
149
195
|
:param w: Column representing weight in df.
|
|
150
196
|
:param groupby: Column, or list of columns, to group by.
|
|
151
197
|
:returns: The share of w-weighted val held by the top 0.1%.
|
|
152
|
-
|
|
153
198
|
"""
|
|
154
199
|
return top_x_pct_share(df, col, 0.001, w, groupby)
|
|
155
200
|
|
|
156
201
|
|
|
157
|
-
def t10_b50(
|
|
202
|
+
def t10_b50(
|
|
203
|
+
df: pd.DataFrame,
|
|
204
|
+
col: str,
|
|
205
|
+
w: Optional[str] = None,
|
|
206
|
+
groupby: Optional[Union[str, List[str]]] = None,
|
|
207
|
+
) -> float:
|
|
158
208
|
"""Calculates ratio between the top 10% and bottom 50% shares.
|
|
159
209
|
|
|
160
210
|
:param df: DataFrame.
|
|
161
211
|
:param col: Name of column in df representing value.
|
|
162
212
|
:param w: Column representing weight in df.
|
|
163
213
|
:param groupby: Column, or list of columns, to group by.
|
|
164
|
-
:returns: The share of w-weighted val held by the top 10% divided by
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
:returns: The share of w-weighted val held by the top 10% divided by the
|
|
215
|
+
share of w-weighted val held by the bottom 50%.
|
|
167
216
|
"""
|
|
168
217
|
t10 = top_10_pct_share(df, col, w, groupby)
|
|
169
218
|
b50 = bottom_50_pct_share(df, col, w, groupby)
|
microdf/io.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import zipfile
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import pandas as pd
|
|
5
|
+
import requests
|
|
5
6
|
|
|
6
7
|
HEADER = {
|
|
7
|
-
"User-Agent":
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
}
|
|
8
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) "
|
|
9
|
+
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
10
|
+
+ "Chrome/50.0.2661.102 Safari/537.36"
|
|
11
|
+
}
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def read_stata_zip(url: str, **kwargs) -> pd.DataFrame:
|
|
@@ -19,11 +19,9 @@ def read_stata_zip(url: str, **kwargs) -> pd.DataFrame:
|
|
|
19
19
|
Pending native support in
|
|
20
20
|
https://github.com/pandas-dev/pandas/issues/26599.
|
|
21
21
|
|
|
22
|
-
:param url: URL string of .zip file containing a single
|
|
23
|
-
|
|
24
|
-
:param **kwargs: Arguments passed to pandas.read_stata().
|
|
22
|
+
:param url: URL string of .zip file containing a single .dta file. :param
|
|
23
|
+
**kwargs: Arguments passed to pandas.read_stata().
|
|
25
24
|
:returns: DataFrame.
|
|
26
|
-
|
|
27
25
|
"""
|
|
28
26
|
r = requests.get(url, headers=HEADER)
|
|
29
27
|
data = io.BytesIO(r.content)
|
microdf/poverty.py
CHANGED
|
@@ -2,15 +2,14 @@ import numpy as np
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def fpl(people: int):
|
|
5
|
+
def fpl(people: int) -> float:
|
|
6
6
|
"""Calculates the federal poverty guideline for a household of a certain
|
|
7
|
-
|
|
7
|
+
size.
|
|
8
8
|
|
|
9
9
|
:param XTOT: The number of people in the household.
|
|
10
|
-
:param people: returns: The federal poverty guideline for the contiguous
|
|
11
|
-
|
|
10
|
+
:param people: returns: The federal poverty guideline for the contiguous 48
|
|
11
|
+
states.
|
|
12
12
|
:returns: The federal poverty guideline for the contiguous 48 states.
|
|
13
|
-
|
|
14
13
|
"""
|
|
15
14
|
return 7820 + 4320 * people
|
|
16
15
|
|
|
@@ -18,8 +17,8 @@ def fpl(people: int):
|
|
|
18
17
|
def poverty_rate(
|
|
19
18
|
df: pd.DataFrame, income: str, threshold: str, w: str = None
|
|
20
19
|
) -> float:
|
|
21
|
-
"""Calculate poverty rate, i.e., the population share with income
|
|
22
|
-
|
|
20
|
+
"""Calculate poverty rate, i.e., the population share with income below
|
|
21
|
+
their poverty threshold.
|
|
23
22
|
|
|
24
23
|
:param df: DataFrame with income, threshold, and possibly weight columns
|
|
25
24
|
for each person/household.
|
|
@@ -43,7 +42,7 @@ def deep_poverty_rate(
|
|
|
43
42
|
df: pd.DataFrame, income: str, threshold: str, w: str = None
|
|
44
43
|
) -> float:
|
|
45
44
|
"""Calculate deep poverty rate, i.e., the population share with income
|
|
46
|
-
|
|
45
|
+
below half their poverty threshold.
|
|
47
46
|
|
|
48
47
|
:param df: DataFrame with income, threshold, and possibly weight columns
|
|
49
48
|
for each person/household.
|
|
@@ -67,7 +66,7 @@ def poverty_gap(
|
|
|
67
66
|
df: pd.DataFrame, income: str, threshold: str, w: str = None
|
|
68
67
|
) -> float:
|
|
69
68
|
"""Calculate poverty gap, i.e., the total gap between income and poverty
|
|
70
|
-
|
|
69
|
+
thresholds for all people in poverty.
|
|
71
70
|
|
|
72
71
|
:param df: DataFrame with income, threshold, and possibly weight columns
|
|
73
72
|
for each household (data should represent households, not persons).
|
|
@@ -91,8 +90,8 @@ def squared_poverty_gap(
|
|
|
91
90
|
df: pd.DataFrame, income: str, threshold: str, w: str = None
|
|
92
91
|
) -> float:
|
|
93
92
|
"""Calculate squared poverty gap, i.e., the total squared gap between
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
income and poverty thresholds for all people in poverty. Also known as
|
|
94
|
+
poverty severity index.
|
|
96
95
|
|
|
97
96
|
:param df: DataFrame with income, threshold, and possibly weight columns
|
|
98
97
|
for each household (data should represent households, not persons).
|
|
@@ -117,7 +116,7 @@ def deep_poverty_gap(
|
|
|
117
116
|
df: pd.DataFrame, income: str, threshold: str, w: str = None
|
|
118
117
|
) -> float:
|
|
119
118
|
"""Calculate deep poverty gap, i.e., the total gap between income and
|
|
120
|
-
|
|
119
|
+
halved poverty thresholds for all people in deep poverty.
|
|
121
120
|
|
|
122
121
|
:param df: DataFrame with income, threshold, and possibly weight columns
|
|
123
122
|
for each household (data should represent households, not persons).
|
microdf/style.py
CHANGED
|
@@ -4,7 +4,7 @@ GRID_COLOR = "#eeeeee" # Previously lighter #f5f5f5.
|
|
|
4
4
|
DPI = 200
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def set_plot_style(dpi: int = DPI):
|
|
7
|
+
def set_plot_style(dpi: int = DPI) -> None:
|
|
8
8
|
"""Set plot style.
|
|
9
9
|
|
|
10
10
|
:param dpi: DPI for saving and displaying figures, defaults to microdf.DPI
|
|
@@ -12,14 +12,14 @@ def set_plot_style(dpi: int = DPI):
|
|
|
12
12
|
:type dpi: int, optional
|
|
13
13
|
"""
|
|
14
14
|
try:
|
|
15
|
-
import seaborn as sns
|
|
16
15
|
import matplotlib as mpl
|
|
17
16
|
import matplotlib.font_manager as fm
|
|
17
|
+
import seaborn as sns
|
|
18
18
|
except ImportError:
|
|
19
19
|
raise ImportError(
|
|
20
|
-
"The function you've called requires extra dependencies. "
|
|
21
|
-
"Please install microdf with the 'charts' extra by running "
|
|
22
|
-
"'pip install microdf[charts]'"
|
|
20
|
+
"The function you've called requires extra dependencies. "
|
|
21
|
+
+ "Please install microdf with the 'charts' extra by running "
|
|
22
|
+
+ "'pip install microdf[charts]'"
|
|
23
23
|
)
|
|
24
24
|
|
|
25
25
|
sns.set_style("white")
|
microdf/tax.py
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
|
|
4
6
|
|
|
5
|
-
def mtr(val, brackets, rates):
|
|
6
|
-
"""Calculates the marginal tax rate applied to a value depending on a
|
|
7
|
-
|
|
7
|
+
def mtr(val: pd.Series, brackets: pd.Series, rates: pd.Series) -> pd.Series:
|
|
8
|
+
"""Calculates the marginal tax rate applied to a value depending on a tax
|
|
9
|
+
schedule.
|
|
8
10
|
|
|
9
11
|
:param val: Value to assess tax on, e.g. wealth or income (list or Series).
|
|
10
12
|
:param brackets: Left side of each bracket (list or Series).
|
|
11
13
|
:param rates: Rate corresponding to each bracket.
|
|
12
14
|
:returns: Series of the size of val representing the marginal tax rate.
|
|
13
|
-
|
|
14
15
|
"""
|
|
15
16
|
df_tax = pd.DataFrame({"brackets": brackets, "rates": rates})
|
|
16
17
|
df_tax["base_tax"] = (
|
|
@@ -24,30 +25,27 @@ def mtr(val, brackets, rates):
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def tax_from_mtrs(
|
|
27
|
-
val,
|
|
28
|
-
brackets,
|
|
29
|
-
rates,
|
|
30
|
-
avoidance_rate=0,
|
|
31
|
-
avoidance_elasticity=0,
|
|
32
|
-
avoidance_elasticity_flat=0,
|
|
33
|
-
):
|
|
28
|
+
val: pd.Series,
|
|
29
|
+
brackets: pd.Series,
|
|
30
|
+
rates: pd.Series,
|
|
31
|
+
avoidance_rate: Optional[float] = 0,
|
|
32
|
+
avoidance_elasticity: Optional[float] = 0,
|
|
33
|
+
avoidance_elasticity_flat: Optional[float] = 0,
|
|
34
|
+
) -> pd.Series:
|
|
34
35
|
"""Calculates tax liability based on a marginal tax rate schedule.
|
|
35
36
|
|
|
36
37
|
:param val: Value to assess tax on, e.g. wealth or income (list or Series).
|
|
37
38
|
:param brackets: Left side of each bracket (list or Series).
|
|
38
39
|
:param rates: Rate corresponding to each bracket.
|
|
39
40
|
:param avoidance_rate: Constant avoidance/evasion rate in percentage terms.
|
|
40
|
-
|
|
41
|
-
:param avoidance_elasticity: Avoidance/evasion elasticity.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Use avoidance_elasticity in most cases.
|
|
48
|
-
Defaults to zero. Should be positive.
|
|
41
|
+
Defaults to zero.
|
|
42
|
+
:param avoidance_elasticity: Avoidance/evasion elasticity. Response of log
|
|
43
|
+
taxable value with respect to tax rate. Defaults to zero. Should be
|
|
44
|
+
positive.
|
|
45
|
+
:param avoidance_elasticity_flat: Response of taxable value with respect to
|
|
46
|
+
tax rate. Use avoidance_elasticity in most cases. Defaults to zero.
|
|
47
|
+
Should be positive.
|
|
49
48
|
:returns: Series of tax liabilities with the same size as val.
|
|
50
|
-
|
|
51
49
|
"""
|
|
52
50
|
assert (
|
|
53
51
|
avoidance_rate == 0
|
microdf/taxcalc.py
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import taxcalc
|
|
5
|
+
|
|
1
6
|
import microdf as mdf
|
|
2
7
|
from microdf._optional import import_optional_dependency
|
|
3
8
|
|
|
4
9
|
|
|
5
|
-
def static_baseline_calc(
|
|
10
|
+
def static_baseline_calc(
|
|
11
|
+
recs: pd.DataFrame, year: int
|
|
12
|
+
) -> "taxcalc.Calculator":
|
|
6
13
|
"""Creates a static Calculator object.
|
|
7
14
|
|
|
8
15
|
:param recs: Records object.
|
|
9
16
|
:param year: Year to advance to.
|
|
10
17
|
:returns: Calculator object.
|
|
11
|
-
|
|
12
18
|
"""
|
|
13
19
|
tc = import_optional_dependency("taxcalc")
|
|
14
20
|
calc = tc.Calculator(records=recs, policy=tc.Policy())
|
|
@@ -17,7 +23,13 @@ def static_baseline_calc(recs, year):
|
|
|
17
23
|
return calc
|
|
18
24
|
|
|
19
25
|
|
|
20
|
-
def add_weighted_metrics(
|
|
26
|
+
def add_weighted_metrics(
|
|
27
|
+
df: pd.DataFrame,
|
|
28
|
+
metric_vars: list,
|
|
29
|
+
w: str = "s006",
|
|
30
|
+
divisor: float = 1e6,
|
|
31
|
+
suffix: str = "_m",
|
|
32
|
+
) -> None:
|
|
21
33
|
"""Adds weighted metrics in millions to a Tax-Calculator pandas DataFrame.
|
|
22
34
|
|
|
23
35
|
Columns are renamed to *_m.
|
|
@@ -27,10 +39,9 @@ def add_weighted_metrics(df, metric_vars, w="s006", divisor=1e6, suffix="_m"):
|
|
|
27
39
|
name.
|
|
28
40
|
:param w: Weight column. Defaults to s006.
|
|
29
41
|
:param divisor: Number by which the product is divided. Defaults to 1e6.
|
|
30
|
-
:param suffix: Suffix to add to each weighted total. Defaults to '_m'
|
|
31
|
-
|
|
42
|
+
:param suffix: Suffix to add to each weighted total. Defaults to '_m' to
|
|
43
|
+
match divisor default of 1e6.
|
|
32
44
|
:returns: Nothing. Weighted columns are added in place.
|
|
33
|
-
|
|
34
45
|
"""
|
|
35
46
|
df[w + suffix] = df[w] / divisor
|
|
36
47
|
metric_vars = mdf.listify(metric_vars)
|
|
@@ -38,15 +49,16 @@ def add_weighted_metrics(df, metric_vars, w="s006", divisor=1e6, suffix="_m"):
|
|
|
38
49
|
df[metric_var + suffix] = df[metric_var] * df[w + suffix]
|
|
39
50
|
|
|
40
51
|
|
|
41
|
-
def n65(
|
|
52
|
+
def n65(
|
|
53
|
+
age_head: pd.Series, age_spouse: pd.Series, elderly_dependents: pd.Series
|
|
54
|
+
) -> pd.Series:
|
|
42
55
|
"""Calculates number of people in the tax unit age 65 or older.
|
|
43
56
|
|
|
44
57
|
:param age_head: Series representing age_head from taxcalc data.
|
|
45
58
|
:param age_spouse: Series representing age_spouse from taxcalc data.
|
|
46
59
|
:param elderly_dependents: Series representing elderly_dependents from
|
|
47
|
-
|
|
60
|
+
taxcalc data.
|
|
48
61
|
:returns: Series representing the number of people age 65 or older.
|
|
49
|
-
|
|
50
62
|
"""
|
|
51
63
|
return (
|
|
52
64
|
(age_head >= 65).astype(int)
|
|
@@ -56,14 +68,14 @@ def n65(age_head, age_spouse, elderly_dependents):
|
|
|
56
68
|
|
|
57
69
|
|
|
58
70
|
def calc_df(
|
|
59
|
-
records=None,
|
|
60
|
-
policy=None,
|
|
61
|
-
year=2020,
|
|
62
|
-
reform=None,
|
|
63
|
-
group_vars=None,
|
|
64
|
-
metric_vars=None,
|
|
65
|
-
group_n65=False,
|
|
66
|
-
):
|
|
71
|
+
records: Optional[pd.DataFrame] = None,
|
|
72
|
+
policy: Optional[taxcalc.Policy] = None,
|
|
73
|
+
year: int = 2020,
|
|
74
|
+
reform: Optional[dict] = None,
|
|
75
|
+
group_vars: Optional[list] = None,
|
|
76
|
+
metric_vars: Optional[list] = None,
|
|
77
|
+
group_n65: Optional[bool] = False,
|
|
78
|
+
) -> pd.DataFrame:
|
|
67
79
|
"""Creates a pandas DataFrame for given Tax-Calculator data.
|
|
68
80
|
|
|
69
81
|
s006 is always included, and RECID is used as an index.
|
|
@@ -71,19 +83,18 @@ def calc_df(
|
|
|
71
83
|
:param records: An optional Records object. If not provided, uses CPS
|
|
72
84
|
records. (Default value = None)
|
|
73
85
|
:param policy: An optional Policy object. If not provided, uses default
|
|
74
|
-
|
|
86
|
+
Policy.
|
|
75
87
|
:param year: An optional year to advance to. If not provided, defaults to
|
|
76
|
-
|
|
88
|
+
2020.
|
|
77
89
|
:param reform: An optional reform to implement for the Policy object.
|
|
78
90
|
(Default value = None)
|
|
79
91
|
:param group_vars: An optional list of column names to include in the
|
|
80
|
-
|
|
92
|
+
DataFrame. (Default value = None)
|
|
81
93
|
:param metric_vars: An optional list of column names to include and
|
|
82
94
|
calculate weighted sums of (in millions named as *_m) in the DataFrame.
|
|
83
95
|
(Default value = None)
|
|
84
96
|
:param group_n65: Whether to calculate and group by n65. Defaults to False.
|
|
85
97
|
:returns: A pandas DataFrame. market_income is also always calculated.
|
|
86
|
-
|
|
87
98
|
"""
|
|
88
99
|
tc = import_optional_dependency("taxcalc")
|
|
89
100
|
# Assign defaults.
|
|
@@ -135,12 +146,11 @@ def calc_df(
|
|
|
135
146
|
return df.set_index("RECID")
|
|
136
147
|
|
|
137
148
|
|
|
138
|
-
def recalculate(df):
|
|
149
|
+
def recalculate(df: pd.DataFrame) -> None:
|
|
139
150
|
"""Recalculates fields in the DataFrame for after components have changed.
|
|
140
151
|
|
|
141
152
|
:param df: DataFrame for use in microdf.
|
|
142
153
|
:returns: Nothing. Updates the DataFrame in place.
|
|
143
|
-
|
|
144
154
|
"""
|
|
145
155
|
# Recalculate TPC's Expanded Cash Income measure.
|
|
146
156
|
cols = df.columns
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
import microdf as mdf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def differences(
|
|
10
|
+
actual: pd.DataFrame,
|
|
11
|
+
expected: pd.DataFrame,
|
|
12
|
+
f_actual: str,
|
|
13
|
+
f_expected: str,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Check for differences between results in afilename and efilename files.
|
|
16
|
+
|
|
17
|
+
:param actual: Actual DataFrame.
|
|
18
|
+
:param expected: Expected DataFrame.
|
|
19
|
+
:param f_actual: Filename of the actual CSV.
|
|
20
|
+
:param f_expected: Filename of the expected CSV.
|
|
21
|
+
"""
|
|
22
|
+
if not np.allclose(actual, expected):
|
|
23
|
+
msg = "COMPARE RESULTS DIFFER\n"
|
|
24
|
+
msg += "-------------------------------------------------\n"
|
|
25
|
+
msg += "--- NEW RESULTS IN {} FILE ---\n"
|
|
26
|
+
msg += "--- if new OK, copy {} to ---\n"
|
|
27
|
+
msg += "--- {} ---\n"
|
|
28
|
+
msg += "--- and rerun test. ---\n"
|
|
29
|
+
msg += "-------------------------------------------------\n"
|
|
30
|
+
raise ValueError(msg.format(f_actual, f_actual, f_expected))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_percentile_agg_compare(tests_path: str) -> None:
|
|
34
|
+
"""
|
|
35
|
+
:param tests_path: Folder path to write test results.
|
|
36
|
+
"""
|
|
37
|
+
N = 1000
|
|
38
|
+
np.random.seed(0)
|
|
39
|
+
df = pd.DataFrame({"val": np.random.rand(N), "w": np.random.rand(N)})
|
|
40
|
+
mdf.add_weighted_quantiles(df, "val", "w")
|
|
41
|
+
percentile_sum = df.groupby("val_percentile")[["val", "w"]].sum()
|
|
42
|
+
F_ACTUAL = "test_percentile_actual.csv"
|
|
43
|
+
F_EXPECTED = "test_percentile_expected.csv"
|
|
44
|
+
percentile_sum.to_csv(os.path.join(tests_path, F_ACTUAL))
|
|
45
|
+
# Re-read as CSV to remove index and ensure CSVs are equal.
|
|
46
|
+
actual = pd.read_csv(os.path.join(tests_path, F_ACTUAL))
|
|
47
|
+
expected = pd.read_csv(os.path.join(tests_path, F_EXPECTED))
|
|
48
|
+
differences(actual, expected, F_ACTUAL, F_EXPECTED)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Test script for enhanced decile_rank functionality."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from microdf import MicroSeries
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_decile_rank() -> None:
|
|
9
|
+
"""Test the enhanced decile_rank method with assert statements."""
|
|
10
|
+
print("Running enhanced decile_rank tests...")
|
|
11
|
+
|
|
12
|
+
# Create test data with some negative values
|
|
13
|
+
test_data = [-5, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20]
|
|
14
|
+
test_weights = [1] * len(test_data)
|
|
15
|
+
test_ms = MicroSeries(test_data, weights=test_weights)
|
|
16
|
+
|
|
17
|
+
default_deciles = test_ms.decile_rank()
|
|
18
|
+
assert all(
|
|
19
|
+
1 <= d <= 10 for d in default_deciles.values
|
|
20
|
+
), "Default deciles should be between 1 and 10"
|
|
21
|
+
|
|
22
|
+
negative_indices = [i for i, val in enumerate(test_data) if val < 0]
|
|
23
|
+
negative_deciles = [default_deciles.values[i] for i in negative_indices]
|
|
24
|
+
assert all(
|
|
25
|
+
d >= 1 for d in negative_deciles
|
|
26
|
+
), "Negative values should be ranked 1-10 in default mode"
|
|
27
|
+
|
|
28
|
+
assert isinstance(
|
|
29
|
+
default_deciles, MicroSeries
|
|
30
|
+
), "decile_rank should return MicroSeries"
|
|
31
|
+
|
|
32
|
+
zero_deciles = test_ms.decile_rank(negatives_in_zero=True)
|
|
33
|
+
negative_zero_deciles = [zero_deciles.values[i] for i in negative_indices]
|
|
34
|
+
assert all(
|
|
35
|
+
d == 0 for d in negative_zero_deciles
|
|
36
|
+
), "Negative values should be in decile 0 when negatives_in_zero=True"
|
|
37
|
+
|
|
38
|
+
non_negative_indices = [i for i, val in enumerate(test_data) if val >= 0]
|
|
39
|
+
non_negative_deciles = [
|
|
40
|
+
zero_deciles.values[i] for i in non_negative_indices
|
|
41
|
+
]
|
|
42
|
+
assert all(
|
|
43
|
+
1 <= d <= 10 for d in non_negative_deciles
|
|
44
|
+
), "Non-negative values should be ranked 1-10 when negatives_in_zero=True"
|
|
45
|
+
|
|
46
|
+
unique_deciles = sorted(zero_deciles.unique())
|
|
47
|
+
assert 0 in unique_deciles, "Decile 0 should exist with negative values"
|
|
48
|
+
assert all(
|
|
49
|
+
d in unique_deciles for d in range(1, 11)
|
|
50
|
+
), "Deciles 1-10 should exist with non-negative values"
|
|
51
|
+
|
|
52
|
+
positive_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
|
53
|
+
positive_ms = MicroSeries(positive_data, weights=[1] * 10)
|
|
54
|
+
|
|
55
|
+
pos_default = positive_ms.decile_rank()
|
|
56
|
+
pos_zero = positive_ms.decile_rank(negatives_in_zero=True)
|
|
57
|
+
assert np.array_equal(
|
|
58
|
+
pos_default.values, pos_zero.values
|
|
59
|
+
), "Both modes should produce identical results for all positive data"
|
|
60
|
+
assert (
|
|
61
|
+
0 not in pos_zero.unique()
|
|
62
|
+
), "Decile 0 should not exist when all values are positive"
|
|
63
|
+
|
|
64
|
+
negative_data = [-10, -8, -6, -4, -2]
|
|
65
|
+
negative_ms = MicroSeries(negative_data, weights=[1] * 5)
|
|
66
|
+
|
|
67
|
+
neg_default = negative_ms.decile_rank()
|
|
68
|
+
neg_zero = negative_ms.decile_rank(negatives_in_zero=True)
|
|
69
|
+
assert all(
|
|
70
|
+
1 <= d <= 10 for d in neg_default.values
|
|
71
|
+
), "Default mode should rank all negative values 1-10"
|
|
72
|
+
assert all(
|
|
73
|
+
d == 0 for d in neg_zero.values
|
|
74
|
+
), "All negative values should be in decile 0 when negatives_in_zero=True"
|
|
75
|
+
|
|
76
|
+
weighted_data = [-1, 0, 1, 2, 3]
|
|
77
|
+
weighted_weights = [0.5, 1.0, 1.5, 2.0, 2.5]
|
|
78
|
+
weighted_ms = MicroSeries(weighted_data, weights=weighted_weights)
|
|
79
|
+
|
|
80
|
+
w_default = weighted_ms.decile_rank()
|
|
81
|
+
w_zero = weighted_ms.decile_rank(negatives_in_zero=True)
|
|
82
|
+
assert np.array_equal(
|
|
83
|
+
w_default.weights.values, weighted_weights
|
|
84
|
+
), "Weights should be preserved in default mode"
|
|
85
|
+
assert np.array_equal(
|
|
86
|
+
w_zero.weights.values, weighted_weights
|
|
87
|
+
), "Weights should be preserved in negatives_in_zero mode"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
test_decile_rank()
|