/* trop_estat_loocv -- Display LOOCV hyperparameter selection diagnostics. Reports the regularization parameters selected via leave-one-out cross-validation, the LOOCV objective function value, the proportion of valid leave-one-out fits, the grid search style, and, when applicable, the coordinates of the first failed observation. Options ------- stability Additional stability checks: flags λ* at grid boundaries, reports grid dimensions, and surfaces the LOOCV search strategy (cycling vs exhaustive). Useful for catching undersized grids in small-sample settings. */ program define trop_estat_loocv version 17 syntax [, STABility TABle2] // --- Verify that trop estimation results are in memory --- if "`e(cmd)'" != "trop" { di as error "last estimates not found" exit 301 } // --- Header --- di as txt "" di as txt "LOOCV Diagnostics" di as txt "{hline 78}" // --- Selected regularization parameters --- di as txt "Selected hyperparameters:" capture confirm scalar e(lambda_time) if !_rc { di as txt " lambda_time = " as res %10.3f e(lambda_time) } else { di as txt " lambda_time = " as res "(not available)" } capture confirm scalar e(lambda_unit) if !_rc { di as txt " lambda_unit = " as res %10.3f e(lambda_unit) } else { di as txt " lambda_unit = " as res "(not available)" } capture confirm scalar e(lambda_nn) if !_rc { di as txt " lambda_nn = " as res %10.3f e(lambda_nn) } else { di as txt " lambda_nn = " as res "(not available)" } // --- Stage-1 univariate initial triple (paper Footnote 2) --- // Cycling LOOCV paths seed Stage-2 coordinate descent with an argmin // from three univariate sweeps. A visible gap between Stage-1 and // the selected triple signals that Stage-2 did real work on a non- // convex Q(lambda) surface. The exhaustive paths leave these // scalars missing and the block is then silent. capture confirm scalar e(stage1_lambda_time) local has_stage1_t = !_rc capture confirm scalar e(stage1_lambda_unit) local has_stage1_u = !_rc capture confirm scalar e(stage1_lambda_nn) local has_stage1_n = !_rc local stage1_visible = 0 if `has_stage1_t' & !missing(e(stage1_lambda_time)) local stage1_visible = 1 if `has_stage1_u' & !missing(e(stage1_lambda_unit)) local stage1_visible = 1 if `has_stage1_n' & !missing(e(stage1_lambda_nn)) local stage1_visible = 1 if `stage1_visible' { di as txt "" di as txt "Stage-1 univariate init (Footnote 2; cycling only):" if `has_stage1_t' & !missing(e(stage1_lambda_time)) { local _mark_t "" capture confirm scalar e(lambda_time) if !_rc & !missing(e(lambda_time)) { if abs(e(stage1_lambda_time) - e(lambda_time)) > 1e-12 /// local _mark_t " *" } di as txt " Stage-1 lambda_time = " as res %10.3f e(stage1_lambda_time) as res "`_mark_t'" } if `has_stage1_u' & !missing(e(stage1_lambda_unit)) { local _mark_u "" capture confirm scalar e(lambda_unit) if !_rc & !missing(e(lambda_unit)) { if abs(e(stage1_lambda_unit) - e(lambda_unit)) > 1e-12 /// local _mark_u " *" } di as txt " Stage-1 lambda_unit = " as res %10.3f e(stage1_lambda_unit) as res "`_mark_u'" } if `has_stage1_n' & !missing(e(stage1_lambda_nn)) { local _mark_n "" capture confirm scalar e(lambda_nn) if !_rc & !missing(e(lambda_nn)) { if abs(e(stage1_lambda_nn) - e(lambda_nn)) > 1e-12 /// local _mark_n " *" } di as txt " Stage-1 lambda_nn = " as res %10.3f e(stage1_lambda_nn) as res "`_mark_n'" } di as txt " (* = Stage-2 cycling polished away from the seed)" } di as txt "" // --- LOOCV performance summary --- di as txt "LOOCV performance:" // Minimized objective function value Q(lambda_hat) capture confirm scalar e(loocv_score) if !_rc { di as txt " Objective Q(" as res "λ̂" as txt ") = " as res %10.4f e(loocv_score) } else { di as txt " Objective Q(" as res "λ̂" as txt ") = " as res "(not available)" } // Valid fits as a fraction of total leave-one-out iterations capture confirm scalar e(loocv_n_valid) local has_valid = !_rc capture confirm scalar e(loocv_n_attempted) local has_attempted = !_rc if `has_valid' & `has_attempted' { local n_valid = e(loocv_n_valid) local n_attempted = e(loocv_n_attempted) if `n_attempted' > 0 { local pct_valid = 100 * `n_valid' / `n_attempted' di as txt " Valid fits = " as res %5.0f `n_valid' /// as txt " / " as res %5.0f `n_attempted' /// as txt " (" as res %5.1f `pct_valid' as txt "%)" // Warn if more than 10% of leave-one-out fits failed if `pct_valid' < 90 { di as err " Warning: High failure rate (>" as res "10%" /// as err ") may indicate data quality issues" } } else { di as txt " Valid fits = " as res "(no attempts recorded)" } } else { di as txt " Valid fits = " as res "(not available)" } // Grid search style (e.g., "auto", "manual") if "`e(grid_style)'" != "" { di as txt " Grid style = " as res "`e(grid_style)'" } else { di as txt " Grid style = " as res "(not available)" } // --- First failed leave-one-out observation, if any --- capture confirm scalar e(loocv_first_failed_t) local has_failed_t = !_rc capture confirm scalar e(loocv_first_failed_i) local has_failed_i = !_rc if `has_failed_t' & `has_failed_i' { local first_t = e(loocv_first_failed_t) local first_i = e(loocv_first_failed_i) // Non-negative indices indicate at least one leave-one-out fit failed if `first_t' >= 0 & `first_i' >= 0 { di as txt "" di as txt "First failed observation:" di as txt " Time index = " as res %5.0f `first_t' di as txt " Unit index = " as res %5.0f `first_i' } } // --- Stability diagnostics (opt-in) --- if "`stability'" != "" { di as txt "" di as txt "Stability diagnostics:" // Grid dimensions. Missing matrices are silently skipped. capture confirm matrix e(lambda_time_grid) local has_time_grid = !_rc capture confirm matrix e(lambda_unit_grid) local has_unit_grid = !_rc capture confirm matrix e(lambda_nn_grid) local has_nn_grid = !_rc local n_cartesian = . local n_t = . local n_u = . local n_n = . // LOOCV strategy (cycling vs exhaustive, per method) local method = e(method) if "`method'" == "joint" & "`e(joint_loocv)'" != "" { di as txt " LOOCV strategy = " as res "`e(joint_loocv)'" /// as txt " (joint; shared-tau extension)" } else if "`method'" == "twostep" & "`e(twostep_loocv)'" != "" { di as txt " LOOCV strategy = " as res "`e(twostep_loocv)'" /// as txt " (twostep; Algorithm 2 default)" } if `has_time_grid' { tempname gt matrix `gt' = e(lambda_time_grid) local n_t = rowsof(`gt') if `n_t' == 1 { local n_t = colsof(`gt') } } if `has_unit_grid' { tempname gu matrix `gu' = e(lambda_unit_grid) local n_u = rowsof(`gu') if `n_u' == 1 { local n_u = colsof(`gu') } } if `has_nn_grid' { tempname gn matrix `gn' = e(lambda_nn_grid) local n_n = rowsof(`gn') if `n_n' == 1 { local n_n = colsof(`gn') } } if `has_time_grid' & `has_unit_grid' & `has_nn_grid' { local n_cartesian = `n_t' * `n_u' * `n_n' di as txt " Cartesian size = " as res %6.0f `n_cartesian' /// as txt " grid points" } // Boundary-hit checks: if λ* coincides with the smallest or // largest grid point, the user-supplied grid is probably too // narrow and LOOCV may be selecting a corner solution. Users // should then expand the grid (grid_style(fine|extended) or // explicit lambda_*_grid()). local boundary_hits = 0 if `has_time_grid' & !missing(e(lambda_time)) { tempname gt matrix `gt' = e(lambda_time_grid) local n_t = rowsof(`gt') if `n_t' == 1 { local n_t = colsof(`gt') } _trop_check_grid_boundary `gt' `=e(lambda_time)' "lambda_time" "`n_t'" local boundary_hits = `boundary_hits' + r(boundary_hit) di as txt " lambda_time grid: " as res %3.0f r(n_grid) /// as txt " points in [" as res %6.3f r(lo) as txt ", " /// as res %6.3f r(hi) as txt "]" /// as res "`r(hit_msg)'" } if `has_unit_grid' & !missing(e(lambda_unit)) { tempname gu matrix `gu' = e(lambda_unit_grid) local n_u = rowsof(`gu') if `n_u' == 1 { local n_u = colsof(`gu') } _trop_check_grid_boundary `gu' `=e(lambda_unit)' "lambda_unit" "`n_u'" local boundary_hits = `boundary_hits' + r(boundary_hit) di as txt " lambda_unit grid: " as res %3.0f r(n_grid) /// as txt " points in [" as res %6.3f r(lo) as txt ", " /// as res %6.3f r(hi) as txt "]" /// as res "`r(hit_msg)'" } if `has_nn_grid' & !missing(e(lambda_nn)) { tempname gn matrix `gn' = e(lambda_nn_grid) local n_n = rowsof(`gn') if `n_n' == 1 { local n_n = colsof(`gn') } _trop_check_grid_boundary `gn' `=e(lambda_nn)' "lambda_nn" "`n_n'" local boundary_hits = `boundary_hits' + r(boundary_hit) di as txt " lambda_nn grid: " as res %3.0f r(n_grid) /// as txt " points in [" as res %6.3f r(lo) as txt ", " /// as res %6.3f r(hi) as txt "]" /// as res "`r(hit_msg)'" } if `boundary_hits' > 0 { di as err " WARNING: `boundary_hits' boundary hit(s) detected." di as err " Consider grid_style(fine) or grid_style(extended)," di as err " or widen the affected lambda_*_grid() option." } else if `has_time_grid' | `has_unit_grid' | `has_nn_grid' { di as txt " No grid-boundary hits (selected λ sits strictly inside each grid)." } if `n_cartesian' < . & `n_cartesian' <= 125 { if "`method'" == "twostep" & "`e(twostep_loocv)'" == "cycling" { di as txt " Recommendation = " as res "twostep_loocv(exhaustive)" /// as txt " is practical on this grid and guarantees the grid argmin." } else if "`method'" == "joint" & "`e(joint_loocv)'" == "cycling" { di as txt " Recommendation = " as res "joint_loocv(exhaustive)" /// as txt " is practical on this grid and guarantees the grid argmin." } } } // --- Table 2 coverage report (opt-in) --- if "`table2'" != "" { capture confirm matrix e(lambda_time_grid) local has_time_grid = !_rc capture confirm matrix e(lambda_unit_grid) local has_unit_grid = !_rc capture confirm matrix e(lambda_nn_grid) local has_nn_grid = !_rc if !`has_time_grid' | !`has_unit_grid' | !`has_nn_grid' { di as err "" di as err " Table 2 coverage: lambda grids are not available in e()." di as err " Run trop with LOOCV (do not pass fixedlambda()) to populate" di as err " e(lambda_time_grid)/e(lambda_unit_grid)/e(lambda_nn_grid)." } else { capture findfile trop_lambda_grid.mata if _rc == 0 { capture mata: mata which trop_report_table2_coverage() if _rc { // Mata function not loaded yet — pull in the lambda grid // module. _trop_load_mata is the canonical loader. capture _trop_load_mata } } tempname _gt _gu _gn matrix `_gt' = e(lambda_time_grid) matrix `_gu' = e(lambda_unit_grid) matrix `_gn' = e(lambda_nn_grid) mata: (void) trop_report_table2_coverage( /// colvec_from_matrix("`_gt'"), /// colvec_from_matrix("`_gu'"), /// colvec_from_matrix("`_gn'")) } } di as txt "{hline 78}" end /* --------------------------------------------------------------------------- Mata helper: convert a Stata matrix (row- or column-vector shaped) into a real colvector suitable for `trop_report_table2_coverage`. Declared local to this ado so it does not leak into the global Mata namespace under another name; the standalone grid-module Mata file should not be touched for a display-layer utility. ---------------------------------------------------------------------------- */ mata: real colvector colvec_from_matrix(string scalar matname) { real matrix M M = st_matrix(matname) if (rows(M) == 1) return(M') if (cols(M) == 1) return(M) return(vec(M)) } end /* --------------------------------------------------------------------------- _trop_check_grid_boundary helper --------------------------------------------------------------------------- Given a grid stored as a Stata matrix (column- or row-vector), the selected λ̂, and a display label, returns via r() the grid size, the min/max, a human-readable hit message, and a 0/1 flag marking whether λ̂ coincides with the grid's lower or upper endpoint (bit-equality, not ε-comparison: LOOCV always selects a grid point so exact match is the right test). The infinity sentinel ("." → +Inf) is handled specially: an infinite upper bound or λ̂ is reported without a boundary flag because `infinity` is a legitimate corner (DID/TWFE special case) rather than an undersized grid. ---------------------------------------------------------------------------- */ program define _trop_check_grid_boundary, rclass args grid lambda_hat label n_grid // Extract min / max of the grid as scalar strings. The grid matrix // may be row- or column-vector shaped; `vec()` flattens both cases // and `[1,1]` turns the reduction's 1×1 matrix into a scalar so // st_local gets a plain string. Missing (Stata's +∞ sentinel) is // propagated by colmax so the helper can recognise it and skip the // upper-boundary hit test in the DID/TWFE λ_nn = ∞ case. mata: st_local("_tmp_lo", strofreal(colmin(vec(st_matrix("`grid'")))[1,1])) mata: st_local("_tmp_hi", strofreal(colmax(vec(st_matrix("`grid'")))[1,1])) local hit_lo = (`lambda_hat' == `_tmp_lo' & `n_grid' > 1) local hit_hi = 0 // "." sentinel for +Inf must not count as an undersized grid. If the // grid contains missing (∞), the upper bound is effectively unbounded // and λ_hat = max_finite_grid_point still isn't a "boundary hit" in // the same sense. Stata reports missing as +Inf in max(). if "`_tmp_hi'" != "." { local hit_hi = (`lambda_hat' == `_tmp_hi' & `n_grid' > 1) } local hit = (`hit_lo' + `hit_hi') local msg "" if `hit_lo' { local msg " ← λ̂ at LOWER boundary" } else if `hit_hi' { local msg " ← λ̂ at UPPER boundary" } return local hit_msg "`msg'" return scalar boundary_hit = `hit' return scalar n_grid = `n_grid' return scalar lo = `_tmp_lo' return scalar hi = `_tmp_hi' end