microdf-python 1.3.4__tar.gz → 1.3.6__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.
Files changed (20) hide show
  1. {microdf_python-1.3.4 → microdf_python-1.3.6}/PKG-INFO +1 -1
  2. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/microdataframe.py +60 -22
  3. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/microseries.py +55 -6
  4. microdf_python-1.3.6/microdf/tests/test_aggregation_errors.py +57 -0
  5. microdf_python-1.3.6/microdf/tests/test_quantile_missing_values.py +134 -0
  6. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf_python.egg-info/PKG-INFO +1 -1
  7. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf_python.egg-info/SOURCES.txt +2 -0
  8. {microdf_python-1.3.4 → microdf_python-1.3.6}/pyproject.toml +1 -1
  9. {microdf_python-1.3.4 → microdf_python-1.3.6}/LICENSE +0 -0
  10. {microdf_python-1.3.4 → microdf_python-1.3.6}/README.md +0 -0
  11. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/__init__.py +0 -0
  12. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/tests/conftest.py +0 -0
  13. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/tests/test_dataframe_weight_storage.py +0 -0
  14. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/tests/test_microseries_dataframe.py +0 -0
  15. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/tests/test_nullify_weights_index.py +0 -0
  16. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf/tests/test_pandas3_compatibility.py +0 -0
  17. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf_python.egg-info/dependency_links.txt +0 -0
  18. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf_python.egg-info/requires.txt +0 -0
  19. {microdf_python-1.3.4 → microdf_python-1.3.6}/microdf_python.egg-info/top_level.txt +0 -0
  20. {microdf_python-1.3.4 → microdf_python-1.3.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -150,9 +150,13 @@ class MicroDataFrame(pd.DataFrame):
150
150
  if pd.api.types.is_numeric_dtype(self[col]):
151
151
  try:
152
152
  results[col] = getattr(self[col], name)(*args, **kwargs)
153
- except Exception:
154
- # Skip columns that can't be aggregated
155
- pass
153
+ except TypeError as exc:
154
+ # Skip columns whose dtype can't take this aggregation.
155
+ # Deliberately narrow: catching every Exception here also
156
+ # swallowed real errors (e.g. the ValueError from
157
+ # gini(negatives=...)) and returned a silently truncated
158
+ # result instead of raising.
159
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
156
160
  return pd.Series(results)
157
161
 
158
162
  return fn
@@ -173,9 +177,13 @@ class MicroDataFrame(pd.DataFrame):
173
177
  result = getattr(self[col], name)(*args, **kwargs)
174
178
  results.append(result)
175
179
  columns.append(col)
176
- except Exception:
177
- # Skip columns that can't be aggregated
178
- pass
180
+ except TypeError as exc:
181
+ # Skip columns whose dtype can't take this aggregation.
182
+ # Deliberately narrow: catching every Exception here also
183
+ # swallowed real errors (e.g. the ValueError from
184
+ # gini(negatives=...)) and returned a silently truncated
185
+ # result instead of raising.
186
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
179
187
 
180
188
  if results:
181
189
  df = pd.DataFrame(results)
@@ -208,9 +216,13 @@ class MicroDataFrame(pd.DataFrame):
208
216
  result = getattr(self[col], name)(*args, **kwargs)
209
217
  results.append(result)
210
218
  columns.append(col)
211
- except Exception:
212
- # Skip columns that can't be aggregated
213
- pass
219
+ except TypeError as exc:
220
+ # Skip columns whose dtype can't take this aggregation.
221
+ # Deliberately narrow: catching every Exception here also
222
+ # swallowed real errors (e.g. the ValueError from
223
+ # gini(negatives=...)) and returned a silently truncated
224
+ # result instead of raising.
225
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
214
226
 
215
227
  if results:
216
228
  df = pd.DataFrame(results)
@@ -225,9 +237,13 @@ class MicroDataFrame(pd.DataFrame):
225
237
  if pd.api.types.is_numeric_dtype(self[col]):
226
238
  try:
227
239
  results[col] = getattr(self[col], name)(*args, **kwargs)
228
- except Exception:
229
- # Skip columns that can't be aggregated
230
- pass
240
+ except TypeError as exc:
241
+ # Skip columns whose dtype can't take this aggregation.
242
+ # Deliberately narrow: catching every Exception here also
243
+ # swallowed real errors (e.g. the ValueError from
244
+ # gini(negatives=...)) and returned a silently truncated
245
+ # result instead of raising.
246
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
231
247
  return pd.Series(results)
232
248
 
233
249
  return fn
@@ -839,9 +855,13 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
839
855
  results[col] = getattr(getattr(self, col), name)(
840
856
  *args, **kwargs
841
857
  )
842
- except Exception:
843
- # Skip columns that can't be aggregated
844
- pass
858
+ except TypeError as exc:
859
+ # Skip columns whose dtype can't take this aggregation.
860
+ # Deliberately narrow: catching every Exception here also
861
+ # swallowed real errors (e.g. the ValueError from
862
+ # gini(negatives=...)) and returned a silently truncated
863
+ # result instead of raising.
864
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
845
865
  # Return plain DataFrame - aggregated results don't have
846
866
  # per-row weights (weights were already applied)
847
867
  return pd.DataFrame(results) if results else pd.DataFrame()
@@ -859,9 +879,13 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
859
879
  results[col] = getattr(getattr(self, col), name)(
860
880
  *args, **kwargs
861
881
  )
862
- except Exception:
863
- # Skip columns that can't be aggregated
864
- pass
882
+ except TypeError as exc:
883
+ # Skip columns whose dtype can't take this aggregation.
884
+ # Deliberately narrow: catching every Exception here also
885
+ # swallowed real errors (e.g. the ValueError from
886
+ # gini(negatives=...)) and returned a silently truncated
887
+ # result instead of raising.
888
+ logger.debug("skipping column %s in %s: %s", col, name, exc)
865
889
  # Return plain DataFrame - aggregated results don't have
866
890
  # per-row weights (weights were already applied)
867
891
  return pd.DataFrame(results) if results else pd.DataFrame()
@@ -921,8 +945,15 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
921
945
  results[col] = getattr(getattr(res, col), name)(
922
946
  *args, **kwargs
923
947
  )
924
- except Exception:
925
- pass
948
+ except TypeError as exc:
949
+ # Skip columns whose dtype can't take this aggregation.
950
+ # Deliberately narrow: catching every Exception here also
951
+ # swallowed real errors (e.g. the ValueError from
952
+ # gini(negatives=...)) and returned a silently truncated
953
+ # result instead of raising.
954
+ logger.debug(
955
+ "skipping column %s in %s: %s", col, name, exc
956
+ )
926
957
  # Return plain DataFrame - aggregated results don't
927
958
  # have per-row weights (weights were already applied)
928
959
  return pd.DataFrame(results) if results else pd.DataFrame()
@@ -940,8 +971,15 @@ class MicroDataFrameGroupBy(pd.core.groupby.generic.DataFrameGroupBy):
940
971
  results[col] = getattr(getattr(res, col), name)(
941
972
  *args, **kwargs
942
973
  )
943
- except Exception:
944
- pass
974
+ except TypeError as exc:
975
+ # Skip columns whose dtype can't take this aggregation.
976
+ # Deliberately narrow: catching every Exception here also
977
+ # swallowed real errors (e.g. the ValueError from
978
+ # gini(negatives=...)) and returned a silently truncated
979
+ # result instead of raising.
980
+ logger.debug(
981
+ "skipping column %s in %s: %s", col, name, exc
982
+ )
945
983
  # Return plain DataFrame - aggregated results don't
946
984
  # have per-row weights (weights were already applied)
947
985
  return pd.DataFrame(results) if results else pd.DataFrame()
@@ -310,7 +310,7 @@ class MicroSeries(pd.Series):
310
310
  )
