microdf-python 0.4.4__py3-none-any.whl → 0.4.6__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/inequality.py CHANGED
@@ -1,9 +1,18 @@
1
+ from typing import List, Optional, Union
2
+
1
3
  import numpy as np
4
+ import pandas as pd
2
5
 
3
6
  import microdf as mdf
4
7
 
5
8
 
6
- def gini(df, col, w=None, negatives=None, groupby=None):
9
+ def gini(
10
+ df: pd.DataFrame,
11
+ col: str,
12
+ w: Optional[str] = None,
13
+ negatives: Optional[str] = None,
14
+ groupby: Optional[Union[str, List[str]]] = None,
15
+ ) -> float:
7
16
  """Calculates Gini index.
8
17
 
9
18
  :param df: DataFrame.
@@ -18,10 +27,14 @@ def gini(df, col, w=None, negatives=None, groupby=None):
18
27
  Defaults to None, which leaves negative values as they are.
19
28
  :param groupby: Column, or list of columns, to group by.
20
29
  :returns: A float, the Gini index.
21
-
22
30
  """
23
31
 
24
- def _gini(df, col, w=None, negatives=None):
32
+ def _gini(
33
+ df: pd.DataFrame,
34
+ col: str,
35
+ w: Optional[str] = None,
36
+ negatives: Optional[str] = None,
37
+ ) -> float:
25
38
  # Requires float numpy arrays (not pandas Series or lists) to work.
26
39
  x = np.array(df[col]).astype("float")
27
40
  if negatives == "zero":
@@ -50,7 +63,13 @@ def gini(df, col, w=None, negatives=None, groupby=None):
50
63
  return df.groupby(groupby).apply(lambda x: _gini(x, col, w, negatives))
51
64
 
52
65
 
53
- def top_x_pct_share(df, col, top_x_pct, w=None, groupby=None):
66
+ def top_x_pct_share(
67
+ df: pd.DataFrame,
68
+ col: str,
69
+ top_x_pct: float,
70
+ w: Optional[str] = None,
71
+ groupby: Optional[Union[str, List[str]]] = None,
72
+ ) -> float:
54
73
  """Calculates top x% share.
55
74
 
56
75
  :param df: DataFrame.
@@ -59,10 +78,11 @@ def top_x_pct_share(df, col, top_x_pct, w=None, groupby=None):
59
78
  :param w: Column representing weight in df.
60
79
  :param groupby: Column, or list of columns, to group by.
61
80
  :returns: The share of w-weighted val held by the top x%.
62
-
63
81
  """
64
82
 
65
- def _top_x_pct_share(df, col, top_x_pct, w=None):
83
+ def _top_x_pct_share(
84
+ df: pd.DataFrame, col: str, top_x_pct: float, w: Optional[str] = None
85
+ ) -> float:
66
86
  threshold = mdf.weighted_quantile(df, col, w, 1 - top_x_pct)
67
87
  top_x_pct_sum = mdf.weighted_sum(df[df[col] >= threshold], col, w)
68
88
  total_sum = mdf.weighted_sum(df, col, w)
@@ -75,7 +95,13 @@ def top_x_pct_share(df, col, top_x_pct, w=None, groupby=None):
75
95
  )
76
96
 
77
97
 
78
- def bottom_x_pct_share(df, col, bottom_x_pct, w=None, groupby=None):
98
+ def bottom_x_pct_share(
99
+ df: pd.DataFrame,
100
+ col: str,
101
+ bottom_x_pct: float,
102
+ w: Optional[str] = None,
103
+ groupby: Optional[Union[str, List[str]]] = None,
104
+ ) -> float:
79
105
  """Calculates bottom x% share.
80
106
 
81
107
  :param df: DataFrame.
@@ -84,12 +110,16 @@ def bottom_x_pct_share(df, col, bottom_x_pct, w=None, groupby=None):
84
110
  :param w: Column representing weight in df.
85
111
  :param groupby: Column, or list of columns, to group by.
86
112
  :returns: The share of w-weighted val held by the bottom x%.
87
-
88
113
  """
89
114
  return 1 - top_x_pct_share(df, col, 1 - bottom_x_pct, w, groupby)
90
115
 
91
116
 
