microdf-python 1.2.2__py3-none-any.whl → 1.3.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.
microdf/microdataframe.py CHANGED
@@ -147,9 +147,7 @@ class MicroDataFrame(pd.DataFrame):
147
147
  for col in self.columns:
148
148
  if pd.api.types.is_numeric_dtype(self[col]):
149
149
  try:
150
- results[col] = getattr(self[col], name)(
151
- *args, **kwargs
152
- )
150
+ results[col] = getattr(self[col], name)(*args, **kwargs)
153
151
  except Exception:
154
152
  # Skip columns that can't be aggregated
155
153
  pass
@@ -224,9 +222,7 @@ class MicroDataFrame(pd.DataFrame):
224
222
  for col in self.columns:
225
223
  if pd.api.types.is_numeric_dtype(self[col]):
226
224
  try:
227
- results[col] = getattr(self[col], name)(
228
- *args, **kwargs
229
- )
225
+ results[col] = getattr(self[col], name)(*args, **kwargs)
230
226
  except Exception:
231
227
  # Skip columns that can't be aggregated
232
228
  pass
@@ -309,7 +305,11 @@ class MicroDataFrame(pd.DataFrame):
309
305
 
310
306
  if isinstance(weights, str):
311
307
  self.weights_col = weights
312
- self.weights = pd.Series(self[weights], dtype=float)
308
+ self.weights = pd.Series(
309
+ np.asarray(self[weights]),
310
+ index=self.index,
311
+ dtype=float,
312
+ )
313
313
  self._link_all_weights()
314
314
  elif weights is not None:
315
315
  if len(weights) != len(self):
@@ -318,14 +318,21 @@ class MicroDataFrame(pd.DataFrame):
318
318
  f"length of DataFrame ({len(self)})."
319
319
  )
320
320
  self.weights_col = None
321
+ # Align weights to self.index. Without this, weighted ops
322
+ # (self[col].multiply(self.weights) in .sum()) align on
323
+ # label, so any non-default index silently produces all-NaN
324
+ # and aggregations collapse to 0. If a Series is passed in,
325
+ # strip its index so we position-align to self.index.
326
+ if isinstance(weights, pd.Series):
327
+ weights = weights.values
321
328
  with warnings.catch_warnings():
322
329
  warnings.filterwarnings("ignore", category=UserWarning)
323
- self.weights = pd.Series(weights, dtype=float)
330
+ self.weights = pd.Series(
331
+ np.asarray(weights), index=self.index, dtype=float
332
+ )
324
333
  self._link_all_weights()
325
334
 
326
- def set_weight_col(
327
- self, column: str, preserve_old: Optional[bool] = False
328
- ) -> None:
335
+ def set_weight_col(self, column: str, preserve_old: Optional[bool] = False) -> None:
329
336
  """Sets the weights for the MicroDataFrame by specifying the name of
330
337
  the weight column.
331
338
 
@@ -422,8 +429,9 @@ class MicroDataFrame(pd.DataFrame):
422
429
  :return: MicroDataFrame with reset index or None if inplace=True.
423
430
  """
424
431
  if inplace:
425
- weights_backup = self.weights.copy()
426
- # Perform in-place reset on the parent DataFrame
432
+ # Snapshot weight *values* positionally — the index is about
433
+ # to change and reset_index preserves row order.
434
+ weight_values = np.asarray(self.weights.values, dtype=float)
427
435
  super().reset_index(
428
436
  level=level,
429
437
  drop=drop,
@@ -433,7 +441,7 @@ class MicroDataFrame(pd.DataFrame):
433
441
  allow_duplicates=allow_duplicates,
434
442
  names=names,
435
443
  )
436
- self.weights = weights_backup
444
+ self.weights = pd.Series(weight_values, index=self.index, dtype=float)
437
445
  self._link_all_weights()
438
446
  return None
439
447
  else:
@@ -446,13 +454,22 @@ class MicroDataFrame(pd.DataFrame):
446
454
  allow_duplicates=allow_duplicates,
447
455
  names=names,
448
456
  )
449
- return MicroDataFrame(res, weights=self.weights)
457
+ out = MicroDataFrame(res, weights=self.weights.values)
458
+ # Ensure weights align to res.index (reset_index changes the
459
+ # index but preserves row order, so pass values positionally).
460
+ out.weights = pd.Series(
461
+ np.asarray(self.weights.values, dtype=float),
462
+ index=out.index,
463
+ dtype=float,
464
+ )
465
+ return out
450
466
 
