microdf-python 1.2.1__tar.gz → 1.2.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -11,15 +11,12 @@ Requires-Dist: numpy
11
11
  Requires-Dist: pandas
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: codecov; extra == "dev"
14
- Requires-Dist: flake8; extra == "dev"
15
- Requires-Dist: flake8-pyproject; extra == "dev"
16
- Requires-Dist: black; extra == "dev"
14
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
17
15
  Requires-Dist: docformatter; extra == "dev"
18
- Requires-Dist: isort; extra == "dev"
19
- Requires-Dist: linecheck; extra == "dev"
20
16
  Requires-Dist: pytest; extra == "dev"
21
17
  Requires-Dist: pytest-cov; extra == "dev"
22
18
  Requires-Dist: setuptools; extra == "dev"
19
+ Requires-Dist: towncrier>=24.8.0; extra == "dev"
23
20
  Dynamic: license-file
24
21
 
25
22
  [![Build](https://github.com/PolicyEngine/microdf/workflows/Pull%20request/badge.svg)](https://github.com/PolicyEngine/microdf/actions)
@@ -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
@@ -323,9 +319,7 @@ class MicroDataFrame(pd.DataFrame):
323
319
  self.weights = pd.Series(weights, dtype=float)
324
320
  self._link_all_weights()
325
321
 
326
- def set_weight_col(
327
- self, column: str, preserve_old: Optional[bool] = False
328
- ) -> None:
322
+ def set_weight_col(self, column: str, preserve_old: Optional[bool] = False) -> None:
329
323
  """Sets the weights for the MicroDataFrame by specifying the name of
330
324
  the weight column.
331
325
 
@@ -594,9 +588,7 @@ class MicroDataFrame(pd.DataFrame):
594
588
  return equal_values and equal_weights
595
589
 
596
590
  @get_args_as_micro_series()
597
- def groupby(
598
- self, by: Union[str, List], *args, **kwargs
599
- ) -> "MicroDataFrameGroupBy":
591
+ def groupby(self, by: Union[str, List], *args, **kwargs) -> "MicroDataFrameGroupBy":
600
592
  """Returns a GroupBy object with MicroSeriesGroupBy objects for each
601
593
  column.
602
594
 
@@ -757,14 +749,10 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
757
749
  self.columns.remove("__tmp_weights")
758
750
  # Filter to only numeric columns
759
751
  self.numeric_columns = [
760
- col
761
- for col in self.columns
762
- if pd.api.types.is_numeric_dtype(self.obj[col])
752
+ col for col in self.columns if pd.api.types.is_numeric_dtype(self.obj[col])
763
753
  ]
764
754
  # Store reference to weights groupby for column selection
765
- self._weights_groupby = copy.deepcopy(
766
- super().__getitem__("__tmp_weights")
767
- )
755
+ self._weights_groupby = copy.deepcopy(super().__getitem__("__tmp_weights"))
768
756
  for fn_name in MicroSeries.SCALAR_FUNCTIONS:
769
757
 
770
758
  def get_fn(name):
@@ -854,18 +842,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
854
842
  results = {}
855
843
  for col in res.numeric_columns:
856
844
  try:
857
- results[col] = getattr(
858
- getattr(res, col), name
859
- )(*args, **kwargs)
845
+ results[col] = getattr(getattr(res, col), name)(
846
+ *args, **kwargs
847
+ )
860
848
  except Exception:
861
849
  pass
862
850
  # Return plain DataFrame - aggregated results don't
863
851
  # have per-row weights (weights were already applied)
864
- return (
865
- pd.DataFrame(results)
866
- if results
867
- else pd.DataFrame()
868
- )
852
+ return pd.DataFrame(results) if results else pd.DataFrame()
869
853
 
870
854
  return fn
871
855
 
@@ -877,18 +861,14 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
877
861
  results = {}
878
862
  for col in res.numeric_columns:
879
863
  try:
880
- results[col] = getattr(
881
- getattr(res, col), name
882
- )(*args, **kwargs)
864
+ results[col] = getattr(getattr(res, col), name)(
865
+ *args, **kwargs
866
+ )
883
867
  except Exception:
884
868
  pass
885
869
  # Return plain DataFrame - aggregated results don't
886
870
  # have per-row weights (weights were already applied)
887
- return (
888
- pd.DataFrame(results)
889
- if results
890
- else pd.DataFrame()
891
- )
871
+ return pd.DataFrame(results) if results else pd.DataFrame()
892
872
 
893
873
  return fn
894
874
 
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import warnings
2
3
  from functools import wraps
3
4
  from typing import Callable, List, Optional, Union
4
5
 
@@ -19,6 +20,50 @@ class MicroSeries(pd.Series):
19
20
  super().__init__(*args, **kwargs)
20
21
  self.set_weights(weights)
21
22
 
23
+ @property
24
+ def _values(self):
25
+ """Internal access to underlying numpy array without warning."""
26
+ return super().values
27
+
28
+ @property
29
+ def values(self):
30
+ """Access underlying numpy array.
31
+
32
+ .. warning::
33
+ Returns a plain numpy array without weights. Operations
34
+ like ``.mean()`` on the result will be unweighted. Use
35
+ MicroSeries methods directly for weighted calculations
36
+ (e.g., ``ms.mean()`` instead of ``ms.values.mean()``).
37
+ """
38
+ warnings.warn(
39
+ "Accessing .values on a MicroSeries returns a plain numpy "
40
+ "array without weights. Operations like .mean() on the "
41
+ "result will be unweighted. Use MicroSeries methods "
42
+ "directly for weighted calculations (e.g., ms.mean() "
43
+ "instead of ms.values.mean()).",
44
+ UserWarning,
45
+ stacklevel=2,
46
+ )
47
+ return super().values
48
+
49
+ def to_numpy(self, *args, **kwargs):
50
+ """Convert to numpy array.
51
+
52
+ .. warning::
53
+ Returns a plain numpy array without weights. Operations
54
+ like ``.mean()`` on the result will be unweighted. Use
55
+ MicroSeries methods directly for weighted calculations.
56
+ """
57
+ warnings.warn(
58
+ "Calling .to_numpy() on a MicroSeries returns a plain "
59
+ "numpy array without weights. Operations like .mean() on "
60
+ "the result will be unweighted. Use MicroSeries methods "
61
+ "directly for weighted calculations.",
62
+ UserWarning,
63
+ stacklevel=2,
64
+ )
65
+ return super().to_numpy(*args, **kwargs)
66
+
22
67
  def weighted_function(fn: Callable) -> Callable:
23
68
  @wraps(fn)
24
69
  def safe_fn(*args, **kwargs):
@@ -51,9 +96,7 @@ class MicroSeries(pd.Series):
51
96
  """
52
97
  if weights is None:
53
98
  if len(self) > 0:
54
- self.weights = pd.Series(
55
- np.ones_like(self.values), dtype=float
56
- )
99
+ self.weights = pd.Series(np.ones_like(self._values), dtype=float)
57
100
  else:
58
101
  if len(weights) != len(self):
59
102
  raise ValueError(
@@ -110,7 +153,7 @@ class MicroSeries(pd.Series):
110
153
  :returns: The weighted mean.
111
154
  :rtype: float
112
155
  """
113
- values = self.values
156
+ values = self._values
114
157
  weights = self.weights
115
158
 
116
159
  if skipna:
@@ -141,12 +184,12 @@ class MicroSeries(pd.Series):
141
184
  :return: Weighted quantile value(s).
142
185
  :rtype: float or pd.Series
143
186
  """
144
- values = np.array(self.values)
187
+ values = np.array(self._values)
145
188
  quantiles = np.atleast_1d(q)
146
189
  sample_weight = np.array(self.weights)
147
- assert np.all(quantiles >= 0) and np.all(
148
- quantiles <= 1
149
- ), "quantiles should be in [0, 1]"
190
+ assert np.all(quantiles >= 0) and np.all(quantiles <= 1), (
191
+ "quantiles should be in [0, 1]"
192
+ )
150
193
  sorter = np.argsort(values)
151
194
  values = values[sorter]
152
195
  sample_weight = sample_weight[sorter]
@@ -154,11 +197,7 @@ class MicroSeries(pd.Series):
154
197
  cumsum_normalized = cumsum / cumsum[-1]
155
198
  result = np.array(
156
199
  [
157
- values[
158
- min(
159
- np.searchsorted(cumsum_normalized, qi), len(values) - 1
160
- )
161
- ]
200
+ values[min(np.searchsorted(cumsum_normalized, qi), len(values) - 1)]
162
201
  for qi in quantiles
163
202
  ]
164
203
  )
@@ -313,7 +352,7 @@ class MicroSeries(pd.Series):
313
352
  "in division by zero."
314
353
  )
315
354
 
316
- order = np.argsort(self.values)
355
+ order = np.argsort(self._values)
317
356
  inverse_order = np.argsort(order)
318
357
  ranks = np.array(self.weights.values)[order].cumsum()[inverse_order]
319
358
  if pct:
@@ -409,9 +448,7 @@ class MicroSeries(pd.Series):
409
448
  return MicroSeries(res, weights=self.weights)
410
449
  return self
411
450
 
412
- def round(
413
- self, decimals: Optional[int] = 0, *args, **kwargs
414
- ) -> "MicroSeries":
451
+ def round(self, decimals: Optional[int] = 0, *args, **kwargs) -> "MicroSeries":
415
452
  res = super().round(decimals=decimals, *args, **kwargs)
416
453
  return MicroSeries(res, weights=self.weights)
417
454
 
@@ -443,14 +480,10 @@ class MicroSeries(pd.Series):
443
480
  def __mul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
444
481
  return MicroSeries(super().__mul__(other), weights=self.weights)
445
482
 
446
- def __floordiv__(
447
- self, other: Union[int, float, pd.Series]
448
- ) -> "MicroSeries":
483
+ def __floordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
449
484
  return MicroSeries(super().__floordiv__(other), weights=self.weights)
450
485
 
451
- def __truediv__(
452
- self, other: Union[int, float, pd.Series]
453
- ) -> "MicroSeries":
486
+ def __truediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
454
487
  return MicroSeries(super().__truediv__(other), weights=self.weights)
455
488
 
456
489
  def __mod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
@@ -480,14 +513,10 @@ class MicroSeries(pd.Series):
480
513
  def __rmul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
481
514
  return MicroSeries(super().__rmul__(other), weights=self.weights)
482
515
 
483
- def __rfloordiv__(
484
- self, other: Union[int, float, pd.Series]
485
- ) -> "MicroSeries":
516
+ def __rfloordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
486
517
  return MicroSeries(super().__rfloordiv__(other), weights=self.weights)
487
518
 
488
- def __rtruediv__(
489
- self, other: Union[int, float, pd.Series]
490
- ) -> "MicroSeries":
519
+ def __rtruediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
491
520
  return MicroSeries(super().__rtruediv__(other), weights=self.weights)
492
521
 
493
522
  def __rmod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
@@ -506,7 +535,7 @@ class MicroSeries(pd.Series):
506
535
  return MicroSeries(super().__rxor__(other), weights=self.weights)
507
536
 
508
537
  def sqrt(self) -> "MicroSeries":
509
- sqrt_values = np.sqrt(self.values)
538
+ sqrt_values = np.sqrt(self._values)
510
539
  return MicroSeries(sqrt_values, index=self.index, weights=self.weights)
511
540
 
512
541
  # comparators
@@ -540,17 +569,13 @@ class MicroSeries(pd.Series):
540
569
  def __imul__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
541
570
  return MicroSeries(super().__imul__(other), weights=self.weights)
542
571
 
543
- def __ifloordiv__(
544
- self, other: Union[int, float, pd.Series]
545
- ) -> "MicroSeries":
572
+ def __ifloordiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
546
573
  return MicroSeries(super().__ifloordiv__(other), weights=self.weights)
547
574
 
548
575
  def __idiv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
549
576
  return MicroSeries(super().__idiv__(other), weights=self.weights)
550
577
 
551
- def __itruediv__(
552
- self, other: Union[int, float, pd.Series]
553
- ) -> "MicroSeries":
578
+ def __itruediv__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
554
579
  return MicroSeries(super().__itruediv__(other), weights=self.weights)
555
580
 
556
581
  def __imod__(self, other: Union[int, float, pd.Series]) -> "MicroSeries":
@@ -590,7 +615,7 @@ class MicroSeries(pd.Series):
590
615
 
591
616
  def __repr__(self) -> str:
592
617
  return pd.DataFrame(
593
- dict(value=self.values, weight=self.weights.values)
618
+ dict(value=self._values, weight=self.weights.values)
594
619
  ).__repr__()
595
620
 
596
621
 
@@ -621,16 +646,12 @@ class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
621
646
  def _init(self):
622
647
  def _weighted_agg(name) -> Callable:
623
648
  def via_micro_series(row, *args, **kwargs):
624
- return getattr(MicroSeries(row.a, weights=row.w), name)(
625
- *args, **kwargs
626
- )
649
+ return getattr(MicroSeries(row.a, weights=row.w), name)(*args, **kwargs)
627
650
 
628
651
  fn = getattr(MicroSeries, name)
629
652
 
630
653
  @wraps(fn)
631
- def _weighted_agg_fn(
632
- *args, **kwargs
633
- ) -> Union[pd.Series, pd.DataFrame]:
654
+ def _weighted_agg_fn(*args, **kwargs) -> Union[pd.Series, pd.DataFrame]:
634
655
  arrays = self.apply(np.array)
635
656
  weights = self.weights.apply(np.array)
636
657
  df = pd.DataFrame(dict(a=arrays, w=weights))
@@ -1,3 +1,5 @@
1
+ import warnings
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
@@ -259,9 +261,7 @@ def test_decile_rank() -> None:
259
261
 
260
262
 
261
263
  def test_copy_equals() -> None:
262
- d = mdf.MicroDataFrame(
263
- {"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
264
- )
264
+ d = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8])
265
265
  d_copy = d.copy()
266
266
  d_copy_diff_weights = d_copy.copy()
267
267
  d_copy_diff_weights.weights *= 2
@@ -273,9 +273,7 @@ def test_copy_equals() -> None:
273
273
 
274
274
 
275
275
  def test_subset() -> None:
276
- df = mdf.MicroDataFrame(
277
- {"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8]
278
- )
276
+ df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]}, weights=[7, 8])
279
277
  df_no_z = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4]}, weights=[7, 8])
280
278
  assert df[["x", "y"]].equals(df_no_z)
281
279
  df_no_z_diff_weights = df_no_z.copy()
@@ -351,9 +349,7 @@ def test_reset_index_inplace() -> None:
351
349
  # Test 4: Multi-level index
352
350
  arrays = [["bar", "bar", "baz", "baz"], ["one", "two", "one", "two"]]
353
351
  multi_index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
354
- df_multi = pd.DataFrame(
355
- {"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index
356
- )
352
+ df_multi = pd.DataFrame({"A": [1, 2, 3, 4], "B": [5, 6, 7, 8]}, index=multi_index)
357
353
  mdf_multi = MicroDataFrame(df_multi, weights=weights)
358
354
  result = mdf_multi.reset_index(level="first")
359
355
  assert "first" in result.columns
@@ -371,9 +367,7 @@ def test_reset_index_inplace() -> None:
371
367
  def test_loc_preserves_weights() -> None:
372
368
  """Test that .loc[] returns MicroDataFrame with proper weights (issue
373
369
  #265)."""
374
- df = mdf.MicroDataFrame(
375
- {"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50]
376
- )
370
+ df = mdf.MicroDataFrame({"one": [1, 1, 1, 1, 1]}, weights=[10, 20, 30, 40, 50])
377
371
 
378
372
  # Filter all rows (should get same weights)
379
373
  filtered = df.loc[df.one == 1]
@@ -381,9 +375,7 @@ def test_loc_preserves_weights() -> None:
381
375
  assert filtered.one.sum() == 150.0 # Weighted sum
382
376
 
383
377
  # Partial filter
384
- df2 = mdf.MicroDataFrame(
385
- {"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
386
- )
378
+ df2 = mdf.MicroDataFrame({"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50])
387
379
  subset = df2.loc[df2.x > 2]
388
380
  assert isinstance(subset, MicroDataFrame)
389
381
  assert subset.x.sum() == 500.0 # 3*30 + 4*40 + 5*50 = 500
@@ -392,9 +384,7 @@ def test_loc_preserves_weights() -> None:
392
384
 
393
385
  def test_iloc_preserves_weights() -> None:
394
386
  """Test that .iloc[] returns MicroDataFrame with proper weights."""
395
- df = mdf.MicroDataFrame(
396
- {"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50]
397
- )
387
+ df = mdf.MicroDataFrame({"x": [1, 2, 3, 4, 5]}, weights=[10, 20, 30, 40, 50])
398
388
 
399
389
  # Select rows by position
400
390
  subset = df.iloc[2:5]
@@ -405,9 +395,7 @@ def test_iloc_preserves_weights() -> None:
405
395
 
406
396
  def test_groupby_column_selection() -> None:
407
397
  """Test that groupby column selection preserves weights (issue #193)."""
408
- d = mdf.MicroDataFrame(
409
- dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6]
410
- )
398
+ d = mdf.MicroDataFrame(dict(g=["a", "a", "b"], y=[1, 2, 3]), weights=[4, 5, 6])
411
399
 
412
400
  # Test single column string selection
413
401
  result_str = d.groupby("g")["y"].sum()
@@ -423,3 +411,45 @@ def test_groupby_column_selection() -> None:
423
411
  result_all = d.groupby("g").sum()
424
412
  assert "weight" not in result_all.columns
425
413
  assert list(result_all.columns) == ["y"]
414
+
415
+
416
+ def test_values_warns() -> None:
417
+ """Accessing .values on a MicroSeries should emit a UserWarning."""
418
+ ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
419
+ with warnings.catch_warnings(record=True) as w:
420
+ warnings.simplefilter("always")
421
+ _ = ms.values
422
+ assert len(w) == 1
423
+ assert issubclass(w[0].category, UserWarning)
424
+ assert "weights" in str(w[0].message).lower()
425
+
426
+
427
+ def test_to_numpy_warns() -> None:
428
+ """Calling .to_numpy() on a MicroSeries should emit a UserWarning."""
429
+ ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
430
+ with warnings.catch_warnings(record=True) as w:
431
+ warnings.simplefilter("always")
432
+ _ = ms.to_numpy()
433
+ assert len(w) == 1
434
+ assert issubclass(w[0].category, UserWarning)
435
+ assert "weights" in str(w[0].message).lower()
436
+
437
+
438
+ def test_mean_no_warning() -> None:
439
+ """Internal .values usage in .mean() should NOT emit a warning."""
440
+ ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
441
+ with warnings.catch_warnings(record=True) as w:
442
+ warnings.simplefilter("always")
443
+ _ = ms.mean()
444
+ user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
445
+ assert len(user_warnings) == 0
446
+
447
+
448
+ def test_repr_no_warning() -> None:
449
+ """Internal .values usage in __repr__ should NOT emit a warning."""
450
+ ms = mdf.MicroSeries([1, 2, 3], weights=[4, 5, 6])
451
+ with warnings.catch_warnings(record=True) as w:
452
+ warnings.simplefilter("always")
453
+ _ = repr(ms)
454
+ user_warnings = [x for x in w if issubclass(x.category, UserWarning)]
455
+ assert len(user_warnings) == 0
@@ -38,23 +38,23 @@ class TestMicroSeriesSubclassPreservation:
38
38
 
39
39
  # Addition
40
40
  result = ms + 1
41
- assert isinstance(
42
- result, MicroSeries
43
- ), f"Got {type(result)} instead of MicroSeries"
41
+ assert isinstance(result, MicroSeries), (
42
+ f"Got {type(result)} instead of MicroSeries"
43
+ )
44
44
  assert hasattr(result, "weights")
45
45
  assert hasattr(result, "set_weights")
46
46
 
47
47
  # Multiplication
48
48
  result = ms * 2
49
- assert isinstance(
50
- result, MicroSeries
51
- ), f"Got {type(result)} instead of MicroSeries"
49
+ assert isinstance(result, MicroSeries), (
50
+ f"Got {type(result)} instead of MicroSeries"
51
+ )
52
52
 
53
53
  # Division
54
54
  result = ms / 2
55
- assert isinstance(
56
- result, MicroSeries
57
- ), f"Got {type(result)} instead of MicroSeries"
55
+ assert isinstance(result, MicroSeries), (
56
+ f"Got {type(result)} instead of MicroSeries"
57
+ )
58
58
 
59
59
  def test_microseries_preserved_after_comparison(self):
60
60
  """Comparison operations should return MicroSeries, not plain
@@ -63,35 +63,33 @@ class TestMicroSeriesSubclassPreservation:
63
63
 
64
64
  # Greater than
65
65
  result = ms > 1
66
- assert isinstance(
67
- result, MicroSeries
68
- ), f"Got {type(result)} instead of MicroSeries"
66
+ assert isinstance(result, MicroSeries), (
67
+ f"Got {type(result)} instead of MicroSeries"
68
+ )
69
69
  assert hasattr(result, "weights")
70
70
 
71
71
  # Less than
72
72
  result = ms < 3
73
- assert isinstance(
74
- result, MicroSeries
75
- ), f"Got {type(result)} instead of MicroSeries"
73
+ assert isinstance(result, MicroSeries), (
74
+ f"Got {type(result)} instead of MicroSeries"
75
+ )
76
76
 
77
77
  def test_microseries_preserved_after_indexing(self):
78
78
  """Indexing operations should return MicroSeries, not plain Series."""
79
- ms = MicroSeries(
80
- [1, 2, 3, 4, 5], weights=np.array([1.0, 2.0, 3.0, 4.0, 5.0])
81
- )
79
+ ms = MicroSeries([1, 2, 3, 4, 5], weights=np.array([1.0, 2.0, 3.0, 4.0, 5.0]))
82
80
 
83
81
  # Boolean indexing
84
82
  result = ms[ms > 2]
85
- assert isinstance(
86
- result, MicroSeries
87
- ), f"Got {type(result)} instead of MicroSeries"
83
+ assert isinstance(result, MicroSeries), (
84
+ f"Got {type(result)} instead of MicroSeries"
85
+ )
88
86
  assert hasattr(result, "weights")
89
87
 
90
88
  # Slice indexing
91
89
  result = ms[1:3]
92
- assert isinstance(
93
- result, MicroSeries
94
- ), f"Got {type(result)} instead of MicroSeries"
90
+ assert isinstance(result, MicroSeries), (
91
+ f"Got {type(result)} instead of MicroSeries"
92
+ )
95
93
 
96
94
 
97
95
  class TestMicroDataFrameSubclassPreservation:
@@ -105,9 +103,7 @@ class TestMicroDataFrameSubclassPreservation:
105
103
 
106
104
  # Column access
107
105
  col = mdf["a"]
108
- assert isinstance(
109
- col, MicroSeries
110
- ), f"Got {type(col)} instead of MicroSeries"
106
+ assert isinstance(col, MicroSeries), f"Got {type(col)} instead of MicroSeries"
111
107
  assert hasattr(col, "weights")
112
108
  assert hasattr(col, "set_weights")
113
109
 
@@ -120,9 +116,9 @@ class TestMicroDataFrameSubclassPreservation:
120
116
 
121
117
  # Column operations
122
118
  result = mdf["a"] + mdf["b"]
123
- assert isinstance(
124
- result, MicroSeries
125
- ), f"Got {type(result)} instead of MicroSeries"
119
+ assert isinstance(result, MicroSeries), (
120
+ f"Got {type(result)} instead of MicroSeries"
121
+ )
126
122
  assert hasattr(result, "weights")
127
123
 
128
124
 
@@ -186,9 +182,7 @@ class TestCopyOnWriteCompatibility:
186
182
 
187
183
  def test_microdataframe_copy_independent(self):
188
184
  """Copying a MicroDataFrame should create an independent copy."""
189
- mdf = MicroDataFrame(
190
- {"a": [1, 2, 3]}, weights=np.array([1.0, 2.0, 3.0])
191
- )
185
+ mdf = MicroDataFrame({"a": [1, 2, 3]}, weights=np.array([1.0, 2.0, 3.0]))
192
186
  mdf_copy = mdf.copy()
193
187
 
194
188
  # Modify original
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.2.1
3
+ Version: 1.2.3
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -11,15 +11,12 @@ Requires-Dist: numpy
11
11
  Requires-Dist: pandas
12
12
  Provides-Extra: dev
13
13
  Requires-Dist: codecov; extra == "dev"
14
- Requires-Dist: flake8; extra == "dev"
15
- Requires-Dist: flake8-pyproject; extra == "dev"
16
- Requires-Dist: black; extra == "dev"
14
+ Requires-Dist: ruff>=0.9.0; extra == "dev"
17
15
  Requires-Dist: docformatter; extra == "dev"
18
- Requires-Dist: isort; extra == "dev"
19
- Requires-Dist: linecheck; extra == "dev"
20
16
  Requires-Dist: pytest; extra == "dev"
21
17
  Requires-Dist: pytest-cov; extra == "dev"
22
18
  Requires-Dist: setuptools; extra == "dev"
19
+ Requires-Dist: towncrier>=24.8.0; extra == "dev"
23
20
  Dynamic: license-file
24
21
 
25
22
  [![Build](https://github.com/PolicyEngine/microdf/workflows/Pull%20request/badge.svg)](https://github.com/PolicyEngine/microdf/actions)
@@ -3,12 +3,9 @@ pandas
3
3
 
4
4
  [dev]
5
5
  codecov
6
- flake8
7
- flake8-pyproject
8
- black
6
+ ruff>=0.9.0
9
7
  docformatter
10
- isort
11
- linecheck
12
8
  pytest
13
9
  pytest-cov
14
10
  setuptools
11
+ towncrier>=24.8.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "microdf-python"
7
- version = "1.2.1"
7
+ version = "1.2.3"
8
8
  description = "Weighted pandas DataFrames and Series for survey microdata"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -20,15 +20,12 @@ dependencies = [
20
20
  [project.optional-dependencies]
21
21
  dev = [
22
22
  "codecov",
23
- "flake8",
24
- "flake8-pyproject",
25
- "black",
23
+ "ruff>=0.9.0",
26
24
  "docformatter",
27
- "isort",
28
- "linecheck",
29
25
  "pytest",
30
26
  "pytest-cov",
31
27
  "setuptools",
28
+ "towncrier>=24.8.0",
32
29
  ]
33
30
  # Note: Documentation uses MyST (Jupyter Book 2.0) which is installed via npm
34
31
  # Run: cd docs && myst build --html
@@ -37,14 +34,35 @@ dev = [
37
34
  where = ["."]
38
35
  include = ["microdf*"]
39
36
 
40
- [tool.isort]
41
- profile = "black"
42
- line_length = 79
37
+ [tool.towncrier]
38
+ package = "microdf_python"
39
+ directory = "changelog.d"
40
+ filename = "CHANGELOG.md"
41
+ title_format = "## [{version}] - {project_date}"
42
+ issue_format = ""
43
+ underlines = ["", "", ""]
43
44
 
44
- [tool.black]
45
- line-length = 79
46
- target-version = ["py313"]
45
+ [[tool.towncrier.type]]
46
+ directory = "breaking"
47
+ name = "Breaking changes"
48
+ showcontent = true
47
49
 
48
- [tool.flake8]
49
- max-line-length = 79
50
- extend-ignore = ["E203", "W503"]
50
+ [[tool.towncrier.type]]
51
+ directory = "added"
52
+ name = "Added"
53
+ showcontent = true
54
+
55
+ [[tool.towncrier.type]]
56
+ directory = "changed"
57
+ name = "Changed"
58
+ showcontent = true
59
+
60
+ [[tool.towncrier.type]]
61
+ directory = "fixed"
62
+ name = "Fixed"
63
+ showcontent = true
64
+
65
+ [[tool.towncrier.type]]
66
+ directory = "removed"
67
+ name = "Removed"
68
+ showcontent = true
File without changes
File without changes
File without changes