This codebase reproduces my thesis results reported in detail here. All data used for reproducing the results are located in the data directory in this repository. A simpler-to-navigate, data-only repository is located here.
To reproduce all thesis results in one shot, run from the repo root:
bash run_all_thesis_results.shThis runs every script below in sequence, saves all terminal output to thesis_results_output.txt, and prints the paths to the two saved figures (Figure 2 and Figure 3) at the end. No plots will be displayed interactively.
Pass --quick to skip the two slow extrapolated-scaling scripts (E_overall_rating_extrapolate_scaling.py and K_outcome_tag_extrapolate_scaling.py), which produce \PairwiseModelExtrapolated and \OutcomeTagPairwiseAvgExtrapolated:
bash run_all_thesis_results.sh --quickThe tables and numbers in the thesis are produced by the scripts described below.
The latest thesis can always be found at
https://raw.githubusercontent.com/morganrivers/forecasting_earth_system/refs/heads/main/tex/thesis.tex
The thesis contains commands which define the key results mentioned throughout the latex file for the thesis. These are directly extracted from the thesis_results_output.txt file using extract_latex_values.py. Running that script will show you the meanings of each of the outputs. You can find the full context for each of these constants within https://raw.githubusercontent.com/morganrivers/forecasting_earth_system/refs/heads/main/tex/thesis.tex.
All scripts live under src/pipeline/. The results (three results tables, shap values for ratings, and Results constants \newcommand values) are populated via the following scripts:
Script: src/pipeline/A_overall_rating_fit_and_evaluate.py
Prints the full held-out test-set results table. Look for === RESULTS (terminal) === and underneath:
Pairwise = ... [95% CI: ...]-- gives\PairwiseModel,\PairwiseCILow,\PairwiseCIHigh- The ridge baseline (risks + org only) pairwise line -- gives
\PairwiseHuman - R^2 lines per method -- gives
\Rsqfrac,\Rsqnollmfrac,\Rsqwithllmfrac(and\Rsqdelta= difference) - Adjusted R^2 on train+val for all orgs -- gives
\RsqTraining
Also writes data/rating_model_outputs/ (predictions, model pickle, feature importances).
cd src/pipeline
python3 A_overall_rating_fit_and_evaluate.py # full results table
python3 A_overall_rating_fit_and_evaluate.py importances # additionally prints feature importance table (Table 2)Script: src/pipeline/B_overall_rating_plot_shap.py
Reads the saved model and training features from data/rating_model_outputs/ (written by
A_overall_rating_fit_and_evaluate.py) and saves src/pipeline/feature_importances_plot.png.
Requires A_overall_rating_fit_and_evaluate.py to have been run first.
cd src/pipeline
python3 B_overall_rating_plot_shap.pyScript: src/pipeline/C_overall_rating_insample_r2.py
Prints R^2 and Adjusted R^2 for the OLS model fit on train+val. Requires
data/rating_model_outputs/all_features.csv (written by A_overall_rating_fit_and_evaluate.py on a prior run).
cd src/pipeline
python3 C_overall_rating_insample_r2.pyScript: src/pipeline/D_overall_rating_generate_oos_predictions.py
Generates data/rf_oos_val_predictions.csv required by E_llm_score_forecast_narratives.py.
cd src/pipeline
python3 D_overall_rating_generate_oos_predictions.pyScript: src/pipeline/E_llm_score_forecast_narratives.py
Calls gemini-2.5-flash-lite to grade each LLM forecast against the post-activity evaluation summary.
Prints Mean Grade: <value> per forecast config (look for the best RF-forced full-context config for
\BestNarrativeGrade, and the risks-only config for \RisksOnlyGrade). Grades cached to
data/forecast_grades/grades_*.jsonl.
cd src/pipeline
python3 E_llm_score_forecast_narratives.py # displays Figure 2 interactively
python3 E_llm_score_forecast_narratives.py --no-show # saves Figure 2 without displaying itFour scripts run in sequence:
-
src/pipeline/F_outcome_tag_train.py --use_test --hardcode_tags-- fits RF+ET classifiers on train/val with hardcoded tags; a required prerequisite for the nolimits run below. -
src/pipeline/F_outcome_tag_train.py --use_test --nolimits --hardcode_tags-- re-fits on train+val, evaluates on test set with no tag limits. -
src/pipeline/G_outcome_tag_evaluate.py --test-- prints per-tagwg_popvalues with feature selection -- gives\OutcomeTagPairwiseAvgFeatSel,\OutcomeTagPairwiseMinFeatSel,\OutcomeTagPairwiseMaxFeatSel. -
src/pipeline/G_outcome_tag_evaluate.py --test --nolimits-- prints per-tagwg_popvalues for the full model -- gives\OutcomeTagPairwiseAvg,\OutcomeTagPairwiseMin,\OutcomeTagPairwiseMax,\OutcomeTagAccBaseline,\OutcomeTagAccModel,\OutcomeTagAccImprovement,\DifferingOutcomePairsFrac,\NumCuratedPredicted.
cd src/pipeline
python3 F_outcome_tag_train.py --use_test --hardcode_tags
python3 F_outcome_tag_train.py --use_test --nolimits --hardcode_tags
python3 G_outcome_tag_evaluate.py --test
python3 G_outcome_tag_evaluate.py --test --nolimitsScript: src/pipeline/H_outcome_tag_shap_stability.py
Computes SHAP importance stability across 3 random splits, writes
data/outcome_tags/shap_split_stability_data.pkl (required by I_outcome_tag_results_table.py).
cd src/pipeline
python3 H_outcome_tag_shap_stability.pyScript: src/pipeline/I_outcome_tag_results_table.py
Prints the LaTeX table (Table 3) with consistent SHAP features and \newcommand lines for
\OutcomeTagPairwiseAvg/Min/Max/BSS. Also prints TAGS_BETTER_THAN_CHANCE_95PCT: N (gives
\NumberTagsBetterThanChance) and tag-rating Pearson correlations -- gives \TagCorrTargetsMet etc.
Look for -- Tag correlation with six_overall_rating (all activities) --.
cd src/pipeline
python3 I_outcome_tag_results_table.py --nolimits --test 2>&1-
\PairwiseModelExtrapolated-- fromsrc/pipeline/J_data_analysis_extrapolate_scaling.py --lc-pop(analysis_17_learning_curve), printspop_proj_5x(log-linear extrapolation to 5x training data for the overall rating pairwise ranking skill of the RF+ET model). -
\OutcomeTagPairwiseAvgExtrapolated-- fromsrc/pipeline/K_outcome_tag_extrapolate_scaling.py --test --nolimits --slowcorrectextrapolate, which fits an exponential saturation curve to each tag's learning data and extrapolates; printsWithin Group Pairwise Ranking Skill extrapolated to N=...values per tag.
cd src/pipeline
python3 J_data_analysis_extrapolate_scaling.py --lc-pop
python3 K_outcome_tag_extrapolate_scaling.py --test --nolimits --slowcorrectextrapolateScript: src/pipeline/L_cost_effectiveness_train_and_score.py
Prints ZAGG Pairwise ordering: ... (gives \CostEffect) and
Pearson r between rating and zagg score (gives \RatingCostCorr).
cd src/pipeline
python3 L_cost_effectiveness_train_and_score.pyScript: src/pipeline/M_data_analysis_eval_set_sizes.py
Prints a SUMMARY TABLE with total column -- gives \NOverallRatings, \NOutcomeTags,
\NCostEffectiveness, \NAiForecasting.
cd src/pipeline
python3 M_data_analysis_eval_set_sizes.pyScript: src/pipeline/N_data_analysis_print_features.py
Script: src/pipeline/O_leakage_report.py
Prints leakage counts and rates across splits from data/LEAKAGERISK.json.
cd src/pipeline
python3 O_leakage_report.pyPrints the feature lists for the overall rating, outcome tag (nolimits), cost-effectiveness, and LLM forecasting models. Feature lists are saved to data/feature_lists/ by their respective training scripts (A, G, L); this script reads and displays them.
cd src/pipeline
python3 N_data_analysis_print_features.pyPrinted by src/pipeline/I_outcome_tag_results_table.py (see above). Look for
-- Tag correlation with six_overall_rating (all activities) -- in output.
Fine-tuning results (\FineTuningLossReduction, \FineTuningEpochs, \FineTuningPairs, \FineTuningCostEuros)
Fine-tuning was run once via src/pipeline/B_fine_tune_2p5_pro.py (requires GCP credentials
and a GCS bucket). Metric values (loss reduction, epochs, pairs, cost) are hardcoded constants recorded
from that run. The fine-tuned model forecasts live in
data/rag_prompts_and_responses/outputs_*fine_tuning*.jsonl and can be evaluated by enabling
the fine-tuning configs in FORECAST_CONFIGS at the top of E_llm_score_forecast_narratives.py.
Some activities in the val and test sets contain post-cutoff information that leaked into inputs used by the pipeline. Two kinds of leakage are tracked:
- Grade leakage (
*grades*source keys, e.g.section_texts_val_grades): the LLM-graded feature files for these activities referenced post-cutoff events (e.g. COVID-19) in the project document sections used for grading. Affects 15 activities across val and test. - Forecast leakage (
*forecast*source keys, e.g.deepseek_minimal_val_forecast,glm_rating_forecast_test): the LLM narrative forecast for these activities directly mentioned post-cutoff events. Affects 75 activities (val + test). 12 activities have both kinds.
The top-level key exclude_test_leakage_risk (default true) enables leakage handling. Set it to false to run without any leakage correction as a comparison baseline.
A_overall_rating_fit_and_evaluate.py — controlled by LEAKAGE_HANDLING_METHOD (top of file):
| Mode | Effect |
|---|---|
"replace_predictions" (default) |
Sets grade features to NaN for grade-leakage activities before training (RF fills from training-set medians). After all predictions are built, overwrites LLM-blended prediction columns for test leakage rows: grade-leakage rows → pred_rf_no_llm; forecast-only rows → pred_rf (base RF, no LLM narrative). |
"median_impute" |
Only the NaN step above; no prediction swap. Useful to isolate the training effect. |
"drop" |
Removes all leakage activities from test_idx (original behaviour). |
G_outcome_tag_train.py — drops test-set grade-leakage activities from evaluation (unchanged; outcome-tag evaluation is harder to substitute).
F_llm_score_forecast_narratives.py — for each forecast config that has a leakage_source_key matching a LEAKAGERISK source, the affected activity IDs are removed from valid_ids before metrics are computed. Currently wired for deepseek_minimal_val_forecast.
Add an entry to data/LEAKAGERISK.json with the structure:
"<activity_id>": {
"activity_id": "<activity_id>",
"split": "val" | "test",
"leakage_sources": {
"<source_name>": ["<reason>"]
}
}Source name conventions: end with _grades for grade leakage, _forecast for narrative forecast leakage. To wire a new forecast source into F_, add "leakage_source_key": "<source_name>" to its entry in FORECAST_CONFIGS.
The data-only repository is hosted at https://github.com/morganrivers/iati_extraction . This repo also contains all data necessary to run the code, which is roughly a subset of the full dataset.
data/merged_overall_ratings.jsonl— overall activity success ratings on a ~1–6 scale (IEG/DSGF ratings)
data/outcome_tags/applied_tags.jsonl— binary outcome tags extracted by LLM; each entry hasactivity_idand a dict oftag_name → binary value
data/outputs_summary_expost.jsonl— LLM-generated ex-post summaries of activity outcomes (used as context when grading forecasts)
data/activity_outcomes.csv— quantitative outcome values (CO2, beneficiaries, costs, etc.) withoutcome_norm,baseline_norm,target_norm,outcome_norm_dollars_per_unit, etc.
data/rating_model_outputs/all_features.csv— all ~58 feature values for every activity (train+val+test) with split label
Features include: LLM-extracted activity properties (finance, integratedness, implementer_performance, targets, context, risks, complexity, activity_scope), country characteristics (gdp_percap, cpia_score, WGI governance indicators), region dummies, planned_duration, planned_expenditure, UMAP embeddings (umap3_x/y/z), sector_distance, country_distance, sector_cluster dummies, missingness indicators, and org dummies.
LLM-graded activity properties (1–6 scale):
data/outputs_finance_grades.jsonl→financefeaturedata/outputs_integratedness_grades.jsonl→integratednessfeaturedata/outputs_implementer_performance_grades.jsonl→implementer_performancefeaturedata/outputs_targets_grades.jsonl→targetsfeaturedata/outputs_context_grades.jsonl→contextfeaturedata/outputs_risks_grades.jsonl→risksfeaturedata/outputs_complexity_grades.jsonl→complexityfeature
Other LLM feature sources:
data/outputs_targets_context_maps.jsonl— pairwise similarity maps (val set); used forsector_distance,country_distancedata/outputs_targets_context_maps_trainval.jsonl— same for train+val (test set run)data/outputs_misc.jsonl— miscellaneous LLM extractions (activity_scopeand other fields)data/llm_planned_expenditure.jsonl→planned_expenditurefeaturedata/llm_planned_duration.jsonl→planned_durationfeaturedata/outputs_finance_sectors_disbursements_baseline_gemini2p5flash.jsonl→sector_cluster_*features
Free-form forecast texts, in data/rag_prompts_and_responses/. Files with val in the name are the ~300-activity validation set (used in the 2-pane comparison figure); no_knn_no_rag_deepseek_minimal_val also covers ~199 test activities.
Key validation-set files:
outputs_exactly_like_halawi_et_al_rag_added_gemini3pro_val_s3_call_1.jsonl— gemini-2.5-pro (KNN+RAG+S1+S2)outputs_exactly_like_halawi_et_al_rag_added_deepseek_minimal_val_s3_call_1.jsonl— deepseek-v3.2 (KNN+RAG+S1+S2)outputs_exactly_like_halawi_et_al_rag_added_forced_rf_deepseek_val_forced_rf_s3_call_1.jsonl— deepseek-v3.2 RF forced (KNN+RAG+S1+S2)outputs_exactly_like_halawi_et_al_rag_added_no_knn_no_rag_forced_rf_deepseek_val_no_knn_no_rag_forced_rf_s3_call_1.jsonl— deepseek-v3.2 RF forced (no KNN, no RAG)outputs_exactly_like_halawi_et_al_rag_added_deepseek_minimal_val_no_goodbadcalls_s3_call_1.jsonl— deepseek-v3.2 (KNN+RAG, no S1/S2)outputs_exactly_like_halawi_et_al_rag_added_no_knn_no_rag_deepseek_minimal_val_s3_call_1.jsonl— deepseek-v3.2 (no KNN, no RAG, no S1, no S2); also serves as test set predictionsoutputs_onlysummary_no_knn_no_rag_onlysummary_no_knn_no_rag_s3_call_1.jsonl— deepseek-v3.2 (minimal prompt, summary only)
Raw grades in data/forecast_grades/grades_*.jsonl — output of the grading LLM per activity, including response_text (reasoning + GRADE: letter), activity_id, and token usage.