microdf-python 1.0.0__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 CHANGED
@@ -44,14 +44,17 @@ class MicroDataFrame(pd.DataFrame):
44
44
  """
45
45
 
46
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
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 = 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
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 = 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
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 = 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
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
- :param weights: Array of weights.
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 weights: np.array
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,16 +255,27 @@ 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]:
270
+ # Let pandas handle the initial slicing
221
271
  result = super().__getitem__(key)
272
+
273
+ # If the result is a DataFrame, re-synchronize the weights
222
274
  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)
275
+ new_weights = self.weights.reindex(result.index)
276
+ return MicroDataFrame(result, weights=new_weights)
277
+
278
+ # Otherwise, the result is a Series or a scalar, so just return it
228
279
  return result
229
280
 
230
281
  def catch_series_relapse(self) -> None:
@@ -301,6 +352,138 @@ class MicroDataFrame(pd.DataFrame):
301
352
  res = MicroDataFrame(res, weights=self.weights.copy(deep))
302
353
  return res
303
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
+
304
487
  def equals(self, other: "MicroDataFrame") -> bool:
305
488
  equal_values = super().equals(other)
306
489
  equal_weights = self.weights.equals(other.weights)
@@ -467,17 +650,29 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
467
650
  elif isinstance(by, str):
468
651
  self.columns.remove(by)
469
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
+ ]
470
659
  for fn_name in MicroSeries.SCALAR_FUNCTIONS:
471
660
 
472
661
  def get_fn(name):
473
662
  def fn(*args, **kwargs):
474
- return MicroDataFrame(
475
- {
476
- col: getattr(getattr(self, col), name)(
663
+ results = {}
664
+ for col in self.numeric_columns:
665
+ try:
666
+ results[col] = getattr(getattr(self, col), name)(
477
667
  *args, **kwargs
478
668
  )
479
- for col in self.columns
480
- }
669
+ except Exception:
670
+ # Skip columns that can't be aggregated
671
+ pass
672
+ return (
673
+ MicroDataFrame(results)
674
+ if results
675
+ else MicroDataFrame()
481
676
  )
482
677
 
483
678
  return fn
@@ -487,13 +682,19 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
487
682
 
488
683
  def get_fn(name) -> Callable:
489
684
  def fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
490
- return MicroDataFrame(
491
- {
492
- col: getattr(getattr(self, col), name)(
685
+ results = {}
686
+ for col in self.numeric_columns:
687
+ try:
688
+ results[col] = getattr(getattr(self, col), name)(
493
689
  *args, **kwargs
494
690
  )
495
- for col in self.columns
496
- }
691
+ except Exception:
692
+ # Skip columns that can't be aggregated
693
+ pass
694
+ return (
695
+ MicroDataFrame(results)
696
+ if results
697
+ else MicroDataFrame()
497
698
  )
498
699
 
499
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,23 @@ 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
+
34
+
35
+ def test_handles_empty_index() -> None:
36
+ arr = np.array([0, 1, 1])
37
+ w = np.array([3, 0, 9])
38
+ df = mdf.MicroDataFrame({"a": arr}, weights=w)
39
+
40
+ empty_index = pd.Index([])
41
+ df[empty_index] # Implicit assert; checking for ValueError
42
+
26
43
 
27
44
  def test_series_getitem() -> None:
28
45
  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.0
3
+ Version: 1.0.2
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
- Author-email: Max Ghenis <max@ubicenter.org>
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=5jYQ76PYjndACJO6hsJCaCL0g0clAsQbWltVcPtJ7yU,18202
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=QSxdJ0ZfXO1XZgER9LRqDt5SGpVGyME03gefixH6spk,8855
6
- microdf_python-1.0.0.dist-info/licenses/LICENSE,sha256=uPs-ASYnzlldpf2z8jeRgQFeEH3FLhSuX0rw0OKWoDU,1067
7
- microdf_python-1.0.0.dist-info/METADATA,sha256=mC0MB2zQwVaqrPXfmh9mIc1KX5lmSO1zS9ilCvZQQg0,2483
8
- microdf_python-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- microdf_python-1.0.0.dist-info/top_level.txt,sha256=T2WFPTygQQMdS3GF8YpZ12DKfMGrspbZ3r7z-e3KfiM,8
10
- microdf_python-1.0.0.dist-info/RECORD,,