statspai.forest¶
forest ¶
Forest-based causal inference estimators for StatsPAI.
Hosts CausalForest (grf-style honest causal forests) and its
companions:
- :func:
causal_forest/ :class:CausalForest— heterogeneous treatment-effect estimation via honest random forests (Wager-Athey 2018; Athey-Tibshirani-Wager 2019). - :func:
iv_forest/ :func:instrumental_forest— instrumental forests for conditional LATEs (Athey-Tibshirani-Wager 2019). - :func:
multi_arm_forest/ :func:lm_forest— multi-arm causal forests and conditionally linear models (Nie-Wager 2021 with GRF weights). - :func:
regression_forest, :func:multi_regression_forest, :func:probability_forest, :func:quantile_forest, :func:survival_forest— the prediction forests of the GRF family. - :func:
variable_importance, :func:best_linear_projection, :func:get_scores— grf's post-estimation for any of them. - :func:
forest_group_effects/ :func:forest_support/ :func:cate_pretrend_test-- group effects with valid SEs (imputation scores forfe=forests), support of counterfactual predictions, and pre-trends by predicted-effect group. - :func:
calibration_test/ :func:test_calibration/ :func:grf_calibration(the same test on predictions made elsewhere) / :func:rate/ :func:honest_variance— post-fit honesty & calibration diagnostics.
This package was previously named statspai.causal — the old
name is kept as a deprecation shim for one minor version cycle.
Use from statspai.forest import ... going forward.
CausalForest ¶
Bases: BaseModel
Causal Forest for heterogeneous treatment effect estimation
This class implements the Causal Forest algorithm, which uses random forests to estimate conditional average treatment effects (CATE) in a non-parametric way.
The method combines ideas from: 1. Honest estimation to avoid overfitting 2. Double machine learning to handle confounding 3. Random forests for flexible function approximation
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_estimators
|
int
|
Number of trees (grf |
2000
|
min_samples_leaf
|
int
|
grf |
5
|
max_depth
|
int
|
Optional depth cap (grf has none). |
None
|
max_samples
|
float
|
Fraction of clusters drawn per tree (grf |
0.5
|
model_y
|
estimator
|
scikit-learn estimators for |
None
|
model_t
|
estimator
|
scikit-learn estimators for |
None
|
discrete_treatment
|
bool
|
Whether treatment is binary (0/1). Continuous treatments are supported by the causal forest itself; doubly-robust averages require a binary treatment. |
True
|
honest
|
bool
|
grf |
True
|
bootstrap
|
None
|
Not used by the grf engine (trees are grown on subsamples drawn without replacement). Retained for the legacy engine. |
None
|
random_state
|
int
|
Seed. Results are reproducible for a given seed and |
None
|
n_jobs
|
int
|
Threads used to grow trees (-1 = all cores). Results do not depend on it: every tree draws from its own seed and the trees are combined in a fixed order. .. versionchanged:: 1.40.0
The default was |
-1
|
verbose
|
int
|
Verbosity level |
0
|
split_rule
|
(grf, cffe, legacy)
|
|
"grf"
|
mtry
|
int
|
Mean number of candidate variables per split (grf default
|
None
|
honesty_fraction
|
float
|
|
0.5
|
honesty_prune_leaves
|
bool
|
|
True
|
alpha
|
float
|
grf |
0.05
|
imbalance_penalty
|
float
|
|
0.0
|
stabilize_splits
|
bool
|
|
True
|
ci_group_size
|
int
|
Trees per little bag ( |
None
|
equalize_cluster_weights
|
bool
|
grf |
False
|
nuisance_folds
|
int
|
Cross-fitting folds for user-supplied nuisance estimators. |
3
|
fe
|
(None, unit, twoway)
|
Panel fixed effects removed within every node and leaf. Requires
|
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
fitted_ |
bool
|
Whether the model has been fitted |
params |
Series
|
Not applicable for non-parametric methods, returns empty Series |
std_errors |
Series
|
Not applicable for non-parametric methods, returns empty Series |
tvalues |
Series
|
Not applicable for non-parametric methods, returns empty Series |
pvalues |
ndarray
|
Not applicable for non-parametric methods, returns empty array |
diagnostics |
dict
|
Model diagnostics and fit statistics |
data_info |
dict
|
Information about the data used in fitting |
Notes
This implementation is inspired by the EconML library's CausalForestDML but adapted to fit the StatsPAI architecture and interface.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> from statspai.forest import CausalForest
>>>
>>> # Generate sample data
>>> np.random.seed(42)
>>> n = 1000
>>> X = np.random.normal(0, 1, (n, 3))
>>> T = np.random.binomial(1, 0.5, n)
>>> Y = X[:, 0] * T + X[:, 1] + np.random.normal(0, 1, n)
>>> data = pd.DataFrame({
... 'Y': Y, 'T': T, 'X1': X[:, 0], 'X2': X[:, 1], 'X3': X[:, 2]
... })
>>>
>>> # Fit Causal Forest
>>> cf = CausalForest(n_estimators=50, random_state=42)
>>> cf = cf.fit('Y ~ T | X1 + X2 + X3', data=data) # returns self
>>> cf
CausalForest(fitted=True, n_estimators=50)
>>>
>>> # Estimate treatment effects
>>> cate = cf.effect(data[['X1', 'X2', 'X3']])
>>> cate.shape
(1000,)
>>> ate = float(cate.mean()) # average treatment effect
fit ¶
fit(formula: Optional[str] = None, data: Optional[DataFrame] = None, Y: Optional[ndarray] = None, T: Optional[ndarray] = None, X: Optional[ndarray] = None, W: Optional[ndarray] = None, clusters: Optional[Any] = None, Y_hat: Optional[Any] = None, W_hat: Optional[Any] = None, id: Optional[Any] = None, time: Optional[Any] = None) -> CausalForest
Fit the Causal Forest model
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
formula
|
str
|
Formula specification in the form "Y ~ T | X1 + X2 + ... [| W1 + W2 + ...]" where Y is outcome, T is treatment, X are effect modifiers, W are covariates |
None
|
data
|
DataFrame
|
Data containing all variables if using formula interface |
None
|
Y
|
array - like
|
Outcome variable (n_samples,) |
None
|
T
|
array - like
|
Treatment variable (n_samples,) |
None
|
X
|
array - like
|
Effect modifier variables (n_samples, n_features) |
None
|
W
|
array - like
|
Control variables for confounding adjustment (n_samples, n_controls) |
None
|
clusters
|
array - like
|
Cluster ids (grf |
None
|
Y_hat
|
array - like or scalar
|
Precomputed nuisances |
None
|
W_hat
|
array - like or scalar
|
Precomputed nuisances |
None
|
id
|
array - like
|
Panel unit and period identifiers for |
None
|
time
|
array - like
|
Panel unit and period identifiers for |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
self |
CausalForest
|
Fitted estimator |
get_nuisances ¶
Return the forest's internal nuisance predictions (read-only copies).
Returns:
| Type | Description |
|---|---|
dict
|
|
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(300, 2))
>>> T = rng.binomial(1, 0.5, size=300)
>>> Y = X[:, 0] * T + rng.normal(size=300)
>>> cf = sp.causal_forest(Y=Y, T=T, X=X, n_estimators=100,
... random_state=0)
>>> nu = cf.get_nuisances()
>>> nu["W_hat"].shape
(300,)
effect ¶
Estimate conditional average treatment effects
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(array - like, shape(n_samples, n_features))
|
Effect modifier variables |
required |
Returns:
| Name | Type | Description |
|---|---|---|
effects |
(array - like, shape(n_samples))
|
Estimated conditional average treatment effects |
predict ¶
Generate treatment effect predictions (required by BaseModel)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
DataFrame
|
Data containing effect modifier variables. If None, uses training data. |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Predicted treatment effects |
Notes
This method is required by the BaseModel interface. For Causal Forest,
"predictions" are treatment effect estimates rather than outcome predictions.
With data=None the GRF engine returns out-of-bag predictions for
the training rows (each row predicted only by trees that did not see
it), which is what every in-sample diagnostic should use.
effect_interval ¶
Pointwise confidence intervals for the CATE.
With the GRF engine the interval is tau(x) +/- z * sqrt(V(x))
where V(x) is the bootstrap-of-little-bags variance (requires
ci_group_size >= 2). Intervals are pointwise, not uniform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
(array - like, shape(n_samples, n_features))
|
Effect modifier variables |
required |
alpha
|
float
|
Significance level (1-alpha is confidence level) |
0.05
|
Returns:
| Name | Type | Description |
|---|---|---|
lower |
(array - like, shape(n_samples))
|
Lower bounds of confidence intervals |
upper |
(array - like, shape(n_samples))
|
Upper bounds of confidence intervals |
oob_effect ¶
Out-of-bag CATE predictions for the training rows (GRF engine).
effect_variance ¶
Little-bag variance of the CATE at X (OOB rows when None).
split_frequencies ¶
Number of splits on each covariate at each depth (GRF engine).
average_treatment_effect ¶
average_treatment_effect(X: Optional[ndarray] = None, T: Optional[ndarray] = None, target_sample: str = 'all', alpha: float = 0.05, clip: float = 0.01, variance: str = 'forest', covariates: Any = 'none', subset: Any = None) -> Dict[str, float]
GRF-style ATE/ATT/ATC/ATO aggregation of CATE predictions.
Forests with fixed effects support target_sample='treated'
only, estimated by imputation; variance applies to them (see
:func:statspai.average_treatment_effect).
The target may be given positionally, as in grf:
cf.average_treatment_effect("treated").
group_effects ¶
group_effects(by: Any = None, *, members: Any = None, n_groups: int = 4, cluster: Any = None, variance: str = 'forest', alpha: float = 0.05, scale: str = 'level', min_rows: int = 1, covariates: Any = 'none') -> DataFrame
Average effects by group; see :func:statspai.forest_group_effects.
forest_diagnostics ¶
forest_diagnostics(X: Optional[ndarray] = None, T: Optional[ndarray] = None, propensity_bounds: Tuple[float, float] = (0.05, 0.95)) -> Dict[str, object]
Overlap and CATE-distribution diagnostics for this fitted forest.
summary ¶
Return a summary of the fitted model
Returns:
| Name | Type | Description |
|---|---|---|
summary |
str
|
Model summary string |
variable_importance ¶
variable_importance(method: Optional[str] = None, decay_exponent: float = 2.0, max_depth: int = 4) -> Series
Variable importance of the causal forest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
(split, permutation)
|
A forest grown with .. versionchanged:: 1.39.0
The default is |
"split"
|
decay_exponent
|
float
|
Options of the |
2.0
|
max_depth
|
float
|
Options of the |
2.0
|
Returns:
| Type | Description |
|---|---|
Series
|
Normalised importance, indexed by covariate. The permutation measure sums to 1; the split measure sums to 1 when every counted depth has at least one split, and to less otherwise (a depth without splits contributes zero while its weight stays in the normaliser, grf's convention). |
best_linear_projection ¶
best_linear_projection(X_test: Optional[ndarray] = None, alpha: float = 0.05, clip: float = 0.01, covariates: Any = 'none') -> DataFrame
Best Linear Projection (BLP) of CATE on features (Semenova-Chernozhukov 2021).
For GRF-engine forests (the default) this is
grf::best_linear_projection: the doubly-robust scores built from
the out-of-bag CATE are regressed on (1, X) on the covariates'
original scale, with observation weights and an HC3 (cluster-robust
when the forest has clusters) covariance. X_test must then be
the training rows' effect modifiers. Legacy forests keep the
standardised-covariate HC1 regression described below.
Constructs the augmented inverse-propensity-weighted (AIPW) doubly-robust
score :math:\Gamma_i and regresses it on :math:X_i with HC1 standard
errors:
.. math:: \Gamma_i = \hat{\tau}(X_i) + \frac{T_i - \hat{e}(X_i)}{\hat{e}(X_i)(1-\hat{e}(X_i))} \bigl(Y_i - \hat{m}(X_i) - (T_i - \hat{e}(X_i))\hat{\tau}(X_i)\bigr).
:math:\Gamma_i is unbiased for :math:\tau(X_i) under standard
cross-fitting / overlap conditions; OLS of :math:\Gamma_i on
:math:(1, X_i) recovers the population BLP coefficients with
valid heteroscedasticity-robust inference (HC1).
This replaces the earlier plug-in OLS of :math:\hat{\tau}(X_i) on
:math:X_i, which produces anti-conservative SEs (the SE on a fitted
model is not the SE on the population BLP).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X_test
|
array - like
|
Features to evaluate the BLP at; defaults to in-sample X. |
None
|
alpha
|
float
|
Significance level for CIs (reported alongside coef/SE). |
0.05
|
clip
|
float
|
Propensity clip (binary discrete treatment only) to prevent the
inverse-propensity term from blowing up under near-violations of
overlap. Counts of clipped units are exposed via |
0.01
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Index |
Notes
Forests with fixed effects (fe=) have no propensity, so the
response is the imputation score Y - alpha_hat_i - gamma_hat_t
of every treated cell (unit and period effects fitted on untreated
cells) and the regression runs over treated cells; standard errors
come from the exact linear weights of that estimator, clustered by
the forest's clusters. covariates adds covariates to that
untreated model (see :func:statspai.average_treatment_effect).
See :func:statspai.forest_group_effects.
References
[@semenova2021debiased], [@athey2019generalized], [@borusyak2024revisiting]
ate ¶
Average Treatment Effect.
Returns a :class:~statspai.core.results.ScalarEffect — a
float whose value is the GRF-style doubly-robust (AIPW)
average, the quantity .se, .ci and .pvalue describe,
with the full aggregation payload in .detail and the plug-in
mean CATE in .detail['plug_in_estimate']. Printing the
result shows the inference; arithmetic and formatting behave
exactly like a bare float.
.. versionchanged:: 1.29.0
The float value was the plug-in mean CATE before 1.29; it is
now the doubly-robust estimate, so the point estimate and its
interval refer to the same quantity. See MIGRATION.md.
att ¶
Average Treatment Effect on the Treated.
Returns a :class:~statspai.core.results.ScalarEffect whose float
value is the GRF-style doubly-robust ATT score, with its own
inference attached as .se / .ci / .pvalue /
.detail and the plug-in mean CATE over treated rows in
.detail['plug_in_estimate'].
.. versionchanged:: 1.29.0
The float value was the plug-in average before 1.29. See
MIGRATION.md.
.. versionchanged:: 1.30.0
Raises :class:MethodIncompatibility for a non-binary
treatment instead of averaging over the rows with T == 1.
IVForestResult ¶
Bases: GRFFamilyForest
A fitted instrumental forest.
Attributes:
| Name | Type | Description |
|---|---|---|
late |
float
|
Average conditional LATE, |
se, ci, pvalue |
(float, tuple, float)
|
Its (cluster-robust) standard error, confidence interval and p-value. |
cate |
ndarray
|
Out-of-bag |
cate_variance |
ndarray or None
|
Little-bag variance of each out-of-bag |
n_obs |
int
|
|
detail |
dict
|
Nuisance sources, first-stage strength, compliance-score range. |
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(600, 2)); Z = rng.binomial(1, 0.5, 600)
>>> W = (Z + rng.normal(size=600) > 0.5).astype(float)
>>> Y = (1 + X[:, 0]) * W + rng.normal(size=600)
>>> fit = sp.iv_forest(y=Y, treat=W, instrument=Z, covariates=X,
... n_estimators=200)
>>> fit.cate.shape
(600,)
predict ¶
tau(x) (and optionally its variance) at newdata.
newdata=None returns the out-of-bag predictions for the
training rows.
get_scores ¶
Doubly-robust scores Gamma_i for E[tau(X)].
compliance_score (Delta(X_i)) or the full
debiasing_weights may be supplied; otherwise the stored
estimates from the fit are used.
average_treatment_effect ¶
average_treatment_effect(alpha: float = 0.05, compliance_score: Any = None, debiasing_weights: Any = None) -> Dict[str, Any]
Average conditional LATE with its (cluster-robust) SE.
best_linear_projection ¶
best_linear_projection(A: Any = None, vce: str = 'HC3', alpha: float = 0.05, compliance_score: Any = None, debiasing_weights: Any = None) -> DataFrame
Regress the doubly-robust scores on (1, A)
(grf::best_linear_projection).
LMForestResult ¶
Bases: GRFFamilyForest
A fitted linear-model forest.
Attributes:
| Name | Type | Description |
|---|---|---|
coefficients |
ndarray
|
Out-of-bag |
coefficient_variance |
ndarray or None
|
Little-bag variances, |
regressor_names, outcome_names |
list of str
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(500, 2)); W = rng.normal(size=(500, 1))
>>> Y = (1 + X[:, 0]) * W[:, 0] + rng.normal(size=500)
>>> sp.lm_forest(y=Y, regressors=W, covariates=X,
... n_estimators=100).regressor_names
['regressors']
predict ¶
{"predictions": (m, K, q)[, "variance_estimates": (m, K, q)]}.
MultiArmForestResult ¶
Bases: GRFFamilyForest
A fitted multi-arm causal forest.
Attributes:
| Name | Type | Description |
|---|---|---|
arms |
list
|
Arm labels, reference first. |
reference |
Any
|
The reference arm. |
ate, ate_se, ci, pvalue |
dict
|
Doubly-robust average effect of each non-reference arm against the reference (keyed by arm label). |
cate |
dict
|
Out-of-bag |
cate_variance |
dict or None
|
Little-bag variances of |
propensities |
DataFrame
|
Out-of-bag |
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(600, 2)); A = rng.integers(0, 3, 600)
>>> Y = X[:, 1] + (A == 1) - 0.5 * (A == 2) + rng.normal(size=600)
>>> fit = sp.multi_arm_forest(y=Y, treat=A, covariates=X, n_estimators=200)
>>> fit.contrasts
[1, 2]
PredictionForest ¶
Bases: GRFFamilyForest
A fitted regression / multi-regression / probability / quantile forest.
Attributes:
| Name | Type | Description |
|---|---|---|
predictions |
ndarray
|
Out-of-bag predictions for the training rows: |
variance |
ndarray or None
|
Out-of-bag little-bag variances (regression, multi-regression and
probability forests with |
outcome_names |
list of str
|
Outcome columns, class labels or quantile labels. |
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(300, 2)); y = X[:, 0] + rng.normal(size=300)
>>> sp.regression_forest(y=y, covariates=X, n_estimators=100).forest_type
'regression_forest'
predict ¶
predict(newdata: Any = None, estimate_variance: bool = False, quantiles: Optional[Sequence[float]] = None) -> DataFrame
Predictions at newdata (out-of-bag for the training rows
when newdata is None).
SurvivalForestResult ¶
Bases: GRFFamilyForest
A fitted survival forest.
Attributes:
| Name | Type | Description |
|---|---|---|
failure_times |
ndarray
|
Grid on which curves are reported. |
predictions |
ndarray
|
Out-of-bag survival curves of the training rows, |
prediction_type |
str
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(300, 2)); T = rng.exponential(size=300)
>>> sf = sp.survival_forest(time=T, event=np.ones(300), covariates=X,
... n_estimators=100)
>>> sf.prediction_type
'Kaplan-Meier'
predict ¶
predict(newdata: Any = None, failure_times: Any = None, prediction_type: Optional[str] = None) -> DataFrame
Survival curves at newdata (OOB for the training rows when
None), evaluated on failure_times (default: the fit grid; other
times take the curve's value at the last grid time at or below).
cate_pretrend_test ¶
cate_pretrend_test(forest: Any, *, n_groups: int = 2, leads: Optional[int] = None, groups: Any = None, time_effects: str = 'common', alpha: float = 0.05, covariates: Any = 'none') -> Dict[str, Any]
Test whether predicted-effect groups had different pre-treatment trends.
Heterogeneity found by a forest can be spurious if the units it ranks
high were already diverging before treatment [aytug2026euro]. This
test sorts units into n_groups quantiles of their mean out-of-bag
prediction (or uses groups) and, on the untreated cells only,
regresses the outcome on unit and period effects and on lead indicators
1[t - g = k] x 1[group] for k = -1, ..., -leads -- the
pre-trend regression of [borusyak2024revisiting], interacted with the
groups. Periods earlier than -leads and never-treated units are the
reference. Using untreated cells only keeps post-treatment effect
heterogeneity out of the test.
Two Wald tests are reported: all leads are zero, and the leads are
equal across groups (for each k, every group's lead equals the
first group's). The second is the test that bears on heterogeneity.
Standard errors are clustered by the forest's clusters with the CR1
factor G/(G-1) (n-1)/(n-K), where K counts the leads and the
period effects (unit effects are nested in unit clusters, fixest's
fixef.K = "nested").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
CausalForest
|
Fitted with |
required |
n_groups
|
int
|
Quantile groups of the unit-level mean OOB prediction. |
2
|
leads
|
int
|
Number of pre-treatment leads (default: |
None
|
groups
|
array - like
|
One label per training row, constant within units; overrides
|
None
|
time_effects
|
('common', 'by_group')
|
|
"common"
|
alpha
|
float
|
|
0.05
|
covariates
|
"none", "auto", list of str or array
|
Time-varying covariates added linearly, as in the imputation model
of :func: |
"none"
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Examples:
>>> import statspai as sp
>>> df = sp.datasets.currency_union_panel(seed=0)
>>> cf = sp.causal_forest(
... data=df, y="log_trade", d="euro",
... x=["pre_trade", "log_gdp_prod", "log_gdppc"],
... id="pair", time="year", fe="twoway",
... n_estimators=400, random_state=0,
... )
>>> res = sp.cate_pretrend_test(cf, n_groups=2, leads=3)
>>> sorted(res["equal_across_groups"])
['df', 'p', 'stat']
References
[borusyak2024revisiting], [aytug2026euro]
forest_group_effects ¶
forest_group_effects(forest: Any, by: Any = None, *, members: Any = None, n_groups: int = 4, cluster: Any = None, variance: str = 'forest', alpha: float = 0.05, scale: str = 'level', min_rows: int = 1, covariates: Any = 'none') -> DataFrame
Average treatment effects by group, with valid standard errors.
A causal forest's predictions are regularised: averaging them over a group gives a number that is shrunk toward the overall mean and whose spread says nothing about sampling error. This function averages an unbiased per-row signal instead and uses the forest only to form groups when asked to:
- Forests with two-way fixed effects (
fe="twoway"): the imputation scoreY - alpha_hat_i - gamma_hat_tof each treated cell, with unit and period effects fitted on the untreated cells [borusyak2024revisiting]. Each group estimate is the ATT over the group's treated cells. Standard errors use the exact linear weights of the imputation estimator; treated residuals are centred on the out-of-bag forest prediction and then on cohort x event-time means (variance="forest") or on those means only (variance="bjs", the conservative convention of :func:statspai.did_imputation). - Pooled forests: the AIPW score of every row; each estimate is the group ATE.
Rows are weighted by the forest's observation weights, so with
equalize_cluster_weights=True every cluster gets equal weight in each
group average (a different estimand from :func:statspai.did_imputation,
which weights treated cells equally).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
CausalForest
|
Fitted with the GRF engine. |
required |
by
|
(None, 'cate_quantile' or array - like)
|
|
None
|
members
|
(n, 2) array-like
|
The two members of each row of dyadic data (exporter and importer,
the two countries of a pair). With |
None
|
n_groups
|
int
|
Number of quantile groups for |
4
|
cluster
|
(None, 'dyadic' or array - like)
|
|
None
|
variance
|
('forest', 'bjs')
|
Forests with fixed effects only; see above. |
"forest"
|
alpha
|
float
|
|
0.05
|
scale
|
('level', 'percent')
|
|
"level"
|
min_rows
|
int
|
Drop groups with fewer eligible rows. |
1
|
covariates
|
"none", "auto", list of str or array
|
Forests with fixed effects only: covariates entering the untreated
outcome model linearly, |
"none"
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per group: |
Examples:
>>> import statspai as sp
>>> df = sp.datasets.currency_union_panel(seed=0)
>>> cf = sp.causal_forest(
... data=df, y="log_trade", d="euro",
... x=["pre_trade", "log_gdp_prod", "log_gdppc"],
... id="pair", time="year", fe="twoway",
... n_estimators=400, random_state=0,
... )
>>> by_country = sp.forest_group_effects(
... cf, members=df[["country_i", "country_j"]].to_numpy(),
... scale="percent",
... )
>>> by_country.index.name
'group'
References
[borusyak2024revisiting], [chernozhukov2025generic], [aronow2015cluster], [kattenberg2023causal]
forest_policy_tree ¶
forest_policy_tree(forest: Any, *, depth: int = 2, cost: float = 0.0, x: Any = None, min_leaf_size: Optional[int] = None, n_splits: int = 21, train_frac: float = 0.5, random_state: int = 0, members: Any = None, alpha: float = 0.05, variance: str = 'bjs', cluster: Any = None, covariates: Any = 'none') -> Dict[str, Any]
A treatment rule for a fixed-effects forest, and what it was worth.
Policy learning [athey2021policy] maximises
:math:V(\pi) = E[\Gamma_i \pi(X_i)] over a class of rules, with
:math:\Gamma a score unbiased for the individual effect. A
within-unit design has no propensity, so the doubly-robust score
:func:statspai.policy_tree uses does not exist; the imputation scores
that give the ATT do, and the rule is learned on those.
Read the population it applies to. Those scores live on treated
cells, so this is a retrospective rule: "of the cells that were
treated, which should have been". It does not say whether an untreated
unit should be treated -- that is an extrapolation of tau(x), which
:func:statspai.average_treatment_effect also refuses for these
forests. Check :func:statspai.forest_support before carrying the rule
to units the design never switched.
With cost=0 and effects that are positive everywhere, treating
every cell is optimal and the tree has nothing to find. cost is
what usually makes targeting a real question: the rule then treats
where tau > cost.
The rule is fitted and priced on disjoint units. A tree chosen to
maximise the value of the very scores it is then priced against is
priced on its own noise. Measured on a design where every rule is
worth exactly the same -- no heterogeneity, and cost equal to the
constant effect, so the true gain over treating everyone is exactly 0
(200 replications, N = 200 units, T = 8) -- the same-sample gain
averaged +0.048, larger than its own standard error of 0.037, and
13.0% of runs reported a significant benefit from targeting where
none existed. Fitted and priced on disjoint halves, with the default
21 splits (400 replications): +0.008 and 0.0%. The split costs
little when the heterogeneity is real: with tau = 0.3 + 0.8 z and
cost = 0.3 the oracle gain is 0.8 * phi(0) = 0.3192, and the
split-sample estimate averaged 0.307 and was significant in every
replication; the same-sample estimate overshoots the oracle (0.3316 in
the earlier study). The split is therefore not optional here;
train_frac moves it, nothing switches it off.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted CausalForest with ``fe="twoway"``
|
Supplies the data and the hyper-parameters; it is not itself used to fit or to price the rule, and is left untouched. |
required |
depth
|
int
|
Tree depth. |
2
|
cost
|
float
|
Cost of treating one cell, in the outcome's units. The rule treats where the effect exceeds it. |
0
|
x
|
list of str or array
|
Policy covariates. Defaults to the forest's effect modifiers. Keep this list short and interpretable -- it is the rule a programme would actually be written in. |
None
|
min_leaf_size
|
int
|
Smallest leaf; default |
None
|
train_frac
|
float
|
Share of units (or, with |
0.5
|
random_state
|
int
|
|
0
|
members
|
array - like
|
The two members of each dyadic row; splitting is then by member. |
None
|
alpha
|
float
|
As :func: |
0.05
|
variance
|
float
|
As :func: |
0.05
|
cluster
|
float
|
As :func: |
0.05
|
covariates
|
float
|
As :func: |
0.05
|
Returns:
| Type | Description |
|---|---|
dict
|
|
See Also
statspai.policy_tree : the doubly-robust version, for designs with a propensity. statspai.rate_split : is any ranking worth targeting on? statspai.forest_support : is a rule safe to carry to untreated units?
References
[@athey2021policy], [@borusyak2024revisiting], [@kattenberg2023causal]
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> rows = []
>>> for i in range(120):
... a, z = rng.normal(), rng.normal()
... g = 4 if i % 3 else 10**6
... for t in range(1, 8):
... d = 1.0 * (t >= g)
... y = a + 0.2 * t + (0.3 + 0.8 * z) * d + rng.normal(0, 0.5)
... rows.append((i, t, y, d, z))
>>> df = pd.DataFrame(rows, columns=["id", "t", "y", "d", "z"])
>>> cf = sp.causal_forest(
... "y ~ d | z", data=df, fe="twoway", unit="id", time="t",
... clusters=df["id"].to_numpy(), n_estimators=100, random_state=0,
... )
>>> res = sp.forest_policy_tree(cf, depth=1, cost=0.3, n_splits=3)
>>> 0.0 <= res["share_treated"] <= 1.0
True
>>> gain = res["gain_over_treat_all"]
>>> sorted(k for k in gain if k in {"estimate", "se", "ci_low", "ci_high"})
['ci_high', 'ci_low', 'estimate', 'se']
>>> gain["n_splits"], res["n_splits"]
(3, 3)
>>> sorted(res["diagnostics"]["split_stability"])[:2]
['root_covariate_counts', 'root_covariate_modal_share']
Many splits, not one. One split is a draw: it fixes both the rule
and its price, and with random_state in reach it is easy to keep
the pair that agrees with you -- the practice
[chernozhukov2025generic] show invalidates inference. n_splits
aggregates by their variational estimation and inference (VEIN): the
value, the treat-all value and the gain are each the median over
splits, their intervals are medians of conditional intervals built at
1 - alpha / 2 (whose median covers at 1 - alpha), and the
p-values are twice the median conditional p-value. Each is aggregated
on its own, as [chernozhukov2025generic] do, so with n_splits > 1
the three medians do not satisfy
gain = value - value_treat_all -- a median of differences is not a
difference of medians. That identity holds within each split, and
n_splits=1 reports it directly. A rule cannot be
averaged either, so the one reported is the rule from the split whose
gain is the median, and diagnostics['split_stability'] says how often each
covariate was chosen at the root and how far the thresholds and treated
shares moved -- if the root covariate changes from split to split, the
rule is not identified by this sample however tight the interval on its
value looks. n_splits=21 costs about twelve seconds on a 150-unit
panel; n_splits=1 reproduces one conditional split and warns.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_splits
|
int
|
Splits to aggregate by VEIN. Odd, so the median is an order statistic. |
21
|
random_state
|
int
|
Seeds the sequence of splits; split |
0
|
forest_support ¶
forest_support(forest: Any, X_new: Any, *, k: int = 10, quantile: float = 0.95, alpha: float = 0.05) -> DataFrame
Check whether new rows lie inside the support that informs tau(x).
Predicting the effect a unit would have had (a country that did not
adopt a currency, a region that was never treated) applies the fitted
tau(x) outside the treated sample. That is a model-based
extrapolation, credible only where the forest has seen similar units
switch treatment. For every row of X_new this reports
cate, its little-bagseand a(1 - alpha)interval;n_outside_range-- effect modifiers outside the[min, max]of the reference rows;knn_ratio-- the distance (on reference-standardised modifiers) to thek-th nearest reference row, divided by thequantileof the reference rows' distance to theirk-th nearest row from a different unit (rows of the same unit are excluded, so a unit's other periods do not shrink the benchmark). Above 1 the row is farther from the data than all but1 - quantileof the reference rows are from other units;supported--n_outside_range == 0andknn_ratio <= 1.
The reference rows are the rows that inform the effect: for a forest with fixed effects, the rows of units whose treatment varies (only they identify the within effect); for a pooled forest, all training rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
CausalForest
|
Fitted with the GRF engine. |
required |
X_new
|
array - like or DataFrame
|
Effect modifiers of the rows to predict (DataFrame columns are matched to the fitted feature names). |
required |
k
|
int
|
|
10
|
quantile
|
float
|
|
0.95
|
alpha
|
float
|
|
0.05
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per row of |
Examples:
>>> import statspai as sp
>>> df = sp.datasets.currency_union_panel(seed=0)
>>> x = ["pre_trade", "log_gdp_prod", "log_gdppc"]
>>> cf = sp.causal_forest(
... data=df, y="log_trade", d="euro", x=x,
... id="pair", time="year", fe="twoway",
... n_estimators=400, random_state=0,
... )
>>> outs = df[df["ever_euro"] == 0]
>>> sup = sp.forest_support(cf, outs[x])
>>> bool(sup["supported"].mean() > 0)
True
rate_split ¶
rate_split(forest: Any, target: str = 'AUTOC', *, n_splits: int = 21, train_frac: float = 0.5, random_state: int = 0, members: Any = None, alpha: float = 0.05, variance: str = 'bjs', cluster: Any = None, covariates: Any = 'none', se_method: str = 'auto', q_grid: int = 100) -> Dict[str, Any]
RATE of the forest's targeting rule, fitted and evaluated on disjoint units.
:func:statspai.rate ranks the rows it also scores. For a forest with
fixed effects that is not a valid test even though the predictions are
out-of-bag: every imputation score carries :math:-\hat\gamma_t,
estimated from the same periods the forest was trained on, so the
ranking and the scores stay correlated. Measured on a design with no
heterogeneity whatsoever (400 replications, N = 150 units, T = 8,
staggered adoption selected on the unit effect, tau = 0.3): AUTOC
averaged -0.028 instead of 0 and a nominal 5% test rejected
16.0% of the time; QINI averaged -0.006 and rejected 12.2%.
This function removes the overlap. The units (or, with members=, the
nodes of a dyadic panel) are split in two; a forest with the same
hyper-parameters is refitted on each half; the training half's forest
ranks the evaluation half's treated cells, whose imputation scores come
from an untreated two-way model fitted on the evaluation half alone.
Nothing the rule saw enters the score it is graded on. On the same null
design, with the default 21 splits, AUTOC then averaged -0.0002 and
rejected 0.5% of the time (QINI +0.0004 and 0.0%): the aggregation
over splits is conservative by construction, so the test errs on the
side of not rejecting. Against tau = 0.3 + 0.5 z it rejected in
every replication. There the estimate averaged 0.383 for AUTOC where
the ideal ranking has 0.452, because a forest grown on half the units
ranks imperfectly: the quantity estimated is the RATE of the rule that
was fitted, not of the best rule. variance='forest' gives the same
rejection rates under the null but narrower intervals under the
alternative (they covered the ideal ranking's AUTOC 89.5% of the time
against 94.5%), which is why 'bjs' is the default here. The study
is tests/reliability/forest_split_evaluation.py; single-split
figures quoted through 1.39.3 (7.5% and 4.0%) came from
halves that were not refitted the way the forest was.
The price is sample: each forest sees half the units, so the rule is
noisier than the one fitted on everything and the RATE it earns is a
lower bound on what the full-sample rule is worth. Report
:func:statspai.rate for the point estimate of the full rule if you
like, but report this one when the claim is that targeting pays.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted CausalForest
|
Supplies the data and the hyper-parameters; it is not itself used to rank or to score, and is left untouched. |
required |
target
|
('AUTOC', 'QINI')
|
|
'AUTOC'
|
train_frac
|
float
|
Share of units (members) that fit the rule. The rest evaluate it. |
0.5
|
random_state
|
int
|
Seed for the split. The refitted forests keep the original's seed. |
0
|
members
|
array - like
|
The two members of each dyadic row. Splitting is then by member and rows that straddle the two halves are dropped (counted in the result), because a flow between a training and an evaluation country belongs to neither. |
None
|
alpha
|
float
|
Passed to :func: |
0.05
|
variance
|
float
|
Passed to :func: |
0.05
|
cluster
|
float
|
Passed to :func: |
0.05
|
covariates
|
float
|
Passed to :func: |
0.05
|
se_method
|
float
|
Passed to :func: |
0.05
|
q_grid
|
float
|
Passed to :func: |
0.05
|
Returns:
| Type | Description |
|---|---|
dict
|
As :func: |
See Also
statspai.rate : the same curve without the split (a diagnostic). statspai.forest_group_effects : effects for named groups rather than a curve.
References
[@yadlowsky2025evaluating]
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> rows = []
>>> for i in range(80):
... a, z = rng.normal(), rng.normal()
... g = 4 if i % 3 else 10**6
... for t in range(1, 7):
... d = 1.0 * (t >= g)
... y = a + 0.2 * t + (0.3 + 0.5 * z) * d + rng.normal(0, 0.5)
... rows.append((i, t, y, d, z))
>>> df = pd.DataFrame(rows, columns=["id", "t", "y", "d", "z"])
>>> cf = sp.causal_forest(
... "y ~ d | z", data=df, fe="twoway", unit="id", time="t",
... clusters=df["id"].values, n_estimators=100, random_state=0,
... )
>>> res = sp.rate_split(cf, target="AUTOC")
>>> res["split_by"], res["n_eval_units"] > 0
('units', True)
Many splits, not one. A single split is a draw, not an estimator:
on the guide's 15-country trade panel six of them ranged over +/-0.08,
and with random_state in reach it is easy to keep the one that
agrees with you -- the practice [chernozhukov2025generic] show
invalidates inference. Their variational estimation and inference
(VEIN) is what n_splits does: the estimate is the median over
splits, the interval is the median of the conditional intervals built
at 1 - alpha / 2 (whose median covers at 1 - alpha), and the
p-value is twice the median conditional p-value. n_splits=21 costs
about twelve seconds on a 150-unit panel; 100, as they use, costs about
a minute. n_splits=1 reproduces a single conditional split and
warns. estimate_min / estimate_max / estimate_iqr in the
result show how much the split was moving the answer.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
Any
|
|
required |
target
|
Any
|
|
required |
train_frac
|
Any
|
|
required |
members
|
Any
|
|
required |
alpha
|
Any
|
|
required |
variance
|
Any
|
|
required |
cluster
|
Any
|
|
required |
covariates
|
Any
|
As described above. |
'none'
|
se_method
|
Any
|
As described above. |
'none'
|
q_grid
|
Any
|
As described above. |
'none'
|
n_splits
|
int
|
Splits to aggregate by VEIN. Odd, so the median is an order statistic. |
21
|
random_state
|
int
|
Seeds the sequence of splits; split |
0
|
average_treatment_effect ¶
average_treatment_effect(forest: 'CausalForest', X: Optional[ndarray] = None, T: Optional[ndarray] = None, target_sample: str = 'all', alpha: float = 0.05, clip: float = 0.01, variance: str = 'forest', covariates: Any = 'none', subset: Any = None) -> Dict[str, Any]
Aggregate CATE predictions into ATE/ATT/ATC/ATO targets.
This mirrors the most-used grf::average_treatment_effect targets:
"all" (ATE), "treated" (ATT), "control" (ATC), and
"overlap" (ATO, weighted by e(X)(1-e(X))).
The estimate is the doubly-robust AIPW influence-function mean
(the estimator grf reports), not a plug-in average of the CATE
predictions. Using the forest's own cross-fitted nuisances
:math:\hat m(X)=\hat E[Y\mid X] and :math:\hat e(X)=\hat
E[T\mid X], the ATE score is
.. math:: \Gamma_i = \hat\tau(X_i) + \frac{T_i-\hat e(X_i)}{\hat e(X_i)(1-\hat e(X_i))} \bigl(Y_i-\hat m(X_i)-(T_i-\hat e(X_i))\hat\tau(X_i)\bigr),
(:func:aipw_scores). ATT / ATC follow grf: the plug-in CATE
mean over the target arm plus a Hajek-normalised doubly-robust
correction, with the two variance components added
(:func:grf_att_atc). se for the ATE is the influence-function
standard error :math:\mathrm{sd}(\Gamma)/\sqrt n. The overlap
target (ATO) is grf's Robinson / R-learner regression of
Y - Y_hat on W - W_hat with an intercept and an HC3 standard
error (:func:grf_overlap_ate); it needs no propensity inversion and
is reported for continuous treatments as well. Given the same forest
outputs all four targets reproduce grf exactly
(tests/reference_parity/test_ml_causal_R_parity.py).
For forests fitted with the GRF engine (the default) the CATE entering
the score is the out-of-bag prediction, observation weights follow
equalize_cluster_weights, standard errors are cluster-robust when
the forest has clusters, and continuous treatments use grf's
debiasing weights -- the definitions used by
grf::average_treatment_effect. X / T may only restate the
training data.
Forests with fixed effects (fe=) have no propensity. For them only
target_sample='treated' is identified, and it is estimated by
imputation [borusyak2024revisiting]: unit and period effects are fitted
on the untreated cells, each treated cell's effect is imputed as
Y - alpha_hat_i - gamma_hat_t, and the ATT is their mean. The
standard error uses the exact linear weights of that estimator,
clustered by the forest's clusters; variance chooses how treated
residuals are centred ('forest': out-of-bag forest prediction, then
cohort x event-time means; 'bjs': cohort x event-time means only,
the conservative Borusyak-Jaravel-Spiess convention, identical to
:func:statspai.did_imputation). The payload also reports
forest_plug_in, the mean OOB prediction over the same cells, which
is shrunk toward zero when effects are heterogeneous.
.. versionchanged:: 1.30.0
Forests with fixed effects return the imputation ATT for
target_sample='treated' instead of raising.
For other forests the doubly-robust scores exist only for the training
sample; for other rows the function falls back to the plug-in CATE
average, sets method='plug_in' and warns.
.. versionchanged:: 1.30.0
target_sample='overlap' is grf's R-learner estimator for
every forest. Earlier releases averaged the AIPW scores with
e(1-e) weights for legacy-engine forests, a different (also
consistent) estimator of the same estimand.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted CausalForest
|
|
required |
X
|
arrays
|
Rows to aggregate over (default: the training sample). |
None
|
T
|
arrays
|
Rows to aggregate over (default: the training sample). |
None
|
target_sample
|
('all', 'treated', 'control', 'overlap')
|
|
'all'
|
alpha
|
float
|
|
0.05
|
clip
|
float
|
Propensity scores are clipped to |
0.01
|
variance
|
('forest', 'bjs')
|
Forests with fixed effects only (see above); ignored otherwise. |
'forest'
|
covariates
|
'none', 'auto', list of str or array
|
Forests with fixed effects only: covariates in the untreated outcome
model |
'none'
|
subset
|
boolean mask or row positions
|
Average over some of the training rows only, as
|
None
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 400
>>> X = rng.normal(size=(n, 3))
>>> T = rng.binomial(1, 0.5, size=n)
>>> tau = 1.0 + X[:, 0] # heterogeneous effect
>>> Y = X[:, 1] + tau * T + rng.normal(scale=0.5, size=n)
>>> df = pd.DataFrame({
... "y": Y, "d": T,
... "x0": X[:, 0], "x1": X[:, 1], "x2": X[:, 2],
... })
>>> cf = sp.causal_forest(
... data=df, formula="y ~ d | x0 + x1 + x2",
... n_estimators=50, random_state=0,
... )
>>> ate = sp.average_treatment_effect(cf, target_sample="all")
>>> ate["estimand"]
'ATE'
>>> att = sp.average_treatment_effect(cf, target_sample="treated")
>>> att["estimand"]
'ATT'
calibrate_cate ¶
calibrate_cate(forest: 'CausalForest', newdata: Optional[ndarray] = None, alpha: float = 0.05, method: str = 'auto', covariates: Any = 'none') -> Dict[str, Any]
Rescale CATE predictions by their best-linear-predictor calibration.
Forest CATE predictions are shrunk toward their mean: leaf averaging and
honest subsampling regularise, so the spread of tau_hat understates
the spread of tau. The calibration regression of
:func:calibration_test estimates the slope beta_differential of
the effect signal on the (out-of-bag) predictions; this function returns
tau_cal(x) = beta_mean * mean(tau_oob)
+ beta_differential * (tau_hat(x) - mean(tau_oob))
for the training rows (newdata=None, out-of-bag) or for new rows.
Because the slope is estimated on out-of-bag predictions it tends to 0
when the effect is homogeneous, so the correction does not manufacture
heterogeneity.
Forests with two-way fixed effects and a binary treatment
(method='auto' or 'imputation'): the slope comes from
regressing imputation scores
on the out-of-bag predictions over the treated cells (see
:func:calibration_test). The scores are unbiased for each cell's
effect, so the slope is a genuine de-attenuation factor, and
tau_bar is the mean prediction over treated cells, so
beta_mean * tau_bar is the imputation ATT. In StatsPAI's
simulations (N = 300 units, T = 8, staggered adoption selected on the
unit effect, 200 replications) this reduced the RMSE of the treated
cells' predictions from 0.634 to 0.570 with effects (1 + x1)(1 +
0.2 e) and from 0.213 to 0.118 with a constant effect. The map is
estimated on treated cells; applying it to other rows assumes it
carries over.
method='within' uses globally within-transformed variables, as
proposed by [@aytug2026attenuated]. Global two-way demeaning is exact
only when the effect is homogeneous, so the heterogeneity test keeps
its size, but the slope is not a de-attenuation factor: in StatsPAI's
simulations (N = 300, T = 6) the OOB predictions had a slope of
0.65-0.91 on the true effect while beta_differential stayed at
0.88-1.07, and calibrated predictions did not reduce RMSE. A warning
is emitted in that case.
.. versionchanged:: 1.30.0
Forests with fixed effects and a binary treatment calibrate against
imputation scores by default (method='within' restores the
earlier regression).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
CausalForest
|
A forest fitted with the GRF engine ( |
required |
newdata
|
array - like
|
Effect-modifier rows to calibrate. |
None
|
alpha
|
float
|
Level for the reported slope confidence intervals. |
0.05
|
method
|
('auto', 'imputation', 'within')
|
Forests with fixed effects only; see :func: |
'auto'
|
covariates
|
'none', 'auto', list of str or array
|
Imputation regression only; see :func: |
'none'
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 1000
>>> X = rng.normal(size=(n, 3))
>>> T = rng.integers(0, 2, n)
>>> Y = X[:, 1] + (1 + X[:, 0]) * T + rng.normal(size=n)
>>> cf = sp.causal_forest(Y=Y, T=T, X=X, n_estimators=400, random_state=0)
>>> cal = sp.calibrate_cate(cf)
>>> cal["cate"].shape
(1000,)
References
[@chernozhukov2025generic], [@aytug2026attenuated]
calibration_test ¶
calibration_test(forest: 'CausalForest', X: Optional[ndarray] = None, Y: Optional[ndarray] = None, T: Optional[ndarray] = None, alpha: float = 0.05, vce: str = 'HC3', method: str = 'auto', covariates: Any = 'none') -> DataFrame
Best-linear-predictor calibration test of CATEs [@chernozhukov2025generic].
The form of grf::test_calibration: the outcome residual
Y - Y_hat is regressed (no intercept) on the treatment residual
scaled by the mean forest prediction and on the treatment residual
times the demeaned prediction,
Y - Y_hat = b1 (W - W_hat) mean(tau_hat)
+ b2 (W - W_hat) (tau_hat - mean(tau_hat)) + e.
b1 = 1 indicates a calibrated mean prediction; b2 = 1 a
calibrated heterogeneity signal, and a significantly positive b2
is evidence of heterogeneity -- the headline finding, showing the
forest captures real heterogeneity rather than noise. Following
grf, t tests each coefficient against 0 and p is
one-sided (alternative > 0) from Student's t with n - 2
degrees of freedom; the default vce='HC3' is grf's.
beta_differential is also the slope by which the predictions
should be rescaled around their mean (see :func:calibrate_cate).
For forests fitted with the GRF engine (the default) tau_hat is the
out-of-bag prediction, the regression uses the forest's observation
weights and a cluster-robust covariance when the forest has clusters,
and the test is defined on the training sample, so X / Y / T
may be omitted (the stored arrays are used) and, if given, must equal
them. The regression itself is the pure operator
:func:grf_calibration.
Forests with two-way fixed effects (fe='twoway') and a binary
treatment use method='imputation' by default: the imputation score
Y - alpha_hat_i - gamma_hat_t of each treated cell (unit and period
effects fitted on untreated cells, [borusyak2024revisiting]) is
regressed on mean(tau_oob) and tau_oob - mean(tau_oob) over the
treated cells. The score is unbiased for each cell's effect, so
beta_differential is the best-linear-predictor slope of the true
effect on the forest prediction (it corrects the forest's shrinkage)
and the one-sided test of it is a heterogeneity test. Standard errors
use the exact linear weights of the imputation estimator, clustered by
the forest's clusters, with a normal reference; vce does not apply.
method='within' keeps the earlier regression on globally
within-transformed variables [aytug2026attenuated], which tests for
heterogeneity but whose slope is not a de-attenuation factor.
.. versionchanged:: 1.30.0
method= / covariates= added; forests with two-way fixed
effects and a binary treatment default to the imputation regression.
t / p are grf's: each coefficient against 0, one-sided,
Student t (the null column is gone; t_vs_zero /
p_one_sided are kept as synonyms). Legacy-engine forests use
grf's regression too: earlier releases regressed an
inverse-propensity pseudo-outcome on [tau_hat, tau_hat -
mean(tau_hat)], so differential_forest_prediction estimated
b2 - b1 rather than b2, and paired rows other than the
training sample with mean-of-Y / mean-of-T stand-in nuisances.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted CausalForest
|
|
required |
X
|
optional arrays
|
The training sample (defaults to the stored arrays). |
None
|
Y
|
optional arrays
|
The training sample (defaults to the stored arrays). |
None
|
T
|
optional arrays
|
The training sample (defaults to the stored arrays). |
None
|
alpha
|
float
|
Level for |
0.05
|
vce
|
('HC3', 'HC2', 'HC1', 'HC0')
|
Heteroskedasticity-robust covariance ( |
'HC3'
|
method
|
('auto', 'imputation', 'within')
|
Forests with fixed effects only: |
'auto'
|
covariates
|
'none', 'auto', list of str or array
|
Covariates in the untreated outcome model of the imputation
regression (see :func: |
'none'
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Rows |
Examples:
sp.test_calibration is an alias of this function.
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 400
>>> X = rng.normal(size=(n, 3))
>>> T = rng.binomial(1, 0.5, size=n)
>>> tau = 1.0 + X[:, 0] # heterogeneous effect
>>> Y = X[:, 1] + tau * T + rng.normal(scale=0.5, size=n)
>>> df = pd.DataFrame({
... "y": Y, "d": T,
... "x0": X[:, 0], "x1": X[:, 1], "x2": X[:, 2],
... })
>>> cf = sp.causal_forest(
... data=df, formula="y ~ d | x0 + x1 + x2",
... n_estimators=50, random_state=0,
... )
>>> ct = sp.calibration_test(cf)
>>> ct.index.tolist()
['mean_forest_prediction', 'differential_forest_prediction']
>>> sp.test_calibration is sp.calibration_test
True
References
[@chernozhukov2025generic], [@athey2019generalized]
forest_diagnostics ¶
forest_diagnostics(forest: 'CausalForest', X: Optional[ndarray] = None, T: Optional[ndarray] = None, propensity_bounds: Tuple[float, float] = (0.05, 0.95)) -> Dict[str, object]
Return overlap and CATE-distribution diagnostics for a fitted forest.
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 400
>>> X = rng.normal(size=(n, 3))
>>> T = rng.binomial(1, 0.5, size=n)
>>> tau = 1.0 + X[:, 0] # heterogeneous effect
>>> Y = X[:, 1] + tau * T + rng.normal(scale=0.5, size=n)
>>> df = pd.DataFrame({
... "y": Y, "d": T,
... "x0": X[:, 0], "x1": X[:, 1], "x2": X[:, 2],
... })
>>> cf = sp.causal_forest(
... data=df, formula="y ~ d | x0 + x1 + x2",
... n_estimators=50, random_state=0,
... )
>>> diag = sp.forest_diagnostics(cf)
>>> bool(diag["n_treated"] + diag["n_control"] == diag["n"])
True
grf_calibration ¶
grf_calibration(*, Y: ndarray, W: ndarray, Y_hat: ndarray, W_hat: ndarray, tau_hat: ndarray, vce: str = 'HC3', alpha: float = 0.05, weights: Optional[ndarray] = None, clusters: Optional[ndarray] = None) -> DataFrame
Calibration test of any CATE predictions [@chernozhukov2025generic].
The grf::test_calibration regression, given its inputs rather than
a fitted forest. Fits, by (weighted) least squares without an
intercept,
.. math:: Y_i - \hat m(X_i) = \beta_1\,(W_i - \hat e(X_i))\,\bar\tau + \beta_2\,(W_i - \hat e(X_i))\,(\hat\tau(X_i) - \bar\tau) + \varepsilon_i,
with :math:\bar\tau the (observation-weighted) mean CATE prediction.
beta_1 = 1 says the mean prediction is calibrated; beta_2 = 1
says the heterogeneity is calibrated, and beta_2 > 0 significantly
is evidence that the predictions order units by their effect at all.
:func:calibration_test runs this regression on a fitted forest's own
out-of-bag predictions. Call this function when the predictions come
from somewhere else: a forest fitted on other folds, a meta-learner, or
a forest whose clustering should be imposed after the fact. The test
is only valid if tau_hat[i] (and Y_hat[i], W_hat[i]) was
not fitted on row i -- or, with clustered data, on any row of
i's cluster. In-sample predictions have learned the noise in
Y and make beta_2 look significant.
t tests each coefficient against 0 and p is the
one-sided p-value for the alternative beta > 0 from Student's
t with n - 2 degrees of freedom -- grf's reporting rule
(lmtest::coeftest then halving). vce is grf's
vcov.type (default "HC3"); grf passes it to
sandwich::vcovCL with clusters (one cluster per observation
when there are none), which is what the covariance here reproduces
(:func:statspai.forest._grf_inference.cluster_robust_vcov).
ci_low / ci_high use the same t quantile. t_vs_zero and
p_one_sided repeat t and p under their earlier names.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
Y
|
array-like of shape (n,)
|
Outcome and treatment. |
required |
W
|
array-like of shape (n,)
|
Outcome and treatment. |
required |
Y_hat
|
array-like of shape (n,)
|
Out-of-sample predictions of |
required |
W_hat
|
array-like of shape (n,)
|
Out-of-sample predictions of |
required |
tau_hat
|
array-like of shape (n,)
|
Out-of-sample CATE predictions to grade. |
required |
vce
|
('HC3', 'HC2', 'HC1', 'HC0')
|
Sandwich covariance type, clustered when |
'HC3'
|
alpha
|
float
|
Level for |
0.05
|
weights
|
array-like of shape (n,)
|
Non-negative observation weights; rows with weight 0 are left out.
|
None
|
clusters
|
array-like of shape (n,)
|
Cluster labels for the covariance. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Index |
Examples:
Grade forests that did not see a school when predicting for it: fit on four folds of schools, predict the fifth, then test with a covariance clustered by school.
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n, n_schools = 600, 30
>>> school = rng.integers(0, n_schools, size=n)
>>> X = rng.normal(size=(n, 3))
>>> W = rng.binomial(1, 0.5, size=n).astype(float)
>>> Y = X[:, 1] + (1.0 + X[:, 0]) * W + rng.normal(size=n)
>>> kw = dict(n_estimators=100, random_state=0)
>>> fold = school % 5
>>> Y_hat, W_hat, tau_hat = np.empty(n), np.empty(n), np.empty(n)
>>> for k in range(5):
... tr, te = fold != k, fold == k
... my = sp.regression_forest(y=Y[tr], covariates=X[tr], **kw)
... mw = sp.regression_forest(y=W[tr], covariates=X[tr], **kw)
... Y_hat[te] = np.ravel(my.predict(X[te]))
... W_hat[te] = np.ravel(mw.predict(X[te]))
... cf = sp.causal_forest(Y=Y[tr], T=W[tr], X=X[tr], **kw)
... tau_hat[te] = cf.effect(X[te])
>>> out = sp.grf_calibration(Y=Y, W=W, Y_hat=Y_hat, W_hat=W_hat,
... tau_hat=tau_hat, clusters=school)
>>> out.index.tolist()
['mean_forest_prediction', 'differential_forest_prediction']
References
[@chernozhukov2025generic], [@athey2019generalized]
honest_variance ¶
honest_variance(forest: 'CausalForest', X: Optional[ndarray] = None, n_splits: int = 25, seed: Optional[int] = None) -> Dict[str, float]
Half-sample spread of the mean CATE prediction (deprecated).
Draws n_splits random halves (without replacement) of the CATE
predictions and reports the standard deviation of the half-sample
means as se. For a half-sample drawn without replacement the
finite-population correction is exactly one half, so this spread
estimates the sampling standard error of the full-sample mean,
sd(tau_hat) / sqrt(n).
It is descriptive: the predictions are held fixed, so the forest's
own estimation error is not propagated. grf has no counterpart to
this function. For GRF-engine forests with X omitted it returns
the doubly-robust ATE of :func:average_treatment_effect and its
influence-function SE instead; use that function directly.
.. versionchanged:: 1.30.0
se was the standard deviation of the half-sample means divided
by sqrt(n_splits) -- the Monte Carlo error of their average,
which shrinks to zero as n_splits grows. It is now the spread
itself.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted CausalForest
|
|
required |
X
|
ndarray
|
|
None
|
n_splits
|
int
|
Number of random half-sample draws. |
25
|
seed
|
int
|
|
None
|
Returns:
| Type | Description |
|---|---|
dict with ``ate``, ``se``, ``ci_low``, ``ci_high`` (95 %).
|
|
Examples:
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 400
>>> X = rng.normal(size=(n, 3))
>>> T = rng.binomial(1, 0.5, size=n)
>>> tau = 1.0 + X[:, 0] # heterogeneous effect
>>> Y = X[:, 1] + tau * T + rng.normal(scale=0.5, size=n)
>>> df = pd.DataFrame({
... "y": Y, "d": T,
... "x0": X[:, 0], "x1": X[:, 1], "x2": X[:, 2],
... })
>>> cf = sp.causal_forest(
... data=df, formula="y ~ d | x0 + x1 + x2",
... n_estimators=50, random_state=0,
... )
>>> hv = sp.honest_variance(cf, n_splits=25, seed=0)
>>> bool(hv["se"] >= 0 and hv["ci_low"] <= hv["ate"] <= hv["ci_high"])
True
rate ¶
rate(forest: 'CausalForest', X: Optional[ndarray] = None, Y: Optional[ndarray] = None, T: Optional[ndarray] = None, target: str = 'AUTOC', q_grid: int = 100, alpha: float = 0.05, seed: Optional[int] = None, priorities: Optional[ndarray] = None, se_method: str = 'auto', n_bootstrap: int = 200, variance: str = 'bjs', cluster: Any = None, members: Any = None, covariates: Any = 'none') -> Dict[str, Any]
Rank-weighted average treatment effect (RATE) [@yadlowsky2025evaluating].
``grf::rank_average_treatment_effect``: evaluates a prioritisation
rule -- by default the forest's own CATE predictions (out-of-bag for
GRF-engine forests), or any ``priorities`` vector (e.g. from a model
trained on another sample) -- on the forest's doubly-robust scores
(:func:`aipw_scores`, ``grf::get_scores``). Units are sorted by
decreasing priority, scores are averaged within tied priorities, and
- **AUTOC** = mean over ``k`` of ``TOC_k``,
- **QINI** = mean over ``k`` of ``(k/n) TOC_k``,
with ``TOC_k`` the mean score of the top ``k`` units minus the overall
mean -- ``grf``'s estimator, reproduced exactly given the same scores
and priorities (the pure operator :func:`rate_from_scores`).
Standard error: ``se_method='influence'`` (default) is the analytic
influence-function SE including the term from estimating the priority
ranks, summed within clusters when the forest has clusters;
``se_method='half_sample'`` is ``grf``'s half-sample bootstrap with
``n_bootstrap`` draws (``R`` in ``grf``, default 200; whole clusters
are drawn), seeded by ``seed``. The two agree to Monte Carlo error.
Because priorities and evaluation share one sample when
``priorities`` is omitted, RATE is then a *diagnostic*; for a
pre-registered test of targeting value, fit the forest on a training
split and evaluate priorities on a held-out split.
**Forests with fixed effects** have no propensity, so the curve is read
on the population they identify -- the treated cells -- through the
imputation scores :math:`\Gamma_{it} = Y_{it} - \hat\alpha_i -
\hat\gamma_t` that :func:`statspai.average_treatment_effect` already
uses there: ``TOC(q) = ATT(top q by priority) - ATT``, a
*retrospective* targeting curve ("the effect was this much larger among
the cells the rule would have prioritised"), not the population RATE a
randomised design gives. ``variance=``, ``cluster=``, ``members=`` and
``covariates=`` are then passed through exactly as in
:func:`statspai.forest_group_effects`, and ``se_method`` becomes:
- ``'imputation'`` (what ``'auto'`` selects): the rank weights and the
imputation weights are both linear, so their composition makes RATE
one more linear functional of ``y`` with the same exact, cluster- or
dyad-robust variance as the ATT. The weights annihilate the fixed
effects exactly (``max |Z'V| < 1e-15`` in the committed fixture) and
sum to zero over treated cells, RATE being a contrast.
- ``'influence'``: the rank-corrected influence function applied to the
imputation scores -- it pays for estimating the ranking but treats
the fixed effects as known.
``variance`` defaults to ``'bjs'`` here, not to the ``'forest'`` that
:func:`statspai.average_treatment_effect` uses, and the reason is
measured. ``'forest'`` centres treated residuals on the out-of-bag
prediction, which nets out the effect heterogeneity and so estimates
the variance of the RATE *of this sample*; the ATT converges to its
population value fast enough that this does not show, but RATE loads on
the tail of the effect distribution and it does. Over 120 replications
(N = 150 units, T = 8, staggered adoption selected on the unit effect,
tau = 0.3 + 0.5 z, priorities held out) nominal 95% intervals covered
the *population* AUTOC 97.5% of the time with ``'bjs'`` and 90.8% with
``'forest'``; against the RATE of the realised sample it was the other
way round, 99.2% and 95.8%. So ``'bjs'`` is conservative for the
population statement most readers want, ``'forest'`` is exact for the
sample one. Bias was -0.006 on 0.452 (AUTOC) and -0.0002 on 0.141
(QINI).
``se_method='half_sample'`` raises for these forests: resampling units
would have to refit the untreated two-way model in every draw, and the
exact variance supersedes it.
**Do not read the default ranking as a test.** With the forest's own
out-of-bag predictions the ranking and the scores share
``gamma_hat_t``, so they stay correlated even though no unit predicts
itself. On the null design above (400 replications, no heterogeneity
at all) AUTOC averaged -0.028 rather than 0 and a nominal 5% test
rejected 16.0% of the time, QINI 12.2%. :func:`statspai.rate_split`,
which refits the rule on one half of the units and scores it on the
other, averaged -0.0002 and rejected 0.5% and 0.0%, with 100% power
against ``tau = 0.3 + 0.5 z``
(``tests/reliability/forest_split_evaluation.py``). A warning fires on
this path;
passing ``priorities=`` from a rule fitted elsewhere also silences it.
.. versionchanged:: 1.31.0
Forests with fixed effects are supported (they previously raised);
the default ``se_method`` is ``'auto'``, which is ``'influence'`` for
every forest that has a propensity, as before.
.. versionchanged:: 1.30.0
Scores are ``grf``'s (unclipped) AIPW scores; tied priorities are
averaged as in ``grf``; QINI uses ``grf``'s ``k/n`` weights, so a
flat TOC now gives exactly 0 (previously ``-mean(score) / (2n)``);
the TOC curve interpolates between cut points as ``grf`` does; the
influence-function SE includes the rank-estimation term
(previously ~30% too large).
Parameters
Parameters
forest : fitted CausalForest
X, Y, T : arrays, optional
The training sample (defaults to the stored arrays).
target : {'AUTOC', 'QINI'}
q_grid : int
Number of equally spaced points in ``(0, 1]`` at which the TOC
curve is reported. Does not affect the estimate.
alpha : float
seed : int, optional
Seed for ``se_method='half_sample'``.
priorities : array, optional
Priority score per training unit (higher = treat first). Defaults
to the forest's CATE predictions.
se_method : {'auto', 'influence', 'half_sample', 'imputation'}
``'auto'`` is ``'influence'``, or ``'imputation'`` for a forest with
fixed effects. ``'imputation'`` is defined only for those.
n_bootstrap : int
Half-sample draws for ``se_method='half_sample'``.
variance : {'bjs', 'forest'}
Fixed-effects forests only: how treated residuals are centred
before the cohort x event-time blocks. ``'bjs'`` (default here) is
conservative for the population RATE, ``'forest'`` exact for the
sample's -- see above.
cluster : array-like or 'dyadic', optional
Fixed-effects forests only; ``'dyadic'`` needs ``members=``.
members : array-like, optional
Fixed-effects forests only: the two members of each dyadic row.
covariates : {'none', 'auto'}, list of str or array, default 'none'
Fixed-effects forests only: time-varying covariates for the
untreated model behind the imputation scores.
Returns
Returns
dict with keys ``estimate``, ``se``, ``ci_low``, ``ci_high``,
``target``, ``toc_curve`` (``(q_grid, 2)``), ``n``, ``method``,
``priority_source``, ``n_clusters``.
References
References
[@yadlowsky2025evaluating]
Examples
Examples
>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(42)
>>> n = 400
>>> X = rng.normal(size=(n, 3))
>>> T = rng.binomial(1, 0.5, size=n)
>>> tau = 1.0 + X[:, 0] # heterogeneous effect
>>> Y = X[:, 1] + tau * T + rng.normal(scale=0.5, size=n)
>>> df = pd.DataFrame({
... "y": Y, "d": T,
... "x0": X[:, 0], "x1": X[:, 1], "x2": X[:, 2],
... })
>>> cf = sp.causal_forest(
... data=df, formula="y ~ d | x0 + x1 + x2",
... n_estimators=50, random_state=0,
... )
>>> res = sp.rate(cf, target="AUTOC")
>>> sorted(res.keys())
['ci_high', 'ci_low', 'estimate', 'method', 'n', 'n_clusters', \
'priority_source', 'se', 'target', 'toc_curve'] >>> res["toc_curve"].shape (100, 2)
best_linear_projection ¶
best_linear_projection(forest: Any, A: Any = None, vce: str = 'HC3', alpha: float = 0.05, **kwargs: Any) -> DataFrame
Best linear projection of the conditional effect on covariates.
Regresses the forest's doubly-robust scores on (1, A) with a
heteroskedasticity- (cluster-, when the forest has clusters) robust
covariance -- grf::best_linear_projection. The coefficients are
the population best linear predictor of tau(X) given A
(Semenova and Chernozhukov 2021); A=None gives the doubly-robust
average effect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted forest, or a DML result
|
|
required |
A
|
array - like or DataFrame
|
Projection covariates, one row per training observation (after missing-value removal). |
None
|
vce
|
('HC3', 'HC0', 'HC1', 'HC2')
|
Covariance type ( |
"HC3"
|
alpha
|
float
|
Significance level for the intervals. |
0.05
|
**kwargs
|
Any
|
Forwarded to the forest's score construction. |
{}
|
Returns:
| Type | Description |
|---|---|
DataFrame
|
Rows |
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(1000, 3)); T = rng.binomial(1, 0.5, 1000)
>>> Y = (1 + X[:, 0]) * T + rng.normal(size=1000)
>>> cf = sp.causal_forest(Y=Y, T=T, X=X, n_estimators=400, random_state=1)
>>> sp.best_linear_projection(cf, A=X[:, :1])
References
[@semenova2021debiased], [@athey2019generalized]
get_scores ¶
Doubly-robust scores of a forest's average effect (grf::get_scores).
The mean of the scores is the forest's doubly-robust average effect; regressing them on covariates is the best linear projection. Every score uses the forest's out-of-bag conditional effect.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted forest
|
|
required |
**kwargs
|
Any
|
Forwarded to the forest's own |
{}
|
Returns:
| Type | Description |
|---|---|
ndarray
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(500, 2)); T = rng.binomial(1, 0.5, 500)
>>> Y = (1 + X[:, 0]) * T + rng.normal(size=500)
>>> cf = sp.causal_forest(Y=Y, T=T, X=X, n_estimators=100, random_state=1)
>>> sp.get_scores(cf).shape
(500,)
References
[@athey2019generalized], [@robins1994estimation]
variable_importance ¶
Split-frequency variable importance of a forest (grf::variable_importance).
For depths d = 1..max_depth the share of splits made on each
covariate at that depth is weighted by d^(-decay_exponent); the
weighted shares are summed and divided by the total weight, so the
importances sum to one. This measures how often the forest uses a
covariate near the root -- for a causal forest, how often it splits on
it to separate effect heterogeneity -- and is not a test of anything.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
forest
|
fitted GRF-engine forest
|
Any of |
required |
decay_exponent
|
float
|
How quickly deeper splits lose weight. |
2
|
max_depth
|
int
|
Deepest level counted. |
4
|
Returns:
| Type | Description |
|---|---|
Series
|
Importance per covariate, in covariate order. The importances sum to one when every counted depth has at least one split, and to less otherwise (a depth without splits contributes zero while its weight stays in the normaliser, grf's convention). |
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(1000, 3)); T = rng.binomial(1, 0.5, 1000)
>>> Y = X[:, 0] * T + rng.normal(size=1000)
>>> cf = sp.causal_forest(Y=Y, T=T, X=X, n_estimators=200, random_state=1)
>>> sp.variable_importance(cf).idxmax()
'X0'
References
[@athey2019generalized]
instrumental_forest ¶
instrumental_forest(*args: Any, **kwargs: Any) -> IVForestResult
Alias of :func:iv_forest under grf's name.
multi_regression_forest ¶
multi_regression_forest(data: Optional[DataFrame] = None, y: Any = None, covariates: Any = None, *, clusters: Any = None, weights: Any = None, equalize_cluster_weights: bool = False, n_estimators: int = 2000, min_samples_leaf: int = 5, max_samples: float = 0.5, mtry: Optional[int] = None, honest: bool = True, honesty_fraction: float = 0.5, honesty_prune_leaves: bool = True, split_alpha: float = 0.05, imbalance_penalty: float = 0.0, ci_group_size: int = 1, max_depth: Optional[int] = None, random_state: Optional[int] = 42, n_jobs: int = 1) -> PredictionForest
Multi-task regression forest for E[Y_1, ..., Y_q | X]
(grf::multi_regression_forest).
Splits maximise the summed between-child variation of all outcomes, so one tree serves every outcome; outcomes should be on comparable scales.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
list of str or 2-D array
|
Outcomes.
(see below for the shared forest options; |
None
|
Returns:
| Type | Description |
|---|---|
PredictionForest
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(500, 2))
>>> Y = np.c_[X[:, 0], -X[:, 0]] + rng.normal(size=(500, 2))
>>> sp.multi_regression_forest(y=Y, covariates=X,
... n_estimators=100).predictions.shape
(500, 2)
References
[@athey2019generalized]
probability_forest ¶
probability_forest(data: Optional[DataFrame] = None, y: Any = None, covariates: Any = None, *, clusters: Any = None, weights: Any = None, equalize_cluster_weights: bool = False, n_estimators: int = 2000, min_samples_leaf: int = 5, max_samples: float = 0.5, mtry: Optional[int] = None, honest: bool = True, honesty_fraction: float = 0.5, honesty_prune_leaves: bool = True, split_alpha: float = 0.05, imbalance_penalty: float = 0.0, ci_group_size: int = 2, max_depth: Optional[int] = None, random_state: Optional[int] = 42, n_jobs: int = 1) -> PredictionForest
Probability forest for P[Y = k | X] (grf::probability_forest).
Trees split on class counts (the Gini-type criterion of a regression forest on the class indicators); leaves hold class frequencies of the honest estimation sample, so every prediction is a proper probability vector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
str or array - like
|
Class labels (any hashable values; classes are sorted). (see below for the shared forest options) |
None
|
Returns:
| Type | Description |
|---|---|
PredictionForest
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(600, 2)); cls = (X[:, 0] > 0).astype(int)
>>> pf = sp.probability_forest(y=cls, covariates=X, n_estimators=200)
>>> pf.predictions.shape
(600, 2)
References
[@athey2019generalized]
quantile_forest ¶
quantile_forest(data: Optional[DataFrame] = None, y: Any = None, covariates: Any = None, *, quantiles: Sequence[float] = (0.1, 0.5, 0.9), regression_splitting: bool = False, clusters: Any = None, weights: Any = None, equalize_cluster_weights: bool = False, n_estimators: int = 2000, min_samples_leaf: int = 5, max_samples: float = 0.5, mtry: Optional[int] = None, honest: bool = True, honesty_fraction: float = 0.5, honesty_prune_leaves: bool = True, split_alpha: float = 0.05, imbalance_penalty: float = 0.0, max_depth: Optional[int] = None, random_state: Optional[int] = 42, n_jobs: int = 1) -> PredictionForest
Quantile forest for conditional quantiles of Y (grf::quantile_forest).
At each node the outcome is relabelled by the interval it falls in
between the node's empirical quantiles, and the split maximises the
between-child separation of those class counts -- so trees chase
changes anywhere in the conditional distribution, not only the mean
(ATW 2019, Sec. 3.3). Predictions are quantiles of the training
outcomes under the forest weights (Meinshausen 2006).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
str or array - like
|
Outcome. |
None
|
quantiles
|
sequence of float
|
Quantiles that calibrate the splits and are predicted by default. |
(0.1, 0.5, 0.9)
|
regression_splitting
|
bool
|
Use ordinary regression splits instead (grf
|
False
|
Returns:
| Type | Description |
|---|---|
PredictionForest
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(800, 2))
>>> y = X[:, 0] + (1 + (X[:, 1] > 0)) * rng.normal(size=800)
>>> qf = sp.quantile_forest(y=y, covariates=X, n_estimators=200)
>>> qf.predict(X[:3]).columns.tolist()
['q0.1', 'q0.5', 'q0.9']
References
[@athey2019generalized], [@meinshausen2006quantile]
regression_forest ¶
regression_forest(data: Optional[DataFrame] = None, y: Any = None, covariates: Any = None, *, clusters: Any = None, weights: Any = None, equalize_cluster_weights: bool = False, n_estimators: int = 2000, min_samples_leaf: int = 5, max_samples: float = 0.5, mtry: Optional[int] = None, honest: bool = True, honesty_fraction: float = 0.5, honesty_prune_leaves: bool = True, split_alpha: float = 0.05, imbalance_penalty: float = 0.0, ci_group_size: int = 2, max_depth: Optional[int] = None, random_state: Optional[int] = 42, n_jobs: int = 1) -> PredictionForest
Honest regression forest for E[Y | X] (grf::regression_forest).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
y
|
str or array - like
|
Outcome. (see below for the shared forest options) |
None
|
Returns:
| Type | Description |
|---|---|
PredictionForest
|
|
Examples:
>>> import numpy as np, statspai as sp
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(500, 3)); y = X[:, 0] ** 2 + rng.normal(size=500)
>>> rf = sp.regression_forest(y=y, covariates=X, n_estimators=200)
>>> rf.predict(X[:5]).shape
(5, 1)
References
[@athey2019generalized], [@wager2018estimation]
tune_causal_forest ¶
tune_causal_forest(formula: Optional[str] = None, data: Optional[DataFrame] = None, Y: Optional[ndarray] = None, T: Optional[ndarray] = None, X: Optional[ndarray] = None, *, parameters: Sequence[str] = ('min_samples_leaf', 'max_samples', 'mtry'), n_draws: int = 40, tune_trees: int = 200, tune_reps: int = 2, n_estimators: int = 2000, random_state: Optional[int] = None, **kwargs: Any) -> Dict[str, Any]
Choose causal-forest settings by out-of-bag R-loss, then fit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
formula
|
Optional[str]
|
As in :func: |
None
|
data
|
Optional[str]
|
As in :func: |
None
|
Y
|
Optional[str]
|
As in :func: |
None
|
T
|
Optional[str]
|
As in :func: |
None
|
X
|
Optional[str]
|
As in :func: |
None
|
parameters
|
sequence of str
|
Which settings to tune, from |
('min_samples_leaf', 'max_samples', 'mtry')
|
n_draws
|
int
|
Random settings tried, besides the defaults. |
40
|
tune_trees
|
int
|
Trees in each trial forest. Small forests are noisy, which is why a tuned setting is adopted only if it beats the defaults clearly (see Notes). |
200
|
tune_reps
|
int
|
Trial forests per setting, with different seeds; their losses are averaged and their spread gives the noise of the comparison. |
2
|
n_estimators
|
int
|
Trees in the final forest. |
2000
|
random_state
|
int
|
|
None
|
**kwargs
|
Any
|
Passed to every forest (for example |
{}
|
Returns:
| Type | Description |
|---|---|
dict
|
|
Notes
The nuisance functions m and e are estimated once, out of fold,
by the forest with the default settings, and shared by every trial, so
settings are compared on the same residuals. When no trial is adopted
that default forest is what is returned. A tuned setting is adopted
only when its loss is below the defaults' by more than twice the
repetition noise; otherwise the defaults are kept and tuned is
False. Picking the minimum of forty
noisy losses without such a margin would select noise.
What to expect, from 12 replications each of four designs with 1,500
rows and 30 draws (root mean squared error of the out-of-bag effects
against the truth, tuned against default): a constant effect, 0.082
against 0.119, with a tuned setting adopted 10 times out of 12, always
with much larger leaves; a step and a fast-varying effect, no
difference, the defaults being kept almost every time; a linear
effect, 0.184 against 0.172, slightly worse. The R-loss is a noisy
criterion and a lower loss is not always a lower error of the effect
itself. Tuning pays when the effect varies little; it is not a free
improvement, and trials is there to be looked at.
grf fits a smooth surface to the trial losses and minimises it.
Here the best trial itself is taken, with the margin above; the two
choose from the same kind of evidence but will not return the same
setting.
Examples:
>>> import numpy as np
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 800
>>> X = rng.normal(size=(n, 4))
>>> W = rng.binomial(1, 0.5, n)
>>> Y = X[:, 0] + W * (1 + 2 * (X[:, 1] > 0)) + rng.normal(size=n)
>>> out = sp.tune_causal_forest(Y=Y, T=W, X=X, n_draws=10,
... n_estimators=200, random_state=1)
>>> sorted(out)[:3]
['best_params', 'default_error', 'forest']
>>> bool(out["tuned_error"] <= out["default_error"])
True
References
[@nie2021quasi], [@athey2019generalized]