/* trop_validate --- Pre-estimation data validation for the TROP estimator. Validates panel data requirements for the triply robust panel estimator: 1. Panel structure: unique (i,t) pairs, strictly increasing time within each unit, integer time index generation. 2. Balance diagnostics: panel dimensions (N, T), missingness rate with tiered diagnostics, detection of entirely missing rows or columns, creation of __trop_valid indicator. 3. Treatment pattern: binary W in {0,1}, absorbing-state verification, classification (single treated, simultaneous, staggered, switching), generation of __trop_ever_treated and __trop_T_start. 4. Outlier detection: Tukey's rule with conservative threshold (k=10). 5. Covariate balance: optional standardized-difference diagnostics. 6. Feasibility checks: hard constraints on identification and LOOCV. 7. Summary report and handoff to estimation routines. */ program define trop_validate, eclass version 17.0 syntax varlist(min=2 max=2 numeric) [if] [in], /// PANelvar(varname) /// TIMevar(varname numeric) /// [NOUNiqueness] /// // skip uniqueness check [NOMONotonicity] /// // skip monotonicity check [NOTIMeindex] /// // skip time-index generation [COVariates(varlist)] /// // covariates for balance diagnostics [MCP] /// // enable extended consistency checks [STATEmcp] /// // alias for mcp [METHod(string)] // estimation method (joint enforces absorbing state) // Clean up variables that may persist from a previous call _trop_cleanup_vars // Parse variable list: outcome and treatment tokenize `varlist' local depvar `1' local treatvar `2' // Mark estimation sample marksample touse, novarlist // Display validation header di as text _newline "{bf:TROP Data Validation: Panel Structure}" di as text "{hline 70}" di as text "Requirements:" di as text " - Unique (i,t) pairs for N x T matrix representation" di as text " - Strictly increasing time within each unit" di as text "{hline 70}" _newline // Preserve original observation order tempvar orig_order gen long `orig_order' = _n // Step 1: Sort data and declare panel structure di as text "Step 1: Sorting and panel setup..." _continue sort `panelvar' `timevar' // Attempt tsset to register the panel structure capture tsset `panelvar' `timevar', noquery local tsset_rc = _rc if `tsset_rc' != 0 { di as text _n " NOTE: tsset failed; proceeding with manual checks" } di as result " done" // Step 2: Verify uniqueness of (i,t) pairs if "`nouniqueness'" == "" { di as text "Step 2: (i,t) uniqueness check..." _continue // If tsset succeeded, Stata already verified uniqueness local already_tsset = (`tsset_rc' == 0) if `already_tsset' { // Panel structure already validated by tsset di as result " passed (panel structure verified)" } else { // tsset unavailable; run explicit duplicate check qui duplicates report `panelvar' `timevar' if `touse' local n_unique = r(unique_value) local n_total = r(N) // Duplicates detected — report and abort if `n_unique' < `n_total' { di as error " FAILED" di "" // Tag duplicate observations qui duplicates tag `panelvar' `timevar' if `touse', gen(__trop_dup_count) // Count duplicate observations qui count if __trop_dup_count > 0 & `touse' local n_dup_obs = r(N) qui tab __trop_dup_count if __trop_dup_count > 0 & `touse', matcell(dup_freq) di as error "{bf:ERROR: Duplicate (i,t) pairs detected}" di as error "The N x T matrix representation requires each (i,t) to be unique." di as error "Found `n_dup_obs' observations with duplicate (i,t)." di as error "(unique pairs: `n_unique' < total observations: `n_total')" di as text _newline "{bf:Consequences:}" di as text " 1. Fixed-effect estimates biased by double counting" di as text " 2. LOOCV cannot uniquely identify the held-out observation" di as text " 3. Unit-distance sums distorted" di as text _newline "{bf:First 10 duplicate (i,t) pairs:}" list `panelvar' `timevar' `depvar' `treatvar' __trop_dup_count /// if __trop_dup_count > 0 & `touse' in 1/10, /// table separator(0) abbrev(12) di as text _newline "{bf:Suggested remedies:}" di as text " 1. Drop duplicate rows:" di as text " {stata duplicates drop `panelvar' `timevar', force}" di as text "" di as text " 2. Collapse to unique (i,t):" di as text " {stata collapse (mean) `depvar' `treatvar', by(`panelvar' `timevar')}" di as text "" di as text " 3. Verify data source for merge or append errors" drop __trop_dup_count error 459 } di as result " passed (unique N x T structure)" } } else { di as text "Step 2/3: (i,t) uniqueness check..." /// as result " skipped" } // Step 3: Verify strict monotonicity of the time variable if "`nomonotonicity'" == "" { di as text "Step 3: Time monotonicity check..." _continue // Compute within-unit time differences tempvar time_diff qui by `panelvar': gen double `time_diff' = `timevar' - `timevar'[_n-1] if `touse' // Detect non-monotonic observations (diff <= 0 means not strictly increasing) qui count if `time_diff' <= 0 & !missing(`time_diff') & `touse' local n_nonmono = r(N) // Non-monotonic time detected — report and abort if `n_nonmono' > 0 { di as error " FAILED" di "" // Flag offending units tempvar has_nonmono qui by `panelvar': egen byte `has_nonmono' = max(`time_diff' <= 0 & !missing(`time_diff') & `touse') di as error "{bf:ERROR: `n_nonmono' non-monotonic time observations detected}" di as error "The time distance dist^time(s,t) = |t - s| requires a strict total order." di as text _newline "{bf:Consequences:}" di as text " 1. Time distance |t-s| loses interpretability" di as text " 2. Exponential time weights break down" di as text " 3. LOOCV time-neighbourhood selection is invalid" // List up to 10 offending units di as text _newline "{bf:First 10 non-monotonic units:}" qui levelsof `panelvar' if `has_nonmono' == 1 & `touse', local(bad_units) local count = 0 foreach uid of local bad_units { if `count' >= 10 { continue, break } di as text _newline " Unit {bf:`uid'}:" qui levelsof `timevar' if `panelvar' == `uid' & `touse', local(times) clean di as text " Time sequence: `times'" local count = `count' + 1 } di as text _newline "{bf:Suggested remedies:}" di as text " 1. Sort the data:" di as text " {stata sort `panelvar' `timevar'}" di as text "" di as text " 2. Inspect the time variable for data-entry errors:" di as text " {stata list `panelvar' `timevar' if `has_nonmono'==1}" di as text "" di as text " 3. Verify consistent time encoding across units" di as text "" di as text " 4. Re-run the uniqueness check to rule out duplicates" error 498 } di as result " passed (time is strictly increasing)" // Persist time differences for the balance check qui gen double __trop_time_diff = `time_diff' if `touse' label variable __trop_time_diff "Time difference between consecutive periods" } else { di as text "Step 3/3: Time monotonicity check..." /// as result " skipped" } // Generate integer time index (1, 2, 3, ...) if "`notimeindex'" == "" { di as text _newline "Additional step: Generating time index..." _continue // Map timevar to consecutive integers qui egen int __trop_tindex = group(`timevar') if `touse' label variable __trop_tindex "Integer time index" // Store time range qui summarize `timevar' if `touse', meanonly local time_min = r(min) local time_max = r(max) local time_range = `time_max' - `time_min' // Verify __trop_tindex is strictly increasing within each unit tempvar tindex_diff qui by `panelvar': gen double `tindex_diff' = __trop_tindex - __trop_tindex[_n-1] if `touse' qui count if `tindex_diff' <= 0 & !missing(`tindex_diff') & `touse' if r(N) > 0 { di as error " FAILED" di as error "ERROR: __trop_tindex is not strictly increasing (internal error)" error 498 } // Return time-range information ereturn scalar time_min = `time_min' ereturn scalar time_max = `time_max' ereturn scalar time_range = `time_range' di as result " done (time_min=`time_min', time_max=`time_max')" } else { di as text _newline "Additional step: Generating time index..." /// as result " skipped" } // Restore original observation order sort `orig_order' // Panel structure validation summary di as text _newline "{hline 70}" di as result "{bf:Panel structure validation passed}" di as text " - (i,t) uniqueness: N x T matrix representation valid" di as text " - Time monotonicity: time-distance computation valid" di as text " - Time index: __trop_tindex generated" di as text "{hline 70}" _newline // ======================================================================== // Balance diagnostics and missingness analysis // ======================================================================== trop_balance_check `depvar' `treatvar' if `touse', /// panelvar(`panelvar') timevar(`timevar') // Retrieve balance-check results for subsequent use local N = e(N) local T = e(T) local N_obs = e(N_obs) local miss_rate = e(miss_rate) local balanced = e(balanced) // ======================================================================== // Step 7: Verify binary treatment variable // ======================================================================== di as text _newline "Step 7: Treatment binary check..." _continue // Count distinct treatment values (excluding missing) qui levelsof `treatvar' if `touse', local(unique_vals) clean local n_unique = r(r) // Check whether values are exactly {0, 1} local has_zero = 0 local has_one = 0 foreach val of local unique_vals { if `val' == 0 local has_zero = 1 if `val' == 1 local has_one = 1 } local is_binary = (`n_unique' == 2) & `has_zero' & `has_one' // Non-binary treatment detected — report and abort if !`is_binary' { di as error " FAILED" di "" di as error "{bf:ERROR: Non-binary treatment variable detected}" di as error "The TROP estimator requires a binary treatment, W in {0,1}." di as error _newline "Found {bf:`n_unique'} distinct values: {bf:`unique_vals'}" // Tabulate observation counts by treatment value di as text _newline "{bf:Distribution of treatment values:}" qui tab `treatvar' if `touse', matrow(vals) matcell(counts) forvalues i = 1/`r(r)' { local val = vals[`i',1] local cnt = counts[`i',1] di as text " W = `val': `cnt' obs" } di as text _newline "{bf:Consequences:}" di as text " 1. Potential-outcomes framework undefined for non-binary W" di as text " 2. ATT requires binary treatment" di as text " 3. Triply-robust property assumes binary treatment" di as text _newline "{bf:Suggested remedies:}" di as text " 1. Check for data-entry errors:" di as text " {stata tab `treatvar'}" di as text "" di as text " 2. Dichotomise the treatment (choose a threshold):" di as text " {stata generate W_binary = (`treatvar' >= threshold)}" di as text "" di as text " 3. For continuous-dose treatments, TROP does not apply." di as text " Consider dose-response methods instead." di as text "" di as text " 4. For multi-valued W, define separate binary contrasts:" di as text " e.g., W_low = (W==1), W_high = (W==2)" di as text " and run TROP separately for each contrast." ereturn scalar data_validated = 0 error 450 } di as result " passed (W in {0,1})" // ======================================================================== // Step 8: Treatment pattern identification // ======================================================================== di as text _newline "Step 8: Treatment pattern identification..." _continue // Sub-step 1: Compute ever-treated indicator qui bysort `panelvar': egen byte __trop_ever_treated = max(`treatvar') if `touse' label variable __trop_ever_treated "Ever-treated indicator" // Count distinct treated units tempvar unit_id_treated qui egen `unit_id_treated' = tag(`panelvar') if __trop_ever_treated == 1 & `touse' qui count if `unit_id_treated' == 1 local N_treated_units = r(N) // Sub-step 2: Compute first treatment time for each unit tempvar first_treat_time qui bysort `panelvar': egen `first_treat_time' = min(`timevar') if `treatvar' == 1 & `touse' qui bysort `panelvar': egen double __trop_T_start = min(`first_treat_time') if `touse' label variable __trop_T_start "First treatment period" // Sub-step 3: Count treatment switches sort `panelvar' `timevar' qui by `panelvar': gen byte __trop_W_diff = (`treatvar' != `treatvar'[_n-1]) if _n > 1 & `touse' qui bysort `panelvar': egen int __trop_n_switches = total(__trop_W_diff) if `touse' // Sub-step 3b: Verify absorbing state — treatment must be non-decreasing // Once treated, a unit remains treated (no treatment reversal). // NOTE: This constraint is only required by method(joint), whose single // scalar-tau WLS assumes an absorbing (non-decreasing) treatment path. // The twostep method (and the Rust core) support arbitrary 0/1 treatment // matrices, including switching treatment, so it is not enforced there. tempvar _W_decrease _has_decrease qui by `panelvar': gen byte `_W_decrease' = (`treatvar'[_n-1] == 1 & `treatvar' == 0) /// if _n > 1 & `touse' qui bysort `panelvar': egen byte `_has_decrease' = max(`_W_decrease') if `touse' qui sum `_has_decrease' if `touse', meanonly if r(max) == 1 & "`method'" == "joint" { // List offending units di as result " done" di "" di as error "{bf:ERROR: Treatment indicator violates absorbing-state assumption}" di as error "Once W_it = 1, subsequent periods must remain treated." di as error "Treatment must be monotonically non-decreasing." // List up to 10 offending units tempvar _unit_tag_viol qui egen `_unit_tag_viol' = tag(`panelvar') if `_has_decrease' == 1 & `touse' qui levelsof `panelvar' if `_unit_tag_viol' == 1 & `touse', local(viol_units) local n_viol : word count `viol_units' di as error "Found `n_viol' offending units" di as text _newline "{bf:Treatment sequences of offending units (first 10):}" local viol_count = 0 foreach uid of local viol_units { if `viol_count' >= 10 { di as text " ... (`n_viol' offending units total)" continue, break } qui levelsof `treatvar' if `panelvar' == `uid' & `touse', local(d_seq) clean di as text " Unit `uid': D = `d_seq'" local viol_count = `viol_count' + 1 } di as text _newline "{bf:Suggested remedies:}" di as text " Convert treatment to absorbing state:" di as text " D[t, i] = 1 for all t >= first treatment period" di as text " e.g.: bysort `panelvar' (`timevar'): replace `treatvar' = 1 if `treatvar'[_n-1] == 1" drop `_W_decrease' `_has_decrease' error 459 } drop `_W_decrease' `_has_decrease' // Sub-step 4: Count distinct adoption cohorts tempvar adoption_time qui egen `adoption_time' = group(__trop_T_start) if !missing(__trop_T_start) & `touse' qui levelsof `adoption_time' if `touse', local(adoption_cohorts) local n_adoption_times : word count `adoption_cohorts' if `n_adoption_times' == 0 { local n_adoption_times = 0 // handle case with no treated units } // Sub-step 5: Count periods containing treated observations qui bysort `timevar': egen byte __trop_any_treated_t = max(`treatvar') if `touse' // Count distinct treated periods tempvar time_id_treated qui egen `time_id_treated' = tag(`timevar') if __trop_any_treated_t == 1 & `touse' qui count if `time_id_treated' == 1 local T_treat_periods = r(N) // Sub-step 6: Classify pattern (priority: switching > staggered > simultaneous > single) qui sum __trop_n_switches if `touse', meanonly local max_switches = r(max) if missing(`max_switches') local max_switches = 0 if `max_switches' >= 2 { local pattern = "switching_treatment" local has_switching = 1 di as result " done" di "" di as text "{bf:WARNING: Switching treatment detected}" di as text "max_switches = {bf:`max_switches'} >= 2" di as text "The estimator assumes no dynamic treatment effects." di as text _newline "{bf:Implications:}" di as text " - Switching treatment lies outside the estimator's theoretical scope" di as text " - TROP estimates contemporaneous effects, not cumulative ones" di as text " - Standard replication exercises exclude such data" di as text " - Consider event-study or dynamic panel methods if needed" } else if `n_adoption_times' > 1 { local pattern = "staggered_adoption" local has_switching = 0 di as result " done" di "" di as text "{bf:Identified as staggered adoption:}" di as text " N_treated_units = {bf:`N_treated_units'}" di as text " Adoption cohorts = {bf:`n_adoption_times'}" di as text " Adoption time distribution:" qui levelsof __trop_T_start if !missing(__trop_T_start) & `touse', local(adoption_times) clean foreach t of local adoption_times { tempvar unit_at_t qui egen `unit_at_t' = tag(`panelvar') if __trop_T_start == `t' & `touse' qui count if `unit_at_t' == 1 local n_units_at_t = r(N) di as text " Period `t': `n_units_at_t' units begin treatment" } } else if `N_treated_units' > 1 { local pattern = "multiple_treated_simultaneous" local has_switching = 0 di as result " done" di "" di as text "{bf:Identified as multiple treated, simultaneous adoption:}" di as text " N_treated_units = {bf:`N_treated_units'}" qui levelsof __trop_T_start if !missing(__trop_T_start) & `touse', local(start_time) clean di as text " Adoption time = {bf:`start_time'} (all treated units start simultaneously)" } else { local pattern = "single_treated_unit" local has_switching = 0 di as result " done" di "" di as text "{bf:Identified as single treated unit:}" di as text " N_treated_units = {bf:1}" qui levelsof `panelvar' if __trop_ever_treated == 1 & `touse', local(treated_unit) clean di as text " Treated unit: {bf:`treated_unit'}" } // Sub-step 7a: Compute n_pre_periods and n_post_periods qui sum __trop_T_start if `touse', meanonly local global_first_treat = r(min) local n_pre_periods = 0 local n_post_periods = `T_treat_periods' if !missing(`global_first_treat') { // n_pre_periods: number of periods before first treatment tempvar _time_tag_pre qui egen `_time_tag_pre' = tag(`timevar') if `timevar' < `global_first_treat' & `touse' qui count if `_time_tag_pre' == 1 local n_pre_periods = r(N) // n_post_periods: number of periods with any D=1 from first treatment onward // (= T_treat_periods, computed in sub-step 5) } // Sub-step 7b: Store results in ereturn ereturn local treatment_pattern "`pattern'" ereturn scalar N_treated_units = `N_treated_units' ereturn scalar T_treat_periods = `T_treat_periods' ereturn scalar has_switching = `has_switching' ereturn scalar max_switches = `max_switches' // Store pre/post period counts in ereturn ereturn scalar n_pre_periods = `n_pre_periods' ereturn scalar n_post_periods = `n_post_periods' // Display verification summary di as text _newline "{hline 70}" di as result "{bf:Treatment pattern verification passed}" di as text " - Binary treatment: W in {0,1} confirmed" di as text " - Pattern: {bf:`pattern'}" di as text " - N_treated_units: {bf:`N_treated_units'}" di as text " - T_treat_periods: {bf:`T_treat_periods'}" if `has_switching' { di as text " - Switching detected: {bf:Yes} (max_switches=`max_switches')" } else { di as text " - Switching detected: No" } di as text "{hline 70}" _newline // ======================================================================== // Step 9: Outlier detection (Tukey's rule, k=10 conservative threshold) // ======================================================================== di as text _newline "{bf:Step 9: Outlier detection (Tukey's rule, k=10)...}" di as text "{hline 70}" di as text "Theoretical basis:" di as text " • Fixed-effect estimation is sensitive to extreme values." di as text " • SVD decomposition amplifies ill-conditioned data." di as text " • Unit-distance computation depends on outcome magnitudes." di as text "{hline 70}" _newline // Step 1: Compute quartiles (excluding missing values) qui _pctile `depvar' if !missing(`depvar') & `touse', p(25 75) local Q1 = r(r1) local Q3 = r(r2) local IQR = `Q3' - `Q1' // Step 2: Define outlier bounds (k=10 conservative threshold) local k = 10 local lower = `Q1' - `k' * `IQR' local upper = `Q3' + `k' * `IQR' // Step 3: Flag potential outliers qui gen byte __trop_outlier_flag = (`depvar' < `lower' | `depvar' > `upper') /// if !missing(`depvar') & `touse' qui replace __trop_outlier_flag = 0 if missing(`depvar') | !`touse' label variable __trop_outlier_flag "Outlier indicator" // Step 4: Count outliers and calculate rate qui count if __trop_outlier_flag == 1 & `touse' local n_outliers = r(N) qui count if !missing(`depvar') & `touse' local N_valid_depvar = r(N) local outlier_rate = cond(`N_valid_depvar' > 0, `n_outliers' / `N_valid_depvar', 0) // Step 5: Report outlier statistics di as text "Quartile diagnostics:" di as text " Q1 (25th percentile) = " %12.4f `Q1' di as text " Q3 (75th percentile) = " %12.4f `Q3' di as text " IQR (Interquartile Range) = " %12.4f `IQR' di as text "" di as text "Outlier bounds (Tukey's rule with k=10):" di as text " Lower bound = Q1 - 10*IQR = " %12.4f `lower' di as text " Upper bound = Q3 + 10*IQR = " %12.4f `upper' di as text "" if `n_outliers' > 0 { di as text "Result: found {bf:`n_outliers'} potential outliers" di as text " Rate: " %5.3f `outlier_rate'*100 "% (based on `N_valid_depvar' valid obs)" // List up to 10 outliers (sorted by absolute deviation from median) tempvar abs_deviation qui gen double `abs_deviation' = abs(`depvar' - ((`Q1' + `Q3')/2)) /// if __trop_outlier_flag == 1 & `touse' di as text _newline "Top 10 outliers (sorted by deviation from median):" preserve qui keep if __trop_outlier_flag == 1 & `touse' qui count local n_to_show = min(r(N), 10) if `n_to_show' > 0 { qui gsort -`abs_deviation' list `panelvar' `timevar' `depvar' in 1/`n_to_show', /// noobs table separator(0) abbrev(12) } restore di as text _newline "WARNING: Outliers are flagged but NOT removed." di as text "Variable __trop_outlier_flag created (0=normal, 1=outlier)" di as text "" di as text "{bf:Suggested actions:}" di as text " 1. Winsorization (recommended):" di as text " replace `depvar' = `upper' if `depvar' > `upper'" di as text " replace `depvar' = `lower' if `depvar' < `lower'" di as text " Reason: Limits extreme values while preserving data structure." di as text "" di as text " 2. Drop outliers:" di as text " drop if __trop_outlier_flag == 1" di as text " Reason: If outliers are due to data entry errors." di as text " WARNING: Dropping observations reduces sample size." di as text "" di as text " 3. Create a dummy variable for outliers:" di as text " gen outlier_dummy = __trop_outlier_flag" di as text "" di as text " 4. Check data source:" di as text " list `panelvar' `timevar' `depvar' if __trop_outlier_flag == 1" di as text " Verify if these are real economic shocks or errors." } else { di as result "Result: {bf:No extreme outliers detected}" di as text "All observations satisfy bounds [" %12.4f `lower' ", " %12.4f `upper' "]" } // ======================================================================== // Covariate balance diagnostics // ======================================================================== // Check if covariates are provided local has_covariates = ("`covariates'" != "") if `has_covariates' == 0 { // No covariates: skip di as text _newline "Covariate balance check: skipped (no covariates provided)" di as text " Baseline model uses factor model only." _newline } else { // ========== Execute covariate balance diagnostics ========== di as text _newline "{hline 60}" di as text "{bf:Covariate balance diagnostics}" di as text "{hline 60}" di as text "Theoretical basis: Covariate bias decomposition" di as text "Covariates: `covariates'" // Step 1: Define pre-treatment period // Reuse __trop_T_start if available capture confirm variable __trop_first_treat_time if _rc != 0 { // If not available, compute manually qui bysort `panelvar': egen __trop_first_treat_time = min(`timevar') if `treatvar' == 1 & `touse' } qui sum __trop_first_treat_time if `touse' local t0 = r(min) // Global first treatment period // Boundary case: no treated observations if missing(`t0') { di as text _newline " NOTE: No treated observations (all W=0), using all-period covariates" local t0 = . } else { di as text "Pre-treatment period definition: t < `t0' (first treatment period)" } // Mark pre-treatment period qui gen byte __trop_is_pretreat = (`timevar' < `t0') if !missing(`t0') & `touse' if missing(`t0') { qui replace __trop_is_pretreat = 1 if `touse' // All periods } // Edge case: no pre-treatment observations qui count if __trop_is_pretreat == 1 & `touse' if r(N) == 0 { di as text _newline "{bf:WARNING}: Pre-treatment period is empty (t_0 = `t0' is the first period)" di as text " Covariate balance cannot be calculated." di as text " Skipping covariate balance diagnostics." _newline drop __trop_is_pretreat } else { // ========== Step 2: Compute Standardized Difference for each covariate ========== local n_covariates = 0 local n_good = 0 local n_acceptable = 0 local n_moderate_imbalance = 0 local n_severe_imbalance = 0 foreach x of local covariates { local n_covariates = `n_covariates' + 1 di as text _newline "Covariate: {bf:`x'}" // === Compute stats for treated units (pre-treatment, ever-treated) === qui sum `x' if __trop_is_pretreat == 1 & __trop_ever_treated == 1 & __trop_valid == 1 & `touse' // Edge case: no treated observations if r(N) == 0 { di as text " {bf:WARNING}: No valid treated observations in pre-treatment period, skipping" continue } local mean_t = r(mean) local sd_t = r(sd) local N_t = r(N) // === Compute stats for control units (pre-treatment, never-treated) === qui sum `x' if __trop_is_pretreat == 1 & __trop_ever_treated == 0 & __trop_valid == 1 & `touse' // Edge case: no control observations if r(N) == 0 { di as text " {bf:WARNING}: No valid control observations in pre-treatment period, skipping" continue } local mean_c = r(mean) local sd_c = r(sd) local N_c = r(N) // === Compute standardized difference === local diff = `mean_t' - `mean_c' local sd_pool = sqrt((`sd_t'^2 + `sd_c'^2) / 2) // Zero variance check if `sd_pool' < 1e-10 { di as text " {bf:WARNING}: Covariate has near-zero variance, StdDiff undefined" di as text " sd_pooled = " %9.3e `sd_pool' " < 1e-10" continue } local std_diff = `diff' / `sd_pool' // === Format output === di as text " - Treated (N=`N_t'): Mean=" %9.3f `mean_t' ", SD=" %8.3f `sd_t' di as text " - Control (N=`N_c'): Mean=" %9.3f `mean_c' ", SD=" %8.3f `sd_c' di as text " - Raw diff: " %9.3f `diff' di as text " - Std diff: " %6.3f `std_diff' // === Severity warnings === if abs(`std_diff') > 0.5 { di as text " {bf:WARNING: Severe imbalance} (|StdDiff| = " %6.3f abs(`std_diff') " > 0.5)" di as text " Covariate bias term may dominate total bias." di as text " Strongly recommended:" di as text " (1) Include `x' in covariates()" di as text " (2) Use PSM pre-matching" di as text " (3) Report sensitivity analysis" local n_severe_imbalance = `n_severe_imbalance' + 1 } else if abs(`std_diff') > 0.25 { di as text " {bf:WARNING: Imbalance} (|StdDiff| = " %6.3f abs(`std_diff') " > 0.25)" di as text " Recommended:" di as text " (1) Include `x' in covariates()" di as text " (2) Report sensitivity analysis" local n_moderate_imbalance = `n_moderate_imbalance' + 1 } else if abs(`std_diff') > 0.1 { di as text " Mild imbalance (|StdDiff| = " %6.3f abs(`std_diff') " <= 0.25, acceptable)" local n_acceptable = `n_acceptable' + 1 } else { di as text " Balanced (|StdDiff| = " %6.3f abs(`std_diff') " <= 0.1)" local n_good = `n_good' + 1 } } // ========== Step 3: Summary Report ========== di as text _newline "{hline 60}" di as text "{bf:Covariate balance summary}" di as text "{hline 60}" di as text "Total covariates: `n_covariates'" di as text "Balanced (|StdDiff|<=0.1): `n_good'" di as text "Acceptable (0.1<|StdDiff|<=0.25): `n_acceptable'" di as text "Imbalanced (0.25<|StdDiff|<=0.5): `n_moderate_imbalance'" di as text "Severely Imbalanced (|StdDiff|>0.5): `n_severe_imbalance'" // Overall assessment if `n_severe_imbalance' > 0 { di as text _newline "{bf:WARNING: Severe overall imbalance}" di as text "Covariate bias may dominate total estimation bias." di as text "Strongly recommended:" di as text " 1. Use covariate extension: trop Y W `covariates', covariates(`covariates') ..." di as text " 2. Pre-matching strategies (PSM/trajectory balancing)" di as text " 3. Sensitivity analysis" } else if `n_moderate_imbalance' > 0 { di as text _newline "Overall assessment: Moderate imbalance" di as text "Recommended: Include imbalanced variables in covariates()" di as text " or perform sensitivity analysis" } else { di as text _newline "Overall assessment: Good covariate balance" di as text "Standard TROP (no covariates) or covariate-adjusted TROP both viable" } di as text "{hline 60}" _newline // ========== Cleanup temporary variables ========== drop __trop_is_pretreat // __trop_first_treat_time may be reused by subsequent steps, keep it } } // ======================================================================== // Feasibility and identification checks // ======================================================================== /* Feasibility checks ensure the TROP estimator and LOOCV are theoretically feasible on the current dataset. Returns hard errors for unidentified or extremely sparse situations. 7 hard constraints: - Check 0.1: Var(Y) > 0 (outcome non-degenerate) - Check 0.2: N_treated_units >= 1 (treated units exist) - Check 1: N_control >= 1 (control observations exist) - Check 2: N_control_units >= 2 (sufficient control units) - Check 3: min_pre_treated >= 2 (pre-treatment period length) - Check 4: for all treated i, control periods >= 1 - Check 5: min_valid_pairs >= 2 (common control periods) */ di as text _newline _newline di as text "{hline 60}" di as text "{bf:TROP Algorithm Feasibility Check}" di as text "{hline 60}" di as text "Theoretical basis: Triply robust assumptions" di as text "Dimensions: 7 hard constraints (2 degeneracy checks + 5 feasibility checks)" di as text "{hline 60}" _newline // === Check 0.1: Var(Y) > 0 === di as text "Check 0.1: Outcome variable variance..." _continue qui sum `depvar' if __trop_valid == 1 & `touse' local var_Y = r(Var) if `var_Y' <= 0 | missing(`var_Y') { di as error " FAILED" di "" di as error "{bf:ERROR}: Outcome variance is zero or missing (Var(Y)=" %9.3e `var_Y' ")" di as error "Causal inference premise: If Y has no variation, treatment effect tau is undefined." di as error "Meaning: All Y observations are constant or missing." di as text _newline "Suggested remedies:" di as text " 1. Check data entry: Confirm if Y is constant due to read errors." di as text " sum `depvar'" di as text " 2. Check variable selection: Ensure correct outcome variable." di as text " describe `depvar'" di as text " 3. Check missing values: If Var is missing, valid obs might be 0." di as text " tab __trop_valid" di as text " 4. If Y has no variation, TROP is not applicable." ereturn scalar data_validated = 0 error 459 } di as result " passed (Var(Y)=" %9.3f `var_Y' ")" // === Check 0.2: N_treated_units >= 1 === di as text "Check 0.2: Existence of ever-treated units..." _continue // Reuse __trop_ever_treated capture confirm variable __trop_ever_treated if _rc != 0 { qui bysort `panelvar': egen byte __trop_ever_treated_check = max(`treatvar') if `touse' local ever_var "__trop_ever_treated_check" } else { local ever_var "__trop_ever_treated" } // Count ever-treated units tempvar unit_tag_treat qui egen `unit_tag_treat' = tag(`panelvar') if `ever_var' == 1 & `touse' qui count if `unit_tag_treat' == 1 local N_treated_units = r(N) if `N_treated_units' < 1 { di as error " FAILED" di "" di as error "{bf:ERROR}: No ever-treated units (N_treated_units=0)" di as error "ATT is undefined when no units receive treatment." di as text _newline "Suggested remedies:" di as text " 1. Check treatment variable W: Confirm if all W=0." di as text " tab `treatvar'" di as text " 2. Check variable selection: Ensure correct treatment variable." di as text " describe `treatvar'" di as text " 3. If no treatment exists, TROP is not applicable." ereturn scalar data_validated = 0 error 459 } di as result " passed (N_treated_units=" %3.0f `N_treated_units' ")" // === Check 1: N_control >= 1 === di as text "Check 1: Existence of global control observations..." _continue qui gen byte __trop_is_control_26 = (`treatvar' == 0 & __trop_valid == 1) if `touse' qui count if __trop_is_control_26 == 1 local N_control = r(N) if `N_control' < 1 { di as error " FAILED" di "" di as error "{bf:ERROR}: No control observations (N_control=0)" di as error "The weighted least squares objective sums over control cells only." di as error "Without control observations, fixed effects are unidentified." di as text _newline "Suggested remedies:" di as text " 1. Check treatment variable: Confirm if all W=1." di as text " list `panelvar' `timevar' `treatvar' in 1/20" di as text " 2. Check missing values: Confirm if all __trop_valid=0." di as text " tab __trop_valid" di as text " 3. Redefine treatment: Relax treatment criteria if too strict." di as text " 4. If truly no controls, TROP is not applicable." capture drop __trop_is_control_26 ereturn scalar data_validated = 0 error 459 } di as result " passed (N_control=" %6.0f `N_control' ")" // === Check 2: N_control_units >= 2 === di as text "Check 2: Control unit size..." _continue qui bysort `panelvar': egen byte __trop_max_treat_26 = max(`treatvar') if `touse' qui gen byte __trop_never_treated_26 = (__trop_max_treat_26 == 0) if !missing(__trop_max_treat_26) tempvar unit_tag_never qui egen `unit_tag_never' = tag(`panelvar') if __trop_never_treated_26 == 1 & `touse' qui count if `unit_tag_never' == 1 local N_control_units = r(N) qui drop __trop_max_treat_26 if `N_control_units' < 2 { di as error " FAILED" di "" di as error "{bf:ERROR}: Insufficient control units (N_control_units=`N_control_units', required >= 2)" di as error "Unit distance computation requires comparison against multiple controls." di as error "With only `N_control_units' never-treated units, unit weights degenerate." di as text _newline "Suggested remedies:" di as text " 1. Reduce treated units: Retain more units as controls." di as text " 2. Expand panel: Include more control regions/firms." di as text " 3. Redefine ever-treated: Reclassify rarely treated units as controls." di as text " 4. If only 1 control exists, TROP is not applicable." di as text " Consider single-control case studies." capture drop __trop_is_control_26 __trop_never_treated_26 ereturn scalar data_validated = 0 error 459 } di as result " passed (N_control_units=" %3.0f `N_control_units' ")" // === Check 3: min_pre_treated >= 2 === di as text "Check 3: Pre-treatment period length..." _continue // Reuse __trop_T_start if available capture confirm variable __trop_T_start if _rc != 0 { tempvar first_treat_time qui bysort `panelvar': egen `first_treat_time' = min(`timevar') if `treatvar' == 1 & `touse' qui bysort `panelvar': egen double __trop_T_start_26 = min(`first_treat_time') if `touse' local T_start_var "__trop_T_start_26" } else { local T_start_var "__trop_T_start" } // Calculate valid pre-treatment periods for each ever-treated unit qui gen byte __trop_is_pre_26 = (`timevar' < `T_start_var') & __trop_valid == 1 if !missing(`T_start_var') & `touse' qui bysort `panelvar': egen int __trop_n_pre_valid_26 = total(__trop_is_pre_26) if `touse' qui sum __trop_n_pre_valid_26 if `ever_var' == 1 & `touse' local min_pre_treated = r(min) if missing(`min_pre_treated') local min_pre_treated = 0 if `min_pre_treated' < 2 { di as error " FAILED" di "" di as error "{bf:ERROR}: Insufficient pre-treatment periods (min_pre_treated=`min_pre_treated', required >= 2)" di as error "Each treated unit requires >= 2 pre-treatment periods because:" di as error " (1) LOOCV holds out one period, requiring at least one remaining." di as error " (2) Unit distance is based on RMSE over common control periods." di as error " (3) Separating alpha_i and beta_t requires at least 2 periods." // List offending units qui levelsof `panelvar' if `ever_var'==1 & __trop_n_pre_valid_26 < 2 & `touse', local(bad_units) clean di as text _newline "Offending units:" di as text " Units with <2 pre-periods: `bad_units'" qui sum __trop_n_pre_valid_26 if `ever_var'==1 & __trop_n_pre_valid_26 < 2 & `touse' di as text " Minimum pre-periods for these units: " %1.0f r(min) di as text _newline "Suggested remedies:" di as text " 1. Check treatment start time: Confirm T_i^{start}." di as text " list `panelvar' `timevar' `treatvar' if inlist(`panelvar', `bad_units')" di as text " 2. Delay treatment start: Ensure >= 2 pre-periods if justifiable." di as text " 3. Drop unit: If truly insufficient history." di as text " drop if inlist(`panelvar', `bad_units')" di as text " 4. If all treated units have only 1 pre-treatment period, TROP LOOCV cannot run reliably." di as text " Consider using fixedlambda() or increasing pre-treatment data." capture drop __trop_is_control_26 __trop_never_treated_26 __trop_is_pre_26 __trop_n_pre_valid_26 capture drop __trop_T_start_26 ereturn scalar data_validated = 0 error 459 } di as result " passed (min_pre_treated=" %3.0f `min_pre_treated' ")" // === Check 4: For all i in Treated, N_{i,control} >= 1 === di as text "Check 4: Control periods for ever-treated units..." _continue qui gen byte __trop_is_control_obs_26 = (`treatvar' == 0) & __trop_valid == 1 if `touse' qui bysort `panelvar': egen int __trop_n_control_i_26 = total(__trop_is_control_obs_26) if `ever_var' == 1 & `touse' qui sum __trop_n_control_i_26 if `ever_var' == 1 & `touse' local min_control_per_unit = r(min) if missing(`min_control_per_unit') local min_control_per_unit = 0 if `min_control_per_unit' < 1 { di as error " FAILED" di "" qui levelsof `panelvar' if `ever_var' == 1 & __trop_n_control_i_26 == 0 & `touse', local(bad_units) clean di as error "{bf:ERROR}: Ever-treated units found with treatment in all periods" di as error "Offending units: `bad_units'" di as error "LOOCV requires control periods (W_it=0) for each treated unit." di as error "These units have no control periods for constructing pseudo-treatment points." di as text _newline "Suggested remedies:" di as text " 1. Check treatment coverage: Confirm if unit is truly treated in all periods." di as text " list `panelvar' `timevar' `treatvar' if `panelvar' inlist(`bad_units')" di as text " 2. Shorten treatment period: Treat only later periods if justifiable." di as text " 3. Drop unit: If unit is truly treated throughout." di as text " drop if `panelvar' inlist(`bad_units')" di as text " Note: Re-check N_control_units >= 2 after dropping." di as text " 4. Redesign: If most units are always treated, TROP may not apply." capture drop __trop_is_control_26 __trop_never_treated_26 __trop_is_pre_26 __trop_n_pre_valid_26 capture drop __trop_is_control_obs_26 __trop_n_control_i_26 __trop_T_start_26 ereturn scalar data_validated = 0 error 459 } di as result " passed (All ever-treated units have control periods)" // === Check 5: min_valid_pairs >= 2 === di as text "Check 5: Common control periods (Mata vectorized)..." _continue // Prepare data: Create panel ID and time ID (using non-tempvar variables) qui egen int __trop_panel_id_26 = group(`panelvar') if `touse' qui egen int __trop_time_id_26 = group(`timevar') if `touse' qui sum __trop_panel_id_26 if `touse' local N_panel = r(max) qui sum __trop_time_id_26 if `touse' local T_panel = r(max) // Mata vectorized computation of min_valid_pairs. // Unit distance denominator: sum_u 1{u!=t}(1-W_iu)(1-W_ju). // Each ever-treated unit needs >= 2 common control periods with at least // one other unit (one held out by LOOCV, leaving >= 1 for distance). // // Algorithm: O(N^2 * T) via matrix multiplication. // C[i,t] = 1{W_{it}=0 & Y_{it} non-missing} (N x T control mask) // overlap = C * C' (N x N common control periods) // min_valid_pairs = min_{i in ever-treated} max_{j!=i} overlap[i,j] // Call pre-compiled Mata function // (inline Mata blocks cause issues in batch mode) mata: st_local("min_valid_pairs", strofreal(_trop_chk_common_ctrl_periods("__trop_panel_id_26", "__trop_time_id_26", "`treatvar'", "`depvar'", "`touse'", strtoreal(st_local("N_panel")), strtoreal(st_local("T_panel"))))) if `min_valid_pairs' < 2 { di as error " FAILED" di "" di as error "{bf:ERROR}: Some ever-treated units have < 2 common control periods with all others" di as error " min_valid_pairs = `min_valid_pairs'" di as error "Unit distance formula:" di as error " dist_{-t}^{unit}(j,i) = sqrt[ sum_u I_u(Y_iu-Y_ju)^2 / sum_u I_u ]" di as error " where I_u = 1{u!=t}(1-W_iu)(1-W_ju), requires sum_u I_u >= 1" di as error "With LOOCV holding out one period, >= 2 common control periods are needed." di as error "Current min_valid_pairs=`min_valid_pairs', distance denominator may be 0." di as text _newline "Suggested remedies:" di as text " 1. Check treatment pattern: Confirm if W matrix is too sparse." di as text " 2. Expand pre-treatment: Add T_pr to increase common control periods." di as text " 3. Reduce treated units: Drop isolated treated units." di as text " 4. Impute missing values: If __trop_valid is too sparse." di as text " Warning: Imputation requires modeling assumptions." di as text " 5. If irreparable, TROP may not suit data." di as text " Consider SC (unit weights only) or DID (uniform weights)." capture drop __trop_is_control_26 __trop_never_treated_26 __trop_is_pre_26 __trop_n_pre_valid_26 capture drop __trop_is_control_obs_26 __trop_n_control_i_26 __trop_T_start_26 ereturn scalar data_validated = 0 error 459 } di as result " passed (min_valid_pairs=" %3.0f `min_valid_pairs' " >= 2)" // ========== All Checks Passed ========== di as text _newline "{hline 60}" di as result "{bf:✓ All TROP feasibility checks passed}" di as text "{hline 60}" di as text "Validation Results:" di as text " - Var(Y) = " %9.3f `var_Y' " > 0 passed" di as text " - N_treated_units = " %3.0f `N_treated_units' " >= 1 passed" di as text " - N_control = " %6.0f `N_control' " >= 1 passed" di as text " - N_control_units = " %3.0f `N_control_units' " >= 2 passed" di as text " - min_pre_treated = " %3.0f `min_pre_treated' " >= 1 passed" di as text " - Ever-treated unit control periods checked passed" di as text " - min_valid_pairs = " %3.0f `min_valid_pairs' " >= 2 passed" di as text "{hline 60}" _newline // ========== Store Diagnostics in e() ========== // Store critical validation metrics qui count if `treatvar' == 1 & __trop_valid == 1 & `touse' local N_treat = r(N) ereturn scalar N_treat = `N_treat' ereturn scalar N_control = `N_control' ereturn scalar N_control_units = `N_control_units' ereturn scalar min_pre_treated = `min_pre_treated' ereturn scalar min_valid_pairs = `min_valid_pairs' // ========== Final data_validated decision ========== // Set to 1 only if all strict constraints pass AND missing rate <= 0.3 if `miss_rate' <= 0.3 { ereturn scalar data_validated = 1 } else { ereturn scalar data_validated = 0 } // ========== Cleanup ========== capture drop __trop_is_control_26 __trop_never_treated_26 __trop_T_start_26 capture drop __trop_is_pre_26 __trop_n_pre_valid_26 __trop_is_control_obs_26 __trop_n_control_i_26 capture drop __trop_ever_treated_check __trop_panel_id_26 __trop_time_id_26 // ======================================================================== // Data Quality Comprehensive Report // ======================================================================== di as text _newline _newline di as text "{hline 70}" di as text "{bf: Data Quality Diagnostic Report}" di as text "{hline 70}" // Panel Dimensions di as text "" di as text "{bf:1. Panel Dimensions}" di as text " Units (N) = " %8.0f `N' di as text " Periods (T) = " %8.0f `T' di as text " Actual obs (N_obs) = " %8.0f `N_obs' di as text " Theoretical obs (NxT) = " %8.0f `N'*`T' // Balance Diagnostics di as text "" di as text "{bf:2. Balance Diagnostics}" di as text " Overall missing rate = " %6.2f `miss_rate'*100 "%" if `balanced' == 1 { di as text " Panel type = {bf:Balanced}" } else if `miss_rate' <= 0.1 { di as text " Panel type = Mildly unbalanced" di as text " (Minimal impact on algorithm)" } else if `miss_rate' <= 0.3 { di as text " Panel type = Moderately unbalanced" di as text " (Monitor estimation precision)" } else { di as text " Panel type = {bf:Severely unbalanced}" di as text " {bf:WARNING: > 30% threshold}" } // Treatment Patterns di as text "" di as text "{bf:3. Treatment Patterns}" local pattern_display = "" if "`e(treatment_pattern)'" == "single_treated_unit" { local pattern_display = "Single Treated Unit" } else if "`e(treatment_pattern)'" == "multiple_treated_simultaneous" { local pattern_display = "Multiple, Simultaneous Adoption" } else if "`e(treatment_pattern)'" == "staggered_adoption" { local pattern_display = "Staggered Adoption" } else if "`e(treatment_pattern)'" == "switching_treatment" { local pattern_display = "Switching Treatment (WARNING)" } di as text " Identified pattern = {bf:`pattern_display'}" di as text " Ever-treated units = " %8.0f e(N_treated_units) di as text " Treatment periods = " %8.0f e(T_treat_periods) if e(has_switching) == 1 { di as text " Treatment switching = {bf:Yes} (max_switches=" %3.0f e(max_switches) ")" di as text " {bf:WARNING: Violates 'no dynamic effects' assumption}" } else { di as text " Treatment switching = No" } // Outlier Diagnostics di as text "" di as text "{bf:4. Outlier Diagnostics}" di as text " Method = Tukey's rule (k=10 conservative)" di as text " IQR = " %12.4f `IQR' di as text " Outlier bounds = [" %10.2f `lower' ", " %10.2f `upper' "]" di as text " Outliers detected = " %8.0f `n_outliers' di as text " Outlier percentage = " %6.3f `outlier_rate'*100 "%" if `n_outliers' > 0 { di as text " {bf:WARNING: Check or handle outliers}" } // Summary di as text "" di as text "{hline 70}" if `miss_rate' <= 0.3 & `n_outliers' / `N_obs' < 0.05 { di as result "{bf:PASS: Data passed all strict constraints}" di as text " Safe to proceed to estimation." } else { di as text "WARNING {bf:Data quality attention needed:}" if `miss_rate' > 0.3 { di as text " - Missing rate > 30% threshold, consider data cleaning." } if `n_outliers' / `N_obs' >= 0.05 { di as text " - Outliers > 5%, recommend inspection." } di as text " TROP can run, but estimation quality may be affected." } di as text "{hline 70}" _newline // ======================================================================== // Data Validation Integrity Report // ======================================================================== // ========== Prerequisites Check ========== if e(data_validated) != 1 { di as text _newline "NOTE: Data validation failed (e(data_validated)=`=e(data_validated)'), skipping summary report." di as text " Summary report requires all strict constraints to pass." exit 0 } // ========== Function 1: Data quality report ========== di as text _newline _newline di as text "{hline 80}" di as text "{bf:TROP Data Validation Integrity Report}" di as text "{hline 80}" // Part 1: Panel Dimensions di as text _newline "{bf:[1. Panel Dimensions]}" di as text " Units (N) = " %8.0f e(N) di as text " Periods (T) = " %8.0f e(T) di as text " Theoretical obs (NxT) = " %8.0f e(N)*e(T) di as text " Actual obs (N_obs) = " %8.0f e(N_obs) local miss_pct = e(miss_rate) * 100 if e(miss_rate) < 0.1 { di as text " Missing rate = " %6.2f `miss_pct' "% (Good)" } else if e(miss_rate) < 0.3 { di as text " Missing rate = " %6.2f `miss_pct' "% (Moderately unbalanced)" } else { di as text " Missing rate = " %6.2f `miss_pct' "% (Severely unbalanced)" } if e(balanced) == 1 { di as text " Balanced = Yes" } else { di as text " Balanced = No" } // Part 2: Treatment Patterns di as text _newline "{bf:[2. Treatment Patterns]}" di as text " Pattern = {bf:`e(treatment_pattern)'}" di as text " Ever-treated units = " %8.0f e(N_treated_units) di as text " Treatment periods = " %8.0f e(T_treat_periods) if e(N_control_units) >= 10 { di as text " Control units = " %8.0f e(N_control_units) " (Sufficient)" } else if e(N_control_units) >= 2 { di as text " Control units = " %8.0f e(N_control_units) " (Low)" } else { di as text " Control units = " %8.0f e(N_control_units) " (Insufficient)" } capture confirm existence e(has_switching) if _rc == 0 { if e(has_switching) == 1 { di as text " Switching detected = {bf:Yes} (max_switches=" %3.0f e(max_switches) ", WARNING: Violates 'no dynamic effects' assumption)" } else { di as text " Switching detected = No" } } di as text _newline "{bf:[3. TROP Algorithm Feasibility]}" if e(N_control_units) >= 100 { di as text " Control units = " %8.0f e(N_control_units) " (Sufficient)" } else if e(N_control_units) >= 2 { di as text " Control units = " %8.0f e(N_control_units) " (Low)" } else { di as text " Control units = " %8.0f e(N_control_units) " (Insufficient)" } if e(min_pre_treated) >= 10 { di as text " Min pre-treat period = " %8.0f e(min_pre_treated) " (Sufficient)" } else if e(min_pre_treated) >= 5 { di as text " Min pre-treat period = " %8.0f e(min_pre_treated) " (Moderate)" } else { di as text " Min pre-treat period = " %8.0f e(min_pre_treated) " (Short)" } if e(min_valid_pairs) >= 10 { di as text " Min paired units = " %8.0f e(min_valid_pairs) " (Sufficient)" } else if e(min_valid_pairs) >= 5 { di as text " Min paired units = " %8.0f e(min_valid_pairs) " (Moderate)" } else { di as text " Min paired units = " %8.0f e(min_valid_pairs) " (Low)" } // Part 4: Outlier Diagnostics qui count if __trop_outlier_flag == 1 & `touse' local n_outliers_27 = r(N) di as text _newline "{bf:[4. Outliers]}" di as text " Method = Tukey 10xIQR rule" if `n_outliers_27' > 0 { local outlier_pct_27 = `n_outliers_27' / e(N_obs) * 100 if `outlier_pct_27' < 1 { di as text " Count = " %8.0f `n_outliers_27' " (" %6.2f `outlier_pct_27' "%) (Normal)" } else if `outlier_pct_27' < 5 { di as text " Count = " %8.0f `n_outliers_27' " (" %6.2f `outlier_pct_27' "%) (Moderate)" } else { di as text " Count = " %8.0f `n_outliers_27' " (" %6.2f `outlier_pct_27' "%) (High)" } di as text " Recommendation = Outliers marked (__trop_outlier_flag=1). User to decide on removal/winsorizing." } else { di as text " Count = 0 (None detected)" } // ========== Temporary variable cleanup ========== // Drop internal variables that are no longer needed local drop_vars "__trop_n_valid_i __trop_n_valid_t __trop_dup_count __trop_time_diff _has_nonmono" local drop_vars "`drop_vars' _is_control _never_treated _is_pre _n_pre__trop_valid" local drop_vars "`drop_vars' _is_control_obs _n_control_i __trop_any_treated_t _first_treat_idx __trop_W_diff __trop_is_pretreat" foreach v of local drop_vars { capture drop `v' } // ========== ereturn field verification ========== di as text _newline "{bf:[5. ereturn Checks]}" // Verify required fields (6 fields) local required_fields "data_validated N T N_obs treatment_pattern miss_rate" local n_required = 0 local n_required_total: word count `required_fields' local missing_required "" foreach field of local required_fields { capture confirm existence e(`field') if _rc == 0 { local n_required = `n_required' + 1 } else { local missing_required "`missing_required' e(`field')" } } // Verify diagnostic fields (7 fields) local diagnostic_fields "N_control N_control_units min_pre_treated min_valid_pairs balanced" local n_diagnostic = 0 local n_diagnostic_total: word count `diagnostic_fields' foreach field of local diagnostic_fields { capture confirm existence e(`field') if _rc == 0 { local n_diagnostic = `n_diagnostic' + 1 } } if `n_required' == `n_required_total' { di as text " Required fields (6) = " %1.0f `n_required' "/" %1.0f `n_required_total' " (All present)" } else { di as text " Required fields (6) = " %1.0f `n_required' "/" %1.0f `n_required_total' " (Missing: `missing_required')" } if `n_diagnostic' >= 5 { di as text " Diagnostic fields (7) = " %1.0f `n_diagnostic' "/" %1.0f `n_diagnostic_total' " (Core fields present)" } else { di as text " Diagnostic fields (7) = " %1.0f `n_diagnostic' "/" %1.0f `n_diagnostic_total' " (Partial missing)" } // If required fields missing, report error if `n_required' < `n_required_total' { di as error _newline "ERROR: Incomplete e() fields. Validation not fully executed." di as error "Missing fields: `missing_required'" di as error "Check implementation of validation steps." ereturn scalar validation_complete = 0 error 459 } // ========== Function 5: StataMCP verification (optional) ========== // Runs when StataMCP is enabled if "`statemcp'" != "" | "`mcp'" != "" { di as text _newline "{bf:[StataMCP Verification]}" di as text " Check 1: e() field relationships" // Check 1.1: N_obs <= N * T if e(N_obs) > e(N) * e(T) { di as text " - N_obs > N*T (Data anomaly)" } else { di as text " - N_obs <= N*T (OK)" } // Check 1.2: miss_rate consistency local miss_calc = 1 - e(N_obs)/(e(N)*e(T)) if abs(e(miss_rate) - `miss_calc') < 1e-10 { di as text " - miss_rate definition consistent" } else { di as text " - miss_rate inconsistent (e=" %9.6f e(miss_rate) ", calc=" %9.6f `miss_calc' ")" } di as text " Check 2: Temporary variable quality" // Check 2.1: __trop_valid completeness qui count if missing(__trop_valid) & `touse' if r(N) == 0 { di as text " - __trop_valid has no missing values" } else { di as text " - __trop_valid has " %6.0f r(N) " missing values" } // Check 2.2: __trop_tindex range qui sum __trop_tindex if `touse', meanonly if r(min) >= 1 & r(max) <= e(T) { di as text " - __trop_tindex range valid [1, " %3.0f e(T) "]" } else { di as text " - __trop_tindex range invalid [" %6.1f r(min) ", " %6.1f r(max) "]" } // Check 2.3: __trop_ever_treated consistency capture confirm variable __trop_ever_treated if _rc == 0 { qui count if __trop_ever_treated == 1 & `touse' tempvar unit_tag_27 qui egen `unit_tag_27' = tag(`panelvar') if __trop_ever_treated == 1 & `touse' qui count if `unit_tag_27' == 1 local n__trop_ever_treated_units = r(N) if `n__trop_ever_treated_units' == e(N_treated_units) { di as text " - __trop_ever_treated matches e(N_treated_units)" } else { di as text " - __trop_ever_treated count(" %3.0f `n__trop_ever_treated_units' ") != e(N_treated_units)(" %3.0f e(N_treated_units) ")" } } } // ========== Validation status summary ========== di as text _newline "{bf:[6. Validation Status]}" if e(data_validated) == 1 & `n_required' == `n_required_total' { di as text " Validation = {bf:Passed}" di as text " Next Step = {bf:Ready for Estimation}" di as text " Temp Variables = {bf:Cleaned}" di as text " ereturn Fields = {bf:Complete}" // Set completion flag ereturn scalar validation_complete = 1 di as text _newline "{bf:PASS: Data validation completed successfully.}" di as text "Ready for fixed effects estimation." } else { di as text " Validation = {bf:Failed}" di as text " Next Step = {bf:Cannot Proceed}" di as text " Recommendation = Review diagnostics above and re-run validation." ereturn scalar validation_complete = 0 } di as text "{hline 80}" di as text "Detailed diagnostics saved in e(). Type {bf:ereturn list} to view." di as text "{hline 80}" _newline // Note: e(data_validated) is set by feasibility checks (strict constraints + missing rate <= 30%) // This module is the final gate for data validation status. end // ---------------------------------------------------------------------------- // Internal routine: balance diagnostics and missingness analysis. // ---------------------------------------------------------------------------- /* trop_balance_check --- Panel balance and missingness diagnostics. Checks: - Panel dimensions (N units, T periods). - Overall missingness rate with tiered diagnostics (note/warning/error). - Full-row missingness: units with no valid observations. - Full-column missingness: periods with no valid observations. Hard threshold: missingness > 30% triggers an error because fixed-effect variance inflates, SVD condition numbers degrade, and LOOCV validation variance increases. Outputs stored in e(): e(N), e(T), e(N_obs), e(miss_rate), e(balanced). Creates persistent variable __trop_valid (byte). */ program define trop_balance_check, eclass version 17.0 syntax varlist(min=2 max=2 numeric) [if] [in], /// PANelvar(varname) /// TIMevar(varname numeric) /// [NOBALance] // skip balance check // Cleanup potential leftover __trop_valid variable capture drop __trop_valid // Parse variable list tokenize `varlist' local depvar `1' local treatvar `2' // Mark sample marksample touse, novarlist // Display start info di as text _newline "{bf:Data Validation Module - Balance Diagnostics}" di as text "{hline 70}" di as text "Theoretical Basis:" di as text " - Identification: FE identification requires non-missing observations." di as text " - Constraint: 30% missing rate hard threshold." di as text "{hline 70}" _newline if "`nobalance'" != "" { di as text "Balance diagnostics skipped." _newline exit } // ======================================================================== // Step 1: Compute Panel Dimensions // ======================================================================== di as text "Step 1: Computing panel dimensions..." _continue // Calculate number of units N qui tab `panelvar' if `touse' local N = r(r) // Calculate number of periods T qui tab `timevar' if `touse' local T = r(r) // Theoretical observations local N_theory = `N' * `T' di as result " Done" di as text " Theoretical panel size: N={bf:`N'} x T={bf:`T'} = {bf:`N_theory'} obs" // ======================================================================== // Step 2: Calculate Missingness Rate // ======================================================================== di as text "Step 2: Calculating missing rate..." _continue // Create missing marker variable qui gen byte __trop_valid = !missing(`depvar', `treatvar') if `touse' label variable __trop_valid "Non-missing indicator (Y and W both observed)" // Count actual non-missing observations qui count if __trop_valid == 1 & `touse' local N_obs = r(N) // Calculate overall missing rate local miss_overall = 1 - `N_obs' / `N_theory' di as result " Done" di as text " Actual observations: {bf:`N_obs'}" di as text " Overall missing rate: " %4.2f `miss_overall'*100 "%" // ======================================================================== // Step 3: Graded Warning System (3-Tier) // ======================================================================== di as text "Step 3: Diagnostic classification..." _continue if `miss_overall' > 0.3 { // HARD ERROR: Severely Unbalanced (>30%) di as error " Failed!" di "" di as error "{bf:ERROR: Severely Unbalanced}" di as error "Missing rate: " %4.1f `miss_overall'*100 "% > 30% (Hard Threshold)" di as error "Theoretical obs: `N_theory' (N=`N', T=`T')" di as error "Actual obs: `N_obs'" di as text _newline "{bf:Theoretical Basis} (Variance Inflation & Instability):" di as text " (A) FE Variance Inflation +43%" di as text " Var(alpha_i) falls as sum(w_is) decreases." di as text " Variance ratio: Var_30%/Var_0% approx 1.43" di as text "" di as text " (B) SVD Condition Number x2 (Numerical Instability)" di as text " kappa(Y_missing) increases, leading to unstable rank selection." di as text "" di as text " (C) CV Loss Variance +43%" di as text " Reduced effective neighborhood for LOOCV." // Compute helper variable for repair suggestions capture drop __trop_n_valid_i qui bysort `panelvar': egen __trop_n_valid_i = total(__trop_valid) if `touse' local cutoff_i = int(0.7 * `T') di as text _newline "{bf:Repair Suggestions} (Prioritized):" di as text " 1. {bf:[Recommended]} Drop high-missingness units:" di as text " {stata drop if __trop_n_valid_i < `cutoff_i'}" di as text " where __trop_n_valid_i is valid obs per unit." di as text " Expected: Keep high-quality units with >= 70% obs." di as text "" di as text " 2. Drop high-missingness periods:" qui bysort `timevar': egen __trop_n_valid_t = total(__trop_valid) if `touse' local cutoff_t = int(0.7 * `N') di as text " {stata drop if __trop_n_valid_t < `cutoff_t'}" di as text " Expected: Keep high-quality periods with >= 70% obs." qui drop __trop_n_valid_t di as text "" di as text " 3. Impute missing values (User responsibility):" di as text " e.g., Mean imputation, forward fill." di as text " {bf:WARNING:} May violate factor model assumptions." di as text "" di as text " 4. Re-evaluate data source suitability." // Set failure flags ereturn scalar data_validated = 0 ereturn scalar miss_rate = `miss_overall' ereturn scalar N = `N' ereturn scalar T = `T' ereturn scalar N_obs = `N_obs' ereturn scalar balanced = 0 error 459 // Severely unbalanced panel } else if `miss_overall' > 0.1 { // WARNING: Moderately Unbalanced (10%-30%) di as result " Moderately Unbalanced" di "" di as text "{bf:WARNING: Moderately Unbalanced}" di as text "Missing rate: " %4.1f `miss_overall'*100 "% (10%-30% range)" di as text "Theoretical obs: `N_theory', Actual obs: `N_obs'" di as text "{bf:Suggestion:} Check missingness patterns to ensure FE estimation is not compromised." di as text " Weights calculation will automatically exclude missing observations." _newline } else if `miss_overall' > 0 { // NOTE: Mildly Unbalanced (0%-10%) di as result " Mildly Unbalanced" di as text "NOTE: Mildly Unbalanced (Missing rate=" %4.2f `miss_overall'*100 "%)" _newline } else { // Fully Balanced di as result " Fully Balanced" di as text "NOTE: Fully balanced panel (miss_rate=0)" _newline } // ======================================================================== // Step 4: Check Full Row Missing (Unit Dimension Hard Constraint) // ======================================================================== di as text "Step 4: Checking for full row missingness (Unit dimension)..." _continue tempvar n_valid_i qui bysort `panelvar': egen `n_valid_i' = total(__trop_valid) if `touse' // Check if any unit is missing in all periods qui count if `n_valid_i' == 0 & `touse' local n_allmiss_units = r(N) if `n_allmiss_units' > 0 { di as error " Failed!" di "" // Get violating unit IDs (deduplicate) tempvar unit_marked qui gen byte `unit_marked' = (`n_valid_i' == 0 & `touse') qui bysort `panelvar': egen byte _allmiss_unit = max(`unit_marked') di as error "{bf:ERROR: Units missing in all periods detected}" di as error "Count: {bf:`n_allmiss_units'} units" di as error "Unit fixed effect alpha_i requires at least one observed period." di as error "If unit i is missing in all periods, alpha_i is undefined." di as text _newline "{bf:Violating Units} (First 10):" list `panelvar' if _allmiss_unit == 1 in 1/10, /// noobs table separator(0) abbrev(12) di as text _newline "{bf:Repair Suggestion}:" di as text " 1. Drop these units:" di as text " {stata drop if _allmiss_unit == 1}" di as text " Reason: These units contribute nothing to estimation." // Set failure flag ereturn scalar data_validated = 0 drop _allmiss_unit error 459 } di as result " Passed (No full row missingness)" // ======================================================================== // Step 5: Check Full Column Missing (Time Dimension Hard Constraint) // ======================================================================== di as text "Step 5: Checking for full column missingness (Time dimension)..." _continue tempvar n_valid_t qui bysort `timevar': egen `n_valid_t' = total(__trop_valid) if `touse' // Check if any period is missing in all units qui count if `n_valid_t' == 0 & `touse' local n_allmiss_periods = r(N) if `n_allmiss_periods' > 0 { di as error " Failed!" di "" // Get violating period IDs (deduplicate) tempvar period_marked qui gen byte `period_marked' = (`n_valid_t' == 0 & `touse') qui bysort `timevar': egen byte _allmiss_period = max(`period_marked') di as error "{bf:ERROR: Periods missing in all units detected}" di as error "Count: {bf:`n_allmiss_periods'} periods" di as error "Time fixed effect beta_t requires at least one observed unit." di as error "If period t is missing in all units, beta_t is undefined." di as text _newline "{bf:Violating Periods} (First 10):" list `timevar' if _allmiss_period == 1 in 1/10, /// noobs table separator(0) abbrev(12) di as text _newline "{bf:Repair Suggestion}:" di as text " 1. Drop these periods:" di as text " {stata drop if _allmiss_period == 1}" di as text " Reason: These periods contribute nothing to estimation." // Set failure flag ereturn scalar data_validated = 0 drop _allmiss_period error 459 } di as result " Passed (No full column missingness)" // ======================================================================== // Step 6: Store Diagnostic Metrics in e() // ======================================================================== di as text "Step 6: Storing diagnostic metrics..." _continue ereturn scalar N = `N' ereturn scalar T = `T' ereturn scalar N_obs = `N_obs' ereturn scalar miss_rate = `miss_overall' ereturn scalar balanced = (`miss_overall' < 1e-10) // Note: data_validated is NOT set to 1 here. Validation requires subsequent checks. di as result " Done" di as text " Stored: e(N), e(T), e(N_obs), e(miss_rate), e(balanced)" // ======================================================================== // Step 7: Cleanup (__trop_valid retained for subsequent use) // ======================================================================== // Clean internal variables capture drop __trop_n_valid_i capture drop __trop_n_valid_t // __trop_valid is retained for use by subsequent validation and estimation steps. // Display summary di as text _newline "{hline 70}" di as result "{bf:✓ Balance Diagnostics Passed}" di as text " - Missing rate: " %4.2f `miss_overall'*100 "%" if e(balanced) == 1 { di as text " - Balance: {bf:Fully Balanced}" } else if `miss_overall' <= 0.1 { di as text " - Balance: Mildly Unbalanced (Minimal impact)" } else { di as text " - Balance: Moderately Unbalanced (Monitor precision)" } di as text " - Full row missing check: Passed (Every unit has at least 1 obs)" di as text " - Full col missing check: Passed (Every period has at least 1 obs)" di as text " - __trop_valid variable: Created (For use in subsequent validation)" di as text "{hline 70}" _newline end // _trop_chk_common_ctrl_periods() is defined in trop_validation.mata, // pre-compiled via load_mata_once.do / compile_all.do. // Inline Mata definitions at ado-file tail may fail during adopath auto-loading.