statspai.bunching¶
bunching ¶
Bunching Estimator for kink/notch analysis.
Estimates behavioural responses to policy thresholds (tax kinks, regulatory notches) by comparing the observed distribution of a running variable to a counterfactual polynomial distribution.
References
Kleven, H. J. & Waseem, M. (2013). Using Notches to Uncover Optimization Frictions and Structural Elasticities. QJE, 128(2), 669-723. [@kleven2013using]
Chetty, R., Friedman, J. N., Olsen, T., & Pistaferri, L. (2011). Adjustment Costs, Firm Responses, and Micro vs. Macro Labor Supply Elasticities. QJE, 126(2), 749-804. [@chetty2011adjustment]
BunchingEstimator ¶
Bunching estimator for kink/notch designs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
running_var
|
str
|
|
required |
threshold
|
float
|
|
required |
bin_width
|
float
|
|
None
|
n_bins
|
int
|
|
50
|
poly_order
|
int
|
|
7
|
bunch_region
|
tuple
|
|
None
|
exclude_region
|
tuple
|
|
None
|
dt
|
float
|
|
None
|
design
|
str
|
|
'kink'
|
n_bootstrap
|
int
|
|
200
|
alpha
|
float
|
|
0.05
|
random_state
|
int
|
|
42
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> base = rng.normal(50000, 8000, 4000)
>>> bunchers = rng.uniform(48000, 50000, 600) # extra mass below the kink
>>> df = pd.DataFrame({"income": np.concatenate([base, bunchers])})
>>> est = sp.BunchingEstimator(
... data=df, running_var="income", threshold=50000,
... dt=0.10, n_bootstrap=50, random_state=0)
>>> res = est.fit()
>>> isinstance(res.estimate, float) # excess mass B
True
>>> bool("elasticity" in res.model_info)
True
NotchResult ¶
Bases: ResultProtocolMixin
Result container for notch bunching analysis.
Attributes:
| Name | Type | Description |
|---|---|---|
excess_bunching |
float
|
Excess mass B in the bunching region (below the notch). |
missing_mass |
float
|
Missing mass H above the notch (the "hole"). |
marginal_buncher |
float
|
Upper bound of the dominated region (x*). |
elasticity |
float or None
|
Structural elasticity estimate (if notch_size provided). |
se_bunching |
float
|
Bootstrap standard error of excess bunching. |
se_elasticity |
float or None
|
Bootstrap standard error of elasticity (if notch_size provided). |
pvalue |
float
|
P-value for H0: excess_bunching = 0. |
ci |
tuple of (float, float)
|
Confidence interval for excess bunching. |
notch_point |
float
|
Location of the notch. |
bin_centers |
ndarray
|
Bin centres of the histogram. |
observed |
ndarray
|
Observed bin counts. |
counterfactual |
ndarray
|
Counterfactual polynomial counts. |
n_obs |
int
|
Number of observations. |
causal_result |
CausalResult
|
Full CausalResult object for interoperability. |
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> from statspai.bunching.notch import NotchResult
>>> rng = np.random.default_rng(0)
>>> income = rng.normal(50000, 12000, 5000)
>>> # induce excess bunching just below the 50,000 notch
>>> shift = rng.uniform(0, 1, 5000) < 0.3
>>> income[shift & (income > 50000) & (income < 53000)] = 49800
>>> df = pd.DataFrame({'income': income})
>>> result = sp.notch(df, x='income', notch_point=50000,
... notch_size=0.10, bin_width=500, n_boot=50)
>>> isinstance(result, NotchResult)
True
>>> isinstance(result.summary(), str)
True
References
[@kleven2013using]
GeneralBunchingResult
dataclass
¶
Bases: ResultProtocolMixin
Output of high-order bunching design.
Carries the naive (Saez first-order) elasticity, the high-order bias-corrected elasticity, its bootstrap standard error and confidence interval, the fitted polynomial order, and the sample size.
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> base = rng.normal(0.0, 1.0, size=2000)
>>> bunchers = np.abs(rng.normal(0.0, 0.05, size=300))
>>> df = pd.DataFrame({"earnings": np.concatenate([base, bunchers])})
>>> res = sp.general_bunching(
... df, running="earnings", cutoff=0.0, n_boot=50
... )
>>> isinstance(res, sp.GeneralBunchingResult)
True
>>> res.n_obs
2300
KinkUnifiedResult
dataclass
¶
Bases: ResultProtocolMixin
Joint RDD + RKD + Bunching estimate at a common cutoff.
Produced by :func:kink_unified. Holds the RDD level shift, the RKD
slope change, and the bunching elasticity (each with a standard
error) estimated on the same running variable.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> x = rng.uniform(-1.0, 1.0, 400)
>>> y = 0.5 * x + 0.8 * (x >= 0) + rng.normal(0, 0.3, 400)
>>> df = pd.DataFrame({"earnings": y, "income": x})
>>> res = sp.kink_unified(df, y="earnings", running="income")
>>> isinstance(res, sp.KinkUnifiedResult)
True
>>> res.n_obs
400
general_bunching ¶
general_bunching(data: DataFrame, running: str, cutoff: float = 0.0, bandwidth: float = 1.0, bin_width: Optional[float] = None, polynomial_order: int = 4, alpha: float = 0.05, n_boot: int = 200, seed: int = 0) -> GeneralBunchingResult
High-order bunching design with bias correction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
running
|
str
|
Running variable (e.g. earnings). |
required |
cutoff
|
float
|
|
0.0
|
bandwidth
|
float
|
|
1.0
|
bin_width
|
float
|
Defaults to bandwidth / 25. |
None
|
polynomial_order
|
int
|
Order of the counterfactual polynomial fit. |
4
|
alpha
|
float
|
|
0.05
|
n_boot
|
int
|
|
200
|
seed
|
int
|
|
0
|
Returns:
| Type | Description |
|---|---|
GeneralBunchingResult
|
Naive (Saez first-order) and high-order bias-corrected elasticities with a bootstrap SE and confidence interval. |
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> base = rng.normal(0.0, 1.0, size=2000)
>>> bunchers = np.abs(
... rng.normal(0.0, 0.05, size=300)
... ) # mass near the kink
>>> df = pd.DataFrame({"earnings": np.concatenate([base, bunchers])})
>>> res = sp.general_bunching(
... df, running="earnings", cutoff=0.0, n_boot=50
... )
>>> res.n_obs
2300
>>> res.polynomial_order
4
>>> bool(np.isfinite(res.bias_corrected_elasticity))
True