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.
Files changed (39) hide show
  1. microdf/__init__.py +6 -162
  2. microdf/microdataframe.py +501 -0
  3. microdf/{generic.py → microseries.py} +43 -352
  4. microdf/tests/{test_generic.py → test_microseries_dataframe.py} +62 -22
  5. microdf_python-1.0.0.dist-info/METADATA +75 -0
  6. microdf_python-1.0.0.dist-info/RECORD +10 -0
  7. microdf/_optional.py +0 -85
  8. microdf/agg.py +0 -90
  9. microdf/chart_utils.py +0 -37
  10. microdf/charts.py +0 -62
  11. microdf/concat.py +0 -29
  12. microdf/constants.py +0 -40
  13. microdf/custom_taxes.py +0 -174
  14. microdf/income_measures.py +0 -63
  15. microdf/inequality.py +0 -219
  16. microdf/io.py +0 -30
  17. microdf/poverty.py +0 -136
  18. microdf/style.py +0 -54
  19. microdf/tax.py +0 -78
  20. microdf/taxcalc.py +0 -161
  21. microdf/tests/test_compare.py +0 -48
  22. microdf/tests/test_decile_rank.py +0 -91
  23. microdf/tests/test_inequality.py +0 -20
  24. microdf/tests/test_io.py +0 -10
  25. microdf/tests/test_optional_dependency.py +0 -54
  26. microdf/tests/test_poverty.py +0 -73
  27. microdf/tests/test_quantile_chg.py +0 -18
  28. microdf/tests/test_tax.py +0 -54
  29. microdf/tests/test_taxcalc.py +0 -25
  30. microdf/tests/test_utils.py +0 -39
  31. microdf/tests/test_weighted.py +0 -74
  32. microdf/ubi.py +0 -46
  33. microdf/utils.py +0 -73
  34. microdf/weighted.py +0 -235
  35. microdf_python-0.4.6.dist-info/METADATA +0 -54
  36. microdf_python-0.4.6.dist-info/RECORD +0 -37
  37. {microdf_python-0.4.6.dist-info → microdf_python-1.0.0.dist-info}/WHEEL +0 -0
  38. {microdf_python-0.4.6.dist-info → microdf_python-1.0.0.dist-info}/licenses/LICENSE +0 -0
  39. {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 .agg import agg, combine_base_reform, pctchg_base_reform
2
- from .chart_utils import currency_format, dollar_format
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
- # agg.py
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))