311
311
  return super().corr(other, *args, **kwargs)
312
312
 
313
- def quantile(self, q: np.array) -> pd.Series:
313
+ def quantile(self, q: np.array, skipna: bool = True) -> pd.Series:
314
314
  """Calculates weighted quantiles of the MicroSeries.
315
315
 
316
316
  Uses the inverse CDF method: the q-th quantile is the smallest
@@ -319,6 +319,11 @@ class MicroSeries(pd.Series):
319
319
 
320
320
  :param q: Quantile(s) to calculate, must be in [0, 1].
321
321
  :type q: float or np.array
322
+ :param skipna: Exclude NaN values (default True). NaN sorts to the
323
+ end of the array, so leaving NaN rows in would let their weight
324
+ inflate the cumulative distribution and push the cutoff upward.
325
+ If False, NaN is returned whenever any value is NaN.
326
+ :type skipna: bool
322
327
 
323
328
  :return: Weighted quantile value(s).
324
329
  :rtype: float or pd.Series
@@ -329,12 +334,22 @@ class MicroSeries(pd.Series):
329
334
  assert np.all(quantiles >= 0) and np.all(quantiles <= 1), (
330
335
  "quantiles should be in [0, 1]"
331
336
  )
337
+ na_mask = pd.isna(values)
338
+ if not skipna and na_mask.any():
339
+ return (
340
+ np.nan
341
+ if np.array(q).shape == ()
342
+ else pd.Series(np.full(len(quantiles), np.nan), index=quantiles)
343
+ )
332
344
  # Drop zero-weight rows before sorting. Without this, q=0 (and
333
345
  # internal plateaus of zero weight) picked a value with 0 weight
334
346
  # that should have been skipped by the inverse CDF. E.g.
335
347
  # MicroSeries([10, 20, 30], weights=[0, 1, 1]).quantile(0)
336
348
  # returned 10 instead of 20.
337
- nonzero = sample_weight > 0
349
+ # Drop NaN rows for the same reason: NaN sorts last, so its weight
350
+ # would inflate the cumulative distribution and push the cutoff up
351
+ # (median of [1, nan, 3] returned 3.0 instead of 1.0).
352
+ nonzero = (sample_weight > 0) & ~na_mask
338
353
  if not nonzero.any():
339
354
  return (
340
355
  np.nan
@@ -359,13 +374,15 @@ class MicroSeries(pd.Series):
359
374
  return pd.Series(result, index=quantiles)
360
375
 
361
376
  @scalar_function
362
- def median(self) -> float:
377
+ def median(self, skipna: bool = True) -> float:
363
378
  """Calculates the weighted median of the MicroSeries.
