Download validation/comprehensive_parameter_sweep.py from sdilbaz/prime_py: direct link, hf CLI and curl.
- Browser
- Download file 18.5 kB
-
https://huggingface.co/spaces/sdilbaz/prime_py/resolve/main/validation/comprehensive_parameter_sweep.py
- Command line
-
hf download hf://spaces/sdilbaz/prime_py/validation/comprehensive_parameter_sweep.py
-
curl -L -o comprehensive_parameter_sweep.py https://huggingface.co/spaces/sdilbaz/prime_py/resolve/main/validation/comprehensive_parameter_sweep.py
18.5 kB
| #!/usr/bin/env python3 | |
| """ | |
| Comprehensive parameter sweep validation: Compare Python vs R PRIME model. | |
| Tests all parameter combinations and reports differences for ALL outputs. | |
| Uses the Hugging Face Space (prime_py) code for validation. | |
| """ | |
| import json | |
| import subprocess | |
| import sys | |
| from pathlib import Path | |
| from typing import Dict, List, Tuple | |
| import numpy as np | |
| import pandas as pd | |
| # Add src to path - USE PRIME_PY CODE (Hugging Face Space) | |
| sys.path.insert(0, str(Path(__file__).parent.parent / "src")) | |
| from primepy.demo import DemoParams, run_demo_and_export | |
| def run_r_model(params_dict: dict, output_dir: Path) -> Dict: | |
| """Run R PRIME model and return all results.""" | |
| r_script = Path(__file__).parent / "validate_r_python_parity.R" | |
| base_dir = Path(__file__).parent.parent | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| # Prepare R arguments | |
| args = [ | |
| "Rscript", | |
| str(r_script), | |
| params_dict["country_iso3"], | |
| str(params_dict["vaccine_efficacy_before"]), | |
| str(params_dict["vaccine_efficacy_after"]), | |
| str(params_dict["coverage_vac"]), | |
| str(params_dict["age_vaccination"]), | |
| params_dict["dataset_year"], | |
| "TRUE" if params_dict["use_discounting"] else "FALSE", | |
| str(params_dict["discount_rate_cost"]), | |
| str(params_dict["discount_rate_benefit"]), | |
| params_dict["vaccine"], | |
| str(output_dir), | |
| "TRUE", # analyseCosts | |
| str(params_dict.get("vaccine_price_total", 26.96)), | |
| str(params_dict.get("vaccine_delivery_cost_total", 15.0)), | |
| str(params_dict.get("cancer_treatment_cost", 1815.0)), | |
| ] | |
| result = subprocess.run(args, capture_output=True, text=True, cwd=base_dir) | |
| if result.returncode != 0: | |
| return {"error": f"R script failed: {result.stderr}"} | |
| # Check status | |
| status_file = output_dir / "r_status.txt" | |
| if not status_file.exists() or "ERROR" in status_file.read_text(): | |
| return { | |
| "error": f"R model error: {status_file.read_text() if status_file.exists() else 'No status file'}" | |
| } | |
| # Read cost-effectiveness results | |
| ce_file = output_dir / "r_cost_effectiveness.csv" | |
| if not ce_file.exists(): | |
| return {"error": f"R CE file not found: {ce_file}"} | |
| r_ce = pd.read_csv(ce_file) | |
| # Convert to dict with standardized keys | |
| results = {} | |
| for _, row in r_ce.iterrows(): | |
| metric = row["metric"] | |
| value = row["value"] | |
| results[metric] = value | |
| return results | |
| def run_python_model(params: DemoParams, output_dir: Path) -> Dict: | |
| """Run Python model and return all results.""" | |
| base_dir = Path(__file__).parent.parent | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| try: | |
| output = run_demo_and_export( | |
| base_dir=base_dir, | |
| params=params, | |
| out_dir=output_dir, | |
| save_intermediates=False, | |
| ) | |
| if "ce_results" not in output: | |
| return {"error": "Python model did not return CE results"} | |
| ce = output["ce_results"] | |
| # Convert to dict with R-compatible keys | |
| results = { | |
| "Vac cohort size": ce.vaccinated_cohort_size, | |
| "CeCx prevented": ce.cases_prevented, | |
| "Deaths prevented": ce.deaths_prevented, | |
| "Life years saved": ce.life_years_saved, | |
| "Nonfatal DALYs prevented": ce.nonfatal_dalys_prevented, # Just YLD, not total | |
| "Vaccine cost": ce.vaccination_costs, | |
| "Costs saved": ce.treatment_costs_saved, | |
| "Net cost": ce.net_costs, | |
| "Cost/death prevented": ce.cost_per_death_prevented, | |
| "Cost/life year saved": ce.cost_per_life_year_saved, | |
| "Cost/DALY prevented": ce.cost_per_daly_prevented, | |
| "GDP/capita": ce.gdp_per_capita, | |
| "CE at 1xGDP/capita?": 1.0 if ce.ce_at_1xgdp else 0.0, | |
| "CE at 3xGDP/capita?": 1.0 if ce.ce_at_3xgdp else 0.0, | |
| } | |
| return results | |
| except Exception as e: | |
| return {"error": f"Python model failed: {str(e)}"} | |
| def compare_results( | |
| r_results: Dict, py_results: Dict, tolerance: float = 0.01 | |
| ) -> Tuple[bool, List[Dict]]: | |
| """ | |
| Compare R and Python results. | |
| Returns (all_match, differences_list) | |
| """ | |
| if "error" in r_results: | |
| return False, [{"metric": "R_ERROR", "error": r_results["error"]}] | |
| if "error" in py_results: | |
| return False, [{"metric": "PY_ERROR", "error": py_results["error"]}] | |
| # Metrics to compare (exclude GDP, Cohort size, Cut-off price which may differ) | |
| metrics_to_compare = [ | |
| "Vac cohort size", | |
| "CeCx prevented", | |
| "Deaths prevented", | |
| "Life years saved", | |
| "Nonfatal DALYs prevented", | |
| "Vaccine cost", | |
| "Costs saved", | |
| "Net cost", | |
| "Cost/death prevented", | |
| "Cost/life year saved", | |
| "Cost/DALY prevented", | |
| "CE at 1xGDP/capita?", | |
| "CE at 3xGDP/capita?", | |
| ] | |
| differences = [] | |
| all_match = True | |
| # Compare only the metrics we care about | |
| for key in metrics_to_compare: | |
| r_val = r_results.get(key) | |
| py_val = py_results.get(key) | |
| if r_val is None: | |
| differences.append( | |
| { | |
| "metric": key, | |
| "r_value": "MISSING", | |
| "py_value": py_val, | |
| "abs_diff": None, | |
| "rel_diff": None, | |
| } | |
| ) | |
| all_match = False | |
| continue | |
| if py_val is None: | |
| differences.append( | |
| { | |
| "metric": key, | |
| "r_value": r_val, | |
| "py_value": "MISSING", | |
| "abs_diff": None, | |
| "rel_diff": None, | |
| } | |
| ) | |
| all_match = False | |
| continue | |
| # Handle NaN/inf | |
| if np.isnan(r_val) and np.isnan(py_val): | |
| continue | |
| if np.isinf(r_val) and np.isinf(py_val) and np.sign(r_val) == np.sign(py_val): | |
| continue | |
| # Calculate differences | |
| abs_diff = abs(py_val - r_val) | |
| if abs(r_val) > 1e-10: | |
| rel_diff = abs_diff / abs(r_val) | |
| else: | |
| rel_diff = abs_diff | |
| if rel_diff > tolerance: | |
| differences.append( | |
| { | |
| "metric": key, | |
| "r_value": r_val, | |
| "py_value": py_val, | |
| "abs_diff": abs_diff, | |
| "rel_diff": rel_diff, | |
| "rel_diff_pct": rel_diff * 100, | |
| } | |
| ) | |
| all_match = False | |
| return all_match, differences | |
| def generate_parameter_combinations() -> List[Tuple[str, Dict]]: | |
| """Generate all parameter combinations to test.""" | |
| base_params = { | |
| "country_iso3": "TUR", | |
| "dataset_year": "2020", | |
| "use_country_sexual_debut": True, | |
| "use_proxy_fallback": False, | |
| "use_new_turkish_population": False, # Use standard data for R comparison | |
| "current_year": 2025, | |
| "oldest_year": 2100, | |
| } | |
| # Parameter sweep ranges | |
| param_sweep = { | |
| "vaccine": ["4vHPV", "9vHPV"], | |
| "age_vaccination": [9, 10, 12], | |
| "coverage_vac": [0.5, 0.7, 0.9], | |
| "vaccine_efficacy_before": [0.8, 0.9, 1.0], | |
| "vaccine_efficacy_after": [0.0, 0.1], | |
| # protection_duration_years: Only test None (lifelong) for R vs Python comparison | |
| # R's RunCountry doesn't support limited protection | |
| "use_discounting": [True, False], | |
| "discount_rate_cost": [0.0, 0.03, 0.05], | |
| "discount_rate_benefit": [0.0, 0.03, 0.05], | |
| # Cost parameters | |
| "vaccine_price_total": [13.48, 26.96, 40.0], | |
| "vaccine_delivery_cost_total": [7.5, 15.0, 30.0], | |
| "cancer_treatment_cost": [1000.0, 1815.0, 450.0], | |
| } | |
| # Generate combinations (with some filtering to reduce total) | |
| test_cases = [] | |
| # Test each parameter individually (one-at-a-time) | |
| for param_name, param_values in param_sweep.items(): | |
| for value in param_values: | |
| # Skip when value is the default (already tested in base case) | |
| defaults = { | |
| "vaccine": "4vHPV", | |
| "age_vaccination": 10, | |
| "coverage_vac": 0.9, | |
| "vaccine_efficacy_before": 1.0, | |
| "vaccine_efficacy_after": 0.0, | |
| "protection_duration_years": None, | |
| "use_discounting": True, | |
| "discount_rate_cost": 0.03, | |
| "discount_rate_benefit": 0.03, | |
| "vaccine_price_total": 26.96, | |
| "vaccine_delivery_cost_total": 15.0, | |
| "cancer_treatment_cost": 1815.0, | |
| "next_n_years": 1, | |
| } | |
| if param_name in defaults and value == defaults[param_name]: | |
| continue # Skip default value, will be tested in base case | |
| params = base_params.copy() | |
| params.update(defaults) | |
| # Change the parameter being tested | |
| params[param_name] = value | |
| test_name = f"{param_name}={value}" | |
| test_cases.append((test_name, params)) | |
| # Add some critical combinations | |
| critical_combos = [ | |
| ( | |
| "base_case", | |
| { | |
| **base_params, | |
| "vaccine": "4vHPV", | |
| "age_vaccination": 10, | |
| "coverage_vac": 0.9, | |
| "vaccine_efficacy_before": 1.0, | |
| "vaccine_efficacy_after": 0.0, | |
| "protection_duration_years": None, | |
| "use_discounting": True, | |
| "discount_rate_cost": 0.03, | |
| "discount_rate_benefit": 0.03, | |
| "vaccine_price_total": 26.96, | |
| "vaccine_delivery_cost_total": 15.0, | |
| "cancer_treatment_cost": 1815.0, | |
| "next_n_years": 1, | |
| }, | |
| ), | |
| # next_n_years_scaling_test REMOVED - not comparable with R (R is single-cohort only) | |
| ( | |
| "9vHPV_age12_cov70", | |
| { | |
| **base_params, | |
| "vaccine": "9vHPV", | |
| "age_vaccination": 12, | |
| "coverage_vac": 0.7, | |
| "vaccine_efficacy_before": 1.0, | |
| "vaccine_efficacy_after": 0.0, | |
| "protection_duration_years": None, | |
| "use_discounting": True, | |
| "discount_rate_cost": 0.03, | |
| "discount_rate_benefit": 0.03, | |
| "vaccine_price_total": 26.96, | |
| "vaccine_delivery_cost_total": 15.0, | |
| "cancer_treatment_cost": 1815.0, | |
| "next_n_years": 1, | |
| }, | |
| ), | |
| # limited_protection_no_discount REMOVED - R doesn't support limited protection | |
| ( | |
| "high_cost_scenario", | |
| { | |
| **base_params, | |
| "vaccine": "9vHPV", | |
| "age_vaccination": 10, | |
| "coverage_vac": 0.9, | |
| "vaccine_efficacy_before": 1.0, | |
| "vaccine_efficacy_after": 0.0, | |
| "protection_duration_years": None, # Lifelong | |
| "use_discounting": True, | |
| "discount_rate_cost": 0.05, | |
| "discount_rate_benefit": 0.05, | |
| "vaccine_price_total": 40.0, | |
| "vaccine_delivery_cost_total": 30.0, | |
| "cancer_treatment_cost": 2000.0, | |
| "next_n_years": 1, | |
| }, | |
| ), | |
| # low_cost_1dose_scenario REMOVED - R doesn't support limited protection (20yr duration) | |
| ] | |
| test_cases.extend(critical_combos) | |
| # Remove duplicates | |
| seen = set() | |
| unique_cases = [] | |
| for name, params in test_cases: | |
| key = json.dumps(params, sort_keys=True) | |
| if key not in seen: | |
| seen.add(key) | |
| unique_cases.append((name, params)) | |
| return unique_cases | |
| def main(): | |
| """Run comprehensive parameter sweep validation.""" | |
| base_dir = Path(__file__).parent.parent | |
| validation_output = base_dir / "validation_output" / "parameter_sweep" | |
| validation_output.mkdir(parents=True, exist_ok=True) | |
| # Generate test cases | |
| test_cases = generate_parameter_combinations() | |
| print(f"{'=' * 80}") | |
| print("COMPREHENSIVE PARAMETER SWEEP VALIDATION") | |
| print(f"{'=' * 80}") | |
| print(f"Total test cases: {len(test_cases)}") | |
| print(f"Output directory: {validation_output}") | |
| print(f"{'=' * 80}\n") | |
| all_results = [] | |
| summary_stats = { | |
| "total": 0, | |
| "passed": 0, | |
| "failed": 0, | |
| "errors": 0, | |
| } | |
| for i, (test_name, params_dict) in enumerate(test_cases, 1): | |
| print(f"\n[{i}/{len(test_cases)}] Testing: {test_name}") | |
| print(f"{'-' * 80}") | |
| test_output = validation_output / test_name.replace("=", "_").replace(" ", "_") | |
| test_output.mkdir(parents=True, exist_ok=True) | |
| # Save test parameters | |
| with open(test_output / "params.json", "w") as f: | |
| json.dump(params_dict, f, indent=2) | |
| # Create DemoParams | |
| params = DemoParams(**params_dict) | |
| # Run Python model | |
| print(" Running Python model...", end=" ", flush=True) | |
| py_results = run_python_model(params, test_output / "python") | |
| if "error" in py_results: | |
| print("❌ ERROR") | |
| print(f" {py_results['error']}") | |
| summary_stats["errors"] += 1 | |
| all_results.append( | |
| { | |
| "test": test_name, | |
| "status": "error", | |
| "error_source": "python", | |
| "error": py_results["error"], | |
| "params": params_dict, | |
| } | |
| ) | |
| continue | |
| print("✓") | |
| # Run R model | |
| print(" Running R model...", end=" ", flush=True) | |
| r_results = run_r_model(params_dict, test_output / "r") | |
| if "error" in r_results: | |
| print("❌ ERROR") | |
| print(f" {r_results['error']}") | |
| summary_stats["errors"] += 1 | |
| all_results.append( | |
| { | |
| "test": test_name, | |
| "status": "error", | |
| "error_source": "r", | |
| "error": r_results["error"], | |
| "params": params_dict, | |
| } | |
| ) | |
| continue | |
| print("✓") | |
| # Compare results | |
| print(" Comparing results...", end=" ", flush=True) | |
| is_match, differences = compare_results(r_results, py_results, tolerance=0.01) | |
| summary_stats["total"] += 1 | |
| if is_match: | |
| print("✅ PASS") | |
| summary_stats["passed"] += 1 | |
| all_results.append( | |
| { | |
| "test": test_name, | |
| "status": "pass", | |
| "params": params_dict, | |
| "r_results": r_results, | |
| "py_results": py_results, | |
| } | |
| ) | |
| else: | |
| print(f"❌ FAIL ({len(differences)} differences)") | |
| summary_stats["failed"] += 1 | |
| # Show top differences | |
| sorted_diffs = sorted( | |
| differences, | |
| key=lambda x: x.get("rel_diff", 0) | |
| if x.get("rel_diff") is not None | |
| else 0, | |
| reverse=True, | |
| ) | |
| for diff in sorted_diffs[:3]: | |
| if "rel_diff_pct" in diff: | |
| print( | |
| f" • {diff['metric']}: {diff['rel_diff_pct']:.2f}% diff (R={diff['r_value']:.4f}, Py={diff['py_value']:.4f})" | |
| ) | |
| else: | |
| print(f" • {diff['metric']}: {diff}") | |
| all_results.append( | |
| { | |
| "test": test_name, | |
| "status": "fail", | |
| "params": params_dict, | |
| "r_results": r_results, | |
| "py_results": py_results, | |
| "differences": differences, | |
| } | |
| ) | |
| # Save detailed results | |
| results_file = validation_output / "validation_results.json" | |
| with open(results_file, "w") as f: | |
| json.dump(all_results, f, indent=2, default=str) | |
| # Create summary DataFrame | |
| summary_rows = [] | |
| for result in all_results: | |
| row = { | |
| "test": result["test"], | |
| "status": result["status"], | |
| } | |
| if result["status"] == "fail" and "differences" in result: | |
| # Add metrics with largest differences | |
| diffs = result["differences"] | |
| sorted_diffs = sorted( | |
| diffs, | |
| key=lambda x: x.get("rel_diff", 0) | |
| if x.get("rel_diff") is not None | |
| else 0, | |
| reverse=True, | |
| ) | |
| for i, diff in enumerate(sorted_diffs[:3], 1): | |
| row[f"top_diff_{i}_metric"] = diff.get("metric", "") | |
| row[f"top_diff_{i}_pct"] = diff.get("rel_diff_pct", "") | |
| summary_rows.append(row) | |
| summary_df = pd.DataFrame(summary_rows) | |
| summary_df.to_csv(validation_output / "summary.csv", index=False) | |
| # Print summary | |
| print(f"\n{'=' * 80}") | |
| print("VALIDATION SUMMARY") | |
| print(f"{'=' * 80}") | |
| print(f"Total tests: {summary_stats['total']}") | |
| print( | |
| f"Passed: {summary_stats['passed']} ({summary_stats['passed'] / max(summary_stats['total'], 1) * 100:.1f}%)" | |
| ) | |
| print( | |
| f"Failed: {summary_stats['failed']} ({summary_stats['failed'] / max(summary_stats['total'], 1) * 100:.1f}%)" | |
| ) | |
| print(f"Errors: {summary_stats['errors']}") | |
| print(f"{'=' * 80}") | |
| if summary_stats["failed"] > 0: | |
| print( | |
| f"\n❌ VALIDATION FAILED: {summary_stats['failed']} test(s) with mismatches" | |
| ) | |
| print("\nFailed tests:") | |
| for result in all_results: | |
| if result["status"] == "fail": | |
| print(f" • {result['test']}") | |
| print(f"\nDetailed results saved to: {results_file}") | |
| return 1 | |
| elif summary_stats["errors"] > 0: | |
| print( | |
| f"\n⚠️ VALIDATION INCOMPLETE: {summary_stats['errors']} test(s) with errors" | |
| ) | |
| return 1 | |
| else: | |
| print("\n✅ ALL TESTS PASSED") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |