microdf-python 0.4.6__py3-none-any.whl → 1.0.0__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 -162
- microdf/microdataframe.py +501 -0
- microdf/{generic.py → microseries.py} +43 -352
- microdf/tests/{test_generic.py → test_microseries_dataframe.py} +62 -22
- microdf_python-1.0.0.dist-info/METADATA +75 -0
- microdf_python-1.0.0.dist-info/RECORD +10 -0
- microdf/_optional.py +0 -85
- microdf/agg.py +0 -90
- microdf/chart_utils.py +0 -37
- microdf/charts.py +0 -62
- microdf/concat.py +0 -29
- microdf/constants.py +0 -40
- microdf/custom_taxes.py +0 -174
- microdf/income_measures.py +0 -63
- microdf/inequality.py +0 -219
- microdf/io.py +0 -30
- microdf/poverty.py +0 -136
- microdf/style.py +0 -54
- microdf/tax.py +0 -78
- microdf/taxcalc.py +0 -161
- microdf/tests/test_compare.py +0 -48
- microdf/tests/test_decile_rank.py +0 -91
- microdf/tests/test_inequality.py +0 -20
- microdf/tests/test_io.py +0 -10
- microdf/tests/test_optional_dependency.py +0 -54
- microdf/tests/test_poverty.py +0 -73
- microdf/tests/test_quantile_chg.py +0 -18
- microdf/tests/test_tax.py +0 -54
- microdf/tests/test_taxcalc.py +0 -25
- microdf/tests/test_utils.py +0 -39
- microdf/tests/test_weighted.py +0 -74
- microdf/ubi.py +0 -46
- microdf/utils.py +0 -73
- microdf/weighted.py +0 -235
- microdf_python-0.4.6.dist-info/METADATA +0 -54
- microdf_python-0.4.6.dist-info/RECORD +0 -37
- {microdf_python-0.4.6.dist-info → microdf_python-1.0.0.dist-info}/WHEEL +0 -0
- {microdf_python-0.4.6.dist-info → microdf_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {microdf_python-0.4.6.dist-info → microdf_python-1.0.0.dist-info}/top_level.txt +0 -0
microdf/__init__.py
CHANGED
|
@@ -1,170 +1,14 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
3
|
-
from .charts import quantile_pct_chg_plot
|
|
4
|
-
from .concat import concat
|
|
5
|
-
from .constants import (
|
|
6
|
-
BENS,
|
|
7
|
-
ECI_REMOVE_COLS,
|
|
8
|
-
HOUSING_CASH_SHARE,
|
|
9
|
-
MCAID_CASH_SHARE,
|
|
10
|
-
MCARE_CASH_SHARE,
|
|
11
|
-
MED_BENS,
|
|
12
|
-
OTHER_CASH_SHARE,
|
|
13
|
-
SNAP_CASH_SHARE,
|
|
14
|
-
SSI_CASH_SHARE,
|
|
15
|
-
TANF_CASH_SHARE,
|
|
16
|
-
VET_CASH_SHARE,
|
|
17
|
-
WIC_CASH_SHARE,
|
|
18
|
-
)
|
|
19
|
-
from .custom_taxes import (
|
|
20
|
-
CARBON_TAX_INCIDENCE,
|
|
21
|
-
FTT_INCIDENCE,
|
|
22
|
-
VAT_INCIDENCE,
|
|
23
|
-
add_carbon_tax,
|
|
24
|
-
add_custom_tax,
|
|
25
|
-
add_ftt,
|
|
26
|
-
add_vat,
|
|
27
|
-
)
|
|
28
|
-
from .generic import MicroDataFrame, MicroSeries
|
|
29
|
-
from .income_measures import cash_income, market_income, tpc_eci
|
|
30
|
-
from .inequality import (
|
|
31
|
-
bottom_50_pct_share,
|
|
32
|
-
bottom_x_pct_share,
|
|
33
|
-
gini,
|
|
34
|
-
t10_b50,
|
|
35
|
-
top_0_1_pct_share,
|
|
36
|
-
top_1_pct_share,
|
|
37
|
-
top_10_pct_share,
|
|
38
|
-
top_50_pct_share,
|
|
39
|
-
top_x_pct_share,
|
|
40
|
-
)
|
|
41
|
-
from .io import read_stata_zip
|
|
42
|
-
from .poverty import (
|
|
43
|
-
deep_poverty_gap,
|
|
44
|
-
deep_poverty_rate,
|
|
45
|
-
fpl,
|
|
46
|
-
poverty_gap,
|
|
47
|
-
poverty_rate,
|
|
48
|
-
squared_poverty_gap,
|
|
49
|
-
)
|
|
50
|
-
from .style import AXIS_COLOR, DPI, GRID_COLOR, TITLE_COLOR, set_plot_style
|
|
51
|
-
from .tax import mtr, tax_from_mtrs
|
|
52
|
-
from .taxcalc import (
|
|
53
|
-
add_weighted_metrics,
|
|
54
|
-
calc_df,
|
|
55
|
-
n65,
|
|
56
|
-
recalculate,
|
|
57
|
-
static_baseline_calc,
|
|
58
|
-
)
|
|
59
|
-
from .ubi import ubi_or_bens
|
|
60
|
-
from .utils import (
|
|
61
|
-
cartesian_product,
|
|
62
|
-
dedup_list,
|
|
63
|
-
flatten,
|
|
64
|
-
listify,
|
|
65
|
-
ordinal_label,
|
|
66
|
-
)
|
|
67
|
-
from .weighted import (
|
|
68
|
-
add_weighted_quantiles,
|
|
69
|
-
quantile_chg,
|
|
70
|
-
weight,
|
|
71
|
-
weighted_mean,
|
|
72
|
-
weighted_median,
|
|
73
|
-
weighted_quantile,
|
|
74
|
-
weighted_sum,
|
|
75
|
-
)
|
|
1
|
+
from .microdataframe import MicroDataFrame, MicroDataFrameGroupBy
|
|
2
|
+
from .microseries import MicroSeries, MicroSeriesGroupBy
|
|
76
3
|
|
|
77
4
|
name = "microdf"
|
|
78
5
|
__version__ = "0.1.0"
|
|
79
6
|
|
|
80
7
|
__all__ = [
|
|
81
|
-
#
|
|
82
|
-
"combine_base_reform",
|
|
83
|
-
"pctchg_base_reform",
|
|
84
|
-
"agg",
|
|
85
|
-
# chart_utils.py
|
|
86
|
-
"dollar_format",
|
|
87
|
-
"currency_format",
|
|
88
|
-
# charts.py
|
|
89
|
-
"quantile_pct_chg_plot",
|
|
90
|
-
# concat.py
|
|
91
|
-
"concat",
|
|
92
|
-
# constants.py
|
|
93
|
-
"BENS",
|
|
94
|
-
"ECI_REMOVE_COLS",
|
|
95
|
-
"HOUSING_CASH_SHARE",
|
|
96
|
-
"MCAID_CASH_SHARE",
|
|
97
|
-
"MCARE_CASH_SHARE",
|
|
98
|
-
"MED_BENS",
|
|
99
|
-
"OTHER_CASH_SHARE",
|
|
100
|
-
"SNAP_CASH_SHARE",
|
|
101
|
-
"SSI_CASH_SHARE",
|
|
102
|
-
"TANF_CASH_SHARE",
|
|
103
|
-
"VET_CASH_SHARE",
|
|
104
|
-
"WIC_CASH_SHARE",
|
|
105
|
-
# custom_taxes.py
|
|
106
|
-
"CARBON_TAX_INCIDENCE",
|
|
107
|
-
"FTT_INCIDENCE",
|
|
108
|
-
"VAT_INCIDENCE",
|
|
109
|
-
"add_custom_tax",
|
|
110
|
-
"add_vat",
|
|
111
|
-
"add_carbon_tax",
|
|
112
|
-
"add_ftt",
|
|
113
|
-
# income_measures.py
|
|
114
|
-
"cash_income",
|
|
115
|
-
"tpc_eci",
|
|
116
|
-
"market_income",
|
|
117
|
-
# inequality.py
|
|
118
|
-
"gini",
|
|
119
|
-
"top_x_pct_share",
|
|
120
|
-
"bottom_x_pct_share",
|
|
121
|
-
"bottom_50_pct_share",
|
|
122
|
-
"top_10_pct_share",
|
|
123
|
-
"top_1_pct_share",
|
|
124
|
-
"top_0_1_pct_share",
|
|
125
|
-
"top_50_pct_share",
|
|
126
|
-
"t10_b50",
|
|
127
|
-
# io.py
|
|
128
|
-
"read_stata_zip",
|
|
129
|
-
# poverty.py
|
|
130
|
-
"fpl",
|
|
131
|
-
"poverty_rate",
|
|
132
|
-
"deep_poverty_rate",
|
|
133
|
-
"poverty_gap",
|
|
134
|
-
"squared_poverty_gap",
|
|
135
|
-
"deep_poverty_gap",
|
|
136
|
-
# style.py
|
|
137
|
-
"AXIS_COLOR",
|
|
138
|
-
"DPI",
|
|
139
|
-
"GRID_COLOR",
|
|
140
|
-
"TITLE_COLOR",
|
|
141
|
-
"set_plot_style",
|
|
142
|
-
# tax.py
|
|
143
|
-
"mtr",
|
|
144
|
-
"tax_from_mtrs",
|
|
145
|
-
# taxcalc.py
|
|
146
|
-
"static_baseline_calc",
|
|
147
|
-
"add_weighted_metrics",
|
|
148
|
-
"n65",
|
|
149
|
-
"calc_df",
|
|
150
|
-
"recalculate",
|
|
151
|
-
# ubi.py
|
|
152
|
-
"ubi_or_bens",
|
|
153
|
-
# utils.py
|
|
154
|
-
"ordinal_label",
|
|
155
|
-
"dedup_list",
|
|
156
|
-
"listify",
|
|
157
|
-
"flatten",
|
|
158
|
-
"cartesian_product",
|
|
159
|
-
# weighted.py
|
|
160
|
-
"weight",
|
|
161
|
-
"weighted_sum",
|
|
162
|
-
"weighted_mean",
|
|
163
|
-
"weighted_quantile",
|
|
164
|
-
"weighted_median",
|
|
165
|
-
"add_weighted_quantiles",
|
|
166
|
-
"quantile_chg",
|
|
167
|
-
# generic.py
|
|
8
|
+
# microseries.py
|
|
168
9
|
"MicroSeries",
|
|
10
|
+
"MicroSeriesGroupBy",
|
|
11
|
+
# microdataframe.py
|
|
169
12
|
"MicroDataFrame",
|
|
13
|
+
"MicroDataFrameGroupBy",
|
|
170
14
|
]
|
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import warnings
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Callable, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from microdf.microseries import MicroSeries, MicroSeriesGroupBy
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MicroDataFrame(pd.DataFrame):
|
|
16
|
+
def __init__(self, *args, weights=None, **kwargs):
|
|
17
|
+
"""A DataFrame-inheriting class for weighted microdata. Weights can be
|
|
18
|
+
provided at initialisation, or using set_weights or set_weight_col.
|
|
19
|
+
|
|
20
|
+
:param weights: Array of weights.
|
|
21
|
+
:type weights: np.array
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(*args, **kwargs)
|
|
24
|
+
self.weights = None
|
|
25
|
+
self.set_weights(weights)
|
|
26
|
+
self._link_all_weights()
|
|
27
|
+
self.override_df_functions()
|
|
28
|
+
|
|
29
|
+
def override_df_functions(self) -> None:
|
|
30
|
+
"""Override DataFrame functions to work with weighted operations."""
|
|
31
|
+
for name in MicroSeries.FUNCTIONS:
|
|
32
|
+
if name in MicroSeries.SCALAR_FUNCTIONS:
|
|
33
|
+
setattr(self, name, self._create_scalar_function(name))
|
|
34
|
+
elif name in MicroSeries.VECTOR_FUNCTIONS:
|
|
35
|
+
setattr(self, name, self._create_vector_function(name))
|
|
36
|
+
elif name in MicroSeries.AGNOSTIC_FUNCTIONS:
|
|
37
|
+
setattr(self, name, self._create_agnostic_function(name))
|
|
38
|
+
|
|
39
|
+
def _create_scalar_function(self, name: str) -> Callable:
|
|
40
|
+
"""Create a scalar function that returns a Series of results.
|
|
41
|
+
|
|
42
|
+
:param name: Name of the function to create
|
|
43
|
+
:return: Function that applies the operation to all columns
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def fn(*args, **kwargs) -> pd.Series:
|
|
47
|
+
results = pd.Series(
|
|
48
|
+
[
|
|
49
|
+
getattr(self[col], name)(*args, **kwargs)
|
|
50
|
+
for col in self.columns
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
results.index = self.columns
|
|
54
|
+
return results
|
|
55
|
+
|
|
56
|
+
return fn
|
|
57
|
+
|
|
58
|
+
def _create_vector_function(self, name: str) -> Callable:
|
|
59
|
+
"""Create a vector function that returns a DataFrame of results.
|
|
60
|
+
|
|
61
|
+
:param name: Name of the function to create
|
|
62
|
+
:return: Function that applies the operation to all columns
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def fn(*args, **kwargs) -> pd.DataFrame:
|
|
66
|
+
results = pd.DataFrame(
|
|
67
|
+
[
|
|
68
|
+
getattr(self[col], name)(*args, **kwargs)
|
|
69
|
+
for col in self.columns
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
results.index = self.columns
|
|
73
|
+
return results
|
|
74
|
+
|
|
75
|
+
return fn
|
|
76
|
+
|
|
77
|
+
def _create_agnostic_function(self, name: str) -> Callable:
|
|
78
|
+
"""Create a function that can be either scalar or vector based on
|
|
79
|
+
input.
|
|
80
|
+
|
|
81
|
+
:param name: Name of the function to create
|
|
82
|
+
:return: Function that applies the operation to all columns
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
86
|
+
# Check if first argument is array-like
|
|
87
|
+
is_array = len(args) > 0 and hasattr(args[0], "__len__")
|
|
88
|
+
|
|
89
|
+
if is_array:
|
|
90
|
+
# Use vector function behavior
|
|
91
|
+
results = pd.DataFrame(
|
|
92
|
+
[
|
|
93
|
+
getattr(self[col], name)(*args, **kwargs)
|
|
94
|
+
for col in self.columns
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
results.index = self.columns
|
|
98
|
+
return results
|
|
99
|
+
else:
|
|
100
|
+
# Use scalar function behavior
|
|
101
|
+
results = pd.Series(
|
|
102
|
+
[
|
|
103
|
+
getattr(self[col], name)(*args, **kwargs)
|
|
104
|
+
for col in self.columns
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
results.index = self.columns
|
|
108
|
+
return results
|
|
109
|
+
|
|
110
|
+
return fn
|
|
111
|
+
|
|
112
|
+
def get_args_as_micro_series(*kwarg_names: tuple) -> Callable:
|
|
113
|
+
"""Decorator for auto-parsing column names into MicroSeries objects. If
|
|
114
|
+
given, kwarg_names limits arguments checked to keyword arguments
|
|
115
|
+
specified.
|
|
116
|
+
|
|
117
|
+
:param arg_names: argument names to restrict to.
|
|
118
|
+
:type arg_names: str
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def arg_series_decorator(fn) -> Callable:
|
|
122
|
+
@wraps(fn)
|
|
123
|
+
def series_function(
|
|
124
|
+
self, *args, **kwargs
|
|
125
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
126
|
+
new_args = []
|
|
127
|
+
new_kwargs = {}
|
|
128
|
+
if len(kwarg_names) == 0:
|
|
129
|
+
for value in args:
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
if value not in self.columns:
|
|
132
|
+
raise Exception("Column not found")
|
|
133
|
+
new_args += [self[value]]
|
|
134
|
+
else:
|
|
135
|
+
new_args += [value]
|
|
136
|
+
for name, value in kwargs.items():
|
|
137
|
+
if isinstance(value, str) and (
|
|
138
|
+
len(kwarg_names) == 0 or name in kwarg_names
|
|
139
|
+
):
|
|
140
|
+
if value not in self.columns:
|
|
141
|
+
raise Exception("Column not found")
|
|
142
|
+
new_kwargs[name] = self[value]
|
|
143
|
+
else:
|
|
144
|
+
new_kwargs[name] = value
|
|
145
|
+
return fn(self, *new_args, **new_kwargs)
|
|
146
|
+
|
|
147
|
+
return series_function
|
|
148
|
+
|
|
149
|
+
return arg_series_decorator
|
|
150
|
+
|
|
151
|
+
def __setitem__(self, *args, **kwargs) -> None:
|
|
152
|
+
super().__setitem__(*args, **kwargs)
|
|
153
|
+
self._link_all_weights()
|
|
154
|
+
|
|
155
|
+
def _link_weights(self, column) -> None:
|
|
156
|
+
# self[column] = ... triggers __setitem__, which forces pd.Series
|
|
157
|
+
# this workaround avoids that
|
|
158
|
+
self[column].__class__ = MicroSeries
|
|
159
|
+
self[column].set_weights(self.weights)
|
|
160
|
+
|
|
161
|
+
def _link_all_weights(self) -> None:
|
|
162
|
+
if self.weights is None:
|
|
163
|
+
if len(self) > 0:
|
|
164
|
+
self.set_weights(np.ones((len(self))))
|
|
165
|
+
for column in self.columns:
|
|
166
|
+
if column != self.weights_col:
|
|
167
|
+
self._link_weights(column)
|
|
168
|
+
|
|
169
|
+
def set_weights(
|
|
170
|
+
self,
|
|
171
|
+
weights: Union[np.ndarray, str],
|
|
172
|
+
preserve_old: Optional[bool] = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Sets the weights for the MicroDataFrame. If a string is received, it
|
|
175
|
+
will be assumed to be the column name of the weight column.
|
|
176
|
+
|
|
177
|
+
:param weights: Array of weights.
|
|
178
|
+
:param preserve_old: If True, keeps the old weights as a column when
|
|
179
|
+
new weights are provided.
|
|
180
|
+
:type weights: np.array
|
|
181
|
+
"""
|
|
182
|
+
if preserve_old and self.weights_col is not None:
|
|
183
|
+
self["old_" + self.weights_col] = self.weights
|
|
184
|
+
|
|
185
|
+
if isinstance(weights, str):
|
|
186
|
+
self.weights_col = weights
|
|
187
|
+
self.weights = pd.Series(self[weights], dtype=float)
|
|
188
|
+
elif weights is not None:
|
|
189
|
+
if len(weights) != len(self):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Length of weights ({len(weights)}) does not match "
|
|
192
|
+
f"length of DataFrame ({len(self)})."
|
|
193
|
+
)
|
|
194
|
+
self.weights_col = None
|
|
195
|
+
with warnings.catch_warnings():
|
|
196
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
197
|
+
self.weights = pd.Series(weights, dtype=float)
|
|
198
|
+
self._link_all_weights()
|
|
199
|
+
|
|
200
|
+
def set_weight_col(
|
|
201
|
+
self, column: str, preserve_old: Optional[bool] = False
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Sets the weights for the MicroDataFrame by specifying the name of
|
|
204
|
+
the weight column.
|
|
205
|
+
|
|
206
|
+
:param weights: Array of weights.
|
|
207
|
+
:param preserve_old: If True, keeps the old weights as a column when
|
|
208
|
+
new weights are provided.
|
|
209
|
+
:type weights: np.array
|
|
210
|
+
"""
|
|
211
|
+
if preserve_old and self.weights_col is not None:
|
|
212
|
+
self["old_" + self.weights_col] = self.weights
|
|
213
|
+
|
|
214
|
+
self.weights = np.array(self[column])
|
|
215
|
+
self.weights_col = column
|
|
216
|
+
self._link_all_weights()
|
|
217
|
+
|
|
218
|
+
def __getitem__(
|
|
219
|
+
self, key: Union[str, List]
|
|
220
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
221
|
+
result = super().__getitem__(key)
|
|
222
|
+
if isinstance(result, pd.DataFrame):
|
|
223
|
+
try:
|
|
224
|
+
weights = self.weights[key]
|
|
225
|
+
except Exception:
|
|
226
|
+
weights = self.weights
|
|
227
|
+
return MicroDataFrame(result, weights=weights)
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
def catch_series_relapse(self) -> None:
|
|
231
|
+
for col in self.columns:
|
|
232
|
+
if self[col].__class__ == pd.Series:
|
|
233
|
+
self._link_weights(col)
|
|
234
|
+
|
|
235
|
+
def __setattr__(self, key, value) -> None:
|
|
236
|
+
super().__setattr__(key, value)
|
|
237
|
+
self.catch_series_relapse()
|
|
238
|
+
|
|
239
|
+
def reset_index(
|
|
240
|
+
self,
|
|
241
|
+
level: Optional[int] = None,
|
|
242
|
+
drop: Optional[bool] = False,
|
|
243
|
+
inplace: Optional[bool] = False,
|
|
244
|
+
col_level: Optional[int] = 0,
|
|
245
|
+
col_fill: Optional[str] = "",
|
|
246
|
+
allow_duplicates: Optional[bool] = None,
|
|
247
|
+
names: Optional[List[str]] = None,
|
|
248
|
+
) -> Union["MicroDataFrame", None]:
|
|
249
|
+
"""Reset the index of the MicroDataFrame.
|
|
250
|
+
|
|
251
|
+
This method supports all parameters of pandas DataFrame.reset_index(),
|
|
252
|
+
including the 'inplace' parameter.
|
|
253
|
+
|
|
254
|
+
:param level: Only remove the given levels from the index. Removes all
|
|
255
|
+
levels by default.
|
|
256
|
+
:param drop: Do not try to insert index into dataframe columns. This
|
|
257
|
+
resets the index to the default integer index.
|
|
258
|
+
:param inplace: Modify the DataFrame in place (do not create a new
|
|
259
|
+
object).
|
|
260
|
+
:param col_level: If the columns have multiple levels, determines which
|
|
261
|
+
level the labels are inserted into.
|
|
262
|
+
:param col_fill: If the columns have multiple levels, determines how
|
|
263
|
+
the other levels are named.
|
|
264
|
+
:param allow_duplicates: Allow duplicate column labels to be created.
|
|
265
|
+
:param names: Using the given string, rename the DataFrame column which
|
|
266
|
+
contains the index data.
|
|
267
|
+
:return: MicroDataFrame with reset index or None if inplace=True.
|
|
268
|
+
"""
|
|
269
|
+
if inplace:
|
|
270
|
+
weights_backup = self.weights.copy()
|
|
271
|
+
# Perform in-place reset on the parent DataFrame
|
|
272
|
+
super().reset_index(
|
|
273
|
+
level=level,
|
|
274
|
+
drop=drop,
|
|
275
|
+
inplace=True,
|
|
276
|
+
col_level=col_level,
|
|
277
|
+
col_fill=col_fill,
|
|
278
|
+
allow_duplicates=allow_duplicates,
|
|
279
|
+
names=names,
|
|
280
|
+
)
|
|
281
|
+
self.weights = weights_backup
|
|
282
|
+
self._link_all_weights()
|
|
283
|
+
return None
|
|
284
|
+
else:
|
|
285
|
+
res = super().reset_index(
|
|
286
|
+
level=level,
|
|
287
|
+
drop=drop,
|
|
288
|
+
inplace=False,
|
|
289
|
+
col_level=col_level,
|
|
290
|
+
col_fill=col_fill,
|
|
291
|
+
allow_duplicates=allow_duplicates,
|
|
292
|
+
names=names,
|
|
293
|
+
)
|
|
294
|
+
return MicroDataFrame(res, weights=self.weights)
|
|
295
|
+
|
|
296
|
+
def copy(self, deep: Optional[bool] = True) -> "MicroDataFrame":
|
|
297
|
+
res = super().copy(deep)
|
|
298
|
+
# This changes the original columns to Series. Undo it:
|
|
299
|
+
for col in self.columns:
|
|
300
|
+
self[col] = MicroSeries(self[col])
|
|
301
|
+
res = MicroDataFrame(res, weights=self.weights.copy(deep))
|
|
302
|
+
return res
|
|
303
|
+
|
|
304
|
+
def equals(self, other: "MicroDataFrame") -> bool:
|
|
305
|
+
equal_values = super().equals(other)
|
|
306
|
+
equal_weights = self.weights.equals(other.weights)
|
|
307
|
+
return equal_values and equal_weights
|
|
308
|
+
|
|
309
|
+
@get_args_as_micro_series()
|
|
310
|
+
def groupby(
|
|
311
|
+
self, by: Union[str, List], *args, **kwargs
|
|
312
|
+
) -> "MicroDataFrameGroupBy":
|
|
313
|
+
"""Returns a GroupBy object with MicroSeriesGroupBy objects for each
|
|
314
|
+
column.
|
|
315
|
+
|
|
316
|
+
:param by: column to group by
|
|
317
|
+
:type by: Union[str, List]
|
|
318
|
+
|
|
319
|
+
return: DataFrameGroupBy object with columns using weights
|
|
320
|
+
rtype: DataFrameGroupBy
|
|
321
|
+
"""
|
|
322
|
+
self["__tmp_weights"] = self.weights
|
|
323
|
+
gb = super().groupby(by, *args, **kwargs)
|
|
324
|
+
weights = copy.deepcopy(gb["__tmp_weights"])
|
|
325
|
+
for col in self.columns: # df.groupby(...)[col]s use weights
|
|
326
|
+
res = gb[col]
|
|
327
|
+
res.__class__ = MicroSeriesGroupBy
|
|
328
|
+
res._init()
|
|
329
|
+
res.weights = weights
|
|
330
|
+
setattr(gb, col, res)
|
|
331
|
+
gb.__class__ = MicroDataFrameGroupBy
|
|
332
|
+
gb._init(by)
|
|
333
|
+
return gb
|
|
334
|
+
|
|
335
|
+
@get_args_as_micro_series()
|
|
336
|
+
def poverty_rate(self, income: str, threshold: str) -> float:
|
|
337
|
+
"""Calculate poverty rate, i.e., the population share with income below
|
|
338
|
+
their poverty threshold.
|
|
339
|
+
|
|
340
|
+
:param income: Column indicating income.
|
|
341
|
+
:type income: str
|
|
342
|
+
:param threshold: Column indicating threshold.
|
|
343
|
+
:type threshold: str
|
|
344
|
+
:return: Poverty rate between zero and one.
|
|
345
|
+
:rtype: float
|
|
346
|
+
"""
|
|
347
|
+
pov = income < threshold
|
|
348
|
+
return pov.sum() / pov.count()
|
|
349
|
+
|
|
350
|
+
@get_args_as_micro_series()
|
|
351
|
+
def deep_poverty_rate(self, income: str, threshold: str) -> float:
|
|
352
|
+
"""Calculate deep poverty rate, i.e., the population share with income
|
|
353
|
+
below half their poverty threshold.
|
|
354
|
+
|
|
355
|
+
:param income: Column indicating income.
|
|
356
|
+
:type income: str
|
|
357
|
+
:param threshold: Column indicating threshold.
|
|
358
|
+
:type threshold: str
|
|
359
|
+
:return: Deep poverty rate between zero and one.
|
|
360
|
+
:rtype: float
|
|
361
|
+
"""
|
|
362
|
+
pov = income < (threshold / 2)
|
|
363
|
+
return pov.sum() / pov.count()
|
|
364
|
+
|
|
365
|
+
@get_args_as_micro_series()
|
|
366
|
+
def poverty_gap(self, income: str, threshold: str) -> float:
|
|
367
|
+
"""Calculate poverty gap, i.e., the total gap between income and
|
|
368
|
+
poverty thresholds for all people in poverty.
|
|
369
|
+
|
|
370
|
+
:param income: Column indicating income.
|
|
371
|
+
:type income: str
|
|
372
|
+
:param threshold: Column indicating threshold.
|
|
373
|
+
:type threshold: str
|
|
374
|
+
:return: Poverty gap.
|
|
375
|
+
:rtype: float
|
|
376
|
+
"""
|
|
377
|
+
gaps = (threshold - income)[threshold > income]
|
|
378
|
+
return gaps.sum()
|
|
379
|
+
|
|
380
|
+
@get_args_as_micro_series()
|
|
381
|
+
def deep_poverty_gap(self, income: str, threshold: str) -> float:
|
|
382
|
+
"""Calculate deep poverty gap, i.e., the total gap between income and
|
|
383
|
+
half of poverty thresholds for all people in deep poverty.
|
|
384
|
+
|
|
385
|
+
:param income: Column indicating income.
|
|
386
|
+
:type income: str
|
|
387
|
+
:param threshold: Column indicating threshold.
|
|
388
|
+
:type threshold: str
|
|
389
|
+
:return: Deep poverty gap.
|
|
390
|
+
:rtype: float
|
|
391
|
+
"""
|
|
392
|
+
deep_threshold = threshold / 2
|
|
393
|
+
gaps = (deep_threshold - income)[deep_threshold > income]
|
|
394
|
+
return gaps.sum()
|
|
395
|
+
|
|
396
|
+
@get_args_as_micro_series()
|
|
397
|
+
def squared_poverty_gap(self, income: str, threshold: str) -> float:
|
|
398
|
+
"""Calculate squared poverty gap, i.e., the total squared gap between
|
|
399
|
+
income and poverty thresholds for all people in poverty. Also known as
|
|
400
|
+
the poverty severity index.
|
|
401
|
+
|
|
402
|
+
:param income: Column indicating income.
|
|
403
|
+
:type income: str
|
|
404
|
+
:param threshold: Column indicating threshold.
|
|
405
|
+
:type threshold: str
|
|
406
|
+
:return: Squared poverty gap.
|
|
407
|
+
:rtype: float
|
|
408
|
+
"""
|
|
409
|
+
gaps = (threshold - income)[threshold > income]
|
|
410
|
+
squared_gaps = gaps**2
|
|
411
|
+
return squared_gaps.sum()
|
|
412
|
+
|
|
413
|
+
@get_args_as_micro_series()
|
|
414
|
+
def poverty_count(
|
|
415
|
+
self,
|
|
416
|
+
income: Union[MicroSeries, str],
|
|
417
|
+
threshold: Union[MicroSeries, str],
|
|
418
|
+
) -> int:
|
|
419
|
+
"""Calculates the number of entities with income below a poverty
|
|
420
|
+
threshold.
|
|
421
|
+
|
|
422
|
+
:param income: income array or column name
|
|
423
|
+
:type income: Union[MicroSeries, str]
|
|
424
|
+
|
|
425
|
+
:param threshold: threshold array or column name
|
|
426
|
+
:type threshold: Union[MicroSeries, str]
|
|
427
|
+
|
|
428
|
+
return: number of entities in poverty
|
|
429
|
+
rtype: int
|
|
430
|
+
"""
|
|
431
|
+
in_poverty = income < threshold
|
|
432
|
+
return in_poverty.sum()
|
|
433
|
+
|
|
434
|
+
def astype(
|
|
435
|
+
self,
|
|
436
|
+
dtype,
|
|
437
|
+
copy: Optional[bool] = True,
|
|
438
|
+
errors: Optional[str] = "raise",
|
|
439
|
+
) -> "MicroDataFrame":
|
|
440
|
+
"""Convert MicroDataFrame to specified data type while preserving
|
|
441
|
+
weights.
|
|
442
|
+
|
|
443
|
+
:param dtype: Data type to convert to. Can be numpy dtype, Python type,
|
|
444
|
+
or dict.
|
|
445
|
+
:param copy: Whether to make a copy of the data (default True).
|
|
446
|
+
:param errors: How to handle conversion errors (default "raise").
|
|
447
|
+
:return: New MicroDataFrame with converted data types and preserved
|
|
448
|
+
weights.
|
|
449
|
+
"""
|
|
450
|
+
converted_df = super().astype(dtype, copy=copy, errors=errors)
|
|
451
|
+
return MicroDataFrame(
|
|
452
|
+
converted_df, weights=self.weights.copy() if copy else self.weights
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def __repr__(self) -> str:
|
|
456
|
+
df = pd.DataFrame(self)
|
|
457
|
+
df["weight"] = self.weights
|
|
458
|
+
return df[[df.columns[-1]] + list(df.columns[:-1])].__repr__()
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
462
|
+
def _init(self, by: Union[str, List]):
|
|
463
|
+
self.columns = list(self.obj.columns)
|
|
464
|
+
if isinstance(by, list):
|
|
465
|
+
for column in by:
|
|
466
|
+
self.columns.remove(column)
|
|
467
|
+
elif isinstance(by, str):
|
|
468
|
+
self.columns.remove(by)
|
|
469
|
+
self.columns.remove("__tmp_weights")
|
|
470
|
+
for fn_name in MicroSeries.SCALAR_FUNCTIONS:
|
|
471
|
+
|
|
472
|
+
def get_fn(name):
|
|
473
|
+
def fn(*args, **kwargs):
|
|
474
|
+
return MicroDataFrame(
|
|
475
|
+
{
|
|
476
|
+
col: getattr(getattr(self, col), name)(
|
|
477
|
+
*args, **kwargs
|
|
478
|
+
)
|
|
479
|
+
for col in self.columns
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
return fn
|
|
484
|
+
|
|
485
|
+
setattr(self, fn_name, get_fn(fn_name))
|
|
486
|
+
for fn_name in MicroSeries.VECTOR_FUNCTIONS:
|
|
487
|
+
|
|
488
|
+
def get_fn(name) -> Callable:
|
|
489
|
+
def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
490
|
+
return MicroDataFrame(
|
|
491
|
+
{
|
|
492
|
+
col: getattr(getattr(self, col), name)(
|
|
493
|
+
*args, **kwargs
|
|
494
|
+
)
|
|
495
|
+
for col in self.columns
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return fn
|
|
500
|
+
|
|
501
|
+
setattr(self, fn_name, get_fn(fn_name))
|