Triply Robust Panel (TROP)#
Triply Robust Panel estimator for panel data with factor confounding.
This module implements the methodology from Athey, Imbens, Qu & Viviano (2025), which combines three robustness components:
Nuclear norm regularized factor model: Estimates interactive fixed effects via matrix completion with nuclear norm penalty ||L||_*
Exponential distance-based unit weights: ω_j = exp(-λ_unit × d(j,i)) where d(j,i) is the pairwise RMSE between units over pre-treatment periods
Exponential time decay weights: θ_s = exp(-λ_time × \(|t-s|\)) weighting periods by proximity to the specific treatment period t
When to use TROP:
Suspected factor structure in the data (e.g., economic cycles, regional shocks)
Unobserved time-varying confounders that affect units differently over time
Standard parallel trends may be violated due to latent common factors
Reasonably long pre-treatment period to estimate factors
Reference: Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2025). Triply Robust Panel Estimators. Working Paper. arXiv:2508.21536
TROP#
Main estimator class for Triply Robust Panel estimation.
- class diff_diff.TROP[source]
Bases:
TROPLocalMixin,TROPGlobalMixinTriply Robust Panel (TROP) estimator.
Implements the exact methodology from Athey, Imbens, Qu & Viviano (2025). TROP combines three robustness components:
Nuclear norm regularized factor model: Estimates interactive fixed effects L_it via matrix completion with nuclear norm penalty ||L||_*
Exponential distance-based unit weights: ω_j = exp(-λ_unit × d(j,i)) where d(j,i) is the RMSE of outcome differences between units
Exponential time decay weights: θ_s = exp(-λ_time × \(|s-t|\)) weighting pre-treatment periods by proximity to treatment
Tuning parameters (λ_time, λ_unit, λ_nn) are selected via leave-one-out cross-validation on control observations.
- Parameters:
method (str, default='local') –
Estimation method to use:
’local’: Per-observation model fitting following Algorithm 2 of Athey et al. (2025). Computes observation-specific weights and fits a model for each treated observation, averaging the individual treatment effects. More flexible but computationally intensive.
’global’: Computationally efficient adaptation using the (1-W) masking principle from Eq. 2. Fits a single model on control observations with global weights, then computes per-observation treatment effects as residuals: tau_it = Y_it - mu - alpha_i - beta_t - L_it for treated cells. ATT is the mean of these effects. For the paper’s full per-treated-cell estimator, use
method='local'.
lambda_time_grid (list, optional) – Grid of time weight decay parameters. 0.0 = uniform weights (disabled). Must not contain inf. Default: [0, 0.1, 0.5, 1, 2, 5].
lambda_unit_grid (list, optional) – Grid of unit weight decay parameters. 0.0 = uniform weights (disabled). Must not contain inf. Default: [0, 0.1, 0.5, 1, 2, 5].
lambda_nn_grid (list, optional) – Grid of nuclear norm regularization parameters. inf = factor model disabled (L=0). Default: [0, 0.01, 0.1, 1].
max_iter (int, default=100) – Maximum iterations for nuclear norm optimization.
tol (float, default=1e-6) – Convergence tolerance for optimization.
alpha (float, default=0.05) – Significance level for confidence intervals.
n_bootstrap (int, default=200) – Number of bootstrap replications for variance estimation. Must be >= 2.
seed (int, optional) – Random seed for reproducibility.
- results_
Estimation results after calling fit().
- Type:
- is_fitted_
Whether the model has been fitted.
- Type:
Examples
>>> from diff_diff import TROP >>> trop = TROP() >>> results = trop.fit( ... data, ... outcome='outcome', ... treatment='treated', ... unit='unit', ... time='period', ... ) >>> results.print_summary()
References
Athey, S., Imbens, G. W., Qu, Z., & Viviano, D. (2025). Triply Robust Panel Estimators. Working Paper. https://arxiv.org/abs/2508.21536
Methods
fit(data, outcome, treatment, unit, time[, ...])Fit the TROP model.
get_params()Get estimator parameters.
set_params(**params)Set estimator parameters.
- __init__(method='local', lambda_time_grid=None, lambda_unit_grid=None, lambda_nn_grid=None, max_iter=100, tol=1e-06, alpha=0.05, n_bootstrap=200, seed=None)[source]
- max_iter: int
- tol: float
- alpha: float
- n_bootstrap: int
- results_: TROPResults | None
- is_fitted_: bool
- fit(data, outcome, treatment, unit, time, survey_design=None)[source]
Fit the TROP model.
- Parameters:
data (pd.DataFrame) – Panel data with observations for multiple units over multiple time periods.
outcome (str) – Name of the outcome variable column.
treatment (str) –
Name of the treatment indicator column (0/1).
IMPORTANT: This should be an ABSORBING STATE indicator, not a treatment timing indicator. For each unit, D=1 for ALL periods during and after treatment:
D[t, i] = 0 for all t < g_i (pre-treatment periods)
D[t, i] = 1 for all t >= g_i (treatment and post-treatment)
where g_i is the treatment start time for unit i.
For staggered adoption, different units can have different g_i. The ATT averages over ALL D=1 cells per Equation 1 of the paper.
unit (str) – Name of the unit identifier column.
time (str) – Name of the time period column.
survey_design (SurveyDesign, optional) – Survey design specification. Supports pweight, strata, PSU, and FPC. Full-design surveys (strata/PSU/FPC) use Rao-Wu rescaled bootstrap; Rust backend is pweight-only (Python fallback for full design). Survey weights enter ATT aggregation only.
- Returns:
Object containing the ATT estimate, standard error, factor estimates, and tuning parameters. The lambda_* attributes show the selected grid values. For lambda_time and lambda_unit, 0.0 means uniform weights; inf is not accepted. For lambda_nn, inf is converted to 1e10 (factor model disabled).
- Return type:
- Raises:
ValueError – If required columns are missing or non-pweight survey design.
- CONVERGENCE_TOL_SVD: float = 1e-10
TROPResults#
Results container for TROP estimation.
- class diff_diff.TROPResults[source]
Bases:
objectResults from a Triply Robust Panel (TROP) estimation.
TROP combines nuclear norm regularized factor estimation with exponential distance-based unit weights and time decay weights.
- att
Average Treatment effect on the Treated (ATT).
- Type:
- se
Standard error of the ATT estimate.
- Type:
- t_stat
T-statistic for the ATT estimate.
- Type:
- p_value
P-value for the null hypothesis that ATT = 0.
- Type:
- n_obs
Number of observations used in estimation.
- Type:
- n_treated
Number of treated units.
- Type:
- n_control
Number of control units.
- Type:
- n_treated_obs
Number of treated unit-time observations.
- Type:
- unit_effects
Estimated unit fixed effects (alpha_i).
- Type:
- time_effects
Estimated time fixed effects (beta_t).
- Type:
- treatment_effects
Individual treatment effects for each treated (unit, time) pair.
- Type:
- lambda_time
Selected time weight decay parameter from grid. 0.0 = uniform time weights (disabled) per Eq. 3.
- Type:
- lambda_unit
Selected unit weight decay parameter from grid. 0.0 = uniform unit weights (disabled) per Eq. 3.
- Type:
- lambda_nn
Selected nuclear norm regularization parameter from grid. inf = factor model disabled (L=0); converted to 1e10 internally for computation.
- Type:
- factor_matrix
Estimated low-rank factor matrix L (n_periods x n_units).
- Type:
np.ndarray
- effective_rank
Effective rank of the factor matrix (sum of singular values / max).
- Type:
- loocv_score
Leave-one-out cross-validation score for selected parameters.
- Type:
- alpha
Significance level for confidence interval.
- Type:
- n_pre_periods
Number of pre-treatment periods.
- Type:
- n_post_periods
Number of post-treatment periods (periods with D=1 observations).
- Type:
- n_bootstrap
Number of bootstrap replications (if bootstrap variance).
- Type:
int, optional
- bootstrap_distribution
Bootstrap distribution of estimates.
- Type:
np.ndarray, optional
Methods
summary([alpha])Generate a formatted summary of the estimation results.
print_summary([alpha])Print the summary to stdout.
to_dict()Convert results to a dictionary.
to_dataframe()Convert results to a pandas DataFrame.
get_treatment_effects_df()Get individual treatment effects as a DataFrame.
get_unit_effects_df()Get unit fixed effects as a DataFrame.
get_time_effects_df()Get time fixed effects as a DataFrame.
- att: float
- se: float
- t_stat: float
- p_value: float
- n_obs: int
- n_treated: int
- n_control: int
- n_treated_obs: int
- lambda_time: float
- lambda_unit: float
- lambda_nn: float
- factor_matrix: ndarray
- effective_rank: float
- loocv_score: float
- alpha: float = 0.05
- n_pre_periods: int = 0
- n_post_periods: int = 0
- __init__(att, se, t_stat, p_value, conf_int, n_obs, n_treated, n_control, n_treated_obs, unit_effects, time_effects, treatment_effects, lambda_time, lambda_unit, lambda_nn, factor_matrix, effective_rank, loocv_score, alpha=0.05, n_pre_periods=0, n_post_periods=0, n_bootstrap=None, bootstrap_distribution=None, survey_metadata=None)
- Parameters:
att (float)
se (float)
t_stat (float)
p_value (float)
n_obs (int)
n_treated (int)
n_control (int)
n_treated_obs (int)
lambda_time (float)
lambda_unit (float)
lambda_nn (float)
factor_matrix (ndarray)
effective_rank (float)
loocv_score (float)
alpha (float)
n_pre_periods (int)
n_post_periods (int)
n_bootstrap (int | None)
bootstrap_distribution (ndarray | None)
survey_metadata (Any | None)
- Return type:
None
- property coef_var: float
SE / abs(ATT). NaN when ATT is 0 or SE non-finite.
- Type:
Coefficient of variation
- summary(alpha=None)[source]
Generate a formatted summary of the estimation results.
- print_summary(alpha=None)[source]
Print the summary to stdout.
- Parameters:
alpha (float | None)
- Return type:
None
- to_dict()[source]
Convert results to a dictionary.
- Returns:
Dictionary containing all estimation results.
- Return type:
Dict[str, Any]
- to_dataframe()[source]
Convert results to a pandas DataFrame.
- Returns:
DataFrame with estimation results.
- Return type:
pd.DataFrame
- get_treatment_effects_df()[source]
Get individual treatment effects as a DataFrame.
- Returns:
DataFrame with unit, time, and treatment effect columns.
- Return type:
pd.DataFrame
- get_unit_effects_df()[source]
Get unit fixed effects as a DataFrame.
- Returns:
DataFrame with unit and effect columns.
- Return type:
pd.DataFrame
- get_time_effects_df()[source]
Get time fixed effects as a DataFrame.
- Returns:
DataFrame with time and effect columns.
- Return type:
pd.DataFrame
- property is_significant: bool
Check if the ATT is statistically significant at the alpha level.
- property significance_stars: str
Return significance stars based on p-value.
Convenience Function#
- diff_diff.trop(data, outcome, treatment, unit, time, survey_design=None, **kwargs)[source]#
Convenience function for TROP estimation.
- Parameters:
data (pd.DataFrame) – Panel data.
outcome (str) – Outcome variable column name.
treatment (str) –
Treatment indicator column name (0/1).
IMPORTANT: This should be an ABSORBING STATE indicator, not a treatment timing indicator. For each unit, D=1 for ALL periods during and after treatment (D[t,i]=0 for t < g_i, D[t,i]=1 for t >= g_i where g_i is the treatment start time for unit i).
unit (str) – Unit identifier column name.
time (str) – Time period column name.
survey_design (SurveyDesign, optional) – Survey design specification. Supports pweight, strata, PSU, and FPC.
**kwargs – Additional arguments passed to TROP constructor.
- Returns:
Estimation results.
- Return type:
Examples
>>> from diff_diff import trop >>> results = trop(data, 'y', 'treated', 'unit', 'time') >>> print(f"ATT: {results.att:.3f}")
Tuning Parameters#
TROP uses leave-one-out cross-validation (LOOCV) to select three tuning parameters:
Parameter |
Description |
Effect |
|---|---|---|
|
Time weight decay |
Higher values weight periods closer to treatment more heavily |
|
Unit distance decay |
Higher values weight similar control units more heavily |
|
Nuclear norm penalty |
Higher values encourage lower-rank factor structure |
Estimation Methods#
TROP supports two estimation methods via the method parameter:
Local Method (method='local', default)
The default method follows Algorithm 2 from the paper:
Grid search with LOOCV: For each (λ_time, λ_unit, λ_nn) combination, compute cross-validation score by treating control observations as pseudo-treated
Per-observation estimation: For each treated observation (i, t):
Compute observation-specific weights θ^{i,t} and ω^{i,t}
Fit weighted model: Y = α + β + L + ε with nuclear norm penalty on L
Compute τ̂_{it} = Y_{it} - α̂_i - β̂_t - L̂_{it}
Average: ATT = mean(τ̂_{it}) over all treated observations
This provides the triple robustness property (Theorem 5.1): the estimator is consistent if any one of the three components (unit weights, time weights, factor model) is correctly specified.
Global Method (method='global')
A computationally efficient adaptation using the (1-W) masking principle
from Eq. 2. Fits a single global model rather than per-treated-cell models.
For the paper’s full per-treated-cell estimator (Algorithm 2), use
method='local'.
Compute weights: Distance-based unit and time weights computed once (distance to center of treated block, RMSE to average treated trajectory), with
(1-W)masking to zero out treated observations.Fit control model: Solve weighted least squares on control data only
\[\min_{\mu, \alpha, \beta, L} \sum_{i,t} (1 - W_{it}) \delta_{it} (Y_{it} - \mu - \alpha_i - \beta_t - L_{it})^2 + \lambda_{nn} \|L\|_*\]Post-hoc treatment effects: For each treated observation:
\[\hat{\tau}_{it} = Y_{it} - \hat{\mu} - \hat{\alpha}_i - \hat{\beta}_t - \hat{L}_{it}, \quad \text{ATT} = \text{mean}(\hat{\tau}_{it})\]
The global method is faster (single optimization vs N_treated optimizations). Treatment effects are heterogeneous per-observation residuals; ATT is their mean.
Feature |
Local (default) |
Global |
|---|---|---|
Treatment effect |
Per-observation τ_{it} (per-obs models) |
Per-observation τ_{it} (single model) |
Fitting |
N_treated models with tailored weights |
One model with global weights |
Speed |
Slower (N_treated fits) |
Faster (single fit) |
Weights |
Observation-specific |
Global (center of treated block) |
Use method='local' for observation-specific weight optimization.
Use method='global' for faster estimation with global weights.
Example Usage#
Basic usage:
from diff_diff import TROP
trop = TROP(
lambda_time_grid=[0.0, 0.5, 1.0, 2.0],
lambda_unit_grid=[0.0, 0.5, 1.0, 2.0],
lambda_nn_grid=[0.0, 0.1, 1.0],
n_bootstrap=200,
seed=42
)
# Note: TROP infers treatment periods from the treatment indicator column.
# The treatment column should be an absorbing state (D=1 for all periods
# during and after treatment starts).
results = trop.fit(
data,
outcome='y',
treatment='treated',
unit='unit_id',
time='period'
)
results.print_summary()
Quick estimation with convenience function:
from diff_diff import trop
results = trop(
data,
outcome='y',
treatment='treated',
unit='unit_id',
time='period',
n_bootstrap=200
)
Using the global method for faster estimation:
from diff_diff import TROP
# Global method: computationally efficient adaptation using (1-W) masking
trop_global = TROP(
method='global',
lambda_time_grid=[0.0, 0.5, 1.0, 2.0],
lambda_unit_grid=[0.0, 0.5, 1.0, 2.0],
lambda_nn_grid=[0.0, 0.1, 1.0],
n_bootstrap=200,
seed=42
)
results_global = trop_global.fit(data, outcome='y', treatment='treated',
unit='unit_id', time='period')
# Compare methods
trop_local = TROP(method='local', ...) # Default (per-observation)
results_local = trop_local.fit(data, ...)
print(f"Local ATT: {results_local.att:.3f}")
print(f"Global ATT: {results_global.att:.3f}")
Examining factor structure:
# Get the estimated factor matrix
L = results.factor_matrix
print(f"Effective rank: {results.effective_rank:.2f}")
# Individual treatment effects
effects_df = results.get_treatment_effects_df()
print(effects_df)
Comparison with Synthetic DiD#
TROP extends Synthetic DiD by adding factor model adjustment:
Feature |
Synthetic DiD |
TROP |
|---|---|---|
Unit weights |
Constrained to sum to 1 |
Exponential distance-based |
Time weights |
Constrained to sum to 1 |
Exponential time decay |
Factor adjustment |
None |
Nuclear norm regularized L |
Robustness |
Doubly robust |
Triply robust |
Use SDID when parallel trends is plausible. Use TROP when you suspect factor confounding (regional shocks, economic cycles, latent factors).