microdf-python 1.0.1__py3-none-any.whl → 1.0.2__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/microdataframe.py +243 -44
- microdf/microseries.py +8 -0
- microdf/tests/test_microseries_dataframe.py +8 -0
- {microdf_python-1.0.1.dist-info → microdf_python-1.0.2.dist-info}/METADATA +2 -2
- microdf_python-1.0.2.dist-info/RECORD +10 -0
- microdf_python-1.0.1.dist-info/RECORD +0 -10
- {microdf_python-1.0.1.dist-info → microdf_python-1.0.2.dist-info}/WHEEL +0 -0
- {microdf_python-1.0.1.dist-info → microdf_python-1.0.2.dist-info}/licenses/LICENSE +0 -0
- {microdf_python-1.0.1.dist-info → microdf_python-1.0.2.dist-info}/top_level.txt +0 -0
microdf/microdataframe.py
CHANGED
|
@@ -44,14 +44,17 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
44
44
|
"""
|
|
45
45
|
|
|
46
46
|
def fn(*args, **kwargs) -> pd.Series:
|
|
47
|
-
results =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
results = {}
|
|
48
|
+
for col in self.columns:
|
|
49
|
+
if pd.api.types.is_numeric_dtype(self[col]):
|
|
50
|
+
try:
|
|
51
|
+
results[col] = getattr(self[col], name)(
|
|
52
|
+
*args, **kwargs
|
|
53
|
+
)
|
|
54
|
+
except Exception:
|
|
55
|
+
# Skip columns that can't be aggregated
|
|
56
|
+
pass
|
|
57
|
+
return pd.Series(results)
|
|
55
58
|
|
|
56
59
|
return fn
|
|
57
60
|
|
|
@@ -63,14 +66,24 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
63
66
|
"""
|
|
64
67
|
|
|
65
68
|
def fn(*args, **kwargs) -> pd.DataFrame:
|
|
66
|
-
results =
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
results = []
|
|
70
|
+
columns = []
|
|
71
|
+
for col in self.columns:
|
|
72
|
+
if pd.api.types.is_numeric_dtype(self[col]):
|
|
73
|
+
try:
|
|
74
|
+
result = getattr(self[col], name)(*args, **kwargs)
|
|
75
|
+
results.append(result)
|
|
76
|
+
columns.append(col)
|
|
77
|
+
except Exception:
|
|
78
|
+
# Skip columns that can't be aggregated
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if results:
|
|
82
|
+
df = pd.DataFrame(results)
|
|
83
|
+
df.index = columns
|
|
84
|
+
return df
|
|
85
|
+
else:
|
|
86
|
+
return pd.DataFrame()
|
|
74
87
|
|
|
75
88
|
return fn
|
|
76
89
|
|
|
@@ -88,24 +101,37 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
88
101
|
|
|
89
102
|
if is_array:
|
|
90
103
|
# Use vector function behavior
|
|
91
|
-
results =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
results = []
|
|
105
|
+
columns = []
|
|
106
|
+
for col in self.columns:
|
|
107
|
+
if pd.api.types.is_numeric_dtype(self[col]):
|
|
108
|
+
try:
|
|
109
|
+
result = getattr(self[col], name)(*args, **kwargs)
|
|
110
|
+
results.append(result)
|
|
111
|
+
columns.append(col)
|
|
112
|
+
except Exception:
|
|
113
|
+
# Skip columns that can't be aggregated
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
if results:
|
|
117
|
+
df = pd.DataFrame(results)
|
|
118
|
+
df.index = columns
|
|
119
|
+
return df
|
|
120
|
+
else:
|
|
121
|
+
return pd.DataFrame()
|
|
99
122
|
else:
|
|
100
123
|
# Use scalar function behavior
|
|
101
|
-
results =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
results = {}
|
|
125
|
+
for col in self.columns:
|
|
126
|
+
if pd.api.types.is_numeric_dtype(self[col]):
|
|
127
|
+
try:
|
|
128
|
+
results[col] = getattr(self[col], name)(
|
|
129
|
+
*args, **kwargs
|
|
130
|
+
)
|
|
131
|
+
except Exception:
|
|
132
|
+
# Skip columns that can't be aggregated
|
|
133
|
+
pass
|
|
134
|
+
return pd.Series(results)
|
|
109
135
|
|
|
110
136
|
return fn
|
|
111
137
|
|
|
@@ -185,6 +211,7 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
185
211
|
if isinstance(weights, str):
|
|
186
212
|
self.weights_col = weights
|
|
187
213
|
self.weights = pd.Series(self[weights], dtype=float)
|
|
214
|
+
self._link_all_weights()
|
|
188
215
|
elif weights is not None:
|
|
189
216
|
if len(weights) != len(self):
|
|
190
217
|
raise ValueError(
|
|
@@ -203,11 +230,24 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
203
230
|
"""Sets the weights for the MicroDataFrame by specifying the name of
|
|
204
231
|
the weight column.
|
|
205
232
|
|
|
206
|
-
|
|
233
|
+
.. deprecated:: 1.0.2
|
|
234
|
+
Use :meth:`set_weights` with a string argument instead.
|
|
235
|
+
This method will be removed in a future version.
|
|
236
|
+
|
|
237
|
+
:param column: Name of the column to use as weights.
|
|
207
238
|
:param preserve_old: If True, keeps the old weights as a column when
|
|
208
239
|
new weights are provided.
|
|
209
|
-
:type
|
|
240
|
+
:type column: str
|
|
210
241
|
"""
|
|
242
|
+
import warnings
|
|
243
|
+
|
|
244
|
+
warnings.warn(
|
|
245
|
+
"set_weight_col is deprecated and will be removed in a "
|
|
246
|
+
"future version. Use set_weights(column_name) instead.",
|
|
247
|
+
DeprecationWarning,
|
|
248
|
+
stacklevel=2,
|
|
249
|
+
)
|
|
250
|
+
|
|
211
251
|
if preserve_old and self.weights_col is not None:
|
|
212
252
|
self["old_" + self.weights_col] = self.weights
|
|
213
253
|
|
|
@@ -215,6 +255,15 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
215
255
|
self.weights_col = column
|
|
216
256
|
self._link_all_weights()
|
|
217
257
|
|
|
258
|
+
def nullify_weights(self) -> None:
|
|
259
|
+
"""Set all weights to 1, effectively making the DataFrame unweighted.
|
|
260
|
+
|
|
261
|
+
This is useful for comparing weighted and unweighted statistics or when
|
|
262
|
+
you want to temporarily ignore weights.
|
|
263
|
+
"""
|
|
264
|
+
self.weights = np.ones(len(self))
|
|
265
|
+
self._link_all_weights()
|
|
266
|
+
|
|
218
267
|
def __getitem__(
|
|
219
268
|
self, key: Union[str, List]
|
|
220
269
|
) -> Union[pd.Series, pd.DataFrame]:
|
|
@@ -303,6 +352,138 @@ class MicroDataFrame(pd.DataFrame):
|
|
|
303
352
|
res = MicroDataFrame(res, weights=self.weights.copy(deep))
|
|
304
353
|
return res
|
|
305
354
|
|
|
355
|
+
def drop(
|
|
356
|
+
self,
|
|
357
|
+
labels=None,
|
|
358
|
+
axis=0,
|
|
359
|
+
index=None,
|
|
360
|
+
columns=None,
|
|
361
|
+
level=None,
|
|
362
|
+
inplace=False,
|
|
363
|
+
errors="raise",
|
|
364
|
+
):
|
|
365
|
+
"""Drop specified labels from rows or columns.
|
|
366
|
+
|
|
367
|
+
This method supports all parameters of pandas DataFrame.drop(),
|
|
368
|
+
including the 'inplace' parameter.
|
|
369
|
+
|
|
370
|
+
:param labels: Index or column labels to drop.
|
|
371
|
+
:param axis: Whether to drop labels from the index (0 or 'index') or
|
|
372
|
+
columns (1 or 'columns').
|
|
373
|
+
:param index: Alternative to specifying axis (labels, axis=0 is
|
|
374
|
+
equivalent to index=labels).
|
|
375
|
+
:param columns: Alternative to specifying axis (labels, axis=1 is
|
|
376
|
+
equivalent to columns=labels).
|
|
377
|
+
:param level: For MultiIndex, level from which the labels will be
|
|
378
|
+
removed.
|
|
379
|
+
:param inplace: If False, return a copy. Otherwise, do operation
|
|
380
|
+
inplace and return None.
|
|
381
|
+
:param errors: If 'ignore', suppress error and only existing labels are
|
|
382
|
+
dropped.
|
|
383
|
+
:return: MicroDataFrame or None if inplace=True.
|
|
384
|
+
"""
|
|
385
|
+
if inplace:
|
|
386
|
+
weights_backup = self.weights.copy()
|
|
387
|
+
# Perform in-place drop on the parent DataFrame
|
|
388
|
+
super().drop(
|
|
389
|
+
labels=labels,
|
|
390
|
+
axis=axis,
|
|
391
|
+
index=index,
|
|
392
|
+
columns=columns,
|
|
393
|
+
level=level,
|
|
394
|
+
inplace=True,
|
|
395
|
+
errors=errors,
|
|
396
|
+
)
|
|
397
|
+
self.weights = weights_backup
|
|
398
|
+
self._link_all_weights()
|
|
399
|
+
return None
|
|
400
|
+
else:
|
|
401
|
+
res = super().drop(
|
|
402
|
+
labels=labels,
|
|
403
|
+
axis=axis,
|
|
404
|
+
index=index,
|
|
405
|
+
columns=columns,
|
|
406
|
+
level=level,
|
|
407
|
+
inplace=False,
|
|
408
|
+
errors=errors,
|
|
409
|
+
)
|
|
410
|
+
return MicroDataFrame(res, weights=self.weights)
|
|
411
|
+
|
|
412
|
+
def merge(
|
|
413
|
+
self,
|
|
414
|
+
right,
|
|
415
|
+
how="inner",
|
|
416
|
+
on=None,
|
|
417
|
+
left_on=None,
|
|
418
|
+
right_on=None,
|
|
419
|
+
left_index=False,
|
|
420
|
+
right_index=False,
|
|
421
|
+
sort=False,
|
|
422
|
+
suffixes=("_x", "_y"),
|
|
423
|
+
copy=True,
|
|
424
|
+
indicator=False,
|
|
425
|
+
validate=None,
|
|
426
|
+
):
|
|
427
|
+
"""Merge DataFrame or named Series objects with a database-style join.
|
|
428
|
+
|
|
429
|
+
This method overrides pandas DataFrame.merge() to return a
|
|
430
|
+
MicroDataFrame.
|
|
431
|
+
|
|
432
|
+
:param right: Object to merge with.
|
|
433
|
+
:param how: Type of merge to be performed.
|
|
434
|
+
:param on: Column or index level names to join on.
|
|
435
|
+
:param left_on: Column or index level names to join on in the left
|
|
436
|
+
DataFrame.
|
|
437
|
+
:param right_on: Column or index level names to join on in the right
|
|
438
|
+
DataFrame.
|
|
439
|
+
:param left_index: Use the index from the left DataFrame as the join
|
|
440
|
+
key(s).
|
|
441
|
+
:param right_index: Use the index from the right DataFrame as the join
|
|
442
|
+
key(s).
|
|
443
|
+
:param sort: Sort the join keys lexicographically in the result
|
|
444
|
+
DataFrame.
|
|
445
|
+
:param suffixes: A length-2 sequence where each element is optionally a
|
|
446
|
+
string indicating the suffix to add to overlapping column names.
|
|
447
|
+
:param copy: If False, avoid copy if possible.
|
|
448
|
+
:param indicator: If True, adds a column to output DataFrame called
|
|
449
|
+
"_merge".
|
|
450
|
+
:param validate: If specified, checks if merge is of specified type.
|
|
451
|
+
:return: MicroDataFrame with merged data.
|
|
452
|
+
"""
|
|
453
|
+
res = super().merge(
|
|
454
|
+
right,
|
|
455
|
+
how=how,
|
|
456
|
+
on=on,
|
|
457
|
+
left_on=left_on,
|
|
458
|
+
right_on=right_on,
|
|
459
|
+
left_index=left_index,
|
|
460
|
+
right_index=right_index,
|
|
461
|
+
sort=sort,
|
|
462
|
+
suffixes=suffixes,
|
|
463
|
+
copy=copy,
|
|
464
|
+
indicator=indicator,
|
|
465
|
+
validate=validate,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# For inner join, both dataframes must have the same weights on
|
|
469
|
+
# matching rows. For now, we'll use the left dataframe's weights.
|
|
470
|
+
# This is a simplification and may need more sophisticated handling
|
|
471
|
+
return MicroDataFrame(res, weights=self.weights)
|
|
472
|
+
|
|
473
|
+
def __getattr__(self, name):
|
|
474
|
+
"""Allow accessing columns as attributes (e.g., df.column_name).
|
|
475
|
+
|
|
476
|
+
This enables more intuitive column access while preserving MicroSeries
|
|
477
|
+
functionality when accessing columns.
|
|
478
|
+
|
|
479
|
+
:param name: Attribute name to access
|
|
480
|
+
:return: MicroSeries if the attribute is a column, otherwise delegates
|
|
481
|
+
to parent
|
|
482
|
+
"""
|
|
483
|
+
if name in self.columns:
|
|
484
|
+
return self[name]
|
|
485
|
+
return super().__getattr__(name)
|
|
486
|
+
|
|
306
487
|
def equals(self, other: "MicroDataFrame") -> bool:
|
|
307
488
|
equal_values = super().equals(other)
|
|
308
489
|
equal_weights = self.weights.equals(other.weights)
|
|
@@ -469,17 +650,29 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
469
650
|
elif isinstance(by, str):
|
|
470
651
|
self.columns.remove(by)
|
|
471
652
|
self.columns.remove("__tmp_weights")
|
|
653
|
+
# Filter to only numeric columns
|
|
654
|
+
self.numeric_columns = [
|
|
655
|
+
col
|
|
656
|
+
for col in self.columns
|
|
657
|
+
if pd.api.types.is_numeric_dtype(self.obj[col])
|
|
658
|
+
]
|
|
472
659
|
for fn_name in MicroSeries.SCALAR_FUNCTIONS:
|
|
473
660
|
|
|
474
661
|
def get_fn(name):
|
|
475
662
|
def fn(*args, **kwargs):
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
663
|
+
results = {}
|
|
664
|
+
for col in self.numeric_columns:
|
|
665
|
+
try:
|
|
666
|
+
results[col] = getattr(getattr(self, col), name)(
|
|
479
667
|
*args, **kwargs
|
|
480
668
|
)
|
|
481
|
-
|
|
482
|
-
|
|
669
|
+
except Exception:
|
|
670
|
+
# Skip columns that can't be aggregated
|
|
671
|
+
pass
|
|
672
|
+
return (
|
|
673
|
+
MicroDataFrame(results)
|
|
674
|
+
if results
|
|
675
|
+
else MicroDataFrame()
|
|
483
676
|
)
|
|
484
677
|
|
|
485
678
|
return fn
|
|
@@ -489,13 +682,19 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
|
|
|
489
682
|
|
|
490
683
|
def get_fn(name) -> Callable:
|
|
491
684
|
def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
685
|
+
results = {}
|
|
686
|
+
for col in self.numeric_columns:
|
|
687
|
+
try:
|
|
688
|
+
results[col] = getattr(getattr(self, col), name)(
|
|
495
689
|
*args, **kwargs
|
|
496
690
|
)
|
|
497
|
-
|
|
498
|
-
|
|
691
|
+
except Exception:
|
|
692
|
+
# Skip columns that can't be aggregated
|
|
693
|
+
pass
|
|
694
|
+
return (
|
|
695
|
+
MicroDataFrame(results)
|
|
696
|
+
if results
|
|
697
|
+
else MicroDataFrame()
|
|
499
698
|
)
|
|
500
699
|
|
|
501
700
|
return fn
|
microdf/microseries.py
CHANGED
|
@@ -66,6 +66,14 @@ class MicroSeries(pd.Series):
|
|
|
66
66
|
|
|
67
67
|
self.weights = pd.Series(weights, dtype=float)
|
|
68
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
|
+
|
|
69
77
|
@vector_function
|
|
70
78
|
def weight(self) -> pd.Series:
|
|
71
79
|
"""Calculates the weighted value of the MicroSeries.
|
|
@@ -23,6 +23,14 @@ def test_df_init() -> None:
|
|
|
23
23
|
df.set_weight_col("w")
|
|
24
24
|
assert df.a.mean() == np.average(arr, weights=w)
|
|
25
25
|
|
|
26
|
+
# Test set_weights with string (column name)
|
|
27
|
+
df2 = mdf.MicroDataFrame()
|
|
28
|
+
df2["a"] = arr
|
|
29
|
+
df2["w"] = w
|
|
30
|
+
df2.set_weights("w") # Using string column name instead of set_weight_col
|
|
31
|
+
assert df2.a.mean() == np.average(arr, weights=w)
|
|
32
|
+
assert np.array_equal(df2.weights.values, w)
|
|
33
|
+
|
|
26
34
|
|
|
27
35
|
def test_handles_empty_index() -> None:
|
|
28
36
|
arr = np.array([0, 1, 1])
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microdf-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Weighted pandas DataFrames and Series for survey microdata
|
|
5
|
-
Author-email: Max Ghenis <max@
|
|
5
|
+
Author-email: Max Ghenis <max@policyengine.org>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.9
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
|
|
2
|
+
microdf/microdataframe.py,sha256=TU4MPHlGSICibPgGkATJYDpVBQ2NNXnyBAqw3kjteeI,25771
|
|
3
|
+
microdf/microseries.py,sha256=MFBStp1IaNVABaf3_Ap_VwXSC3bA8V7rXD4TEG_091g,22452
|
|
4
|
+
microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
|
|
5
|
+
microdf/tests/test_microseries_dataframe.py,sha256=2pm_fLZPbv-ccDKnEyqIiw-_pTZwt9J2lnM5YsZHzx4,9401
|
|
6
|
+
microdf_python-1.0.2.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
|
|
7
|
+
microdf_python-1.0.2.dist-info/METADATA,sha256=sOLX9w2xCHwyNeLcW7BpXu6iiTTeaTmxNdMsdJpmD7s,2486
|
|
8
|
+
microdf_python-1.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
microdf_python-1.0.2.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
|
|
10
|
+
microdf_python-1.0.2.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
microdf/__init__.py,sha256=ldRvhE8t4cD7qlxQ5yLndMnWEUYpDw4m-jCDV_psXkY,319
|
|
2
|
-
microdf/microdataframe.py,sha256=vM-vddwp50JQv7OoyoCmC9AbkTUT0dvEnCokSzvpe4s,18329
|
|
3
|
-
microdf/microseries.py,sha256=URFT9bqry5Q_qezl3ge9lW8FSbQ6qg0XRaFQwN0IkwQ,22130
|
|
4
|
-
microdf/tests/conftest.py,sha256=u-EMyX1-u_nM-YO0RJYCzYHQDXxUI2WQE6GkyJlErqg,150
|
|
5
|
-
microdf/tests/test_microseries_dataframe.py,sha256=UwcaSCo-1wVjp9TtUgG632ziQ8yOJvr6T7PQipgUhAs,9102
|
|
6
|
-
microdf_python-1.0.1.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
|
|
7
|
-
microdf_python-1.0.1.dist-info/METADATA,sha256=so2yCume7-DDPKDnx25w-q0TWfQjGRdB47ydO-J5-Qs,2483
|
|
8
|
-
microdf_python-1.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
microdf_python-1.0.1.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
|
|
10
|
-
microdf_python-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|