451
467
  def copy(self, deep: Optional[bool] = True) -> "MicroDataFrame":
452
468
  res = super().copy(deep)
453
- # This changes the original columns to Series. Undo it:
454
- for col in self.columns:
455
- self[col] = MicroSeries(self[col])
469
+ # super().copy() corrupts self's column types to plain Series.
470
+ # Restore them in O(N) instead of O(N²) by calling
471
+ # _link_all_weights once rather than per-column __setitem__.
472
+ self._link_all_weights()
456
473
  res = MicroDataFrame(res, weights=self.weights.copy(deep))
457
474
  return res
458
475
 
@@ -486,8 +503,11 @@ class MicroDataFrame(pd.DataFrame):
486
503
  dropped.
487
504
  :return: MicroDataFrame or None if inplace=True.
488
505
  """
506
+ row_drop = axis in (0, "index") or index is not None
489
507
  if inplace:
490
- weights_backup = self.weights.copy()
508
+ # Snapshot the pre-drop weights keyed by the pre-drop index so
509
+ # we can reindex to the surviving rows after the drop.
510
+ pre_drop_weights = pd.Series(self.weights.values, index=self.index.copy())
491
511
  # Perform in-place drop on the parent DataFrame
492
512
  super().drop(
493
513
  labels=labels,
@@ -498,7 +518,15 @@ class MicroDataFrame(pd.DataFrame):
498
518
  inplace=True,
499
519
  errors=errors,
500
520
  )
501
- self.weights = weights_backup
521
+ if row_drop:
522
+ surviving = pre_drop_weights.reindex(self.index)
523
+ self.weights = pd.Series(
524
+ surviving.values, index=self.index, dtype=float
525
+ )
526
+ else:
527
+ self.weights = pd.Series(
528
+ pre_drop_weights.values, index=self.index, dtype=float
529
+ )
502
530
  self._link_all_weights()
503
531
  return None
504
532
  else:
@@ -511,7 +539,19 @@ class MicroDataFrame(pd.DataFrame):
511
539
  inplace=False,
512
540
  errors=errors,
513
541
  )
514
- return MicroDataFrame(res, weights=self.weights)
542
+ if row_drop:
543
+ # Row drop: keep only the weights for surviving rows,
544
+ # in the order of the resulting DataFrame.
545
+ pre_drop_weights = pd.Series(self.weights.values, index=self.index)
546
+ new_weights = pre_drop_weights.reindex(res.index).values
547
+ else:
548
+ new_weights = self.weights.values
549
+ out = MicroDataFrame(res, weights=new_weights)
550
+ # Guard against the set_weights path building weights with a
551
+ # default RangeIndex, which would misalign against res.index
552
+ # and silently zero weighted aggregations.
553
+ out.weights = pd.Series(new_weights, index=out.index, dtype=float)
554
+ return out
515
555
 
516
556
  def merge(
517
557
  self,
@@ -554,7 +594,17 @@ class MicroDataFrame(pd.DataFrame):
554
594
  :param validate: If specified, checks if merge is of specified type.
555
595
  :return: MicroDataFrame with merged data.
556
596
  """
