microdf-python 1.1.1__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 +14 -0
- microdf/microdataframe.py +883 -0
- microdf/microseries.py +643 -0
- microdf/tests/conftest.py +9 -0
- microdf/tests/test_microseries_dataframe.py +395 -0
- microdf_python-1.1.1.dist-info/METADATA +73 -0
- microdf_python-1.1.1.dist-info/RECORD +10 -0
- microdf_python-1.1.1.dist-info/WHEEL +5 -0
- microdf_python-1.1.1.dist-info/licenses/LICENSE +21 -0
- microdf_python-1.1.1.dist-info/top_level.txt +1 -0
microdf/microseries.py
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MicroSeries(pd.Series):
|
|
12
|
+
def __init__(self, *args, weights: np.array = None, **kwargs):
|
|
13
|
+
"""A Series-inheriting class for weighted microdata. Weights can be
|
|
14
|
+
provided at initialisation, or using set_weights.
|
|
15
|
+
|
|
16
|
+
:param weights: Array of weights.
|
|
17
|
+
:type weights: np.array
|
|
18
|
+
"""
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.set_weights(weights)
|
|
21
|
+
|
|
22
|
+
def weighted_function(fn: Callable) -> Callable:
|
|
23
|
+
@wraps(fn)
|
|
24
|
+
def safe_fn(*args, **kwargs):
|
|
25
|
+
try:
|
|
26
|
+
return fn(*args, **kwargs)
|
|
27
|
+
except ZeroDivisionError:
|
|
28
|
+
return np.NaN
|
|
29
|
+
|
|
30
|
+
return safe_fn
|
|
31
|
+
|
|
32
|
+
@weighted_function
|
|
33
|
+
def scalar_function(fn: Callable) -> Callable:
|
|
34
|
+
fn._rtype = float
|
|
35
|
+
return fn
|
|
36
|
+
|
|
37
|
+
@weighted_function
|
|
38
|
+
def vector_function(fn: Callable) -> Callable:
|
|
39
|
+
fn._rtype = pd.Series
|
|
40
|
+
return fn
|
|
41
|
+
|
|
42
|
+
def set_weights(
|
|
43
|
+
self, weights: np.array, preserve_old: Optional[bool] = False
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Sets the weight values.
|
|
46
|
+
|
|
47
|
+
:param weights: Array of weights.
|
|
48
|
+
:param preserve_old: If True, keeps the old weights as a column when
|
|
49
|
+
new weights are provided.
|
|
50
|
+
:type weights: np.array.
|
|
51
|
+
"""
|
|
52
|
+
if weights is None:
|
|
53
|
+
if len(self) > 0:
|
|
54
|
+
self.weights = pd.Series(
|
|
55
|
+
np.ones_like(self.values), dtype=float
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
if len(weights) != len(self):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Length of weights ({len(weights)}) does not match "
|
|
61
|
+
f"length of DataFrame ({len(self)})."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if preserve_old and self.weights is not None:
|
|
65
|
+
self["old_weights"] = self.weights
|
|
66
|
+
|
|
67
|
+
self.weights = pd.Series(weights, dtype=float)
|
|
68
|
+
|
|
69
|
+
def nullify_weights(self) -> None:
|
|
70
|
+
"""Set all weights to 1, effectively making the Series unweighted.
|
|
71
|
+
|
|
72
|
+
This is useful for comparing weighted and unweighted statistics or when
|
|
73
|
+
you want to temporarily ignore weights.
|
|
74
|
+
"""
|
|
75
|
+
self.weights = pd.Series(np.ones(len(self)), dtype=float)
|
|
76
|
+
|
|
77
|
+
@vector_function
|
|
78
|
+
def weight(self) -> pd.Series:
|
|
79
|
+
"""Calculates the weighted value of the MicroSeries.
|
|
80
|
+
|
|
81
|
+
:returns: A Series multiplying the MicroSeries by its weight.
|
|
82
|
+
:rtype: pd.Series
|
|
83
|
+
"""
|
|
84
|
+
return self.multiply(self.weights)
|
|
85
|
+
|
|
86
|
+
@scalar_function
|
|
87
|
+
def sum(self) -> float:
|
|
88
|
+
"""Calculates the weighted sum of the MicroSeries.
|
|
89
|
+
|
|
90
|
+
:returns: The weighted sum.
|
|
91
|
+
:rtype: float
|
|
92
|
+
"""
|
|
93
|
+
return self.multiply(self.weights).sum()
|
|
94
|
+
|
|
95
|
+
@scalar_function
|
|
96
|
+
def count(self) -> float:
|
|
97
|
+
"""Calculates the weighted count of the MicroSeries.
|
|
98
|
+
|
|
99
|
+
:returns: The weighted count.
|
|
100
|
+
"""
|
|
101
|
+
return self.weights.sum()
|
|
102
|
+
|
|
103
|
+
@scalar_function
|
|
104
|
+
def mean(self) -> float:
|
|
105
|
+
"""Calculates the weighted mean of the MicroSeries.
|
|
106
|
+
|
|
107
|
+
:returns: The weighted mean.
|
|
108
|
+
:rtype: float
|
|
109
|
+
"""
|
|
110
|
+
return np.average(self.values, weights=self.weights)
|
|
111
|
+
|
|
112
|
+
def quantile(self, q: np.array) -> pd.Series:
|
|
113
|
+
"""Calculates weighted quantiles of the MicroSeries.
|
|
114
|
+
|
|
115
|
+
Uses the inverse CDF method: the q-th quantile is the smallest
|
|
116
|
+
value where the cumulative weight proportion >= q. This matches
|
|
117
|
+
the default behavior of R's survey::svyquantile.
|
|
118
|
+
|
|
119
|
+
:param q: Quantile(s) to calculate, must be in [0, 1].
|
|
120
|
+
:type q: float or np.array
|
|
121
|
+
|
|
122
|
+
:return: Weighted quantile value(s).
|
|
123
|
+
:rtype: float or pd.Series
|
|
124
|
+
"""
|
|
125
|
+
values = np.array(self.values)
|
|
126
|
+
quantiles = np.atleast_1d(q)
|
|
127
|
+
sample_weight = np.array(self.weights)
|
|
128
|
+
assert np.all(quantiles >= 0) and np.all(
|
|
129
|
+
quantiles <= 1
|
|
130
|
+
), "quantiles should be in [0, 1]"
|
|
131
|
+
sorter = np.argsort(values)
|
|
132
|
+
values = values[sorter]
|
|
133
|
+
sample_weight = sample_weight[sorter]
|
|
134
|
+
cumsum = np.cumsum(sample_weight)
|
|
135
|
+
cumsum_normalized = cumsum / cumsum[-1]
|
|
136
|
+
result = np.array(
|
|
137
|
+
[
|
|
138
|
+
values[
|
|
139
|
+
min(
|
|
140
|
+
np.searchsorted(cumsum_normalized, qi), len(values) - 1
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
for qi in quantiles
|
|
144
|
+
]
|
|
145
|
+
)
|
|
146
|
+
if np.array(q).shape == ():
|
|
147
|
+
return result[0]
|
|
148
|
+
return pd.Series(result, index=quantiles)
|
|
149
|
+
|
|
150
|
+
@scalar_function
|
|
151
|
+
def median(self) -> float:
|
|
152
|
+
"""Calculates the weighted median of the MicroSeries.
|
|
153
|
+
|
|
154
|
+
:returns: The weighted median of a DataFrame's column.
|
|
155
|
+
:rtype: float
|
|
156
|
+
"""
|
|
157
|
+
return self.quantile(0.5)
|
|
158
|
+
|
|
159
|
+
@scalar_function
|
|
160
|
+
def gini(self, negatives: Optional[str] = None) -> float:
|
|
161
|
+
"""Calculates Gini index.
|
|
162
|
+
|
|
163
|
+
:param negatives: An optional string indicating how to treat negative
|
|
164
|
+
values of x:
|
|
165
|
+
'zero' replaces negative values with zeroes.
|
|
166
|
+
'shift' subtracts the minimum value from all values of x,
|
|
167
|
+
when this minimum is negative. That is, it adds the absolute
|
|
168
|
+
minimum value.
|
|
169
|
+
Defaults to None, which leaves negative values as they are.
|
|
170
|
+
:type q: str
|
|
171
|
+
:returns: Gini index.
|
|
172
|
+
:rtype: float
|
|
173
|
+
"""
|
|
174
|
+
x = np.array(self).astype("float")
|
|
175
|
+
if negatives == "zero":
|
|
176
|
+
x[x < 0] = 0
|
|
177
|
+
if negatives == "shift" and np.amin(x) < 0:
|
|
178
|
+
x -= np.amin(x)
|
|
179
|
+
if (self.weights != np.ones(len(self))).any(): # Varying weights.
|
|
180
|
+
sorted_indices = np.argsort(self)
|
|
181
|
+
sorted_x = np.array(self[sorted_indices])
|
|
182
|
+
sorted_w = np.array(self.weights[sorted_indices])
|
|
183
|
+
cumw = np.cumsum(sorted_w)
|
|
184
|
+
cumxw = np.cumsum(sorted_x * sorted_w)
|
|
185
|
+
return np.sum(cumxw[1:] * cumw[:-1] - cumxw[:-1] * cumw[1:]) / (
|
|
186
|
+
cumxw[-1] * cumw[-1]
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
sorted_x = np.sort(self)
|
|
190
|
+
n = len(x)
|
|
191
|
+
cumxw = np.cumsum(sorted_x)
|
|
192
|
+
# The above formula, with all weights equal to 1 simplifies to:
|
|
193
|
+
return (n + 1 - 2 * np.sum(cumxw) / cumxw[-1]) / n
|
|
194
|
+
|
|
195
|
+
@scalar_function
|
|
196
|
+
def top_x_pct_share(self, top_x_pct: float) -> float:
|
|
197
|
+
"""Calculates top x% share.
|
|
198
|
+
|
|
199
|
+
:param top_x_pct: Decimal between 0 and 1 of the top %, e.g. 0.1,
|
|
200
|
+
0.001.
|
|
201
|
+
:type top_x_pct: float
|
|
202
|
+
:returns: The weighted share held by the top x%.
|
|
203
|
+
:rtype: float
|
|
204
|
+
"""
|
|
205
|
+
threshold = self.quantile(1 - top_x_pct)
|
|
206
|
+
top_x_pct_sum = self[self >= threshold].sum()
|
|
207
|
+
total_sum = self.sum()
|
|
208
|
+
return top_x_pct_sum / total_sum
|
|
209
|
+
|
|
210
|
+
@scalar_function
|
|
211
|
+
def bottom_x_pct_share(self, bottom_x_pct: float) -> float:
|
|
212
|
+
"""Calculates bottom x% share.
|
|
213
|
+
|
|
214
|
+
:param bottom_x_pct: Decimal between 0 and 1 of the top %, e.g. 0.1,
|
|
215
|
+
0.001.
|
|
216
|
+
:type bottom_x_pct: float
|
|
217
|
+
:returns: The weighted share held by the bottom x%.
|
|
218
|
+
:rtype: float
|
|
219
|
+
"""
|
|
220
|
+
return 1 - self.top_x_pct_share(1 - bottom_x_pct)
|
|
221
|
+
|
|
222
|
+
@scalar_function
|
|
223
|
+
def bottom_50_pct_share(self) -> float:
|
|
224
|
+
"""Calculates bottom 50% share.
|
|
225
|
+
|
|
226
|
+
:returns: The weighted share held by the bottom 50%.
|
|
227
|
+
:rtype: float
|
|
228
|
+
"""
|
|
229
|
+
return self.bottom_x_pct_share(0.5)
|
|
230
|
+
|
|
231
|
+
@scalar_function
|
|
232
|
+
def top_50_pct_share(self) -> float:
|
|
233
|
+
"""Calculates top 50% share.
|
|
234
|
+
|
|
235
|
+
:returns: The weighted share held by the top 50%.
|
|
236
|
+
:rtype: float
|
|
237
|
+
"""
|
|
238
|
+
return self.top_x_pct_share(0.5)
|
|
239
|
+
|
|
240
|
+
@scalar_function
|
|
241
|
+
def top_10_pct_share(self) -> float:
|
|
242
|
+
"""Calculates top 10% share.
|
|
243
|
+
|
|
244
|
+
:returns: The weighted share held by the top 10%.
|
|
245
|
+
:rtype: float
|
|
246
|
+
"""
|
|
247
|
+
return self.top_x_pct_share(0.1)
|
|
248
|
+
|
|
249
|
+
@scalar_function
|
|
250
|
+
def top_1_pct_share(self) -> float:
|
|
251
|
+
"""Calculates top 1% share.
|
|
252
|
+
|
|
253
|
+
:returns: The weighted share held by the top 50%.
|
|
254
|
+
:rtype: float
|
|
255
|
+
"""
|
|
256
|
+
return self.top_x_pct_share(0.01)
|
|
257
|
+
|
|
258
|
+
@scalar_function
|
|
259
|
+
def top_0_1_pct_share(self) -> float:
|
|
260
|
+
"""Calculates top 0.1% share.
|
|
261
|
+
|
|
262
|
+
:returns: The weighted share held by the top 0.1%.
|
|
263
|
+
:rtype: float
|
|
264
|
+
"""
|
|
265
|
+
return self.top_x_pct_share(0.001)
|
|
266
|
+
|
|
267
|
+
@scalar_function
|
|
268
|
+
def t10_b50(self) -> float:
|
|
269
|
+
"""Calculates ratio between the top 10% and bottom 50% shares.
|
|
270
|
+
|
|
271
|
+
:returns: The weighted share held by the top 10% divided by the
|
|
272
|
+
weighted share held by the bottom 50%.
|
|
273
|
+
"""
|
|
274
|
+
t10 = self.top_10_pct_share()
|
|
275
|
+
b50 = self.bottom_50_pct_share()
|
|
276
|
+
return t10 / b50
|
|
277
|
+
|
|
278
|
+
@vector_function
|
|
279
|
+
def cumsum(self) -> pd.Series:
|
|
280
|
+
logger.warning(
|
|
281
|
+
"cumsum() returns cumulative sums of weighted values as a regular "
|
|
282
|
+
"pandas Series. The original weights have already been applied "
|
|
283
|
+
"and cannot be reused with the cumulative results."
|
|
284
|
+
)
|
|
285
|
+
return pd.Series(self * self.weights).cumsum()
|
|
286
|
+
|
|
287
|
+
@vector_function
|
|
288
|
+
def rank(self, pct: Optional[bool] = False) -> pd.Series:
|
|
289
|
+
weights_sum = self.weights.values.sum()
|
|
290
|
+
if weights_sum == 0:
|
|
291
|
+
raise ZeroDivisionError(
|
|
292
|
+
"Cannot calculate rank with zero total weight. "
|
|
293
|
+
"All weights in the MicroSeries are zero, which would result "
|
|
294
|
+
"in division by zero."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
order = np.argsort(self.values)
|
|
298
|
+
inverse_order = np.argsort(order)
|
|
299
|
+
ranks = np.array(self.weights.values)[order].cumsum()[inverse_order]
|
|
300
|
+
if pct:
|
|
301
|
+
ranks /= weights_sum
|
|
302
|
+
ranks = np.where(ranks > 1.0, 1.0, ranks)
|
|
303
|
+
return MicroSeries(ranks, index=self.index, weights=self.weights)
|
|
304
|
+
|
|
305
|
+
@vector_function
|
|
306
|
+
def decile_rank(self, negatives_in_zero: Optional[bool] = False):
|
|
307
|
+
"""Calculate decile ranks (1-10) with optional zero decile for
|
|
308
|
+
negatives.
|
|
309
|
+
|
|
310
|
+
:param negatives_in_zero: If True, negative values are assigned to
|
|
311
|
+
decile 0. If False (default), all values are ranked 1-10.
|
|
312
|
+
:type negatives_in_zero: bool
|
|
313
|
+
:returns: MicroSeries with decile ranks
|
|
314
|
+
:rtype: MicroSeries
|
|
315
|
+
"""
|
|
316
|
+
if negatives_in_zero:
|
|
317
|
+
negative_mask = self < 0
|
|
318
|
+
if negative_mask.any():
|
|
319
|
+
non_negative_values = self[~negative_mask]
|
|
320
|
+
if len(non_negative_values) > 0:
|
|
321
|
+
non_neg_ranks = non_negative_values.rank(pct=True)
|
|
322
|
+
deciles = np.minimum(np.ceil(non_neg_ranks * 10), 10)
|
|
323
|
+
else:
|
|
324
|
+
deciles = np.array([])
|
|
325
|
+
|
|
326
|
+
result = np.zeros(len(self))
|
|
327
|
+
result[negative_mask] = 0
|
|
328
|
+
if len(deciles) > 0:
|
|
329
|
+
result[~negative_mask] = deciles
|
|
330
|
+
|
|
331
|
+
return MicroSeries(result, weights=self.weights)
|
|
332
|
+
|
|
333
|
+
# Default behavior: rank all values 1-10
|
|
334
|
+
return MicroSeries(
|
|
335
|
+
np.minimum(np.ceil(self.rank(pct=True) * 10), 10),
|
|
336
|
+
weights=self.weights,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
@vector_function
|
|
340
|
+
def quintile_rank(self) -> "MicroSeries":
|
|
341
|
+
return MicroSeries(
|
|
342
|
+
np.minimum(np.ceil(self.rank(pct=True) * 5), 5),
|
|
343
|
+
weights=self.weights,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@vector_function
|
|
347
|
+
def quartile_rank(self) -> "MicroSeries":
|
|
348
|
+
return MicroSeries(
|
|
349
|
+
np.minimum(np.ceil(self.rank(pct=True) * 4), 4),
|
|
350
|
+
weights=self.weights,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@vector_function
|
|
354
|
+
def percentile_rank(self) -> "MicroSeries":
|
|
355
|
+
return MicroSeries(
|
|
356
|
+
np.minimum(np.ceil(self.rank(pct=True) * 100), 100),
|
|
357
|
+
weights=self.weights,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def groupby(self, *args, **kwargs) -> "MicroSeriesGroupBy":
|
|
361
|
+
gb = super().groupby(*args, **kwargs)
|
|
362
|
+
gb.__class__ = MicroSeriesGroupBy
|
|
363
|
+
gb._init()
|
|
364
|
+
gb.weights = pd.Series(self.weights).groupby(*args, **kwargs)
|
|
365
|
+
return gb
|
|
366
|
+
|
|
367
|
+
def copy(self, deep: Optional[bool] = True):
|
|
368
|
+
res = super().copy(deep)
|
|
369
|
+
res = MicroSeries(res, weights=self.weights.copy(deep))
|
|
370
|
+
return res
|
|
371
|
+
|
|
372
|
+
def clip(
|
|
373
|
+
self,
|
|
374
|
+
lower: Optional[float] = None,
|
|
375
|
+
upper: Optional[float] = None,
|
|
376
|
+
axis: Optional[int] = None,
|
|
377
|
+
inplace: Optional[bool] = False,
|
|
378
|
+
*args,
|
|
379
|
+
**kwargs,
|
|
380
|
+
) -> "MicroSeries":
|
|
381
|
+
res = super().clip(
|
|
382
|
+
lower=lower,
|
|
383
|
+
upper=upper,
|
|
384
|
+
axis=axis,
|
|
385
|
+
inplace=inplace,
|
|
386
|
+
*args,
|
|
387
|
+
**kwargs,
|
|
388
|
+
)
|
|
389
|
+
if not inplace:
|
|
390
|
+
return MicroSeries(res, weights=self.weights)
|
|
391
|
+
return self
|
|
392
|
+
|
|
393
|
+
def round(
|
|
394
|
+
self, decimals: Optional[int] = 0, *args, **kwargs
|
|
395
|
+
) -> "MicroSeries":
|
|
396
|
+
res = super().round(decimals=decimals, *args, **kwargs)
|
|
397
|
+
return MicroSeries(res, weights=self.weights)
|
|
398
|
+
|
|
399
|
+
def equals(self, other: "MicroSeries") -> bool:
|
|
400
|
+
equal_values = super().equals(other)
|
|
401
|
+
equal_weights = self.weights.equals(other.weights)
|
|
402
|
+
return equal_values and equal_weights
|
|
403
|
+
|
|
404
|
+
def __getitem__(
|
|
405
|
+
self, key: Union[str, int, slice, List, np.ndarray]
|
|
406
|
+
) -> Union["MicroSeries", pd.Series]:
|
|
407
|
+
result = super().__getitem__(key)
|
|
408
|
+
if isinstance(result, pd.Series):
|
|
409
|
+
weights = self.weights.__getitem__(key)
|
|
410
|
+
return MicroSeries(result, weights=weights)
|
|
411
|
+
return result
|
|
412
|
+
|
|
413
|
+
def __getattr__(self, name: str) -> "MicroSeries":
|
|
414
|
+
return MicroSeries(super().__getattr__(name), weights=self.weights)
|
|
415
|
+
|
|
416
|
+
# operators
|
|
417
|
+
|
|
418
|
+
def __add__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
419
|
+
return MicroSeries(super().__add__(other), weights=self.weights)
|
|
420
|
+
|
|
421
|
+
def __sub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
422
|
+
return MicroSeries(super().__sub__(other), weights=self.weights)
|
|
423
|
+
|
|
424
|
+
def __mul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
425
|
+
return MicroSeries(super().__mul__(other), weights=self.weights)
|
|
426
|
+
|
|
427
|
+
def __floordiv__(
|
|
428
|
+
self, other: Union[int, float, pd.Series]
|
|
429
|
+
) -> "MicroSeries":
|
|
430
|
+
return MicroSeries(super().__floordiv__(other), weights=self.weights)
|
|
431
|
+
|
|
432
|
+
def __truediv__(
|
|
433
|
+
self, other: Union[int, float, pd.Series]
|
|
434
|
+
) -> "MicroSeries":
|
|
435
|
+
return MicroSeries(super().__truediv__(other), weights=self.weights)
|
|
436
|
+
|
|
437
|
+
def __mod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
438
|
+
return MicroSeries(super().__mod__(other), weights=self.weights)
|
|
439
|
+
|
|
440
|
+
def __pow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
441
|
+
return MicroSeries(super().__pow__(other), weights=self.weights)
|
|
442
|
+
|
|
443
|
+
def __xor__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
444
|
+
return MicroSeries(super().__xor__(other), weights=self.weights)
|
|
445
|
+
|
|
446
|
+
def __and__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
447
|
+
return MicroSeries(super().__and__(other), weights=self.weights)
|
|
448
|
+
|
|
449
|
+
def __or__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
450
|
+
return MicroSeries(super().__or__(other), weights=self.weights)
|
|
451
|
+
|
|
452
|
+
def __invert__(self) -> "MicroSeries":
|
|
453
|
+
return MicroSeries(super().__invert__(), weights=self.weights)
|
|
454
|
+
|
|
455
|
+
def __radd__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
456
|
+
return MicroSeries(super().__radd__(other), weights=self.weights)
|
|
457
|
+
|
|
458
|
+
def __rsub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
459
|
+
return MicroSeries(super().__rsub__(other), weights=self.weights)
|
|
460
|
+
|
|
461
|
+
def __rmul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
462
|
+
return MicroSeries(super().__rmul__(other), weights=self.weights)
|
|
463
|
+
|
|
464
|
+
def __rfloordiv__(
|
|
465
|
+
self, other: Union[int, float, pd.Series]
|
|
466
|
+
) -> "MicroSeries":
|
|
467
|
+
return MicroSeries(super().__rfloordiv__(other), weights=self.weights)
|
|
468
|
+
|
|
469
|
+
def __rtruediv__(
|
|
470
|
+
self, other: Union[int, float, pd.Series]
|
|
471
|
+
) -> "MicroSeries":
|
|
472
|
+
return MicroSeries(super().__rtruediv__(other), weights=self.weights)
|
|
473
|
+
|
|
474
|
+
def __rmod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
475
|
+
return MicroSeries(super().__rmod__(other), weights=self.weights)
|
|
476
|
+
|
|
477
|
+
def __rpow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
478
|
+
return MicroSeries(super().__rpow__(other), weights=self.weights)
|
|
479
|
+
|
|
480
|
+
def __rand__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
481
|
+
return MicroSeries(super().__rand__(other), weights=self.weights)
|
|
482
|
+
|
|
483
|
+
def __ror__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
484
|
+
return MicroSeries(super().__ror__(other), weights=self.weights)
|
|
485
|
+
|
|
486
|
+
def __rxor__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
487
|
+
return MicroSeries(super().__rxor__(other), weights=self.weights)
|
|
488
|
+
|
|
489
|
+
def sqrt(self) -> "MicroSeries":
|
|
490
|
+
sqrt_values = np.sqrt(self.values)
|
|
491
|
+
return MicroSeries(sqrt_values, index=self.index, weights=self.weights)
|
|
492
|
+
|
|
493
|
+
# comparators
|
|
494
|
+
|
|
495
|
+
def __lt__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
496
|
+
return MicroSeries(super().__lt__(other), weights=self.weights)
|
|
497
|
+
|
|
498
|
+
def __le__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
499
|
+
return MicroSeries(super().__le__(other), weights=self.weights)
|
|
500
|
+
|
|
501
|
+
def __eq__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
502
|
+
return MicroSeries(super().__eq__(other), weights=self.weights)
|
|
503
|
+
|
|
504
|
+
def __ne__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
505
|
+
return MicroSeries(super().__ne__(other), weights=self.weights)
|
|
506
|
+
|
|
507
|
+
def __ge__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
508
|
+
return MicroSeries(super().__ge__(other), weights=self.weights)
|
|
509
|
+
|
|
510
|
+
def __gt__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
511
|
+
return MicroSeries(super().__gt__(other), weights=self.weights)
|
|
512
|
+
|
|
513
|
+
# assignment operators
|
|
514
|
+
|
|
515
|
+
def __iadd__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
516
|
+
return MicroSeries(super().__iadd__(other), weights=self.weights)
|
|
517
|
+
|
|
518
|
+
def __isub__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
519
|
+
return MicroSeries(super().__isub__(other), weights=self.weights)
|
|
520
|
+
|
|
521
|
+
def __imul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
522
|
+
return MicroSeries(super().__imul__(other), weights=self.weights)
|
|
523
|
+
|
|
524
|
+
def __ifloordiv__(
|
|
525
|
+
self, other: Union[int, float, pd.Series]
|
|
526
|
+
) -> "MicroSeries":
|
|
527
|
+
return MicroSeries(super().__ifloordiv__(other), weights=self.weights)
|
|
528
|
+
|
|
529
|
+
def __idiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
530
|
+
return MicroSeries(super().__idiv__(other), weights=self.weights)
|
|
531
|
+
|
|
532
|
+
def __itruediv__(
|
|
533
|
+
self, other: Union[int, float, pd.Series]
|
|
534
|
+
) -> "MicroSeries":
|
|
535
|
+
return MicroSeries(super().__itruediv__(other), weights=self.weights)
|
|
536
|
+
|
|
537
|
+
def __imod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
538
|
+
return MicroSeries(super().__imod__(other), weights=self.weights)
|
|
539
|
+
|
|
540
|
+
def __ipow__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
|
|
541
|
+
return MicroSeries(super().__ipow__(other), weights=self.weights)
|
|
542
|
+
|
|
543
|
+
# other
|
|
544
|
+
|
|
545
|
+
def __neg__(self) -> "MicroSeries":
|
|
546
|
+
return MicroSeries(super().__neg__(), weights=self.weights)
|
|
547
|
+
|
|
548
|
+
def __pos__(self) -> "MicroSeries":
|
|
549
|
+
return MicroSeries(super().__pos__(), weights=self.weights)
|
|
550
|
+
|
|
551
|
+
def astype(
|
|
552
|
+
self,
|
|
553
|
+
dtype,
|
|
554
|
+
copy: Optional[bool] = True,
|
|
555
|
+
errors: Optional[str] = "raise",
|
|
556
|
+
) -> "MicroSeries":
|
|
557
|
+
"""Convert MicroSeries to specified data type while preserving weights.
|
|
558
|
+
|
|
559
|
+
:param dtype: Data type to convert to. Can be numpy dtype or Python
|
|
560
|
+
type.
|
|
561
|
+
:param copy: Whether to make a copy of the data (default True).
|
|
562
|
+
:param errors: How to handle conversion errors (default "raise").
|
|
563
|
+
:return: New MicroSeries with converted data type and preserved
|
|
564
|
+
weights.
|
|
565
|
+
"""
|
|
566
|
+
converted_series = super().astype(dtype, copy=copy, errors=errors)
|
|
567
|
+
return MicroSeries(
|
|
568
|
+
converted_series,
|
|
569
|
+
weights=self.weights.copy() if copy else self.weights,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def __repr__(self) -> str:
|
|
573
|
+
return pd.DataFrame(
|
|
574
|
+
dict(value=self.values, weight=self.weights.values)
|
|
575
|
+
).__repr__()
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
MicroSeries.SCALAR_FUNCTIONS = [
|
|
579
|
+
fn
|
|
580
|
+
for fn in dir(MicroSeries)
|
|
581
|
+
if "_rtype" in dir(getattr(MicroSeries, fn))
|
|
582
|
+
and getattr(getattr(MicroSeries, fn), "_rtype") == float
|
|
583
|
+
]
|
|
584
|
+
MicroSeries.VECTOR_FUNCTIONS = [
|
|
585
|
+
fn
|
|
586
|
+
for fn in dir(MicroSeries)
|
|
587
|
+
if "_rtype" in dir(getattr(MicroSeries, fn))
|
|
588
|
+
and getattr(getattr(MicroSeries, fn), "_rtype") == pd.Series
|
|
589
|
+
]
|
|
590
|
+
MicroSeries.AGNOSTIC_FUNCTIONS = ["quantile"]
|
|
591
|
+
MicroSeries.FUNCTIONS = sum(
|
|
592
|
+
[
|
|
593
|
+
MicroSeries.SCALAR_FUNCTIONS,
|
|
594
|
+
MicroSeries.VECTOR_FUNCTIONS,
|
|
595
|
+
MicroSeries.AGNOSTIC_FUNCTIONS,
|
|
596
|
+
],
|
|
597
|
+
[],
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
|
|
602
|
+
def _init(self):
|
|
603
|
+
def _weighted_agg(name) -> Callable:
|
|
604
|
+
def via_micro_series(row, *args, **kwargs):
|
|
605
|
+
return getattr(MicroSeries(row.a, weights=row.w), name)(
|
|
606
|
+
*args, **kwargs
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
fn = getattr(MicroSeries, name)
|
|
610
|
+
|
|
611
|
+
@wraps(fn)
|
|
612
|
+
def _weighted_agg_fn(
|
|
613
|
+
*args, **kwargs
|
|
614
|
+
) -> Union[pd.Series, pd.DataFrame]:
|
|
615
|
+
arrays = self.apply(np.array)
|
|
616
|
+
weights = self.weights.apply(np.array)
|
|
617
|
+
df = pd.DataFrame(dict(a=arrays, w=weights))
|
|
618
|
+
is_array = len(args) > 0 and hasattr(args[0], "__len__")
|
|
619
|
+
if (
|
|
620
|
+
name in MicroSeries.SCALAR_FUNCTIONS
|
|
621
|
+
or name in MicroSeries.AGNOSTIC_FUNCTIONS
|
|
622
|
+
and not is_array
|
|
623
|
+
):
|
|
624
|
+
result = df.agg(
|
|
625
|
+
lambda row: via_micro_series(row, *args, **kwargs),
|
|
626
|
+
axis=1,
|
|
627
|
+
)
|
|
628
|
+
elif (
|
|
629
|
+
name in MicroSeries.VECTOR_FUNCTIONS
|
|
630
|
+
or name in MicroSeries.AGNOSTIC_FUNCTIONS
|
|
631
|
+
and is_array
|
|
632
|
+
):
|
|
633
|
+
result = df.apply(
|
|
634
|
+
lambda row: via_micro_series(row, *args, **kwargs),
|
|
635
|
+
axis=1,
|
|
636
|
+
)
|
|
637
|
+
return result.stack()
|
|
638
|
+
return result
|
|
639
|
+
|
|
640
|
+
return _weighted_agg_fn
|
|
641
|
+
|
|
642
|
+
for fn_name in MicroSeries.FUNCTIONS:
|
|
643
|
+
setattr(self, fn_name, _weighted_agg(fn_name))
|