statspai.utils¶
utils ¶
Utility functions for StatsPAI.
Provides Stata-style data manipulation tools: - Variable labels (label_var, get_label) - Pairwise correlation with stars (pwcorr) - Winsorization (winsor)
pwcorr ¶
pwcorr(data: DataFrame, vars: Optional[List[str]] = None, stars: bool = True, method: str = 'pearson', decimals: int = 3, output: str = 'text', listwise: bool = False, obs: bool = False) -> Union[str, DataFrame]
Pairwise correlation matrix with significance stars.
Equivalent to Stata's pwcorr var1 var2 var3, star(0.05).
Missing values are handled pairwise by default, exactly as Stata's
pwcorr: each correlation uses every row on which that pair is
observed, so a missing value in a third variable never changes the
correlation between two complete variables. Pass listwise=True
for Stata's pwcorr, listwise (one common estimation sample).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
vars
|
list of str
|
Variables to correlate. Default: all numeric columns. |
None
|
stars
|
bool
|
Show significance stars (* p<0.1, ** p<0.05, *** p<0.01). |
True
|
method
|
str
|
|
'pearson'
|
decimals
|
int
|
Decimal places. |
3
|
output
|
str
|
|
'text'
|
listwise
|
bool
|
Drop every row with a missing value in any of |
False
|
obs
|
bool
|
Print the number of observations used for each pair beneath the
coefficient (Stata |
False
|
Returns:
| Type | Description |
|---|---|
str or DataFrame
|
Formatted correlation matrix. With |
Examples:
>>> import statspai as sp
>>> df = sp.cps_wage()
>>> out = sp.pwcorr(df, vars=['log_wage', 'education', 'experience'])
>>> isinstance(out, str)
True
>>> bool('***' in out) # significant pairwise correlations are starred
True
>>> # Raw correlation matrix instead of formatted text
>>> cm = sp.pwcorr(df, vars=['log_wage', 'education'], output='dataframe')
>>> round(float(cm.loc['log_wage', 'education']), 3)
0.335
>>> int(cm.attrs['nobs'].loc['log_wage', 'education']) == len(df)
True
winsor ¶
winsor(data: DataFrame, vars: Optional[List[str]] = None, cuts: Tuple[float, float] = (1, 99), replace: bool = False, suffix: str = '_w', method: str = 'stata', subset: Optional[Union[str, Series, ndarray]] = None, by: Optional[Union[str, List[str]]] = None, trim: bool = False) -> DataFrame
Winsorize variables at specified percentiles.
Equivalent to Stata's winsor2 var1 var2, cuts(1 99), including its
percentile definition, if qualifier (subset=) and by().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
vars
|
list of str
|
Variables to winsorize. Default: all numeric columns. |
None
|
cuts
|
tuple of (float, float)
|
Lower and upper percentile cutoffs. |
(1, 99)
|
replace
|
bool
|
If True, overwrite original columns. If False, create new
columns with |
False
|
suffix
|
str
|
Suffix for new winsorized columns (when |
'_w'
|
method
|
str
|
Percentile definition. |
'stata'
|
subset
|
str, boolean Series or array
|
Rows used to compute the cutoffs and winsorized, as |
None
|
by
|
str or list of str
|
Compute the cutoffs within groups, as |
None
|
trim
|
bool
|
Set the values beyond the cutoffs to missing instead of pulling
them in ( |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
DataFrame with winsorized variables. |
Examples:
>>> import statspai as sp
>>> df = sp.cps_wage()
>>> # Winsorize at 1st and 99th percentile -> adds 'log_wage_w'
>>> out = sp.winsor(df, vars=['log_wage'], cuts=(1, 99))
>>> 'log_wage_w' in out.columns
True
>>> bool(out['log_wage_w'].max() <= df['log_wage'].max()) # tails clipped
True
>>> len(out) == len(df) # winsorizing does not drop rows
True
>>> # Replace in place instead of adding a suffixed column
>>> repl = sp.winsor(df, vars=['log_wage'], replace=True)
>>> 'log_wage_w' in repl.columns
False
Notes
Winsorization replaces values below the cuts[0]-th percentile
with that percentile value, and values above the cuts[1]-th
percentile with that percentile value. Unlike trimming, winsorization
does not remove observations.
The 'stata' percentile is also R's quantile(type = 2), so
sp.winsor(df, [v], cuts=(1, 99)) equals farr::winsorize(v, 0.01)
and trim=True equals farr::truncate(v, 0.01).
Before 1.33 the cutoffs used numpy's linear interpolation, which
differs from winsor2 on every variable whose n p / 100 is not
an integer; on a long-tailed variable the 99th percentile moved by
0.02% (51,003.45 against Stata's 51,015).
dgp_bartik ¶
dgp_bartik(n_regions: int = 50, n_industries: int = 10, effect: float = 1.0, seed: int | None = None, endogenous: bool = False) -> dict
Generate shift-share (Bartik) instrument data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_regions
|
int
|
Number of regions. |
50
|
n_industries
|
int
|
Number of industries. |
10
|
effect
|
float
|
True effect of the Bartik instrument on the outcome. |
1.0
|
seed
|
int or None
|
Random seed. |
None
|
endogenous
|
bool
|
If True, generate a structural shift-share IV instead of the reduced
form: an endogenous regressor |
False
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Examples:
dgp_bunching ¶
dgp_bunching(n: int = 10000, kink_point: float = 50000.0, elasticity: float = 0.3, seed: int | None = None, t0: float = 0.0, t1: float = 0.2) -> DataFrame
Generate earnings that bunch at a kink in the marginal tax schedule.
Counterfactual earnings z* are those under the flat rate t0. At the
kink the marginal rate rises to t1; with iso-elastic earnings
z = z* ((1 - t) / (1 - t0))^elasticity:
z* <= kink_point-- unaffected;z* >= kink_point * ((1 - t0) / (1 - t1))^elasticity-- earnz* ((1 - t1) / (1 - t0))^elasticity;- in between -- locate exactly at
kink_point(the bunchers).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
10000
|
kink_point
|
float
|
Location of the kink (e.g., tax threshold). |
50000.0
|
elasticity
|
float
|
Earnings elasticity with respect to the net-of-tax rate. |
0.3
|
seed
|
int or None
|
Random seed. |
None
|
t0
|
float
|
Marginal tax rate below and above the kink ( |
0.0
|
t1
|
float
|
Marginal tax rate below and above the kink ( |
0.0
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
>>> import statspai as sp
>>> df = sp.dgp_bunching(n=5000, kink_point=50000, elasticity=0.2, seed=0)
>>> df.attrs['true_effect'], df.attrs['dt']
(0.2, 0.2)
>>> bool((df['income'] <= df['counterfactual_income'] + 1e-9).all())
True
>>> bool((df['income'] == 50000).sum() == df.attrs['n_bunchers'] > 0)
True
dgp_cluster_rct ¶
dgp_cluster_rct(n_clusters: int = 50, cluster_size: int = 20, effect: float = 0.3, icc: float = 0.1, seed: int | None = None) -> DataFrame
Generate cluster-randomised trial data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_clusters
|
int
|
Number of clusters. |
50
|
cluster_size
|
int
|
Number of individuals per cluster. |
20
|
effect
|
float
|
Treatment effect. |
0.3
|
icc
|
float
|
Intra-cluster correlation coefficient (0-1). |
0.1
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_did ¶
dgp_did(n_units: int = 100, n_periods: int = 10, effect: float = 0.5, staggered: bool = False, n_groups: int = 4, heterogeneous: bool = False, seed: int | None = None, ddd: bool = False, ddd_confound: float = 1.0) -> DataFrame
Generate Difference-in-Differences panel data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_units
|
int
|
Number of cross-sectional units. |
100
|
n_periods
|
int
|
Number of time periods. |
10
|
effect
|
float
|
Average treatment effect on the treated. |
0.5
|
staggered
|
bool
|
If True, treatment adoption is staggered across groups. |
False
|
n_groups
|
int
|
Number of treatment-timing groups (used when |
4
|
heterogeneous
|
bool
|
If True, the treatment effect varies by unit. |
False
|
seed
|
int or None
|
Random seed for reproducibility. |
None
|
ddd
|
bool
|
Triple-difference design (non-staggered only). Adds a binary
|
False
|
ddd_confound
|
float
|
Treated-group x post shock shared by both subgroups ( |
1.0
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_iv ¶
dgp_iv(n: int = 500, effect: float = 0.5, first_stage: float = 0.4, n_instruments: int = 1, seed: int | None = None) -> DataFrame
Generate Instrumental Variables data with endogeneity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
500
|
effect
|
float
|
True causal effect of treatment on outcome. |
0.5
|
first_stage
|
float
|
Strength of the instrument in the first stage. |
0.4
|
n_instruments
|
int
|
Number of instruments. |
1
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_observational ¶
dgp_observational(n: int = 1000, effect: float = 0.5, confounding: float = 0.3, seed: int | None = None) -> DataFrame
Generate observational data with selection on observables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
1000
|
effect
|
float
|
True treatment effect. |
0.5
|
confounding
|
float
|
Strength of confounding (higher = more selection bias). |
0.3
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_panel ¶
dgp_panel(n_units: int = 100, n_periods: int = 20, fe_sd: float = 1.0, te_sd: float = 0.5, ar1_coef: float = 0.5, seed: int | None = None) -> DataFrame
Generate panel data with unit/time fixed effects and AR(1) covariate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_units
|
int
|
Number of units. |
100
|
n_periods
|
int
|
Number of time periods. |
20
|
fe_sd
|
float
|
Standard deviation of unit fixed effects. |
1.0
|
te_sd
|
float
|
Standard deviation of time fixed effects. |
0.5
|
ar1_coef
|
float
|
AR(1) coefficient for the covariate process. |
0.5
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rct ¶
dgp_rct(n: int = 500, effect: float = 0.3, p_treat: float = 0.5, heterogeneous: bool = False, n_covariates: int = 3, seed: int | None = None) -> DataFrame
Generate Randomised Controlled Trial data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
500
|
effect
|
float
|
Average treatment effect. |
0.3
|
p_treat
|
float
|
Probability of assignment to treatment. |
0.5
|
heterogeneous
|
bool
|
If True, the treatment effect varies with the first covariate. |
False
|
n_covariates
|
int
|
Number of pre-treatment covariates. |
3
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rd ¶
dgp_rd(n: int = 1000, effect: float = 0.3, cutoff: float = 0.0, fuzzy: bool = False, bandwidth_relevant: float = 0.5, seed: int | None = None) -> DataFrame
Generate Regression Discontinuity data (sharp or fuzzy).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
1000
|
effect
|
float
|
Treatment effect at the cutoff. The effect is constant, so in the fuzzy design it is also the complier effect. |
0.3
|
cutoff
|
float
|
RD cutoff value. The running variable is centred on it. |
0.0
|
fuzzy
|
bool
|
If True, generate a fuzzy RD design: the probability of treatment
is |
False
|
bandwidth_relevant
|
float
|
Spread of the running variable, which is uniform on
|
0.5
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rd_2d ¶
Generate 2D boundary RD data.
Treatment is assigned when x1 >= 0 (vertical boundary).
True effect = effect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
2000
|
effect
|
float
|
Treatment effect at boundary. |
2.0
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rd_hte ¶
dgp_rd_hte(n: int = 3000, ate: float = 2.0, hte_coef: float = 1.5, cutoff: float = 0.0, seed: int | None = None) -> DataFrame
Generate RD data with heterogeneous treatment effects.
CATE(z) = ate + hte_coef * z, where z ~ N(0,1). So ATE = ate and the slope of heterogeneity = hte_coef.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
3000
|
ate
|
float
|
Average treatment effect at cutoff. |
2.0
|
hte_coef
|
float
|
Slope of heterogeneity w.r.t. covariate z. |
1.5
|
cutoff
|
float
|
RD cutoff. |
0.0
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rd_kink ¶
dgp_rd_kink(n: int = 2000, kink: float = 0.8, cutoff: float = 0.0, seed: int | None = None) -> DataFrame
Generate Regression Kink Design data (Card et al. 2015).
The treatment function has a kink (change in slope) at the cutoff.
The true kink effect on the outcome equals kink.
DGP: T = X + kink_T * max(X, 0), Y = 0.4*T + noise True RKD = 0.4 * kink_T / kink_T = kink (simplified)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
2000
|
kink
|
float
|
True kink in slope at cutoff. |
0.8
|
cutoff
|
float
|
Kink point location. |
0.0
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rd_multi ¶
dgp_rd_multi(n: int = 3000, cutoffs: list | None = None, effects: list | None = None, seed: int | None = None) -> DataFrame
Generate multi-cutoff RD data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int
|
Sample size. |
3000
|
cutoffs
|
list of float
|
Cutoff values. Default [0.0, 1.0]. |
None
|
effects
|
list of float
|
Treatment effects at each cutoff. Default [2.0, 3.0]. |
None
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_rdit ¶
dgp_rdit(n_periods: int = 200, effect: float = 2.0, cutoff_period: int = 100, seasonality: bool = True, seed: int | None = None) -> DataFrame
Generate Regression Discontinuity in Time data (Hausman-Rapson 2018).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_periods
|
int
|
Number of time periods. |
200
|
effect
|
float
|
Treatment effect at the policy change date: a permanent level shift of this size. |
2.0
|
cutoff_period
|
int
|
Period of the policy change. |
100
|
seasonality
|
bool
|
Include monthly seasonality. |
True
|
Notes
y = 0.01 t + effect * 1[t >= cutoff_period] + season + e with
AR(1) errors, e_t = 0.3 e_{t-1} + nu_t and nu_t ~ N(0, 0.5^2).
The autocorrelation is in the errors only, so the level shift is
effect at every horizon.
seed : int or None
Random seed.
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
dgp_synth ¶
dgp_synth(n_units: int = 20, n_periods: int = 30, treated_unit: int = 0, treatment_time: int = 20, effect: float = 0.5, seed: int | None = None) -> DataFrame
Generate synthetic control data with a factor model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_units
|
int
|
Number of units (including the treated unit). |
20
|
n_periods
|
int
|
Number of time periods. |
30
|
treated_unit
|
int
|
Index of the treated unit. |
0
|
treatment_time
|
int
|
Period when treatment begins. |
20
|
effect
|
float
|
Treatment effect (additive, constant post-treatment). |
0.5
|
seed
|
int or None
|
Random seed. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: |
Examples:
outlier_indicator ¶
outlier_indicator(data: DataFrame, vars: Sequence[str], cuts: tuple = (1, 99), combined: bool = False) -> DataFrame
Flag outlier observations by percentile cutoffs.
Returns 0/1 indicator columns for values outside [cuts[0], cuts[1]]
percentile bounds. Mirrors the pywinsor2.outlier_indicator API.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
vars
|
list of str
|
Columns to check. |
required |
cuts
|
tuple of (float, float)
|
Lower and upper percentile cutoffs. |
(1, 99)
|
combined
|
bool
|
If True, also add a column |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Copy of |
Examples:
rank ¶
rank(data: DataFrame, var: str, by: Optional[Union[str, Sequence[str]]] = None, method: str = 'average', ascending: bool = True) -> Series
Rank a variable, optionally within groups.
Equivalent to Stata's egen newvar = rank(var), by(group) with
method / ascending mapping to Stata's tie-handling options
(checked in Stata 18):
'average'→ Stata default'min'→track(1 + the number of lower values)'min'withascending=False→field(1 + the number of higher)'first'→unique(ties in row order)'max'/'dense'have no Stata counterpart.
sp.egen(df, "rank", var, rank_ties="field") is the same with
Stata's if and missing-value handling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
var
|
str
|
Column to rank. |
required |
by
|
str or list of str
|
Group variables. If given, ranks are computed within each group. |
None
|
method
|
(average, min, max, dense, first)
|
|
'average'
|
ascending
|
bool
|
|
True
|
Returns:
| Type | Description |
|---|---|
pd.Series aligned to ``data.index``.
|
|
Examples:
rowcount ¶
Row-wise count of non-missing values (Stata egen rownonmiss).
Examples:
rowmax ¶
Row-wise max across vars, ignoring NaN.
Examples:
rowmean ¶
Row-wise mean across vars, ignoring NaN (Stata egen rowmean).
Examples:
rowmin ¶
Row-wise min across vars, ignoring NaN.
Examples:
rowsd ¶
Row-wise standard deviation across vars (Stata egen rowsd).
Uses the sample standard deviation (ddof=1), matching Stata.
Examples:
rowtotal ¶
Row-wise sum across vars, treating NaN as 0 (Stata egen rowtotal).
Examples:
read_data ¶
read_data(path: str, encoding: Optional[str] = None, extended_missing: str = 'nan', **kwargs: Any) -> DataFrame
Read data from any common format, preserving variable labels.
Automatically detects format from file extension and stores
Stata/SPSS/SAS variable labels in df.attrs['_labels'].
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
File path. Supported: .dta, .csv, .tsv (tab-separated), .xlsx, .xls, .parquet, .sas7bdat, .sav, .feather, .json. |
required |
encoding
|
str
|
Character encoding (for .csv and .tsv). |
None
|
extended_missing
|
(nan, column)
|
What to do with Stata's extended missing values |
'nan'
|
**kwargs
|
Any
|
Passed to the underlying pandas reader. A CSV / TSV file over 4 MB
read without further keywords (other than |
{}
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
With variable labels in |
Warns:
| Type | Description |
|---|---|
UserWarning
|
When a .dta file labels an extended missing value and
|
Examples:
write_data ¶
write_data(data: DataFrame, path: Union[str, Path], *, labels: Optional[Dict[str, str]] = None, value_labels: Optional[Dict[str, Dict[Any, str]]] = None, data_label: Optional[str] = None, extended_missing: str = 'column', **kwargs: Any) -> Path
Write data to a file, keeping variable and value labels.
The counterpart of :func:read_data. For Stata .dta the variable
labels (data.attrs['_labels']), value labels
(data.attrs['_value_labels']) and dataset label
(data.attrs['_data_label']) are written into the file, so Stata's
describe / label list and :func:read_data see them again.
pandas.DataFrame.to_stata writes none of these unless each is
passed by hand. So are the rest of what :func:read_data keeps of a
.dta file: the names of the value-label sets, the extended missing
values, notes and characteristics, and display formats (see Notes).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Data to write. The row index is not written. |
required |
path
|
str or Path
|
Output file. The extension picks the format: .dta, .csv, .tsv, .xlsx, .parquet, .feather, .json. |
required |
labels
|
dict
|
|
None
|
value_labels
|
dict
|
|
None
|
data_label
|
str
|
Dataset label (Stata's |
None
|
extended_missing
|
(column, nan)
|
For .dta only. |
'column'
|
**kwargs
|
Any
|
Passed to the underlying pandas writer. For .dta, |
{}
|
Returns:
| Type | Description |
|---|---|
Path
|
The file written. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
MethodIncompatibility
|
(a |
Warns:
| Type | Description |
|---|---|
UserWarning
|
When |
Notes
Labels for columns that are not in data (dropped since the labels
were attached) are skipped. Labels of extended missing values
(data.attrs['_missing_labels']) are written into the file's label
tables.
A value-label set keeps the name in data.attrs['_value_label_names'],
and variables that share a name and the same labels share one set in the
file, as they did in Stata. A variable whose labels no longer match the
others under its name gets a set named after itself. Without a recorded
name the set is named after the variable. A name that was attached to a
variable without ever being defined is not kept.
Notes (data.attrs['_notes']) and characteristics
(data.attrs['_characteristics']), each {variable or '_dta': ...},
are written for the columns still in data.
A display format in data.attrs['_formats'] is written when Stata
would accept it for the column; a datetime64 column read from a
%td / %tm / %tq variable is written back with that unit
unless convert_dates= says otherwise. Each numeric column is stored
in the smallest Stata type that holds its values exactly (as Stata's
compress does); no value is rounded.
The file is written beside path and moved into place when complete,
so a failed write leaves any existing file as it was.
Examples:
>>> import statspai as sp
>>> import pandas as pd
>>> df = pd.DataFrame({'wage': [10.0, 20.0], 'female': [0, 1]})
>>> sp.label_var(df, 'wage', 'Monthly wage (CNY)')
>>> out = sp.write_data(
... df, 'survey.dta',
... value_labels={'female': {0: 'male', 1: 'female'}},
... )
>>> sp.get_label(sp.read_data('survey.dta'), 'wage')
'Monthly wage (CNY)'
scalar_iv_projection ¶
scalar_iv_projection(data: DataFrame, treat: str, instruments: List[str], covariates: Optional[List[str]] = None, new_col: Optional[str] = None, return_column: bool = False) -> Union[DataFrame, Series]
Project a vector of instruments onto a scalar first-stage index.
Fits the first-stage OLS regression::
treat ~ intercept + Z1 + Z2 + ... + X1 + X2 + ...
and returns the in-sample fitted values. This scalar "first-stage index" can then be passed as a single-instrument input to estimators that don't accept vector-valued Z (e.g. StatsPAI's PLIV / IIVM implementations).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
treat
|
str
|
Endogenous treatment variable name. |
required |
instruments
|
list of str
|
Excluded-instrument column names. Must have length ≥ 1. |
required |
covariates
|
list of str
|
Exogenous controls to include in the first stage. |
None
|
new_col
|
str
|
Name of the column for the scalar index. Defaults to
|
None
|
return_column
|
bool
|
If True, return just the fitted pandas Series. If False,
return a copy of |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame or Series
|
By default, |
Examples:
>>> import statspai as sp
>>> import numpy as np, pandas as pd
>>> rng = np.random.default_rng(0)
>>> n = 300
>>> age = rng.normal(40, 8, n)
>>> parent_edu = rng.integers(8, 16, n).astype(float)
>>> z1, z2 = rng.normal(0, 1, n), rng.normal(0, 1, n)
>>> schooling = 12 + 0.8 * z1 + 0.5 * z2 + 0.1 * (age - 40) + rng.normal(0, 1, n)
>>> earnings = 5000 + 800 * schooling + 50 * age + rng.normal(0, 2000, n)
>>> df = pd.DataFrame({
... 'earnings': earnings, 'schooling': schooling,
... 'quarter_of_birth': z1, 'distance_to_college': z2,
... 'age': age, 'parent_edu': parent_edu,
... })
>>> # PLIV with two excluded instruments — project onto a scalar index first
>>> df_aug = sp.scalar_iv_projection(
... df, treat='schooling',
... instruments=['quarter_of_birth', 'distance_to_college'],
... covariates=['age', 'parent_edu'],
... )
>>> 'schooling_iv_hat' in df_aug.columns
True
>>> result = sp.dml(df_aug, y='earnings', treat='schooling',
... covariates=['age', 'parent_edu'],
... model='pliv',
... instrument='schooling_iv_hat')
Notes
Projecting instruments onto a scalar index loses power relative to a full multi-instrument 2SLS when the instruments are heterogeneous, but the bias of the causal estimate remains valid under the same exogeneity conditions. A proper multi-instrument DML (vector r(X) and a Cragg-Donald-style first-stage test) is on the roadmap.
decode ¶
decode(df: DataFrame, columns: Union[str, Sequence[str], None] = None, missing: bool = False) -> DataFrame
Replace value-labelled codes by their label texts.
The counterpart of Stata's decode. A .dta file read by
sp.read_data keeps its labelled variables as numeric codes (what
Stata itself stores and estimates on); this gives the readable version
for tables, plots and exports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Data with value labels in |
required |
columns
|
str or list of str
|
Columns to decode. Default: every column that has value labels. |
None
|
missing
|
bool
|
Also show the labels of extended missing values ( |
False
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
A copy in which each decoded column is an ordered categorical whose
categories follow the codes. A code without a label is shown as
the number. Decoded columns no longer appear in
|
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
(a |
Examples:
>>> import statspai as sp
>>> import pandas as pd
>>> df = pd.DataFrame({'edu': [3, 1, 2, 1], 'wage': [30., 10., 20., 12.]})
>>> sp.label_values(df, 'edu', {1: 'primary', 2: 'secondary', 3: 'tertiary'})
>>> out = sp.decode(df)
>>> out['edu'].tolist()
['tertiary', 'primary', 'secondary', 'primary']
>>> list(out['edu'].cat.categories) # code order, not alphabetical
['primary', 'secondary', 'tertiary']
>>> df['edu'].tolist() # the input is not modified
[3, 1, 2, 1]
describe ¶
Stata-style describe — variable names, types, labels, and
non-missing counts in one table.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
|
required |
columns
|
list
|
Subset of columns. Default: all. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Columns: variable, type, n, n_missing, label. |
Examples:
>>> import statspai as sp
>>> import pandas as pd
>>> df = pd.DataFrame({'wage': [10.0, 20.0, 15.0],
... 'edu': [12, 16, 14]})
>>> sp.label_var(df, 'wage', 'Monthly wage (CNY)')
>>> tbl = sp.describe(df)
>>> list(tbl.columns)
['variable', 'type', 'n', 'n_missing', 'label', 'value_labels']
>>> tbl['label'].tolist()
['Monthly wage (CNY)', '']
>>> sp.label_values(df, 'edu', {12: 'high school', 16: 'college'})
>>> sp.describe(df)['value_labels'].tolist()
['', '12=high school, 16=college']
get_label ¶
Get the label for a variable, falling back to the column name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
|
required |
var
|
str
|
|
required |
Returns:
| Type | Description |
|---|---|
str
|
The label if set, otherwise the column name itself. |
Examples:
get_labels ¶
Get all variable labels as a dictionary.
Returns:
| Type | Description |
|---|---|
dict
|
{column_name: label} for all labeled columns. Unlabeled columns map to their own name. |
Examples:
label_values ¶
label_values(df: DataFrame, var: Union[str, Sequence[str]], labels: Optional[Dict[Any, str]]) -> None
Attach value labels to the codes of one or more variables.
Equivalent to Stata's label define yn 0 "No" 1 "Yes" followed by
label values female union yn. The data are not changed: the
columns keep their numeric codes, and the labels are used by
:func:describe, sp.tab, :func:decode, sp.regtable(labels=)
and written to .dta / Parquet by sp.write_data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
DataFrame to label (modified in place). |
required |
var
|
str or list of str
|
Column(s) the labels apply to. |
required |
labels
|
dict or None
|
|
required |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
(a |
Examples:
label_var ¶
Attach a human-readable label to a variable.
Equivalent to Stata's label variable wage "Monthly wage (CNY)".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
DataFrame to label (modified in place). |
required |
var
|
str
|
Column name. |
required |
label
|
str
|
Human-readable label. |
required |
Examples:
label_vars ¶
label_vars(df: DataFrame, labels: Union[Dict[str, str], DataFrame], rename: Optional[Dict[str, str]] = None) -> None
Attach labels to multiple variables at once, or copy them from a frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
DataFrame to label (modified in place). |
required |
labels
|
dict or DataFrame
|
|
required |
rename
|
dict
|
|
None
|
Examples:
>>> import statspai as sp
>>> import pandas as pd
>>> df = pd.DataFrame({'wage': [10.0, 20.0],
... 'edu': [12, 16],
... 'exp': [5, 8]})
>>> sp.label_vars(df, {
... 'wage': 'Monthly wage (CNY)',
... 'edu': 'Years of education',
... 'exp': 'Work experience (years)',
... })
>>> sp.get_label(df, 'exp')
'Work experience (years)'
A merge drops the labels; copy them back from the frame that had them:
diff ¶
diff(data: DataFrame, var: Union[str, Sequence[str]], n: Union[int, Sequence[int]] = 1, *, id: Optional[str] = None, time: Optional[str] = None, delta: Optional[float] = None) -> Union[Series, DataFrame]
Stata's difference operator Dn.x, read from the time variable.
D.x is x - L.x; D2.x is the difference of the difference
(x - 2 L.x + L2.x), not x - L2.x. Missing wherever a period it
needs is absent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
var
|
str or list of str
|
|
required |
n
|
int or list of int
|
The order of the difference ( |
1
|
id
|
str
|
Default: the declaration of :func: |
None
|
time
|
str
|
Default: the declaration of :func: |
None
|
delta
|
float
|
|
None
|
Returns:
| Type | Description |
|---|---|
Series or DataFrame
|
Named |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
As :func: |
Examples:
get_xtset ¶
The declaration sp.xtset stored on data (None if none).
lag ¶
lag(data: DataFrame, var: Union[str, Sequence[str]], n: Union[int, Sequence[int]] = 1, *, id: Optional[str] = None, time: Optional[str] = None, delta: Optional[float] = None) -> Union[Series, DataFrame]
Stata's lag operator Ln.x, read from the time variable.
L.x at time t is x at t - delta in the same panel, and
missing when that period is not in the data -- not the row above.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
var
|
str or list of str
|
The variable(s) to lag. |
required |
n
|
int or list of int
|
The lag order; |
1
|
id
|
str
|
The panel and time variables. Default: the declaration of
:func: |
None
|
time
|
str
|
The panel and time variables. Default: the declaration of
:func: |
None
|
delta
|
float
|
The length of one period (default: the declared one, else 1). |
None
|
Returns:
| Type | Description |
|---|---|
Series or DataFrame
|
Aligned to |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
No time variable given or declared; a time variable with missing, non-integer or repeated values; a non-numeric variable. |
Examples:
>>> import pandas as pd
>>> import statspai as sp
>>> df = pd.DataFrame({"t": [1, 2, 3, 5], "x": [10.0, 12.0, 15.0, 19.0]})
>>> sp.lag(df, "x", time="t").tolist()
[nan, 10.0, 12.0, nan]
>>> list(sp.lag(df, "x", n=[1, 2], time="t").columns)
['L.x', 'L2.x']
References
StataCorp. Stata 18 User's Guide, [U] 11.4.4 Time-series varlists.
lead ¶
lead(data: DataFrame, var: Union[str, Sequence[str]], n: Union[int, Sequence[int]] = 1, *, id: Optional[str] = None, time: Optional[str] = None, delta: Optional[float] = None) -> Union[Series, DataFrame]
Stata's lead operator Fn.x, read from the time variable.
F.x at time t is x at t + delta in the same panel, and
missing when that period is not in the data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
|
required |
var
|
str or list of str
|
|
required |
n
|
int or list of int
|
The lead order ( |
1
|
id
|
str
|
Default: the declaration of :func: |
None
|
time
|
str
|
Default: the declaration of :func: |
None
|
delta
|
float
|
|
None
|
Returns:
| Type | Description |
|---|---|
Series or DataFrame
|
Named |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
As :func: |
Examples:
xtset ¶
xtset(data: DataFrame, id: Optional[str] = None, time: Optional[str] = None, delta: float = 1.0) -> PanelDeclaration
Declare the panel and time variables of a DataFrame (Stata xtset).
sp.xtset(df, id="firm", time="year") is Stata's xtset firm
year; sp.xtset(df, time="t") is tsset t. The declaration is
stored in data.attrs (in place, as Stata changes the dataset in
memory) and is read by :func:lag / :func:lead / :func:diff and
by formulas that use L.x / F.x / D.x. Called with neither
id nor time it returns the current declaration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
The data. Only |
required |
id
|
str
|
The panel variable. |
None
|
time
|
str
|
The time variable: whole numbers of periods (Stata dates are
integers) or a pandas |
None
|
delta
|
float
|
The length of one period, Stata's |
1
|
Returns:
| Type | Description |
|---|---|
PanelDeclaration
|
The variables, and Stata's description of the panel: balance
( |
Raises:
| Type | Description |
|---|---|
MethodIncompatibility
|
When the time variable has missing or non-integer values, repeats
a period within a panel (Stata r(451)), is a datetime, or has two
periods closer than |
Examples:
>>> import pandas as pd
>>> import statspai as sp
>>> df = pd.DataFrame({"id": [1, 1, 1, 2, 2], "t": [1, 2, 4, 1, 2],
... "x": [1.0, 4.0, 9.0, 2.0, 3.0]})
>>> decl = sp.xtset(df, id="id", time="t")
>>> decl.balanced, decl.gaps
('unbalanced', True)
>>> sp.lag(df, "x").tolist() # t = 4 has no t = 3 before it
[nan, 1.0, nan, nan, 2.0]
References
StataCorp. Stata 18 Base Reference Manual, [XT] xtset and
[TS] tsset.