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/generic.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
from typing import Callable, Union
|
|
2
|
-
from functools import wraps
|
|
3
|
-
import warnings
|
|
4
1
|
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import warnings
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Callable, List, Optional, Union
|
|
6
|
+
|
|
5
7
|
import numpy as np
|
|
6
8
|
import pandas as pd
|
|
7
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
8
12
|
|
|
9
13
|
class MicroSeries(pd.Series):
|
|
10
14
|
def __init__(self, *args, weights: np.array = None, **kwargs):
|
|
11
|
-
"""A Series-inheriting class for weighted microdata.
|
|
12
|
-
|
|
15
|
+
"""A Series-inheriting class for weighted microdata. Weights can be
|
|
16
|
+
provided at initialisation, or using set_weights.
|
|
13
17
|
|
|
14
18
|
:param weights: Array of weights.
|
|
15
19
|
:type weights: np.array
|
|
@@ -76,7 +80,7 @@ class MicroSeries(pd.Series):
|
|
|
76
80
|
|
|
77
81
|
@scalar_function
|
|
78
82
|
def mean(self) -> float:
|
|
79
|
-
"""Calculates the weighted mean of the MicroSeries
|
|
83
|
+
"""Calculates the weighted mean of the MicroSeries.
|
|
80
84
|
|
|
81
85
|
:returns: The weighted mean.
|
|
82
86
|
:rtype: float
|
|
@@ -121,7 +125,7 @@ class MicroSeries(pd.Series):
|
|
|
121
125
|
return self.quantile(0.5)
|
|
122
126
|
|
|
123
127
|
@scalar_function
|
|
124
|
-
def gini(self, negatives: str = None) -> float:
|
|
128
|
+
def gini(self, negatives: Optional[str] = None) -> float:
|
|
125
129
|
"""Calculates Gini index.
|
|
126
130
|
|
|
127
131
|
:param negatives: An optional string indicating how to treat negative
|
|
@@ -172,7 +176,7 @@ class MicroSeries(pd.Series):
|
|
|
172
176
|
return top_x_pct_sum / total_sum
|
|
173
177
|
|
|
174
178
|
@scalar_function
|
|
175
|
-
def bottom_x_pct_share(self, bottom_x_pct) -> float:
|
|
179
|
+
def bottom_x_pct_share(self, bottom_x_pct: float) -> float:
|
|
176
180
|
"""Calculates bottom x% share.
|
|
177
181
|
|
|
178
182
|
:param bottom_x_pct: Decimal between 0 and 1 of the top %, e.g. 0.1,
|
|
@@ -232,9 +236,8 @@ class MicroSeries(pd.Series):
|
|
|
232
236
|
def t10_b50(self) -> float:
|
|
233
237
|
"""Calculates ratio between the top 10% and bottom 50% shares.
|
|
234
238
|
|
|
235
|
-
:returns: The weighted share held by the top 10% divided by
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
:returns: The weighted share held by the top 10% divided by the
|
|
240
|
+
weighted share held by the bottom 50%.
|
|
238
241
|
"""
|
|
239
242
|
t10 = self.top_10_pct_share()
|
|
240
243
|
b50 = self.bottom_50_pct_share()
|
|
@@ -242,181 +245,274 @@ class MicroSeries(pd.Series):
|
|
|
242
245
|
|
|
243
246
|
@vector_function
|
|
244
247
|
def cumsum(self) -> pd.Series:
|
|
248
|
+
logger.warning(
|
|
249
|
+
"cumsum() returns cumulative sums of weighted values as a regular "
|
|
250
|
+
"pandas Series. The original weights have already been applied "
|
|
251
|
+
"and cannot be reused with the cumulative results."
|
|
252
|
+
)
|
|
245
253
|
return pd.Series(self * self.weights).cumsum()
|
|
246
254
|
|
|
247
255
|
@vector_function
|
|
248
|
-
def rank(self, pct=False) -> pd.Series:
|
|
256
|
+
def rank(self, pct: Optional[bool] = False) -> pd.Series:
|
|
257
|
+
weights_sum = self.weights.values.sum()
|
|
258
|
+
if weights_sum == 0:
|
|
259
|
+
raise ZeroDivisionError(
|
|
260
|
+
"Cannot calculate rank with zero total weight. "
|
|
261
|
+
"All weights in the MicroSeries are zero, which would result "
|
|
262
|
+
"in division by zero."
|
|
263
|
+
)
|
|
264
|
+
|
|
249
265
|
order = np.argsort(self.values)
|
|
250
266
|
inverse_order = np.argsort(order)
|
|
251
267
|
ranks = np.array(self.weights.values)[order].cumsum()[inverse_order]
|
|
252
268
|
if pct:
|
|
253
|
-
ranks /=
|
|
254
|
-
np.where(ranks > 1.0, 1.0, ranks)
|
|
255
|
-
return
|
|
269
|
+
ranks /= weights_sum
|
|
270
|
+
ranks = np.where(ranks > 1.0, 1.0, ranks)
|
|
271
|
+
return MicroSeries(ranks, index=self.index, weights=self.weights)
|
|
256
272
|
|
|
257
273
|
@vector_function
|
|
258
|
-
def decile_rank(self):
|
|
259
|
-
|
|
274
|
+
def decile_rank(self, negatives_in_zero: Optional[bool] = False):
|
|
275
|
+
"""Calculate decile ranks (1-10) with optional zero decile for
|
|
276
|
+
negatives.
|
|
277
|
+
|
|
278
|
+
:param negatives_in_zero: If True, negative values are assigned to
|
|
279
|
+
decile 0. If False (default), all values are ranked 1-10.
|
|
280
|
+
:type negatives_in_zero: bool
|
|
281
|
+
:returns: MicroSeries with decile ranks
|
|
282
|
+
:rtype: MicroSeries
|
|
283
|
+
"""
|
|
284
|
+
if negatives_in_zero:
|
|
285
|
+
negative_mask = self < 0
|
|
286
|
+
if negative_mask.any():
|
|
287
|
+
non_negative_values = self[~negative_mask]
|
|
288
|
+
if len(non_negative_values) > 0:
|
|
289
|
+
non_neg_ranks = non_negative_values.rank(pct=True)
|
|
290
|
+
deciles = np.minimum(np.ceil(non_neg_ranks * 10), 10)
|
|
291
|
+
else:
|
|
292
|
+
deciles = np.array([])
|
|
293
|
+
|
|
294
|
+
result = np.zeros(len(self))
|
|
295
|
+
result[negative_mask] = 0
|
|
296
|
+
if len(deciles) > 0:
|
|
297
|
+
result[~negative_mask] = deciles
|
|
298
|
+
|
|
299
|
+
return MicroSeries(result, weights=self.weights)
|
|
300
|
+
|
|
301
|
+
# Default behavior: rank all values 1-10
|
|
302
|
+
return MicroSeries(
|
|
303
|
+
np.minimum(np.ceil(self.rank(pct=True) * 10), 10),
|
|
304
|
+
weights=self.weights,
|
|
305
|
+
)
|
|
260
306
|
|
|
261
307
|
@vector_function
|
|
262
|
-
def quintile_rank(self):
|
|
263
|
-
return MicroSeries(
|
|
308
|
+
def quintile_rank(self) -> "MicroSeries":
|
|
309
|
+
return MicroSeries(
|
|
310
|
+
np.minimum(np.ceil(self.rank(pct=True) * 5), 5),
|
|
311
|
+
weights=self.weights,
|
|
312
|
+
)
|
|
264
313
|
|
|
265
314
|
@vector_function
|
|
266
|
-
def quartile_rank(self):
|
|
267
|
-
return MicroSeries(
|
|
315
|
+
def quartile_rank(self) -> "MicroSeries":
|
|
316
|
+
return MicroSeries(
|
|
317
|
+
np.minimum(np.ceil(self.rank(pct=True) * 4), 4),
|
|
318
|
+
weights=self.weights,
|
|
319
|
+
)
|
|
268
320
|
|
|
269
321
|
@vector_function
|
|
270
|
-
def percentile_rank(self):
|
|
271
|
-
return MicroSeries(
|
|
322
|
+
def percentile_rank(self) -> "MicroSeries":
|
|
323
|
+
return MicroSeries(
|
|
324
|
+
np.minimum(np.ceil(self.rank(pct=True) * 100), 100),
|
|
325
|
+
weights=self.weights,
|
|
326
|
+
)
|
|
272
327
|
|
|
273
|
-
def groupby(self, *args, **kwargs):
|
|
328
|
+
def groupby(self, *args, **kwargs) -> "MicroSeriesGroupBy":
|
|
274
329
|
gb = super().groupby(*args, **kwargs)
|
|
275
330
|
gb.__class__ = MicroSeriesGroupBy
|
|
276
331
|
gb._init()
|
|
277
332
|
gb.weights = pd.Series(self.weights).groupby(*args, **kwargs)
|
|
278
333
|
return gb
|
|
279
334
|
|
|
280
|
-
def copy(self, deep=True):
|
|
335
|
+
def copy(self, deep: Optional[bool] = True):
|
|
281
336
|
res = super().copy(deep)
|
|
282
337
|
res = MicroSeries(res, weights=self.weights.copy(deep))
|
|
283
338
|
return res
|
|
284
339
|
|
|
285
|
-
def
|
|
340
|
+
def clip(
|
|
341
|
+
self,
|
|
342
|
+
lower: Optional[float] = None,
|
|
343
|
+
upper: Optional[float] = None,
|
|
344
|
+
axis: Optional[int] = None,
|
|
345
|
+
inplace: Optional[bool] = False,
|
|
346
|
+
*args,
|
|
347
|
+
**kwargs,
|
|
348
|
+
) -> "MicroSeries":
|
|
349
|
+
res = super().clip(
|
|
350
|
+
lower=lower,
|
|
351
|
+
upper=upper,
|
|
352
|
+
axis=axis,
|
|
353
|
+
inplace=inplace,
|
|
354
|
+
*args,
|
|
355
|
+
**kwargs,
|
|
356
|
+
)
|
|
357
|
+
if not inplace:
|
|
358
|
+
return MicroSeries(res, weights=self.weights)
|
|
359
|
+
return self
|
|
360
|
+
|
|
361
|
+
def round(
|
|
362
|
+
self, decimals: Optional[int] = 0, *args, **kwargs
|
|
363
|
+
) -> "MicroSeries":
|
|
364
|
+
res = super().round(decimals=decimals, *args, **kwargs)
|
|
365
|
+
return MicroSeries(res, weights=self.weights)
|
|
366
|
+
|
|
367
|
+
def equals(self, other: "MicroSeries") -> bool:
|
|
286
368
|
equal_values = super().equals(other)
|
|
287
369
|
equal_weights = self.weights.equals(other.weights)
|
|
288
370
|
return equal_values and equal_weights
|
|
289
371
|
|
|
290
|
-
def __getitem__(
|
|
372
|
+
def __getitem__(
|
|
373
|
+
self, key: Union[str, int, slice, List, np.ndarray]
|
|
374
|
+
) -> Union["MicroSeries", pd.Series]:
|
|
291
375
|
result = super().__getitem__(key)
|
|
292
376
|
if isinstance(result, pd.Series):
|
|
293
377
|
weights = self.weights.__getitem__(key)
|
|
294
378
|
return MicroSeries(result, weights=weights)
|
|
295
379
|
return result
|
|
296
380
|
|
|
297
|
-
def __getattr__(self, name):
|
|
381
|
+
def __getattr__(self, name: str) -> "MicroSeries":
|
|
298
382
|
return MicroSeries(super().__getattr__(name), weights=self.weights)
|
|
299
383
|
|
|
300
384
|
# operators
|
|
301
385
|
|
|
302
|
-
def __add__(self, other):
|
|
386
|
+
def __add__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
303
387
|
return MicroSeries(super().__add__(other), weights=self.weights)
|
|
304
388
|
|
|
305
|
-
def __sub__(self, other):
|
|
389
|
+
def __sub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
306
390
|
return MicroSeries(super().__sub__(other), weights=self.weights)
|
|
307
391
|
|
|
308
|
-
def __mul__(self, other):
|
|
392
|
+
def __mul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
309
393
|
return MicroSeries(super().__mul__(other), weights=self.weights)
|
|
310
394
|
|
|
311
|
-
def __floordiv__(
|
|
395
|
+
def __floordiv__(
|
|
396
|
+
self, other: Union[int, float, pd.Series]
|
|
397
|
+
) -> "MicroSeries":
|
|
312
398
|
return MicroSeries(super().__floordiv__(other), weights=self.weights)
|
|
313
399
|
|
|
314
|
-
def __truediv__(
|
|
400
|
+
def __truediv__(
|
|
401
|
+
self, other: Union[int, float, pd.Series]
|
|
402
|
+
) -> "MicroSeries":
|
|
315
403
|
return MicroSeries(super().__truediv__(other), weights=self.weights)
|
|
316
404
|
|
|
317
|
-
def __mod__(self, other):
|
|
405
|
+
def __mod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
318
406
|
return MicroSeries(super().__mod__(other), weights=self.weights)
|
|
319
407
|
|
|
320
|
-
def __pow__(self, other):
|
|
408
|
+
def __pow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
321
409
|
return MicroSeries(super().__pow__(other), weights=self.weights)
|
|
322
410
|
|
|
323
|
-
def __xor__(self, other):
|
|
411
|
+
def __xor__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
324
412
|
return MicroSeries(super().__xor__(other), weights=self.weights)
|
|
325
413
|
|
|
326
|
-
def __and__(self, other):
|
|
414
|
+
def __and__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
327
415
|
return MicroSeries(super().__and__(other), weights=self.weights)
|
|
328
416
|
|
|
329
|
-
def __or__(self, other):
|
|
417
|
+
def __or__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
330
418
|
return MicroSeries(super().__or__(other), weights=self.weights)
|
|
331
419
|
|
|
332
|
-
def __invert__(self):
|
|
420
|
+
def __invert__(self) -> "MicroSeries":
|
|
333
421
|
return MicroSeries(super().__invert__(), weights=self.weights)
|
|
334
422
|
|
|
335
|
-
def __radd__(self, other):
|
|
423
|
+
def __radd__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
336
424
|
return MicroSeries(super().__radd__(other), weights=self.weights)
|
|
337
425
|
|
|
338
|
-
def __rsub__(self, other):
|
|
426
|
+
def __rsub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
339
427
|
return MicroSeries(super().__rsub__(other), weights=self.weights)
|
|
340
428
|
|
|
341
|
-
def __rmul__(self, other):
|
|
429
|
+
def __rmul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
342
430
|
return MicroSeries(super().__rmul__(other), weights=self.weights)
|
|
343
431
|
|
|
344
|
-
def __rfloordiv__(
|
|
432
|
+
def __rfloordiv__(
|
|
433
|
+
self, other: Union[int, float, pd.Series]
|
|
434
|
+
) -> "MicroSeries":
|
|
345
435
|
return MicroSeries(super().__rfloordiv__(other), weights=self.weights)
|
|
346
436
|
|
|
347
|
-
def __rtruediv__(
|
|
437
|
+
def __rtruediv__(
|
|
438
|
+
self, other: Union[int, float, pd.Series]
|
|
439
|
+
) -> "MicroSeries":
|
|
348
440
|
return MicroSeries(super().__rtruediv__(other), weights=self.weights)
|
|
349
441
|
|
|
350
|
-
def __rmod__(self, other):
|
|
442
|
+
def __rmod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
351
443
|
return MicroSeries(super().__rmod__(other), weights=self.weights)
|
|
352
444
|
|
|
353
|
-
def __rpow__(self, other):
|
|
445
|
+
def __rpow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
354
446
|
return MicroSeries(super().__rpow__(other), weights=self.weights)
|
|
355
447
|
|
|
356
|
-
def __rand__(self, other):
|
|
448
|
+
def __rand__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
357
449
|
return MicroSeries(super().__rand__(other), weights=self.weights)
|
|
358
450
|
|
|
359
|
-
def __ror__(self, other):
|
|
451
|
+
def __ror__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
360
452
|
return MicroSeries(super().__ror__(other), weights=self.weights)
|
|
361
453
|
|
|
362
|
-
def __rxor__(self, other):
|
|
454
|
+
def __rxor__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
363
455
|
return MicroSeries(super().__rxor__(other), weights=self.weights)
|
|
364
456
|
|
|
365
457
|
# comparators
|
|
366
458
|
|
|
367
|
-
def __lt__(self, other):
|
|
459
|
+
def __lt__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
368
460
|
return MicroSeries(super().__lt__(other), weights=self.weights)
|
|
369
461
|
|
|
370
|
-
def __le__(self, other):
|
|
462
|
+
def __le__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
371
463
|
return MicroSeries(super().__le__(other), weights=self.weights)
|
|
372
464
|
|
|
373
|
-
def __eq__(self, other):
|
|
465
|
+
def __eq__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
374
466
|
return MicroSeries(super().__eq__(other), weights=self.weights)
|
|
375
467
|
|
|
376
|
-
def __ne__(self, other):
|
|
468
|
+
def __ne__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
377
469
|
return MicroSeries(super().__ne__(other), weights=self.weights)
|
|
378
470
|
|
|
379
|
-
def __ge__(self, other):
|
|
471
|
+
def __ge__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
380
472
|
return MicroSeries(super().__ge__(other), weights=self.weights)
|
|
381
473
|
|
|
382
|
-
def __gt__(self, other):
|
|
474
|
+
def __gt__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
383
475
|
return MicroSeries(super().__gt__(other), weights=self.weights)
|
|
384
476
|
|
|
385
477
|
# assignment operators
|
|
386
478
|
|
|
387
|
-
def __iadd__(self, other):
|
|
479
|
+
def __iadd__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
388
480
|
return MicroSeries(super().__iadd__(other), weights=self.weights)
|
|
389
481
|
|
|
390
|
-
def __isub__(self, other):
|
|
482
|
+
def __isub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
391
483
|
return MicroSeries(super().__isub__(other), weights=self.weights)
|
|
392
484
|
|
|
393
|
-
def __imul__(self, other):
|
|
485
|
+
def __imul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
394
486
|
return MicroSeries(super().__imul__(other), weights=self.weights)
|
|
395
487
|
|
|
396
|
-
def __ifloordiv__(
|
|
488
|
+
def __ifloordiv__(
|
|
489
|
+
self, other: Union[int, float, pd.Series]
|
|
490
|
+
) -> "MicroSeries":
|
|
397
491
|
return MicroSeries(super().__ifloordiv__(other), weights=self.weights)
|
|
398
492
|
|
|
399
|
-
def __idiv__(self, other):
|
|
493
|
+
def __idiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
400
494
|
return MicroSeries(super().__idiv__(other), weights=self.weights)
|
|
401
495
|
|
|
402
|
-
def __itruediv__(
|
|
496
|
+
def __itruediv__(
|
|
497
|
+
self, other: Union[int, float, pd.Series]
|
|
498
|
+
) -> "MicroSeries":
|
|
403
499
|
return MicroSeries(super().__itruediv__(other), weights=self.weights)
|
|
404
500
|
|
|
405
|
-
def __imod__(self, other):
|
|
501
|
+
def __imod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
406
502
|
return MicroSeries(super().__imod__(other), weights=self.weights)
|
|
407
503
|
|
|
408
|
-
def __ipow__(self, other):
|
|
504
|
+
def __ipow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
409
505
|
return MicroSeries(super().__ipow__(other), weights=self.weights)
|
|
410
506
|
|
|
411
507
|
# other
|
|
412
508
|
|
|
413
|
-
def __neg__(self
|
|
414
|
-
return MicroSeries(super().__neg__(
|
|
509
|
+
def __neg__(self) -> "MicroSeries":
|
|
510
|
+
return MicroSeries(super().__neg__(), weights=self.weights)
|
|
415
511
|
|
|
416
|
-
def __pos__(self
|
|
417
|
-
return MicroSeries(super().__pos__(
|
|
512
|
+
def __pos__(self) -> "MicroSeries":
|
|
513
|
+
return MicroSeries(super().__pos__(), weights=self.weights)
|
|
418
514
|
|
|
419
|
-
def __repr__(self):
|
|
515
|
+
def __repr__(self) -> str:
|
|
420
516
|
return pd.DataFrame(
|
|
421
517
|
dict(value=self.values, weight=self.weights.values)
|
|
422
518
|
).__repr__()
|
|
@@ -456,7 +552,9 @@ class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
|
|
|
456
552
|
fn = getattr(MicroSeries, name)
|
|
457
553
|
|
|
458
554
|
@wraps(fn)
|
|
459
|
-
def _weighted_agg_fn(
|
|
555
|
+
def _weighted_agg_fn(
|
|
556
|
+
*args, **kwargs
|
|
557
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
460
558
|
arrays = self.apply(np.array)
|
|
461
559
|
weights = self.weights.apply(np.array)
|
|
462
560
|
df = pd.DataFrame(dict(a=arrays, w=weights))
|
|
@@ -489,7 +587,7 @@ class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
|
|
|
489
587
|
|
|
490
588
|
|
|
491
589
|
class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
492
|
-
def _init(self, by: Union[str,
|
|
590
|
+
def _init(self, by: Union[str, List]):
|
|
493
591
|
self.columns = list(self.obj.columns)
|
|
494
592
|
if isinstance(by, list):
|
|
495
593
|
for column in by:
|
|
@@ -515,8 +613,8 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
515
613
|
setattr(self, fn_name, get_fn(fn_name))
|
|
516
614
|
for fn_name in MicroSeries.VECTOR_FUNCTIONS:
|
|
517
615
|
|
|
518
|
-
def get_fn(name):
|
|
519
|
-
def fn(*args, **kwargs):
|
|
616
|
+
def get_fn(name) -> Callable:
|
|
617
|
+
def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
520
618
|
return MicroDataFrame(
|
|
521
619
|
{
|
|
522
620
|
col: getattr(getattr(self, col), name)(
|
|
@@ -533,9 +631,8 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
533
631
|
|
|
534
632
|
class MicroDataFrame(pd.DataFrame):
|
|
535
633
|
def __init__(self, *args, weights=None, **kwargs):
|
|
536
|
-
"""A DataFrame-inheriting class for weighted microdata.
|
|
537
|
-
|
|
538
|
-
set_weight_col.
|
|
634
|
+
"""A DataFrame-inheriting class for weighted microdata. Weights can be
|
|
635
|
+
provided at initialisation, or using set_weights or set_weight_col.
|
|
539
636
|
|
|
540
637
|
:param weights: Array of weights.
|
|
541
638
|
:type weights: np.array
|
|
@@ -546,11 +643,11 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
546
643
|
self._link_all_weights()
|
|
547
644
|
self.override_df_functions()
|
|
548
645
|
|
|
549
|
-
def override_df_functions(self):
|
|
646
|
+
def override_df_functions(self) -> None:
|
|
550
647
|
for name in MicroSeries.FUNCTIONS:
|
|
551
648
|
|
|
552
|
-
def get_fn(name):
|
|
553
|
-
def fn(*args, **kwargs):
|
|
649
|
+
def get_fn(name) -> Callable:
|
|
650
|
+
def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
554
651
|
is_array = len(args) > 0 and hasattr(args[0], "__len__")
|
|
555
652
|
if (
|
|
556
653
|
name in MicroSeries.SCALAR_FUNCTIONS
|
|
@@ -584,17 +681,19 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
584
681
|
setattr(self, name, get_fn(name))
|
|
585
682
|
|
|
586
683
|
def get_args_as_micro_series(*kwarg_names: tuple) -> Callable:
|
|
587
|
-
"""Decorator for auto-parsing column names into MicroSeries objects.
|
|
588
|
-
|
|
684
|
+
"""Decorator for auto-parsing column names into MicroSeries objects. If
|
|
685
|
+
given, kwarg_names limits arguments checked to keyword arguments
|
|
589
686
|
specified.
|
|
590
687
|
|
|
591
688
|
:param arg_names: argument names to restrict to.
|
|
592
689
|
:type arg_names: str
|
|
593
690
|
"""
|
|
594
691
|
|
|
595
|
-
def arg_series_decorator(fn):
|
|
692
|
+
def arg_series_decorator(fn) -> Callable:
|
|
596
693
|
@wraps(fn)
|
|
597
|
-
def series_function(
|
|
694
|
+
def series_function(
|
|
695
|
+
self, *args, **kwargs
|
|
696
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
598
697
|
new_args = []
|
|
599
698
|
new_kwargs = {}
|
|
600
699
|
if len(kwarg_names) == 0:
|
|
@@ -620,26 +719,26 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
620
719
|
|
|
621
720
|
return arg_series_decorator
|
|
622
721
|
|
|
623
|
-
def __setitem__(self, *args, **kwargs):
|
|
722
|
+
def __setitem__(self, *args, **kwargs) -> None:
|
|
624
723
|
super().__setitem__(*args, **kwargs)
|
|
625
724
|
self._link_all_weights()
|
|
626
725
|
|
|
627
|
-
def _link_weights(self, column):
|
|
726
|
+
def _link_weights(self, column) -> None:
|
|
628
727
|
# self[column] = ... triggers __setitem__, which forces pd.Series
|
|
629
728
|
# this workaround avoids that
|
|
630
729
|
self[column].__class__ = MicroSeries
|
|
631
730
|
self[column].set_weights(self.weights)
|
|
632
731
|
|
|
633
|
-
def _link_all_weights(self):
|
|
732
|
+
def _link_all_weights(self) -> None:
|
|
634
733
|
if self.weights is None:
|
|
635
734
|
self.set_weights(np.ones((len(self))))
|
|
636
735
|
for column in self.columns:
|
|
637
736
|
if column != self.weights_col:
|
|
638
737
|
self._link_weights(column)
|
|
639
738
|
|
|
640
|
-
def set_weights(self, weights) -> None:
|
|
641
|
-
"""Sets the weights for the MicroDataFrame. If a string is received,
|
|
642
|
-
|
|
739
|
+
def set_weights(self, weights: np.ndarray) -> None:
|
|
740
|
+
"""Sets the weights for the MicroDataFrame. If a string is received, it
|
|
741
|
+
will be assumed to be the column name of the weight column.
|
|
643
742
|
|
|
644
743
|
:param weights: Array of weights.
|
|
645
744
|
:type weights: np.array
|
|
@@ -665,7 +764,9 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
665
764
|
self.weight_col = column
|
|
666
765
|
self._link_all_weights()
|
|
667
766
|
|
|
668
|
-
def __getitem__(
|
|
767
|
+
def __getitem__(
|
|
768
|
+
self, key: Union[str, List]
|
|
769
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
669
770
|
result = super().__getitem__(key)
|
|
670
771
|
if isinstance(result, pd.DataFrame):
|
|
671
772
|
try:
|
|
@@ -675,21 +776,21 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
675
776
|
return MicroDataFrame(result, weights=weights)
|
|
676
777
|
return result
|
|
677
778
|
|
|
678
|
-
def catch_series_relapse(self):
|
|
779
|
+
def catch_series_relapse(self) -> None:
|
|
679
780
|
for col in self.columns:
|
|
680
781
|
if self[col].__class__ == pd.Series:
|
|
681
782
|
self._link_weights(col)
|
|
682
783
|
|
|
683
|
-
def __setattr__(self, key, value):
|
|
784
|
+
def __setattr__(self, key, value) -> None:
|
|
684
785
|
super().__setattr__(key, value)
|
|
685
786
|
self.catch_series_relapse()
|
|
686
787
|
|
|
687
|
-
def reset_index(self):
|
|
788
|
+
def reset_index(self) -> "MicroDataFrame":
|
|
688
789
|
res = super().reset_index()
|
|
689
790
|
res = MicroDataFrame(res, weights=self.weights)
|
|
690
791
|
return res
|
|
691
792
|
|
|
692
|
-
def copy(self, deep=True):
|
|
793
|
+
def copy(self, deep: Optional[bool] = True) -> "MicroDataFrame":
|
|
693
794
|
res = super().copy(deep)
|
|
694
795
|
# This changes the original columns to Series. Undo it:
|
|
695
796
|
for col in self.columns:
|
|
@@ -697,19 +798,20 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
697
798
|
res = MicroDataFrame(res, weights=self.weights.copy(deep))
|
|
698
799
|
return res
|
|
699
800
|
|
|
700
|
-
def equals(self, other) -> bool:
|
|
801
|
+
def equals(self, other: "MicroDataFrame") -> bool:
|
|
701
802
|
equal_values = super().equals(other)
|
|
702
803
|
equal_weights = self.weights.equals(other.weights)
|
|
703
804
|
return equal_values and equal_weights
|
|
704
805
|
|
|
705
806
|
@get_args_as_micro_series()
|
|
706
|
-
def groupby(
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
each
|
|
807
|
+
def groupby(
|
|
808
|
+
self, by: Union[str, List], *args, **kwargs
|
|
809
|
+
) -> "MicroDataFrameGroupBy":
|
|
810
|
+
"""Returns a GroupBy object with MicroSeriesGroupBy objects for each
|
|
811
|
+
column.
|
|
710
812
|
|
|
711
813
|
:param by: column to group by
|
|
712
|
-
:type by: Union[str,
|
|
814
|
+
:type by: Union[str, List]
|
|
713
815
|
|
|
714
816
|
return: DataFrameGroupBy object with columns using weights
|
|
715
817
|
rtype: DataFrameGroupBy
|
|
@@ -729,8 +831,8 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
729
831
|
|
|
730
832
|
@get_args_as_micro_series()
|
|
731
833
|
def poverty_rate(self, income: str, threshold: str) -> float:
|
|
732
|
-
"""Calculate poverty rate, i.e., the population share with income
|
|
733
|
-
|
|
834
|
+
"""Calculate poverty rate, i.e., the population share with income below
|
|
835
|
+
their poverty threshold.
|
|
734
836
|
|
|
735
837
|
:param income: Column indicating income.
|
|
736
838
|
:type income: str
|
|
@@ -791,8 +893,8 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
791
893
|
@get_args_as_micro_series()
|
|
792
894
|
def squared_poverty_gap(self, income: str, threshold: str) -> float:
|
|
793
895
|
"""Calculate squared poverty gap, i.e., the total squared gap between
|
|
794
|
-
income and poverty thresholds for all people in poverty.
|
|
795
|
-
|
|
896
|
+
income and poverty thresholds for all people in poverty. Also known as
|
|
897
|
+
the poverty severity index.
|
|
796
898
|
|
|
797
899
|
:param income: Column indicating income.
|
|
798
900
|
:type income: str
|
|
@@ -802,7 +904,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
802
904
|
:rtype: float
|
|
803
905
|
"""
|
|
804
906
|
gaps = (threshold - income)[threshold > income]
|
|
805
|
-
squared_gaps = gaps
|
|
907
|
+
squared_gaps = gaps**2
|
|
806
908
|
return squared_gaps.sum()
|
|
807
909
|
|
|
808
910
|
@get_args_as_micro_series()
|
|
@@ -811,8 +913,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
811
913
|
income: Union[MicroSeries, str],
|
|
812
914
|
threshold: Union[MicroSeries, str],
|
|
813
915
|
) -> int:
|
|
814
|
-
"""
|
|
815
|
-
Calculates the number of entities with income below a poverty
|
|
916
|
+
"""Calculates the number of entities with income below a poverty
|
|
816
917
|
threshold.
|
|
817
918
|
|
|
818
919
|
:param income: income array or column name
|
|
@@ -827,7 +928,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
827
928
|
in_poverty = income < threshold
|
|
828
929
|
return in_poverty.sum()
|
|
829
930
|
|
|
830
|
-
def __repr__(self):
|
|
931
|
+
def __repr__(self) -> str:
|
|
831
932
|
df = pd.DataFrame(self)
|
|
832
933
|
df["weight"] = self.weights
|
|
833
934
|
return df[[df.columns[-1]] + list(df.columns[:-1])].__repr__()
|
microdf/income_measures.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
1
3
|
import microdf as mdf
|
|
2
4
|
|
|
3
5
|
# See
|
|
@@ -5,7 +7,7 @@ import microdf as mdf
|
|
|
5
7
|
# for a comparison of income measures used here.
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def cash_income(df):
|
|
10
|
+
def cash_income(df: pd.DataFrame) -> pd.Series:
|
|
9
11
|
"""Calculates income after taxes and cash transfers.
|
|
10
12
|
|
|
11
13
|
Defined as aftertax_income minus non-cash benefits.
|
|
@@ -22,7 +24,6 @@ def cash_income(df):
|
|
|
22
24
|
* vet_ben
|
|
23
25
|
* wic_ben
|
|
24
26
|
:returns: A pandas Series with the cash income for each row in df.
|
|
25
|
-
|
|
26
27
|
"""
|
|
27
28
|
return (
|
|
28
29
|
df.aftertax_income
|
|
@@ -38,27 +39,25 @@ def cash_income(df):
|
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
41
|
-
def tpc_eci(df):
|
|
42
|
+
def tpc_eci(df: pd.DataFrame) -> pd.Series:
|
|
42
43
|
"""Approximates Tax Policy Center's Expanded Cash Income measure.
|
|
43
44
|
|
|
44
45
|
Subtracts WIC, housing assistance, veteran's benefits, Medicare, and
|
|
45
|
-
Medicaid from expanded_income. ECI adds income measures not modeled in
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
Medicaid from expanded_income. ECI adds income measures not modeled in Tax-
|
|
47
|
+
Calculator, so these are ignored and will create a discrepancy compared to
|
|
48
|
+
TPC's ECI.
|
|
48
49
|
|
|
49
50
|
:param df: DataFrame with columns from Tax-Calculator.
|
|
50
51
|
:returns: pandas Series with TPC's ECI.
|
|
51
|
-
|
|
52
52
|
"""
|
|
53
53
|
return df.expanded_income - df[mdf.ECI_REMOVE_COLS].sum(axis=1)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def market_income(df):
|
|
57
|
-
"""Approximates CBO's market income concept, which is income
|
|
58
|
-
|
|
56
|
+
def market_income(df: pd.DataFrame) -> pd.Series:
|
|
57
|
+
"""Approximates CBO's market income concept, which is income before social
|
|
58
|
+
insurance, means-tested transfers, and taxes.
|
|
59
59
|
|
|
60
60
|
:param df: DataFrame with expanded_income and benefits.
|
|
61
61
|
:returns: pandas Series of the same length as df.
|
|
62
|
-
|
|
63
62
|
"""
|
|
64
63
|
return df.expanded_income - df[mdf.BENS].sum(axis=1)
|