Coupled ENSO x IOD x MJO analysis, official classifications, regional impact boxes
Follow-up to analysis/analysis.py: couple ENSO/IOD/MJO using official categorical
phases instead of raw numeric indices, score impact against small regional boxes
instead of one South-Asia-wide average, and -- per two follow-up questions -- use
each driver only where it demonstrably earns its keep, searching all combinations
of the three drivers rather than a fixed chain anchored on ENSO. Code:
analysis/coupled_analysis.py, built on classification functions in
analysis/load_data.py.
What changed
Official classifications, not numeric indices -- ENSO: NOAA CPC's RONI phase rule
plus strength tier (Weak/Moderate/Strong/Very Strong for El Nino, Weak/Moderate/Strong
for La Nina, from peak RONI per episode; same +-0.5degC thresholds and 5-consecutive-
season persistence rule as the older ONI, just a different underlying SST signal --
see analysis/load_data.py's load_roni() docstring). IOD: BOM/JAMSTEC's +-0.4 DMI
threshold (no official strength-tier standard exists for IOD). MJO: official BOM/CPC
RMM phase grouped by convective-center location (Indian Ocean 2-3 / Maritime
Continent 6-7 / Other-active / Inactive), at daily resolution for cyclones and
monthly-dominant-group for precip/discharge.
2026-07 update -- reclassified from ONI to RONI. CPC replaced ONI with RONI
(Relative Oceanic Nino Index) as its primary ENSO index in February 2026; the
site-wide dashboard migrated to it shortly after (see docs/DATA.md's "Why Overview
is RONI-based" section). This analysis followed: coupled_analysis.py's load()
reclassifies enso_phase/enso_strength from RONI immediately after reading
monthly_table.csv/cyclone_storms.csv (see _reclassify_enso_roni()), rather than
changing what those CSVs themselves carry -- they stay ONI-classified so analysis.py
(the older, numeric-index legacy report, which plots the raw ONI trace directly and
would become internally inconsistent if the phase labels next to it silently switched
index) is unaffected. The numbers below are from that RONI-classified re-run; the
table shows what changed from the original ONI-classified pass.
Small regional impact boxes, not one South Asia average -- 9 precipitation boxes matched to river catchments, cyclone genesis split Arabian Sea / Bay of Bengal, discharge already per-basin (11 basins) -- see the SAHF expansion below for how these grew from the original 5 boxes / 8 basins.
2026-07-21 update -- expanded to full South Asia Hydromet Forum (SAHF) regional
coverage. SAHF membership is Afghanistan, Bangladesh, Bhutan, India, Maldives,
Myanmar, Nepal, Pakistan, Sri Lanka. The original 5 precipitation boxes and 8
discharge basins already covered Bangladesh/Nepal/Bhutan/India/Pakistan, but had no
dedicated region for Sri Lanka, Myanmar, or Afghanistan, and no coverage at all for
Maldives (the ERA5 precipitation extent's southern edge at 5N cut off the
archipelago). Three new precipitation regions -- Afghanistan, Sri Lanka (paired with
the Mahaweli basin), Myanmar (paired with the Irrawaddy basin) -- fell entirely
inside the already-downloaded ERA5 extent, so no new download was needed for them.
Two new discharge basins -- Mahaweli (Sri Lanka) and Irrawaddy (Myanmar), both
already defined in load_data.py's BASINS/RIVER_BASIN_POINTS but never fetched
-- were completed from a delivered GloFAS extract rather than a live per-basin pull;
see docs/DATA.md's "Done as of 2026-07-21" river-discharge note for exactly what
was copied in directly versus locally re-derived. Afghanistan's own candidate river,
Helmand (endorheic, terminating in the Sistan Basin/Hamun wetlands on the Iran
border -- its other option, the Kabul, is an Indus tributary and so already
represented via discharge:indus), was initially left out of this pass since it had
no pre-fetched delivery and needed a live ~6.5-hour EWDS pull; it was added as an
immediate follow-up (see the Helmand discharge basin addition below), so
precip:afghanistan and discharge:helmand are now paired the same way Sri
Lanka/Mahaweli and Myanmar/Irrawaddy are.
New results (RONI-classified, same method as everything else in this report):
| Target | Winner | Runner-up (delta-BIC) | Outlier-robust? | Permutation p |
|---|---|---|---|---|
| Sri Lanka precip | MJO alone | Null (4.30) | yes | 0.001 -- survives |
| Myanmar (Irrawaddy) precip | ENSO alone | Null (5.62) | yes | 0.001 -- survives |
| Mahaweli (Sri Lanka) discharge | ENSO alone | Null (14.36) | yes | 0.005 -- survives |
| Afghanistan precip | Null (nothing beats it) | ENSO (6.39) | yes | n/a -- no grouping to test |
| Irrawaddy (Myanmar) discharge | Null (nothing beats it) | ENSO (9.33) | yes | n/a -- no grouping to test |
| Helmand (Afghanistan) discharge | Null (nothing beats it) | ENSO (0.55) | yes | n/a -- no grouping to test |
Sri Lanka's MJO-alone result is notable as the only precipitation region (as opposed to the Arabian Sea cyclone target) that selects an MJO-driven model -- unlike Arabian Sea cyclogenesis (daily resolution, thousands of exposure-days), Sri Lanka precip is monthly-resolution with the same n=196 JJAS-month sample as the rest of the precip search, so this is a materially different kind of MJO signal and worth treating as a new, distinct finding rather than a confirmation of the Arabian Sea result. Myanmar and Mahaweli both land as clean ENSO-alone results, consistent with the rest of the region. Afghanistan, Irrawaddy, and Helmand join the existing Null-winning majority -- now 11 of 19 non-cyclone targets finding no phase-based grouping that beats Null, up from 8 of 13. Helmand's case is the flattest of the three: ENSO trails Null by only 0.55 BIC points, about as close to a toss-up as this search produces -- worth flagging as "no confirmed relationship" rather than reading much into either direction.
Full recommendation table, delta-BIC summary, and per-target composites/figures are
regenerated in place in coupled_recommendation.csv, coupled_delta_bic_summary.csv,
coupled_tuple_composites/, and figs/I_*.png -- same files as before, now with 21
targets instead of 15 (see the Maldives precipitation region addition below for the
21st, and the Helmand discharge basin addition below for the 22nd).
Maldives precipitation region addition. The 2026-07-21 SAHF widening above moved
the ERA5 extent's southern edge from 5N down to 1S specifically to bring the
Maldives archipelago inside the domain (see docs/DATA.md's box-map note); a ninth
precipitation box (precip:maldives, 1S-8N/72-74E) was added once that data existed.
Like Afghanistan, it has no paired discharge basin -- no perennial river exists on
the archipelago. Result: Null (nothing beats it, delta-BIC 7.81 vs. ENSO
runner-up), outlier-robust, no permutation test to run (same "Null with no
grouping to test" shape as Afghanistan/Irrawaddy above). This brings the Null-winning
total to 11 of 21 targets. On the dashboard's Outlook tab, Null-selected targets
still surface a best-effort, clearly-labeled low-confidence exploratory lean (see
docs/DATA.md's Outlook section) rather than no advisory at all -- for Maldives that
currently leans toward a precipitation decrease under Positive IOD conditions, the
largest-magnitude (but statistically unconfirmed) of the three descriptive composites.
Helmand discharge basin addition. Unlike Mahaweli/Irrawaddy, no pre-fetched delivery existed for Helmand, so it was a live per-basin EWDS pull (pour point near Chahar Burjak, Afghanistan, upstream of the Sistan Basin/Hamun wetlands on the Iran border) -- the full ~47-request historical sequence ran as a detached background process (~6.5 hours). Result: Null (nothing beats it, delta-BIC only 0.55 vs. ENSO runner-up), outlier-robust, no permutation test to run -- and worth calling out specifically as the closest Null-over-driver margin in this entire report; every other Null-winning target here clears its runner-up by at least ~3 BIC points, so Helmand is the one case where "no confirmed relationship" is a genuinely close call rather than a clean verdict. This brings the Null-winning total to 12 of 22 targets. On the Outlook tab, Helmand's best-effort exploratory lean (same low-confidence caveat as above) points toward increased discharge under El Nino (+50 m3/s vs. roughly -14 m3/s for both Neutral and La Nina) -- notable directionally since it runs opposite the ENSO-alone-confirmed Ganges/Brahmaputra/Krishna pattern in the rest of the region, though given the 0.55 BIC margin this should be read as a hint worth watching in future seasons, not a validated finding.
Method: search every combination, not a fixed chain
An earlier pass only tested a fixed order -- ENSO alone, then ENSO+IOD, then ENSO+IOD+MJO -- which meant IOD alone, MJO alone, IOD+MJO, and ENSO+MJO (skipping IOD) were never actually considered. That's a real gap: a target could have a clean MJO-only relationship that a chain anchored on ENSO would never surface. This version fixes it by fitting all 8 possible models for every target -- Null (no phase effect), ENSO, IOD, MJO, ENSO x IOD, ENSO x MJO, IOD x MJO, ENSO x IOD x MJO -- and ranking them by BIC (Bayesian Information Criterion, OLS for precip/discharge, Poisson GLM with an exposure-days offset for cyclone counts). BIC penalizes added parameters more heavily than AIC, which matters here: some candidates have up to ~26 cells fit on as few as ~180 monthly samples, so a criterion that doesn't penalize complexity would just pick the most complex model every time regardless of whether it generalizes.
AIC and BIC aren't valid for OLS and GLM on different scales by default, so both are
computed from a common formula (AIC = 2k - 2*loglik, BIC = k*ln(n) - 2*loglik)
so the two model families are directly comparable.
Robustness check (precip/discharge only): refit all 8 models with the single most extreme month dropped, flag if the winner changes -- a "best" model resting on one outlier in a thin cell isn't a trustworthy pattern even if BIC favors it.
Independence check: are ENSO/IOD/MJO themselves confounded with each other? A chi-square test of independence, run on the actual working subsample each search used, before trusting any single-driver "winner" as cleanly attributable to that driver alone.
Year-block permutation test: BIC and the F/LR-test p-values baked into it assume roughly independent observations, but ENSO and IOD persist for months at a stretch within a year (and MJO has its own weeks-long persistence). A test that treats each month or day as independent will look more confident than it should. So for every winning model, whole years of driver-phase labels were reshuffled (preserving each year's internal label sequence, breaking only the year-to-year pairing with the target), the model refit under 500-1,000 such reshuffles, and an empirical p-value computed from where the real result falls in that distribution -- a second, assumption-light check independent of BIC.
Result: exhaustive search changes the answer, and the permutation test changes it again
| Target | Winner | Runner-up (delta-BIC) | Outlier-robust? | Permutation p |
|---|---|---|---|---|
| Cyclone: Arabian Sea | MJO alone | Null (8.6) | n/a | 0.004 -- survives |
| Cyclone: Bay of Bengal | IOD alone (nominal) | ENSO x MJO (2.7) | n/a | 0.357 -- does NOT survive |
| Ganges, Brahmaputra, Krishna discharge | ENSO alone | Null (2.2-14.3) | yes | 0.002-0.021 -- survive |
| Ganges Plain, South Peninsular precip | ENSO alone | Null (14.6-16.7) | yes | 0.001 -- survive |
| Indus-NW, Central Peninsular precip | Null (nothing beats it) | ENSO (0.1-3.6) | yes | n/a -- no grouping to test |
| Godavari, Indus, Mahanadi, Meghna, NE India/BD precip, Ravi discharge | Null (nothing beats it) | ENSO (3.0-7.2) | yes | n/a -- no grouping to test |
Arabian Sea cyclogenesis is the one result that's now doubly confirmed. It wins the 8-model BIC search by a clean margin, every MJO-group bucket has thousands of days of exposure, and the pattern survives a deliberately conservative year-block permutation test (p=0.004) -- meaning it's not an artifact of treating autocorrelated months/days as independent. Genesis rate rises roughly monotonically, ~6x, from Maritime-Continent-convective months (~0.5 storms/year-equivalent) to Indian-Ocean-convective months (~3.2). This target is graded purely by MJO group, so it is unaffected by the ONI-to-RONI reclassification above.
Bay of Bengal's "IOD alone" result does not survive and should be dropped. It won the BIC comparison (beating the previous, more complex full-tuple answer), and every bucket had plenty of raw exposure -- but the permutation test (p=0.357) shows that result is indistinguishable from what you'd get by randomly reassigning which years got which IOD state. The likely explanation: IOD is essentially constant for months at a time, so the true number of independent "IOD episodes" behind this result is much smaller than the raw month/day count suggested -- exactly the gap the exhaustive BIC search couldn't see on its own, because BIC assumes independent observations too. Practical read: Bay of Bengal cyclogenesis has no confirmed phase-based relationship in this data -- it behaves like the Null-winning discharge/precip targets, just with a nominal (non-robust) preference for IOD.
Precipitation and river discharge, reclassified: the ENSO-alone winners all survive the permutation test comfortably (p=0.001-0.021). Indus-NW and Central Peninsular precip both flip from a nominal (already outlier-fragile) ENSO signal under ONI to Null under RONI -- under the old ONI classification both were flagged unreliable anyway (delta-BIC only 0.2 and 2.1, and Central Peninsular's winner flipped on dropping the single largest month), so this isn't a case of a solid result being lost, it's a marginal, already-flagged result crossing over to no-relationship once the underlying SST index changed which months count as which phase. Ganges Plain and South Peninsular precip, and Ganges/Brahmaputra/Krishna discharge, all stay ENSO-selected and robust under RONI, with delta-BIC actually higher than under ONI (e.g. Ganges Plain 10.3 to 16.7, Ganges discharge 3.3 to 14.3) -- for the targets with a real relationship, RONI's classification sharpened the group separation rather than weakening it. No precip region or discharge basin selects IOD alone, MJO alone, or any two-way/three-way combination; 8 of 13 non-cyclone targets find no phase-based grouping that beats Null at all (up from 6 under ONI).
Are ENSO, IOD and MJO confounded with each other?
Yes, modestly -- between ENSO and IOD, and (under the RONI reclassification above) now also between ENSO and MJO at monthly resolution. On the full monthly record (n=917 months) ENSO x IOD are not independent (chi-square p<0.0001, Cramer's V=0.183); on the JJAS/MJO-covered subsample actually used by the precip/discharge search (n=196 months) that association is somewhat stronger (p=0.0009, Cramer's V=0.219) -- both consistent with the well-known climatological tendency for El Nino to co-occur with positive IOD. On that same subsample, ENSO x MJO-monthly-group is also not independent (p=0.0062, Cramer's V=0.214) -- comparable in strength to the ENSO-IOD association, and a change from the ONI-classified pass, which found this pair consistent with independence (p=0.06). IOD x MJO-monthly-group stays consistent with independence (p=0.29). At cyclone's daily resolution, ENSO x MJO and IOD x MJO both come back "not independent" by p-value (p<0.0001), but the effect size is negligible (Cramer's V=0.03-0.04) -- with n=17,876 days, even a trivial association clears p<0.05, so this is a sample-size artifact, not a real confound, and essentially unchanged from the ONI-classified pass. Net effect: MJO's relationship with Arabian Sea genesis (the daily-resolution result) is not meaningfully entangled with ENSO or IOD, which is one more reason to trust that result -- but no precip/discharge target selected an MJO-involving model in either the ONI or RONI pass, so the newly- confounded monthly-resolution ENSO-MJO pair doesn't currently affect any selected result; it would matter if a future target ever selected MJO or an ENSO x MJO combination at monthly resolution. IOD's now-discredited Bay of Bengal result being confounded with ENSO was a live concern going in, but turned out to be moot -- it failed on autocorrelation grounds before the confounding question even mattered.
Are these concurrent or lagged relationships?
Concurrent (same-period) only, everywhere in this coupled analysis. Precip and
discharge are matched to ENSO/IOD/MJO phase in the same JJAS month; cyclone genesis
is matched to the MJO phase on its own genesis day and the ENSO/IOD phase of its
own genesis month. There is no lag structure anywhere in coupled_analysis.py.
The original analysis.py does have a lagged cross-correlation section (Section C:
driver leads target by 0-9 months) -- but only for single drivers (ONI or DMI alone)
against precip/discharge anomalies, not extended to MJO, not extended to cyclones, and
not part of this tuple-coupling framework. So right now: the coupled results above are
concurrent-only, and the one place lags are tested at all is a separate, older,
single-driver analysis.
This matters for two different reasons depending on the target: - Precip/discharge: a lagged coupled analysis is arguably the more useful framing for forecasting -- e.g., does the pre-monsoon (April-May) ENSO/IOD/MJO state predict JJAS rainfall, rather than asking whether concurrent-month phase correlates with concurrent-month rainfall (which is more of a diagnostic/composite question than a forecasting one). - Cyclones: concurrent-day MJO is actually the physically correct framing -- MJO's influence on cyclogenesis operates through the convective/wind-shear environment at the time of genesis, not a lead relationship the way seasonal ENSO/IOD forecasting would use.
Adding a lagged version of the precip/discharge search (same 8-model exhaustive search, at several lead times) is a natural next step if useful -- happy to build it.
Caveats
- Regional precip boxes are approximate catchment bounding boxes, not survey-grade watershed polygons.
- IOD strength tiers were intentionally not added -- no official standard exists for them the way it does for ENSO.
- The outlier-drop robustness check was only applied to precip/discharge (cyclone counts are less susceptible to single-point domination, being built from many days of exposure). The year-block permutation test was applied to both.
- The permutation test is only implemented for single-factor winners (ENSO alone, IOD alone, or MJO alone) -- no target in this run selected a two-way or three-way model, so this wasn't a binding limitation here, but it would need extending if one ever does.
monthly_table.csvandcyclone_storms.csvcarry the new columns;analysis.pystill runs unmodified against the same files.monthly_table.csv/cyclone_storms.csvon disk stay ONI-classified -- RONI reclassification happens only insidecoupled_analysis.py'sload()(see_reclassify_enso_roni()), soanalysis.pysees the original ONI labels unchanged. If those CSVs are ever regenerated fromload_data.pydirectly and something downstream reads theirenso_phasecolumn without going throughcoupled_analysis.py'sload(), it will get ONI, not RONI -- worth checking before adding a new consumer of either file.
Where to look
analysis/coupled_analysis.py-- the scriptanalysis/coupled_recommendation.csv-- the per-target winner + robustness flagsanalysis/coupled_delta_bic_summary.csv,figs/I_delta_bic_heatmap.png-- full 8-model comparison, every target, one table/figureanalysis/coupled_stats_output.txt-- full numeric log (every model's AIC/BIC, robustness checks, tuple composite tables)analysis/coupled_results.json-- key numbersanalysis/coupled_tuple_composites/*.csv-- full tuple composite tables for every target, regardless of which model wonanalysis/figs/I_*_selected_*.png-- the headline figure per target, drawn at whichever model actually won (single-factor bar, two-factor heatmap, full MJO-faceted heatmap, or a "no pattern found" placeholder for Null)