364
379
 
380
+ :param skipna: Exclude NaN values (default True).
381
+ :type skipna: bool
365
382
  :returns: The weighted median of a DataFrame's column.
366
383
  :rtype: float
367
384
  """
368
- return self.quantile(0.5)
385
+ return self.quantile(0.5, skipna=skipna)
369
386
 
370
387
  @scalar_function
371
388
  def gini(self, negatives: Optional[str] = None) -> float:
@@ -386,8 +403,9 @@ class MicroSeries(pd.Series):
386
403
  w = np.asarray(self.weights.values, dtype=float)
387
404
  if negatives == "zero":
388
405
  x = np.where(x < 0, 0.0, x)
389
- elif negatives == "shift" and len(x) > 0 and np.amin(x) < 0:
390
- x = x - np.amin(x)
406
+ elif negatives == "shift":
407
+ if len(x) > 0 and np.amin(x) < 0:
408
+ x = x - np.amin(x)
391
409
  elif negatives is not None:
392
410
  raise ValueError(
393
411
  f"Unknown negatives option {negatives!r}; expected "
@@ -873,6 +891,37 @@ class MicroSeriesGroupBy(pd.core.groupby.generic.SeriesGroupBy):
873
891
  or name in MicroSeries.AGNOSTIC_FUNCTIONS
874
892
  and is_array
875
893
  ):
894
+ if name in MicroSeries.AGNOSTIC_FUNCTIONS and not df.empty:
895
+ # Concatenate values without keys: concat rejects missing
896
+ # MultiIndex keys even when groupby(dropna=False) retains
897
+ # them. Reuse the grouping levels and codes so missing
898
+ # labels keep the same representation as scalar results.
899
+ results = [
900
+ via_micro_series(row, *args, **kwargs)
901
+ for _, row in df.iterrows()
902
+ ]
903
+ result = pd.concat(results)
904
+ group_index = (
905
+ df.index
906
+ if isinstance(df.index, pd.MultiIndex)
907
+ else pd.MultiIndex.from_arrays([df.index])
908
+ )
909
+ quantile_codes, quantile_levels = result.index.factorize(
910
+ sort=False
911
+ )
912
+ result.index = pd.MultiIndex(
913
+ levels=[*group_index.levels, quantile_levels],
914
+ codes=[
915
+ codes.repeat(len(results[0]))
916
+ for codes in group_index.codes
917
+ ]
918
+ + [quantile_codes],
919
+ names=[*df.index.names, result.index.name],
920
+ # Existing group codes are valid; checking would
921
+ # rewrite their retained missing labels to -1.
922
+ verify_integrity=False,
923
+ )
924
+ return result
876
925
  result = df.apply(
877
926
  lambda row: via_micro_series(row, *args, **kwargs),
878
927
  axis=1,
@@ -0,0 +1,57 @@
1
+ import microdf as mdf
2
+ import numpy as np
3
+ import pandas as pd
4
+ import pytest
5
+
6
+
7
+ def test_aggregation_surfaces_real_errors():
8
+ """A genuine argument error must raise, not be swallowed per column."""
9
+ df = mdf.MicroDataFrame(pd.DataFrame({"x": [1, 2, 3]}), weights=[1, 1, 1])
10
+ with pytest.raises(ValueError, match="Unknown negatives option"):
11
+ df.gini(negatives="bogus")
12
+
13
+
14
+ def test_aggregation_still_skips_non_numeric_columns():
15
+ """Narrowing the guard must not change which columns aggregate."""
16
+ df = mdf.MicroDataFrame(
17
+ pd.DataFrame(
18
+ {
19
+ "x": [1, 2, 3],
20
+ "s": ["a", "b", "c"],
21
+ "dt": pd.to_datetime(["2020-01-01"] * 3),
22
+ }
23
+ ),
24
+ weights=[1, 2, 3],
25
+ )
26
+ assert list(df.sum().index) == ["x"]
27
+ assert df.sum()["x"] == 14
28
+ assert list(df.mean().index) == ["x"]
29
+
30
+
31
+ def test_gini_shift_accepts_nonnegative_and_negative_columns():
32
+ frame = mdf.MicroDataFrame(
33
+ {"positive": [1, 2, 3], "negative": [-1, 1, 3]}, weights=[1, 1, 1]
34
+ )
35
+ # Shift leaves [1, 2, 3] unchanged and changes [-1, 1, 3] to [0, 2, 4].
36
+ # The pairwise-difference Ginis are 2/9 and 4/9, respectively.
37
+ expected = pd.Series({"positive": 2 / 9, "negative": 4 / 9})
38
+ pd.testing.assert_series_equal(frame.gini(negatives="shift"), expected)
39
+
40
+
41
+ @pytest.mark.parametrize("selected", [False, True])
42
+ def test_grouped_gini_shift_accepts_positive_groups(selected):
43
+ frame = mdf.MicroDataFrame(
44
+ {"group": ["a", "a", "a", "b", "b", "b"], "value": [1, 2, 3, -1, 1, 3]},
45
+ weights=[1, 1, 1, 1, 1, 1],
46
+ )
47
+ grouped = frame.groupby("group")
48
+ if selected:
49
+ grouped = grouped[["value"]]
50
+ expected = pd.DataFrame(
51
+ {"value": [2 / 9, 4 / 9]}, index=pd.Index(["a", "b"], name="group")
52
+ )
53
+ pd.testing.assert_frame_equal(grouped.gini(negatives="shift"), expected)
54
+
55
+
56
+ def test_gini_shift_accepts_empty_series():
57
+ assert np.isnan(mdf.MicroSeries([], weights=[]).gini(negatives="shift"))
@@ -0,0 +1,134 @@
1
+ import microdf as mdf
2
+ import numpy as np
3
+ import pandas as pd
4
+ import pytest
5
+
6
+
7
+ def test_quantile_skips_nan():
8
+ """NaN weight must not inflate the cumulative distribution.
9
+
10
+ Dropping a NaN row should give the same answer as never having had
11
+ it: the inverse-CDF quantile of [1, nan, 3] equals that of [1, 3].
12
+ """
13
+ with_nan = mdf.MicroSeries([1.0, np.nan, 3.0], weights=[1, 1, 1])
14
+ without_nan = mdf.MicroSeries([1.0, 3.0], weights=[1, 1])
15
+ assert with_nan.median() == without_nan.median()
16
+ assert with_nan.quantile(0.5) == without_nan.quantile(0.5)
17
+
18
+ q = [0.25, 0.5, 0.75]
19
+ np.testing.assert_array_equal(
20
+ mdf.MicroSeries([1.0, np.nan, 3.0, 5.0], weights=[1, 1, 1, 1]).quantile(q),
21
+ mdf.MicroSeries([1.0, 3.0, 5.0], weights=[1, 1, 1]).quantile(q),
22
+ )
23
+
24
+
25
+ def test_quantile_skipna_false_propagates_nan():
26
+ """Skipna=False returns NaN when any value is NaN, like mean/var."""
27
+ s = mdf.MicroSeries([1.0, np.nan, 3.0], weights=[1, 1, 1])
28
+ assert np.isnan(s.quantile(0.5, skipna=False))
29
+ assert np.isnan(s.median(skipna=False))
30
+ assert s.quantile([0.25, 0.75], skipna=False).isna().all()
31
+
32
+
33
+ def test_quantile_all_nan_returns_nan():
34
+ s = mdf.MicroSeries([np.nan, np.nan], weights=[1, 1])
35
+ assert np.isnan(s.median())
36
+
37
+
38
+ @pytest.mark.parametrize("skipna", [True, False])
39
+ @pytest.mark.parametrize("q", [-0.1, 1.1, [0.5, 1.1]])
40
+ def test_quantile_validates_bounds_with_missing_values(q, skipna):
41
+ series = mdf.MicroSeries([1.0, np.nan, 3.0], weights=[1, 7, 1])
42
+ with pytest.raises(AssertionError, match="quantiles should be in"):
43
+ series.quantile(q, skipna=skipna)
44
+
45
+
46
+ @pytest.mark.parametrize("skipna", [True, False])
47
+ @pytest.mark.parametrize("multiple_keys", [False, True])
48
+ def test_grouped_quantiles_preserve_missing_groups(skipna, multiple_keys):
49
+ series = mdf.MicroSeries(
50
+ [1.0, np.nan, 3.0, 5.0, np.nan, np.nan], weights=[1, 1, 1, 1, 1, 1]
51
+ )
52
+ groups = ["a", "a", "b", "b", "c", "c"]
53
+ keys = [groups, [1, 1, 2, 2, 3, 3]] if multiple_keys else groups
54
+ grouped = series.groupby(keys)
55
+ quantiles = [0.25, 0.75]
56
+ result = grouped.quantile(quantiles, skipna=skipna)
57
+ # Scalar calls retain every group. Vector calls must retain the same
58
+ # groups, including the partial-NaN and all-NaN groups.
59
+ for quantile in quantiles:
60
+ pd.testing.assert_series_equal(
61
+ result.xs(quantile, level=-1),
62
+ grouped.quantile(quantile, skipna=skipna),
63
+ )
64
+ first_group = 1.0 if skipna else np.nan
65
+ np.testing.assert_allclose(
66
+ result.to_numpy(),
67
+ [first_group, first_group, 3.0, 5.0, np.nan, np.nan],
68
+ equal_nan=True,
69
+ )
70
+
71
+
72
+ def test_grouped_quantiles_preserve_repeated_requests():
73
+ series = mdf.MicroSeries([1.0, np.nan, 3.0, 5.0], weights=[1, 1, 1, 1])
74
+ result = series.groupby(["a", "a", "b", "b"]).quantile([0.5, 0.5], skipna=False)
75
+ assert result.index.tolist() == [("a", 0.5), ("a", 0.5), ("b", 0.5), ("b", 0.5)]
76
+ np.testing.assert_allclose(
77
+ result.to_numpy(), [np.nan, np.nan, 3.0, 3.0], equal_nan=True
78
+ )
79
+
80
+
81
+ @pytest.mark.parametrize("quantiles", [[0.75, 0.25], [0.5, 0.5], []])
82
+ @pytest.mark.parametrize("skipna", [True, False])
83
+ @pytest.mark.parametrize("sort", [True, False])
84
+ def test_grouped_quantiles_preserve_missing_multiple_keys(quantiles, skipna, sort):
85
+ """Missing group keys survive alongside missing values and repeated q."""
86
+ frame = mdf.MicroDataFrame(
87
+ {
88
+ "region": ["north", "north", None, "south", "south"],
89
+ "year": [2024, 2024, 2024, np.nan, 2025],
90
+ "income": [10.0, np.nan, 20.0, 30.0, 40.0],
91
+ },
92
+ weights=[1, 4, 2, 3, 1],
93
+ )
94
+ grouped = frame.groupby(["region", "year"], dropna=False, sort=sort)["income"]
95
+ result = grouped.quantile(quantiles, skipna=skipna)
96
+
97
+ # Each retained group has one nonmissing value. With skipna=False,
98
+ # the north group is NaN because it also contains a missing value.
99
+ north = 10.0 if skipna else np.nan
100
+ groups = [("north", 2024.0, north)]
101
+ if sort:
102
+ groups += [
103
+ ("south", 2025.0, 40.0),
104
+ ("south", np.nan, 30.0),
105
+ (np.nan, 2024.0, 20.0),
106
+ ]
107
+ else:
108
+ groups += [
109
+ (np.nan, 2024.0, 20.0),
110
+ ("south", np.nan, 30.0),
111
+ ("south", 2025.0, 40.0),
112
+ ]
113
+ expected_index = pd.MultiIndex.from_tuples(
114
+ [(region, year, q) for region, year, _ in groups for q in quantiles],
115
+ names=["region", "year", None],
116
+ )
117
+ expected_values = [value for _, _, value in groups for _ in quantiles]
118
+ assert result.index.names == expected_index.names
119
+ if quantiles:
120
+ for level in range(3):
121
+ pd.testing.assert_index_equal(
122
+ result.index.get_level_values(level),
123
+ expected_index.get_level_values(level),
124
+ )
125
+ assert result.index.nlevels == 3
126
+ np.testing.assert_allclose(result.to_numpy(), expected_values, equal_nan=True)
127
+ for q in set(quantiles):
128
+ if quantiles.count(q) == 1:
129
+ selected = result.xs(q, level=-1)
130
+ scalar = grouped.quantile(q, skipna=skipna)
131
+ assert selected.index.equals(scalar.index)
132
+ np.testing.assert_allclose(
133
+ selected.to_numpy(), scalar.to_numpy(), equal_nan=True
134
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.3.4
3
+ Version: 1.3.6
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -5,10 +5,12 @@ microdf/__init__.py
5
5
  microdf/microdataframe.py
6
6
  microdf/microseries.py
7
7
  microdf/tests/conftest.py
8
+ microdf/tests/test_aggregation_errors.py
8
9
  microdf/tests/test_dataframe_weight_storage.py
9
10
  microdf/tests/test_microseries_dataframe.py
10
11
  microdf/tests/test_nullify_weights_index.py
11
12
  microdf/tests/test_pandas3_compatibility.py
13
+ microdf/tests/test_quantile_missing_values.py
12
14
  microdf_python.egg-info/PKG-INFO
13
15
  microdf_python.egg-info/SOURCES.txt
14
16
  microdf_python.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "microdf-python"
7
- version = "1.3.4"
7
+ version = "1.3.6"
8
8
  description = "Weighted pandas DataFrames and Series for survey microdata"
9
9
  readme = "README.md"
10
10
  authors = [
File without changes
File without changes
File without changes