Skip to content

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

fit

fit() -> CausalResult

Run bunching estimation.

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]

summary

summary() -> str

Return a formatted summary table.

plot

plot(figsize: Tuple[float, float] = (10, 6), title: Optional[str] = None) -> Any

Plot observed histogram with counterfactual overlay.

Returns:

Type Description
(fig, ax) : matplotlib figure and axes

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