microdf-python 1.1.0__tar.gz → 1.1.1__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.1.0
3
+ Version: 1.1.1
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -112,17 +112,18 @@ class MicroSeries(pd.Series):
112
112
  def quantile(self, q: np.array) -> pd.Series:
113
113
  """Calculates weighted quantiles of the MicroSeries.
114
114
 
115
- Doesn't exactly match unweighted quantiles of stacked values.
116
- See stackoverflow.com/q/21844024#comment102342137_29677616.
115
+ Uses the inverse CDF method: the q-th quantile is the smallest
116
+ value where the cumulative weight proportion >= q. This matches
117
+ the default behavior of R's survey::svyquantile.
117
118
 
118
- :param q: Array of quantiles to calculate.
119
- :type q: np.array
119
+ :param q: Quantile(s) to calculate, must be in [0, 1].
120
+ :type q: float or np.array
120
121
 
121
- :return: Array of weighted quantiles.
122
- :rtype: pd.Series
122
+ :return: Weighted quantile value(s).
123
+ :rtype: float or pd.Series
123
124
  """
124
125
  values = np.array(self.values)
125
- quantiles = np.array(q)
126
+ quantiles = np.atleast_1d(q)
126
127
  sample_weight = np.array(self.weights)
127
128
  assert np.all(quantiles >= 0) and np.all(
128
129
  quantiles <= 1
@@ -130,11 +131,20 @@ class MicroSeries(pd.Series):
130
131
  sorter = np.argsort(values)
131
132
  values = values[sorter]
132
133
  sample_weight = sample_weight[sorter]
133
- weighted_quantiles = np.cumsum(sample_weight) - 0.5 * sample_weight
134
- weighted_quantiles /= np.sum(sample_weight)
135
- result = np.interp(quantiles, weighted_quantiles, values)
136
- if quantiles.shape == ():
137
- return result
134
+ cumsum = np.cumsum(sample_weight)
135
+ cumsum_normalized = cumsum / cumsum[-1]
136
+ result = np.array(
137
+ [
138
+ values[
139
+ min(
140
+ np.searchsorted(cumsum_normalized, qi), len(values) - 1
141
+ )
142
+ ]
143
+ for qi in quantiles
144
+ ]
145
+ )
146
+ if np.array(q).shape == ():
147
+ return result[0]
138
148
  return pd.Series(result, index=quantiles)
139
149
 
140
150
  @scalar_function
@@ -112,6 +112,55 @@ def test_median() -> None:
112
112
  assert series.median() == 4
113
113
 
114
114
 
115
+ def test_weighted_quantile_skewed() -> None:
116
+ # 99% of the population has 0 income, 1% has 1M
117
+ # The median should be 0, not an interpolated value
118
+ series = mdf.MicroSeries([0, 1_000_000], weights=[99, 1])
119
+ assert series.median() == 0
120
+ assert series.quantile(0.5) == 0
121
+ # 99th percentile is still 0 since exactly 99% have 0
122
+ assert series.quantile(0.99) == 0
123
+ # Only quantile > 0.99 gives 1M
124
+ assert series.quantile(1.0) == 1_000_000
125
+ # Test multiple quantiles
126
+ result = series.quantile([0.1, 0.5, 0.99, 1.0])
127
+ assert result[0.1] == 0
128
+ assert result[0.5] == 0
129
+ assert result[0.99] == 0
130
+ assert result[1.0] == 1_000_000
131
+
132
+
133
+ def test_weighted_quantile_boundaries() -> None:
134
+ # Test q=0 returns minimum, q=1 returns maximum
135
+ series = mdf.MicroSeries([10, 20, 30], weights=[1, 1, 1])
136
+ assert series.quantile(0.0) == 10
137
+ assert series.quantile(1.0) == 30
138
+
139
+
140
+ def test_weighted_quantile_equal_weights() -> None:
141
+ # With equal weights, should match "replicated" interpretation
142
+ # Values: 1, 2, 3 each with weight 2 -> like [1,1,2,2,3,3]
143
+ series = mdf.MicroSeries([1, 2, 3], weights=[2, 2, 2])
144
+ # cumsum_normalized = [2/6, 4/6, 6/6] = [0.333, 0.667, 1.0]
145
+ # median (0.5): smallest where cumsum >= 0.5 -> index 1 -> value 2
146
+ assert series.median() == 2
147
+ # 0.25 quantile: smallest where cumsum >= 0.25 -> index 0 -> value 1
148
+ assert series.quantile(0.25) == 1
149
+ # 0.75 quantile: smallest where cumsum >= 0.75 -> index 2 -> value 3
150
+ assert series.quantile(0.75) == 3
151
+
152
+
153
+ def test_weighted_quantile_unsorted_input() -> None:
154
+ # Ensure sorting works correctly
155
+ series = mdf.MicroSeries([30, 10, 20], weights=[1, 2, 1])
156
+ # Sorted: values [10, 20, 30], weights [2, 1, 1]
157
+ # cumsum_normalized = [0.5, 0.75, 1.0]
158
+ assert series.quantile(0.0) == 10
159
+ assert series.quantile(0.5) == 10 # cumsum[0]=0.5 >= 0.5
160
+ assert series.quantile(0.6) == 20 # cumsum[1]=0.75 >= 0.6
161
+ assert series.quantile(1.0) == 30
162
+
163
+
115
164
  def test_unweighted_groupby() -> None:
116
165
  df = mdf.MicroDataFrame({"x": [1, 2], "y": [3, 4], "z": [5, 6]})
117
166
  assert (df.groupby("x").z.sum().values == np.array([5.0, 6.0])).all()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microdf-python
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: Weighted pandas DataFrames and Series for survey microdata
5
5
  Author-email: Max Ghenis <max@policyengine.org>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "microdf-python"
7
- version = "1.1.0"
7
+ version = "1.1.1"
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