92
- def bottom_50_pct_share(df, col, w=None, groupby=None):
117
+ def bottom_50_pct_share(
118
+ df: pd.DataFrame,
119
+ col: str,
120
+ w: Optional[str] = None,
121
+ groupby: Optional[Union[str, List[str]]] = None,
122
+ ) -> float:
93
123
  """Calculates bottom 50% share.
94
124
 
95
125
  :param df: DataFrame.
@@ -97,12 +127,16 @@ def bottom_50_pct_share(df, col, w=None, groupby=None):
97
127
  :param w: Column representing weight in df.
98
128
  :param groupby: Column, or list of columns, to group by.
99
129
  :returns: The share of w-weighted val held by the bottom 50%.
100
-
101
130
  """
102
131
  return bottom_x_pct_share(df, col, 0.5, w, groupby)
103
132
 
104
133
 
105
- def top_50_pct_share(df, col, w=None, groupby=None):
134
+ def top_50_pct_share(
135
+ df: pd.DataFrame,
136
+ col: str,
137
+ w: Optional[str] = None,
138
+ groupby: Optional[Union[str, List[str]]] = None,
139
+ ) -> float:
106
140
  """Calculates top 50% share.
107
141
 
108
142
  :param df: DataFrame.
@@ -110,12 +144,16 @@ def top_50_pct_share(df, col, w=None, groupby=None):
110
144
  :param w: Column representing weight in df.
111
145
  :param groupby: Column, or list of columns, to group by.
112
146
  :returns: The share of w-weighted val held by the top 50%.
113
-
114
147
  """
115
148
  return top_x_pct_share(df, col, 0.5, w, groupby)
116
149
 
117
150
 
118
- def top_10_pct_share(df, col, w=None, groupby=None):
151
+ def top_10_pct_share(
152
+ df: pd.DataFrame,
153
+ col: str,
154
+ w: Optional[str] = None,
155
+ groupby: Optional[Union[str, List[str]]] = None,
156
+ ) -> float:
119
157
  """Calculates top 10% share.
120
158
 
121
159
  :param df: DataFrame.
@@ -123,12 +161,16 @@ def top_10_pct_share(df, col, w=None, groupby=None):
123
161
  :param w: Column representing weight in df.
124
162
  :param groupby: Column, or list of columns, to group by.
125
163
  :returns: The share of w-weighted val held by the top 10%.
126
-
127
164
  """
128
165
  return top_x_pct_share(df, col, 0.1, w, groupby)
129
166
 
130
167
 
131
- def top_1_pct_share(df, col, w=None, groupby=None):
168
+ def top_1_pct_share(
169
+ df: pd.DataFrame,
170
+ col: str,
171
+ w: Optional[str] = None,
172
+ groupby: Optional[Union[str, List[str]]] = None,
173
+ ) -> float:
132
174
  """Calculates top 1% share.
133
175
 
134
176
  :param df: DataFrame.
@@ -136,12 +178,16 @@ def top_1_pct_share(df, col, w=None, groupby=None):
136
178
  :param w: Column representing weight in df.
137
179
  :param groupby: Column, or list of columns, to group by.
138
180
  :returns: The share of w-weighted val held by the top 1%.
139
-
140
181
  """
141
182
  return top_x_pct_share(df, col, 0.01, w, groupby)
142
183
 
143
184
 
144
- def top_0_1_pct_share(df, col, w=None, groupby=None):
185
+ def top_0_1_pct_share(
186
+ df: pd.DataFrame,
187
+ col: str,
188
+ w: Optional[str] = None,
189
+ groupby: Optional[Union[str, List[str]]] = None,
190
+ ) -> float:
145
191
  """Calculates top 0.1% share.
146
192
 
147
193
  :param df: DataFrame.
@@ -149,21 +195,24 @@ def top_0_1_pct_share(df, col, w=None, groupby=None):
149
195
  :param w: Column representing weight in df.
150
196
  :param groupby: Column, or list of columns, to group by.
151
197
  :returns: The share of w-weighted val held by the top 0.1%.
152
-
153
198
  """
154
199
  return top_x_pct_share(df, col, 0.001, w, groupby)
155
200
 
156
201
 
157
- def t10_b50(df, col, w=None, groupby=None):
202
+ def t10_b50(
203
+ df: pd.DataFrame,
204
+ col: str,
205
+ w: Optional[str] = None,
206
+ groupby: Optional[Union[str, List[str]]] = None,
207
+ ) -> float:
158
208
  """Calculates ratio between the top 10% and bottom 50% shares.
159
209
 
160
210
  :param df: DataFrame.
161
211
  :param col: Name of column in df representing value.
162
212
  :param w: Column representing weight in df.
163
213
  :param groupby: Column, or list of columns, to group by.
164
- :returns: The share of w-weighted val held by the top 10% divided by
165
- the share of w-weighted val held by the bottom 50%.
166
-
214
+ :returns: The share of w-weighted val held by the top 10% divided by the
215
+ share of w-weighted val held by the bottom 50%.
167
216
  """
