*! Counterfactual outcome prediction for trop /* trop_predict_y0 Generates predicted counterfactual outcome Y(0) for each observation. The additive model is: Y(0) = mu + alpha_i + beta_t + L_{t,i} + X_{t,i}'*gamma where alpha_i are unit fixed effects, beta_t are time fixed effects, L_{t,i} is the low-rank interaction component, and X_{t,i}'*gamma is the covariate contribution (zero when no covariates are specified). Two-step method: Treated units: Y(0) = Y_obs - tau_hat (exact decomposition) Control units: Y(0) = alpha_i + beta_t + L_{t,i} + X'gamma Joint method: All units: Y(0) = mu + alpha_i + beta_t + L_{t,i} + X'gamma Required stored estimates: e(alpha) unit fixed effects (N x 1) e(beta) time fixed effects (T x 1) e(factor_matrix) low-rank interaction (T x N, column-major) e(mu) intercept (joint method) e(tau) observation-level treatment effects (two-step method) e(gamma) covariate coefficients (1 x p, optional) e(n_covariates) number of covariates (scalar, optional) e(covariates) covariate variable names (string, optional) */ program define trop_predict_y0 version 17 syntax newvarname [if] [in] marksample touse, novarlist // Initialize prediction variable. qui gen double `varlist' = . // Construct panel and time indices consistent with estimation sample. tempvar panel_idx time_idx qui egen `panel_idx' = group(`e(panelvar)') if e(sample) qui egen `time_idx' = group(`e(timevar)') if e(sample) // Retrieve estimation method; default is two-step. local method "`e(method)'" if "`method'" == "" { local method "twostep" } // Dispatch to Mata. local treatvar "`e(treatvar)'" local depvar "`e(depvar)'" mata: _trop_predict_y0("`varlist'", "`panel_idx'", "`time_idx'", /// "`method'", "`touse'", "`treatvar'", "`depvar'") label variable `varlist' "Counterfactual prediction Y(0)" end // --------------------------------------------------------------------------- // Mata: counterfactual prediction // --------------------------------------------------------------------------- version 17 mata: mata set matastrict on void _trop_predict_y0( string scalar varname, string scalar panel_var, string scalar time_var, string scalar method, string scalar touse_var, string scalar treatvar, string scalar depvar ) { real colvector panel_idx, time_idx real colvector alpha, beta real matrix L real colvector pred real scalar mu, n, i, i_idx, t_idx real colvector xgamma panel_idx = st_data(., panel_var, touse_var) time_idx = st_data(., time_var, touse_var) alpha = st_matrix("e(alpha)") beta = st_matrix("e(beta)") // T x N low-rank interaction matrix, column-major layout. L = st_matrix("e(factor_matrix)") // Covariate contribution X*gamma (per observation). xgamma = _trop_predict_xgamma(touse_var) n = rows(panel_idx) pred = J(n, 1, .) if (method == "twostep") { _trop_predict_y0_twostep(varname, panel_var, time_var, touse_var, treatvar, depvar, alpha, beta, L, n, xgamma) } else { // Joint method: reconstruct Y(0) with global intercept. mu = st_numscalar("e(mu)") if (mu == .) { mu = 0 printf("{txt}(note: e(mu) not found; defaulting to 0)\n") } for (i = 1; i <= n; i++) { i_idx = panel_idx[i] t_idx = time_idx[i] if (i_idx < 1 | i_idx > rows(alpha)) continue if (t_idx < 1 | t_idx > rows(beta)) continue if (t_idx > rows(L) | i_idx > cols(L)) continue pred[i] = mu + alpha[i_idx] + beta[t_idx] + L[t_idx, i_idx] + xgamma[i] } st_store(., varname, touse_var, pred) } } /*────────────────────────────────────────────────────────────────────────────── _trop_predict_xgamma() Computes the per-observation covariate contribution X*gamma for prediction. Returns a column vector of length n (estimation sample), where each element is sum_k(x_{it,k} * gamma_k). Returns zeros when no covariates are present. ──────────────────────────────────────────────────────────────────────────────*/ real colvector _trop_predict_xgamma(string scalar touse_var) { real scalar n_cov, k real matrix gamma_mat real colvector xg, xk, touse_data string scalar cov_names_str string rowvector cov_names n_cov = st_numscalar("e(n_covariates)") if (n_cov >= . || n_cov < 1) { // No covariates: return zeros sized to the touse sample. touse_data = st_data(., touse_var) return(J(sum(touse_data), 1, 0)) } gamma_mat = st_matrix("e(gamma)") if (cols(gamma_mat) < n_cov) { // Gamma not available; fall back to zeros. touse_data = st_data(., touse_var) return(J(sum(touse_data), 1, 0)) } cov_names_str = st_global("e(covariates)") if (cov_names_str == "") { touse_data = st_data(., touse_var) return(J(sum(touse_data), 1, 0)) } cov_names = tokens(cov_names_str) // Compute X*gamma = sum_k x_k * gamma_k xk = st_data(., cov_names[1], touse_var) xg = xk :* gamma_mat[1, 1] for (k = 2; k <= n_cov; k++) { xk = st_data(., cov_names[k], touse_var) xg = xg :+ xk :* gamma_mat[1, k] } return(xg) } void _trop_predict_y0_twostep( string scalar varname, string scalar panel_var, string scalar time_var, string scalar touse_var, string scalar treatvar, string scalar depvar, real colvector alpha, real colvector beta, real matrix L, real scalar n, real colvector xgamma ) { /* Two-step counterfactual prediction. Treated observations: Y(0) = Y_obs - tau_hat This preserves the exact identity Y_obs - Y(0) = tau_hat. Control observations: Y(0) = alpha[i] + beta[t] + L[t,i] e(tau) is stored in time-major order (t = 1..T within i = 1..N for treated cells). Treated observations are sorted accordingly before tau assignment. */ real colvector panel_idx, time_idx, treat, y_data, pred real matrix tau_vec real scalar i, i_idx, t_idx, k, n_tau, n_treated, obs_i real matrix treated_info panel_idx = st_data(., panel_var, touse_var) time_idx = st_data(., time_var, touse_var) treat = st_data(., treatvar, touse_var) y_data = st_data(., depvar, touse_var) pred = J(n, 1, .) tau_vec = st_matrix("e(tau)") n_tau = rows(tau_vec) // Control observations: reconstruct from additive model. for (i = 1; i <= n; i++) { i_idx = panel_idx[i] t_idx = time_idx[i] if (i_idx < . && t_idx < . && treat[i] != 1) { if (i_idx >= 1 & i_idx <= rows(alpha) & t_idx >= 1 & t_idx <= rows(beta) & t_idx <= rows(L) & i_idx <= cols(L)) { pred[i] = alpha[i_idx] + beta[t_idx] + L[t_idx, i_idx] + xgamma[i] } } } // Treated observations: subtract observation-level treatment effect. if (n_tau > 0) { n_treated = 0 for (i = 1; i <= n; i++) { if (treat[i] == 1 && panel_idx[i] < . && time_idx[i] < .) { n_treated++ } } if (n_treated == n_tau) { // Collect treated observations with row indices. treated_info = J(n_treated, 3, .) k = 0 for (i = 1; i <= n; i++) { if (treat[i] == 1 && panel_idx[i] < . && time_idx[i] < .) { k++ treated_info[k, 1] = time_idx[i] treated_info[k, 2] = panel_idx[i] treated_info[k, 3] = i } } // Sort by (time, panel) to align with time-major order of e(tau). treated_info = sort(treated_info, (1, 2)) // Assign Y(0) = Y_obs - tau_hat. for (k = 1; k <= n_treated; k++) { obs_i = treated_info[k, 3] if (y_data[obs_i] < . && tau_vec[k, 1] < .) { pred[obs_i] = y_data[obs_i] - tau_vec[k, 1] } } } else { // Count mismatch: fall back to additive model for treated units. printf("{txt}(note: treated count %g differs from e(tau) length %g; " + "using additive model)\n", n_treated, n_tau) for (i = 1; i <= n; i++) { if (treat[i] == 1) { i_idx = panel_idx[i] t_idx = time_idx[i] if (i_idx >= 1 & i_idx <= rows(alpha) & t_idx >= 1 & t_idx <= rows(beta) & t_idx <= rows(L) & i_idx <= cols(L)) { pred[i] = alpha[i_idx] + beta[t_idx] + L[t_idx, i_idx] + xgamma[i] } } } } } else { // No observation-level effects available; use additive model. for (i = 1; i <= n; i++) { if (treat[i] == 1) { i_idx = panel_idx[i] t_idx = time_idx[i] if (i_idx >= 1 & i_idx <= rows(alpha) & t_idx >= 1 & t_idx <= rows(beta) & t_idx <= rows(L) & i_idx <= cols(L)) { pred[i] = alpha[i_idx] + beta[t_idx] + L[t_idx, i_idx] + xgamma[i] } } } } st_store(., varname, touse_var, pred) } end