Fix/trop paper conformance#84
Conversation
Address four issues identified in the implementation assessment:
Issue A (Critical): Expand control unit selection
- Modified _compute_observation_weights() to use D[t,j]==0 instead of
only never-treated units, allowing pre-treatment observations of
eventually-treated units to serve as controls
Issue B (Moderate): Per-observation distance excludes target period
- Distance computation now correctly excludes target period t as
specified in Equation 3 of the paper (1{u ≠ t})
- Added compute_unit_distance_for_obs() to Rust backend
Issue C (Moderate-High): Weighted nuclear norm solver
- Implemented _weighted_nuclear_norm_solve() using iterative weighted
soft-impute (Mazumder et al. 2010)
- Properly handles W=0 observations to prevent L from absorbing
treatment effects
Issue D (Minor): Stratified bootstrap sampling
- Modified _bootstrap_variance() to sample control and treated units
separately, preserving treatment ratio per Algorithm 3
Also updated:
- Tutorial notebook with precise paper terminology
- Added TestPaperConformanceFixes test class with 5 new tests
- All 38 TROP tests pass
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Updates compute_weight_matrix:
- Allow ALL units in weight computation (not just untreated at target)
- The (1 - D_js) masking in the loss handles treatment exclusion
- Normalize time and unit weights to sum to 1 (probability weights)
- Distance still excludes target period per Equation 3
Updates estimate_model:
- Use weighted proximal gradient for L update instead of direct soft-thresholding
- L ← prox_{η·λ_nn·||·||_*}(L + η·(W ⊙ (R - L)))
- Step size η ≤ 1/max(W) for convergence
- For W=0 cells (treated obs), L remains unchanged
These changes align the Rust backend with the Python implementation
which was already fixed in the previous commit.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review: PR #84 - Fix/trop paper conformanceAuthor: igerber Executive SummaryThis PR addresses four conformance issues between the TROP estimator implementation and the original paper (Athey, Imbens, Qu & Viviano, 2025). The fixes are well-documented with clear references to paper equations and algorithms. The changes are methodologically correct and improve the estimator's alignment with the paper specification. Test coverage is comprehensive. Part 1: Methodology ReviewIssue A: Control Set Definition (Lines 959-1070 in trop.py)Paper Reference: Equation 2 (page 7) Change: The control set for weight computation now includes pre-treatment observations of eventually-treated units, not just never-treated units. Assessment: Correct. The paper's objective sums over ALL observations where Code Review: # Valid control units at time t: D[t, j] == 0
valid_control_at_t = D_stored[t, :] == 0This correctly identifies units not treated at time Issue B: Distance Computation Excludes Target Period (Lines 615-660 in trop.py)Paper Reference: Equation 3 (page 7) specifies Change: Assessment: Correct. The indicator function Code Review: valid = np.ones(n_periods, dtype=bool)
valid[target_period] = False # Exclude target periodProperly implements the paper specification. Issue C: Weighted Nuclear Norm Solver (Lines 1133-1226 in trop.py)Paper Reference: Equation 2 (page 7) Change: New Assessment: Correct. The previous implementation used unweighted soft-thresholding. The paper's objective includes observation weights in the squared loss term, requiring a weighted soft-impute approach. Algorithm Implementation: # Proximal gradient iteration: L_{k+1} = prox_{lambda}(L_k + W * (R - L_k))
gradient_step = L + W_norm * (R_masked - L)
L = self._soft_threshold_svd(gradient_step, lambda_nn)This follows the standard proximal gradient algorithm for weighted matrix completion (Mazumder et al. 2010). Minor Concern: The step size is implicitly 1 with normalized weights. The Rust implementation computes Issue D: Stratified Bootstrap (Lines 1544-1578 in trop.py)Paper Reference: Algorithm 3 (page 27) Change: Bootstrap now samples control and treated units separately (stratified sampling) to preserve the treatment ratio. Assessment: Correct. The paper specifies sampling Code Review: sampled_control = rng.choice(control_units, size=n_control_units, replace=True)
sampled_treated = rng.choice(treated_units, size=n_treated_units, replace=True)
sampled_units = np.concatenate([sampled_control, sampled_treated])Properly implements Algorithm 3's stratified sampling. Rust Backend ConsistencyThe Rust implementation in
Assessment: Both Python and Rust implementations are consistent. Part 2: Issues FoundCritical IssuesNone identified. Medium Issues
Minor Issues
Part 3: Security AssessmentNo security issues identified. This PR modifies statistical estimation code without any:
Part 4: Documentation AssessmentStrengths:
Gaps:
Part 5: Performance AssessmentPotential Concerns:
Recommendation: Run benchmarks on larger datasets to quantify any performance impact. The correctness improvement likely justifies moderate slowdown. Part 6: Maintainability AssessmentStrengths:
No Breaking Changes: The public API remains unchanged. Internal method signatures were modified but these are private ( RecommendationsMust Fix (before merge)None - this PR is ready for merge. Should Fix
Nice to Have
Final Assessment
Overall Verdict: Approved This PR correctly addresses four methodological discrepancies between the implementation and the TROP paper. The fixes are well-documented, thoroughly tested (292 new test lines), and consistently implemented in both Python and Rust backends. The changes improve statistical correctness and should be merged. Review generated by Claude Code |
Address code review feedback to remove unused API parameters: Rust backend (trop.rs): - Remove control_unit_idx and unit_dist_matrix from loocv_grid_search - Remove control_unit_idx, treated_obs_t/i, unit_dist_matrix from bootstrap_trop_variance - Remove unit_dist_boot computation in bootstrap (no longer needed) - Remove control_units and unit_dist from internal functions Python (trop.py): - Update _rust_loocv_grid_search call to use new signature - Update _rust_bootstrap_trop_variance call to use new signature - Remove unused variable preparation for removed parameters Tests (test_rust_backend.py): - Update test calls to use new API signatures - Remove unused variable assignments The precomputed unit_dist_matrix is no longer needed by the Rust backend since per-observation distances are computed dynamically to properly exclude the target period per Equation 3 of the paper. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
No description provided.