168
217
  t10 = top_10_pct_share(df, col, w, groupby)
169
218
  b50 = bottom_50_pct_share(df, col, w, groupby)
microdf/io.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import io
2
2
  import zipfile
3
- import requests
3
+
4
4
  import pandas as pd
5
+ import requests
5
6
 
6
7
  HEADER = {
7
- "User-Agent":
8
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) " +
9
- "AppleWebKit/537.36 (KHTML, like Gecko) " +
10
- "Chrome/50.0.2661.102 Safari/537.36"
11
- }
8
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) "
9
+ + "AppleWebKit/537.36 (KHTML, like Gecko) "
10
+ + "Chrome/50.0.2661.102 Safari/537.36"
11
+ }
12
12
 
13
13
 
14
14
  def read_stata_zip(url: str, **kwargs) -> pd.DataFrame:
@@ -19,11 +19,9 @@ def read_stata_zip(url: str, **kwargs) -> pd.DataFrame:
19
19
  Pending native support in
20
20
  https://github.com/pandas-dev/pandas/issues/26599.
21
21
 
22
- :param url: URL string of .zip file containing a single
23
- .dta file.
24
- :param **kwargs: Arguments passed to pandas.read_stata().
22
+ :param url: URL string of .zip file containing a single .dta file. :param
23
+ **kwargs: Arguments passed to pandas.read_stata().
25
24
  :returns: DataFrame.
26
-
27
25
  """
28
26
  r = requests.get(url, headers=HEADER)
29
27
  data = io.BytesIO(r.content)
microdf/poverty.py CHANGED
@@ -2,15 +2,14 @@ import numpy as np
2
2
  import pandas as pd
3
3
 
4
4
 
5
- def fpl(people: int):
5
+ def fpl(people: int) -> float:
6
6
  """Calculates the federal poverty guideline for a household of a certain
7
- size.
7
+ size.
8
8
 
9
9
  :param XTOT: The number of people in the household.
10
- :param people: returns: The federal poverty guideline for the contiguous
11
- 48 states.
10
+ :param people: returns: The federal poverty guideline for the contiguous 48
11
+ states.
12
12
  :returns: The federal poverty guideline for the contiguous 48 states.
13
-
14
13
  """
15
14
  return 7820 + 4320 * people
16
15
 
@@ -18,8 +17,8 @@ def fpl(people: int):
18
17
  def poverty_rate(
19
18
  df: pd.DataFrame, income: str, threshold: str, w: str = None
20
19
  ) -> float:
21
- """Calculate poverty rate, i.e., the population share with income
22
- below their poverty threshold.
20
+ """Calculate poverty rate, i.e., the population share with income below
21
+ their poverty threshold.
23
22
 
24
23
  :param df: DataFrame with income, threshold, and possibly weight columns
25
24
  for each person/household.
@@ -43,7 +42,7 @@ def deep_poverty_rate(
43
42
  df: pd.DataFrame, income: str, threshold: str, w: str = None
44
43
  ) -> float:
45
44
  """Calculate deep poverty rate, i.e., the population share with income
46
- below half their poverty threshold.
45
+ below half their poverty threshold.
47
46
 
48
47
  :param df: DataFrame with income, threshold, and possibly weight columns
49
48
  for each person/household.
@@ -67,7 +66,7 @@ def poverty_gap(
67
66
  df: pd.DataFrame, income: str, threshold: str, w: str = None
68
67
  ) -> float:
69
68
  """Calculate poverty gap, i.e., the total gap between income and poverty
70
- thresholds for all people in poverty.
69
+ thresholds for all people in poverty.
71
70
 
72
71
  :param df: DataFrame with income, threshold, and possibly weight columns
73
72
  for each household (data should represent households, not persons).
@@ -91,8 +90,8 @@ def squared_poverty_gap(
91
90
  df: pd.DataFrame, income: str, threshold: str, w: str = None
92
91
  ) -> float:
93
92
  """Calculate squared poverty gap, i.e., the total squared gap between
