CrisisLens
We built a humanitarian intelligence platform that connects fragmented data on need, funding, and coverage. CrisisLens brings ensemble machine learning, explainable country scores, and scenario projections into an interactive 3D globe. Explore a country, compare its models, and understand the reasoning for the score.
The problem
A country can have a large humanitarian need, a substantial funding gap, and very little visibility in a global overview. The data needed to understand that situation lives across plans, funding records, population estimates, and sector-level reports. A single headline number hides how those signals relate.
We built CrisisLens as a command center for exploring those relationships. The interface connects a 3D globe, country comparisons, natural-language analysis through Databricks Genie, and scenario projections. The goal is to help someone ask better prioritization questions and inspect the evidence behind a score.
My contribution
I worked on the data and modeling layer: cleaning multi-country datasets, exploratory analysis, feature engineering, SHAP analysis, ensemble training, validation, country peer mapping, and integrating predictive outputs into the visualization. The full interface and command center were a team effort.
The data pipeline aligns records by country identifiers and year, derives funding and need indicators, and exports a compact country-level artifact. The web application consumes the exported JSON artifact. Databricks-backed analysis is a separate server-side path.
| Layer | What it contributes |
|---|---|
| Source records | Humanitarian needs, plan requirements, funding flows, population, and sector-level signals. |
| Country features | Funding gap, coverage mismatch, people in need as a population share, funding per person, and cluster anomalies. |
| Model outputs | Individual scores, a blended score, model spread, dominant SHAP driver, and peer countries. |
| Decision interface | A geographic entry point into comparisons, natural-language questions, and projected scenarios. |
Country totals require particular care. Counting unique people across overlapping humanitarian categories requires deduplication. Some exported cumulative fields exceed plausible population bounds; those totals are excluded from this case study.
What is the model actually learning?
The regression target is a constructed neglect index. It combines funding shortfall, coverage mismatch, the share of the population in need, pooled funding, and sector anomalies. Each term is normalized before weighting.
0.35 · norm(funding gap)
+ 0.25 · norm(coverage mismatch)
+ 0.20 · norm(people in need / population)
+ 0.10 · (1 − norm(log(1 + pooled funding)))
+ 0.10 · norm(nonnegative maximum BBR z-score)
]
This makes the index interpretable, but it also determines how to read the evaluation. The same-year model receives features that participate in the target’s definition. A high R² measures how closely the model reproduces this scoring rule. Assessing humanitarian urgency requires validation against independent outcomes or expert judgments.
I compared LightGBM, random forests, XGBoost, gradient boosting, and a stacking model. The simple blend weights base-model predictions by nonnegative cross-validation scores and clips its output to the index range.
def weighted_average_ensemble(predictions, cv_results, base_model_keys):
weights = {k: max(cv_results[k]["mean"], 0.0)
for k in base_model_keys}
total = max(sum(weights.values()), 1e-9)
blended = sum(weights[k] * predictions[k]
for k in base_model_keys) / total
return np.clip(blended, 0, 100)The original function expects NumPy as np. This is the project’s blending rule. Its zero-total-weight fallback also produces zero, an edge case worth handling explicitly in a production service.
Results, with the evaluation context attached
| Model | Saved mean R² | Saved SD |
|---|---|---|
| LightGBM | 0.8623 | 0.0392 |
| RandomForest | 0.9731 | 0.0290 |
| XGBoost | 0.9816 | 0.0152 |
| GBR | 0.9838 | 0.0197 |
| Stacking | 0.9883 | 0.0108 |
The training entry point configures five-fold country-grouped validation for same-year scoring and time-based splits for forecasts. These numbers are archived results from the codebase. Reproducing the benchmark would require a complete run manifest beyond the metadata saved in the metric JSON.
Forecasting is the harder test. The saved temporal classifier reports F1 = 0.4264 at one year and 0.3034 at two years. At one year, it records 42 true positives, 103 false positives, and 10 false negatives. These counts show the tradeoff between catching cases and raising false alarms.
The temporal files mark these results as out-of-fold and store decision thresholds of 0.0 and 0.0403. Evaluating an operational alerting policy would require the full label-construction procedure, held-out calibration, and a documented threshold-selection process.
Forecasting and simulation
A forecast example pairs the features of a country at year T with that same country’s index at T + h. The source defines one- and two-year training horizons. Its 6-, 12-, 18-, and 24-month display steps map to those trained horizons. Intermediate display steps use approximations based on the annual models.
# Pair features at year T with the SAME country's target at T + h.
# Condensed from shared/features.py; feat_all has one row per country/year.
targets = feat_all[["country_iso3", "year", "neglect_score"]].rename(
columns={"year": "year_target", "neglect_score": "future_neglect"}
)
targets["year"] = targets["year_target"] - horizon_years
paired = feat_all[feat_all["year"] >= 2015].merge(
targets[["country_iso3", "year", "future_neglect"]],
on=["country_iso3", "year"],
how="inner",
validate="one_to_one", # added guard for this explanatory excerpt
)
X = paired[FEATURE_COLS].fillna(0).values
y = paired["future_neglect"].valuesMatching both country and time is essential. A row shift across a mixed-country table could silently pair one country’s features with another country’s outcome. The additional merge validation in the excerpt makes duplicate country-year records fail visibly.
The temporal projector can produce eight quarterly steps. After a prediction, it changes funding and need-related state variables using hand-set feedback rules, then predicts again. Those paths are scenarios conditional on the update rule. Estimating the causal effect of a funding intervention would require a separate evaluation.
Inspect the score, then inspect its neighbors

The saved SHAP plot is dominated by funding-gap and coverage-mismatch features. That is consistent with their large weights in the target definition. SHAP attributes a model prediction to its input features. Causal explanations require additional evidence about the underlying mechanisms.
Country peer mapping adds a second way to inspect an output. The notebook fits cosine nearest neighbors to scaled feature vectors, asks for six neighbors, and removes the self-match to retain five peers. The resulting peers share similar modeled profiles and can span different geographic regions.
Model disagreement is another useful cue. CrisisLens exports the standard deviation across model predictions. That value summarizes variation across the fitted models. A fuller uncertainty assessment would also consider data quality, sampling variation, and distribution shifts.
Bringing the analysis into an interface
A globe makes the country the entry point. From there, someone can inspect the signals, compare peer countries, ask a natural-language question, or explore a projected trajectory. The full project includes pointer, hand-tracking, and voice interaction alongside the Databricks integration.