557
- res = super().merge(
597
+ # Attach the left weights as a temporary column so pandas' merge
598
+ # propagates them onto every surviving output row (including
599
+ # many-to-many row duplications, inner-join filtering, and
600
+ # left-with-missing NaNs). We then strip the column back off.
601
+ tmp = "__microdf_weights__"
602
+ # Avoid clobbering if this exact name is already used.
603
+ while tmp in self.columns or tmp in right.columns:
604
+ tmp += "_"
605
+ left_df = pd.DataFrame(self).copy()
606
+ left_df[tmp] = np.asarray(self.weights.values, dtype=float)
607
+ res = left_df.merge(
558
608
  right,
559
609
  how=how,
560
610
  on=on,
@@ -568,11 +618,17 @@ class MicroDataFrame(pd.DataFrame):
568
618
  indicator=indicator,
569
619
  validate=validate,
570
620
  )
571
-
572
- # For inner join, both dataframes must have the same weights on
573
- # matching rows. For now, we'll use the left dataframe's weights.
574
- # This is a simplification and may need more sophisticated handling
575
- return MicroDataFrame(res, weights=self.weights)
621
+ # Pull out the propagated weights. Rows with no left match in a
622
+ # right/outer join get NaN weight fill with 0 so they don't
623
+ # poison later aggregations (a user who needs a different
624
+ # convention can override afterwards).
625
+ merged_weights = res[tmp].fillna(0).to_numpy(dtype=float)
626
+ res = res.drop(columns=[tmp])
627
+ out = MicroDataFrame(res, weights=merged_weights)
628
+ # Ensure the weights Series aligns with res.index regardless of
629
+ # the default-RangeIndex behavior of set_weights.
630
+ out.weights = pd.Series(merged_weights, index=out.index, dtype=float)
631
+ return out
576
632
 
577
633
  def __getattr__(self, name):
578
634
  """Allow accessing columns as attributes (e.g., df.column_name).
@@ -594,9 +650,7 @@ class MicroDataFrame(pd.DataFrame):
594
650
  return equal_values and equal_weights
595
651
 
596
652
  @get_args_as_micro_series()
597
- def groupby(
598
- self, by: Union[str, List], *args, **kwargs
599
- ) -> "MicroDataFrameGroupBy":
653
+ def groupby(self, by: Union[str, List], *args, **kwargs) -> "MicroDataFrameGroupBy":
600
654
  """Returns a GroupBy object with MicroSeriesGroupBy objects for each
601
655
  column.
602
656
 
@@ -606,10 +660,16 @@ class MicroDataFrame(pd.DataFrame):
606
660
  return: DataFrameGroupBy object with columns using weights
607
661
  rtype: DataFrameGroupBy
608
662
  """
609
- self["__tmp_weights"] = self.weights
610
- gb = super().groupby(by, *args, **kwargs)
663
+ # Build the groupby on a *copy* that carries a ``__tmp_weights``
664
+ # column. We used to set this column on ``self`` directly, which
665
+ # permanently leaked the weight column onto the caller's
666
+ # DataFrame — any later ``df.sum()`` or ``list(df.columns)``
667
+ # would then include it.
668
+ staged = pd.DataFrame(self).copy()
669
+ staged["__tmp_weights"] = np.asarray(self.weights.values, dtype=float)
670
+ gb = staged.groupby(by, *args, **kwargs)
611
671
  weights = copy.deepcopy(gb["__tmp_weights"])
612
- for col in self.columns: # df.groupby(...)[col]s use weights
672
+ for col in staged.columns: # df.groupby(...)[col]s use weights
613
673
  res = gb[col]
614
674
  res.__class__ = MicroSeriesGroupBy
615
675
  res._init()
@@ -757,14 +817,10 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
757
817
  self.columns.remove("__tmp_weights")
758
818
  # Filter to only numeric columns
759
819
  self.numeric_columns = [
760
- col
761
- for col in self.columns
762
- if pd.api.types.is_numeric_dtype(self.obj[col])
820
+ col for col in self.columns if pd.api.types.is_numeric_dtype(self.obj[col])
763
821
  ]
764
822
  # Store reference to weights groupby for column selection
765
- self._weights_groupby = copy.deepcopy(
766
- super().__getitem__("__tmp_weights")
767
- )
823
+ self._weights_groupby = copy.deepcopy(super().__getitem__("__tmp_weights"))
768
824
  for fn_name in MicroSeries.SCALAR_FUNCTIONS:
769
825
 
770
826
  def get_fn(name):
@@ -854,18 +910,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
854
910
  results = {}
855
911
  for col in res.numeric_columns:
856
912
  try:
857
- results[col] = getattr(
858
- getattr(res, col), name
859
- )(*args, **kwargs)
913
+ results[col] = getattr(getattr(res, col), name)(
914
+ *args, **kwargs
915
+ )
860
916
  except Exception:
861
917
  pass
862
918
  # Return plain DataFrame - aggregated results don't
863
919
  # have per-row weights (weights were already applied)
864
- return (
865
- pd.DataFrame(results)
866
- if results
867
- else pd.DataFrame()
868
- )
920
+ return pd.DataFrame(results) if results else pd.DataFrame()
869
921
 
870
922
  return fn
871
923
 
@@ -877,18 +929,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
877
929
  results = {}
878
930
  for col in res.numeric_columns:
879
931
  try:
880
- results[col] = getattr(
881
- getattr(res, col), name
882
- )(*args, **kwargs)
932
+ results[col] = getattr(getattr(res, col), name)(
933
+ *args, **kwargs
934
+ )
883
935
  except Exception:
884
936
  pass
885
937
  # Return plain DataFrame - aggregated results don't
886
938
  # have per-row weights (weights were already applied)
887
- return (
888
- pd.DataFrame(results)
889
- if results
890
- else pd.DataFrame()
891
- )
939
+ return pd.DataFrame(results) if results else pd.DataFrame()
892
940
 
893
941
  return fn
894
942