94
- income and poverty thresholds for all people in poverty.
95
- Also known as poverty severity index.
93
+ income and poverty thresholds for all people in poverty. Also known as
94
+ poverty severity index.
96
95
 
97
96
  :param df: DataFrame with income, threshold, and possibly weight columns
98
97
  for each household (data should represent households, not persons).
@@ -117,7 +116,7 @@ def deep_poverty_gap(
117
116
  df: pd.DataFrame, income: str, threshold: str, w: str = None
118
117
  ) -> float:
119
118
  """Calculate deep poverty gap, i.e., the total gap between income and
120
- halved poverty thresholds for all people in deep poverty.
119
+ halved poverty thresholds for all people in deep poverty.
121
120
 
122
121
  :param df: DataFrame with income, threshold, and possibly weight columns
123
122
  for each household (data should represent households, not persons).
microdf/style.py CHANGED
@@ -4,7 +4,7 @@ GRID_COLOR = "#eeeeee" # Previously lighter #f5f5f5.
4
4
  DPI = 200
5
5
 
6
6
 
7
- def set_plot_style(dpi: int = DPI):
7
+ def set_plot_style(dpi: int = DPI) -> None:
8
8
  """Set plot style.
9
9
 
10
10
  :param dpi: DPI for saving and displaying figures, defaults to microdf.DPI
@@ -12,14 +12,14 @@ def set_plot_style(dpi: int = DPI):
12
12
  :type dpi: int, optional
13
13
  """
14
14
  try:
15
- import seaborn as sns
16
15
  import matplotlib as mpl
17
16
  import matplotlib.font_manager as fm
17
+ import seaborn as sns
18
18
  except ImportError:
19
19
  raise ImportError(
20
- "The function you've called requires extra dependencies. " +
21
- "Please install microdf with the 'charts' extra by running " +
22
- "'pip install microdf[charts]'"
20
+ "The function you've called requires extra dependencies. "
21
+ + "Please install microdf with the 'charts' extra by running "
22
+ + "'pip install microdf[charts]'"
23
23
  )
24
24
 
25
25
  sns.set_style("white")
microdf/tax.py CHANGED
@@ -1,16 +1,17 @@
1
+ from typing import Optional
2
+
1
3
  import numpy as np
2
4
  import pandas as pd
3
5
 
4
6
 
5
- def mtr(val, brackets, rates):
6
- """Calculates the marginal tax rate applied to a value depending on a
7
- tax schedule.
7
+ def mtr(val: pd.Series, brackets: pd.Series, rates: pd.Series) -> pd.Series:
8
+ """Calculates the marginal tax rate applied to a value depending on a tax
9
+ schedule.
8
10
 
9
11
  :param val: Value to assess tax on, e.g. wealth or income (list or Series).
10
12
  :param brackets: Left side of each bracket (list or Series).
11
13
  :param rates: Rate corresponding to each bracket.
12
14
  :returns: Series of the size of val representing the marginal tax rate.
13
-
14
15
  """
15
16
  df_tax = pd.DataFrame({"brackets": brackets, "rates": rates})
16
17
  df_tax["base_tax"] = (
@@ -24,30 +25,27 @@ def mtr(val, brackets, rates):
24
25
 
25
26
 
26
27
  def tax_from_mtrs(
27
- val,
28
- brackets,
29
- rates,
30
- avoidance_rate=0,
31
- avoidance_elasticity=0,
32
- avoidance_elasticity_flat=0,
33
- ):
28
+ val: pd.Series,
29
+ brackets: pd.Series,
30
+ rates: pd.Series,
31
+ avoidance_rate: Optional[float] = 0,
32
+ avoidance_elasticity: Optional[float] = 0,
33
+ avoidance_elasticity_flat: Optional[float] = 0,
34
+ ) -> pd.Series:
34
35
  """Calculates tax liability based on a marginal tax rate schedule.
35
36
 
36
37
  :param val: Value to assess tax on, e.g. wealth or income (list or Series).
37
38
  :param brackets: Left side of each bracket (list or Series).
38
39
  :param rates: Rate corresponding to each bracket.
39
40
  :param avoidance_rate: Constant avoidance/evasion rate in percentage terms.
40
- Defaults to zero.
41
- :param avoidance_elasticity: Avoidance/evasion elasticity.
42
- Response of log taxable value with respect
43
- to tax rate.
44
- Defaults to zero. Should be positive.
45
- :param avoidance_elasticity_flat: Response of taxable value with respect
46
- to tax rate.
47
- Use avoidance_elasticity in most cases.
48
- Defaults to zero. Should be positive.
41
+ Defaults to zero.
42
+ :param avoidance_elasticity: Avoidance/evasion elasticity. Response of log
43
+ taxable value with respect to tax rate. Defaults to zero. Should be
44
+ positive.
45
+ :param avoidance_elasticity_flat: Response of taxable value with respect to
46
+ tax rate. Use avoidance_elasticity in most cases. Defaults to zero.
47
+ Should be positive.
49
48
  :returns: Series of tax liabilities with the same size as val.
50
-
51
49
  """
52
50
  assert (
53
51
  avoidance_rate == 0
microdf/taxcalc.py CHANGED
@@ -1,14 +1,20 @@
1
+ from typing import Optional
2
+
3
+ import pandas as pd
4
+ import taxcalc
5
+
1
6
  import microdf as mdf
2
7
  from microdf._optional import import_optional_dependency
3
8
 
4
9
 
5
- def static_baseline_calc(recs, year):
10
+ def static_baseline_calc(
11
+ recs: pd.DataFrame, year: int
12
+ ) -> "taxcalc.Calculator":
6
13
  """Creates a static Calculator object.
7
14
 
8
15
  :param recs: Records object.
9
16
  :param year: Year to advance to.
10
17
  :returns: Calculator object.
11
-
12
18
  """
13
19
  tc = import_optional_dependency("taxcalc")
14
20
  calc = tc.Calculator(records=recs, policy=tc.Policy())
@@ -17,7 +23,13 @@ def static_baseline_calc(recs, year):
17
23
  return calc
18
24
 
19
25
 
20
- def add_weighted_metrics(df, metric_vars, w="s006", divisor=1e6, suffix="_m"):
26
+ def add_weighted_metrics(
27
+ df: pd.DataFrame,
28
+ metric_vars: list,
29
+ w: str = "s006",
30
+ divisor: float = 1e6,
31
+ suffix: str = "_m",
32
+ ) -> None:
21
33
  """Adds weighted metrics in millions to a Tax-Calculator pandas DataFrame.
22
34
 
23
35
  Columns are renamed to *_m.
@@ -27,10 +39,9 @@ def add_weighted_metrics(df, metric_vars, w="s006", divisor=1e6, suffix="_m"):
27
39
  name.
28
40
  :param w: Weight column. Defaults to s006.
29
41
  :param divisor: Number by which the product is divided. Defaults to 1e6.
30
- :param suffix: Suffix to add to each weighted total. Defaults to '_m'
31
- to match divisor default of 1e6.
42
+ :param suffix: Suffix to add to each weighted total. Defaults to '_m' to
43
+ match divisor default of 1e6.
32
44
  :returns: Nothing. Weighted columns are added in place.
33
-
34
45
  """
35
46
  df[w + suffix] = df[w] / divisor
36
47
  metric_vars = mdf.listify(metric_vars)
@@ -38,15 +49,16 @@ def add_weighted_metrics(df, metric_vars, w="s006", divisor=1e6, suffix="_m"):
38
49
  df[metric_var + suffix] = df[metric_var] * df[w + suffix]
39
50
 
40
51
 
41
- def n65(age_head, age_spouse, elderly_dependents):
52
+ def n65(
53
+ age_head: pd.Series, age_spouse: pd.Series, elderly_dependents: pd.Series
54
+ ) -> pd.Series:
42
55
  """Calculates number of people in the tax unit age 65 or older.
43
56
 
44
57
  :param age_head: Series representing age_head from taxcalc data.
45
58
  :param age_spouse: Series representing age_spouse from taxcalc data.
46
59
  :param elderly_dependents: Series representing elderly_dependents from
47
- taxcalc data.
60
+ taxcalc data.
48
61
  :returns: Series representing the number of people age 65 or older.
49
-
50
62
  """
51
63
  return (
52
64
  (age_head >= 65).astype(int)
@@ -56,14 +68,14 @@ def n65(age_head, age_spouse, elderly_dependents):
56
68
 
57
69
 
58
70
  def calc_df(
59
- records=None,
60
- policy=None,
61
- year=2020,
62
- reform=None,
63
- group_vars=None,
64
- metric_vars=None,
65
- group_n65=False,
66
- ):
71
+ records: Optional[pd.DataFrame] = None,
72
+ policy: Optional[taxcalc.Policy] = None,
73
+ year: int = 2020,
74
+ reform: Optional[dict] = None,
75
+ group_vars: Optional[list] = None,
76
+ metric_vars: Optional[list] = None,
77
+ group_n65: Optional[bool] = False,
78
+ ) -> pd.DataFrame:
67
79
  """Creates a pandas DataFrame for given Tax-Calculator data.
68
80
 
69
81
  s006 is always included, and RECID is used as an index.
@@ -71,19 +83,18 @@ def calc_df(
71
83
  :param records: An optional Records object. If not provided, uses CPS
72
84
  records. (Default value = None)
73
85
  :param policy: An optional Policy object. If not provided, uses default
74
- Policy.
86
+ Policy.
75
87
  :param year: An optional year to advance to. If not provided, defaults to
76
- 2020.
88
+ 2020.
77
89
  :param reform: An optional reform to implement for the Policy object.
78
90
  (Default value = None)
79
91
  :param group_vars: An optional list of column names to include in the
80
- DataFrame. (Default value = None)
92
+ DataFrame. (Default value = None)
81
93
  :param metric_vars: An optional list of column names to include and
82
94
  calculate weighted sums of (in millions named as *_m) in the DataFrame.
83
95
  (Default value = None)
84
96
  :param group_n65: Whether to calculate and group by n65. Defaults to False.
85
97
  :returns: A pandas DataFrame. market_income is also always calculated.
86
-
87
98
  """
88
99
  tc = import_optional_dependency("taxcalc")
89
100
  # Assign defaults.
@@ -135,12 +146,11 @@ def calc_df(
135
146
  return df.set_index("RECID")
136
147
 
137
148
 
138
- def recalculate(df):
149
+ def recalculate(df: pd.DataFrame) -> None:
139
150
  """Recalculates fields in the DataFrame for after components have changed.
140
151
 
141
152
  :param df: DataFrame for use in microdf.
142
153
  :returns: Nothing. Updates the DataFrame in place.
143
-
144
154
  """
145
155
  # Recalculate TPC's Expanded Cash Income measure.
146
156
  cols = df.columns
@@ -0,0 +1,9 @@
1
+ import os
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.fixture(scope="session")
7
+ def tests_path() -> str:
8
+ """"""
9
+ return os.path.abspath(os.path.dirname(__file__))
@@ -0,0 +1,48 @@
1
+ import os
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+
6
+ import microdf as mdf
7
+
8
+
9
+ def differences(
10
+ actual: pd.DataFrame,
11
+ expected: pd.DataFrame,
12
+ f_actual: str,
13
+ f_expected: str,
14
+ ) -> None:
15
+ """Check for differences between results in afilename and efilename files.
16
+
17
+ :param actual: Actual DataFrame.
18
+ :param expected: Expected DataFrame.
19
+ :param f_actual: Filename of the actual CSV.
20
+ :param f_expected: Filename of the expected CSV.
21
+ """
22
+ if not np.allclose(actual, expected):
23
+ msg = "COMPARE RESULTS DIFFER\n"
24
+ msg += "-------------------------------------------------\n"
25
+ msg += "--- NEW RESULTS IN {} FILE ---\n"
26
+ msg += "--- if new OK, copy {} to ---\n"
27
+ msg += "--- {} ---\n"
28
+ msg += "--- and rerun test. ---\n"
29
+ msg += "-------------------------------------------------\n"
30
+ raise ValueError(msg.format(f_actual, f_actual, f_expected))
31
+
32
+
33
+ def test_percentile_agg_compare(tests_path: str) -> None:
34
+ """
35
+ :param tests_path: Folder path to write test results.
36
+ """
37
+ N = 1000
38
+ np.random.seed(0)
39
+ df = pd.DataFrame({"val": np.random.rand(N), "w": np.random.rand(N)})
40
+ mdf.add_weighted_quantiles(df, "val", "w")
41
+ percentile_sum = df.groupby("val_percentile")[["val", "w"]].sum()
42
+ F_ACTUAL = "test_percentile_actual.csv"
43
+ F_EXPECTED = "test_percentile_expected.csv"
44
+ percentile_sum.to_csv(os.path.join(tests_path, F_ACTUAL))
45
+ # Re-read as CSV to remove index and ensure CSVs are equal.
46
+ actual = pd.read_csv(os.path.join(tests_path, F_ACTUAL))
47
+ expected = pd.read_csv(os.path.join(tests_path, F_EXPECTED))
48
+ differences(actual, expected, F_ACTUAL, F_EXPECTED)
@@ -0,0 +1,91 @@
1
+ """Test script for enhanced decile_rank functionality."""
2
+
3
+ import numpy as np
4
+
5
+ from microdf import MicroSeries
6
+
7
+
8
+ def test_decile_rank() -> None:
9
+ """Test the enhanced decile_rank method with assert statements."""
10
+ print("Running enhanced decile_rank tests...")
11
+
12
+ # Create test data with some negative values
13
+ test_data = [-5, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20]
14
+ test_weights = [1] * len(test_data)
15
+ test_ms = MicroSeries(test_data, weights=test_weights)
16
+
17
+ default_deciles = test_ms.decile_rank()
18
+ assert all(
19
+ 1 <= d <= 10 for d in default_deciles.values
20
+ ), "Default deciles should be between 1 and 10"
21
+
22
+ negative_indices = [i for i, val in enumerate(test_data) if val < 0]
23
+ negative_deciles = [default_deciles.values[i] for i in negative_indices]
24
+ assert all(
25
+ d >= 1 for d in negative_deciles
26
+ ), "Negative values should be ranked 1-10 in default mode"
27
+
28
+ assert isinstance(
29
+ default_deciles, MicroSeries
30
+ ), "decile_rank should return MicroSeries"
31
+
32
+ zero_deciles = test_ms.decile_rank(negatives_in_zero=True)
33
+ negative_zero_deciles = [zero_deciles.values[i] for i in negative_indices]
34
+ assert all(
35
+ d == 0 for d in negative_zero_deciles
36
+ ), "Negative values should be in decile 0 when negatives_in_zero=True"
37
+
38
+ non_negative_indices = [i for i, val in enumerate(test_data) if val >= 0]
39
+ non_negative_deciles = [
40
+ zero_deciles.values[i] for i in non_negative_indices
41
+ ]
42
+ assert all(
43
+ 1 <= d <= 10 for d in non_negative_deciles
44
+ ), "Non-negative values should be ranked 1-10 when negatives_in_zero=True"
45
+
46
+ unique_deciles = sorted(zero_deciles.unique())
47
+ assert 0 in unique_deciles, "Decile 0 should exist with negative values"
48
+ assert all(
49
+ d in unique_deciles for d in range(1, 11)
50
+ ), "Deciles 1-10 should exist with non-negative values"
51
+
52
+ positive_data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
53
+ positive_ms = MicroSeries(positive_data, weights=[1] * 10)
54
+
55
+ pos_default = positive_ms.decile_rank()
56
+ pos_zero = positive_ms.decile_rank(negatives_in_zero=True)
57
+ assert np.array_equal(
58
+ pos_default.values, pos_zero.values
59
+ ), "Both modes should produce identical results for all positive data"
60
+ assert (
61
+ 0 not in pos_zero.unique()
62
+ ), "Decile 0 should not exist when all values are positive"
63
+
64
+ negative_data = [-10, -8, -6, -4, -2]
65
+ negative_ms = MicroSeries(negative_data, weights=[1] * 5)
66
+
67
+ neg_default = negative_ms.decile_rank()
68
+ neg_zero = negative_ms.decile_rank(negatives_in_zero=True)
69
+ assert all(
70
+ 1 <= d <= 10 for d in neg_default.values
71
+ ), "Default mode should rank all negative values 1-10"
72
+ assert all(
73
+ d == 0 for d in neg_zero.values
74
+ ), "All negative values should be in decile 0 when negatives_in_zero=True"
75
+
76
+ weighted_data = [-1, 0, 1, 2, 3]
77
+ weighted_weights = [0.5, 1.0, 1.5, 2.0, 2.5]
78
+ weighted_ms = MicroSeries(weighted_data, weights=weighted_weights)
79
+
80
+ w_default = weighted_ms.decile_rank()
81
+ w_zero = weighted_ms.decile_rank(negatives_in_zero=True)
82
+ assert np.array_equal(
83
+ w_default.weights.values, weighted_weights
84
+ ), "Weights should be preserved in default mode"
85
+ assert np.array_equal(
86
+ w_zero.weights.values, weighted_weights
87
+ ), "Weights should be preserved in negatives_in_zero mode"
88
+
89
+
90
+ if __name__ == "__main__":
91
+ test_decile_rank()