← Cuba hurricanes AA

Appendix B — Population Data Validation: WorldPop vs. ONEI

The trigger framework uses gridded population to estimate exposure inside the ZMA. Before relying on those exposure numbers, this appendix sanity-checks the WorldPop raster we use against an independent official tabulation from ONEI (Oficina Nacional de Estadística e Información de Cuba). The two are not fully independent — both descend from Cuba’s 2012 census — but the comparison still diagnoses (i) whether province-level totals are coherent across products, (ii) where they disagree, and (iii) the order of magnitude of structural uncertainty on absolute counts.

B.0.1 Inputs

Role Source
Polygons Field Maps edge-matched humanitarian admin1 (fieldmaps/edge-matched/humanitarian/intl/adm1/CUB.parquet, Blob container global)
Raster WorldPop Global 2 R2025A, 2026, constrained, 1km (worldpop/pop_count/global_pop_2026_CN_1km_R2025A_UA_v1.tif, Blob container raster)
Official ref. ONEI 2025 projection (end-2024), 16 provinces, Blob container projects at {PROJECT_PREFIX}/raw/unfpa/CUB_ADM1_POP_2024.xlsx, sheet CUB_ADM1_POP_2024, column T_TL

The xlsx is published via ONEI’s Observatorio de Igualdad de Género. Despite the “2024” filename, rows are tagged year = 2025 (this is the 2025-published projection for the 2024 reference year).

B.0.2 Zonal sums

Code
import io
import unicodedata

import geopandas as gpd
import numpy as np
import ocha_stratus as stratus
import pandas as pd
from exactextract import exact_extract

from src.constants import PROJECT_PREFIX


def norm_name(s: str) -> str:
    """Accent-fold + lowercase for joining province names across sources."""
    return (
        unicodedata.normalize("NFKD", str(s))
        .encode("ascii", "ignore")
        .decode("ascii")
        .strip()
        .lower()
    )


adm1_raw = stratus.load_blob_data(
    "fieldmaps/edge-matched/humanitarian/intl/adm1/CUB.parquet",
    container_name="global",
    stage="dev",
)
adm1 = gpd.read_parquet(io.BytesIO(adm1_raw))

da = stratus.open_blob_cog(
    "worldpop/pop_count/global_pop_2026_CN_1km_R2025A_UA_v1.tif",
    container_name="raster",
    stage="dev",
)
if "band" in da.dims and da.sizes.get("band", 1) == 1:
    da = da.squeeze("band", drop=True)

if str(adm1.crs) != str(da.rio.crs):
    adm1 = adm1.to_crs(da.rio.crs)

# exactextract handles partial-pixel coverage; sum is area-weighted
stats = exact_extract(
    da, adm1, ops=["sum"], output="pandas", include_cols=["adm1_name"]
)
sum_col = next(
    c for c in stats.columns if c == "sum" or c.endswith("_sum")
)
stats = stats.rename(columns={sum_col: "pop_worldpop2026"})
stats["name_key"] = stats["adm1_name"].map(norm_name)
Code
xlsx_blob = f"{PROJECT_PREFIX}/raw/unfpa/CUB_ADM1_POP_2024.xlsx"
xlsx_bytes = stratus.load_blob_data(
    xlsx_blob, container_name="projects", stage="dev"
)
official = pd.read_excel(
    io.BytesIO(xlsx_bytes), sheet_name="CUB_ADM1_POP_2024"
)
official = (
    official[["ADM1_PCODE", "ADM1_NAME", "T_TL"]]
    .rename(columns={"T_TL": "pop_official"})
    .assign(name_key=lambda d: d["ADM1_NAME"].map(norm_name))
)

merged = (
    stats.merge(official, on="name_key", validate="one_to_one")
    .sort_values("ADM1_PCODE")
    .reset_index(drop=True)
)
merged["diff"] = merged["pop_worldpop2026"] - merged["pop_official"]
merged["pct_diff"] = 100 * merged["diff"] / merged["pop_official"]
merged[
    ["ADM1_PCODE", "adm1_name", "pop_worldpop2026", "pop_official", "diff", "pct_diff"]
].round(0)
ADM1_PCODE adm1_name pop_worldpop2026 pop_official diff pct_diff
0 21 Pinar del Río 571761.0 515208 56553.0 11.0
1 22 Artemisa 519591.0 452430 67161.0 15.0
2 23 La Habana 2064220.0 1749964 314256.0 18.0
3 24 Mayabeque 375264.0 330260 45004.0 14.0
4 25 Matanzas 682393.0 619159 63234.0 10.0
5 26 Villa Clara 754200.0 665447 88753.0 13.0
6 27 Cienfuegos 392076.0 342709 49367.0 14.0
7 28 Sancti Spíritus 449585.0 404037 45548.0 11.0
8 29 Ciego de Ávila 421381.0 376919 44462.0 12.0
9 30 Camagüey 744029.0 653203 90826.0 14.0
10 31 Las Tunas 517247.0 475343 41904.0 9.0
11 32 Holguín 977871.0 911674 66197.0 7.0
12 33 Granma 791095.0 749289 41806.0 6.0
13 34 Santiago de Cuba 1022260.0 963915 58345.0 6.0
14 35 Guantánamo 483772.0 465429 18343.0 4.0
15 40 Isla de la Juventud 82039.0 73021 9018.0 12.0

Admin0 (Cuba) aggregates:

Code
total_wp = merged["pop_worldpop2026"].sum()
total_off = merged["pop_official"].sum()
print(f"WorldPop 2026 (constrained, sum of adm1): {total_wp:>12,.0f}")
print(f"ONEI 2024 projection (sum of adm1):      {total_off:>12,.0f}")
print(f"Difference: {total_wp - total_off:>+,.0f} ({100*(total_wp/total_off - 1):+.2f}%)")
WorldPop 2026 (constrained, sum of adm1):   10,848,785
ONEI 2024 projection (sum of adm1):         9,748,007
Difference: +1,100,778 (+11.29%)

B.0.3 Same shape, different totals

The +11% national gap could mean either (a) the two datasets disagree on the spatial distribution of people across provinces, or (b) the spatial distribution is similar and only the national total differs. To tell them apart, compare each province’s share of the national total in both datasets.

Code
share = merged.assign(
    wp_share=lambda d: 100 * d["pop_worldpop2026"] / d["pop_worldpop2026"].sum(),
    onei_share=lambda d: 100 * d["pop_official"] / d["pop_official"].sum(),
)
share["share_diff_pp"] = share["wp_share"] - share["onei_share"]
share[["ADM1_PCODE", "adm1_name", "wp_share", "onei_share", "share_diff_pp"]].round(2)
ADM1_PCODE adm1_name wp_share onei_share share_diff_pp
0 21 Pinar del Río 5.27 5.29 -0.01
1 22 Artemisa 4.79 4.64 0.15
2 23 La Habana 19.03 17.95 1.08
3 24 Mayabeque 3.46 3.39 0.07
4 25 Matanzas 6.29 6.35 -0.06
5 26 Villa Clara 6.95 6.83 0.13
6 27 Cienfuegos 3.61 3.52 0.10
7 28 Sancti Spíritus 4.14 4.14 -0.00
8 29 Ciego de Ávila 3.88 3.87 0.02
9 30 Camagüey 6.86 6.70 0.16
10 31 Las Tunas 4.77 4.88 -0.11
11 32 Holguín 9.01 9.35 -0.34
12 33 Granma 7.29 7.69 -0.39
13 34 Santiago de Cuba 9.42 9.89 -0.47
14 35 Guantánamo 4.46 4.77 -0.32
15 40 Isla de la Juventud 0.76 0.75 0.01

Shares match within ~1 percentage point across all 16 provinces. The Pearson correlation between provincial totals is essentially 1:

Code
r = np.corrcoef(merged["pop_worldpop2026"], merged["pop_official"])[0, 1]
print(f"Pearson r (admin1 totals): {r:.4f}")
Pearson r (admin1 totals): 0.9970

B.0.4 Decomposing the per-province difference

Per-province pct_diff ranges from +4% (Guantánamo) to +18% (La Habana). That is not the +11.3% you would get from a pure national rescale. Both components matter and they compose multiplicatively:

\[\text{WP}_i = \text{ONEI}_i \times \underbrace{\frac{\sum \text{WP}}{\sum \text{ONEI}}}_{\text{rescale}} \times \underbrace{\frac{s^{\text{WP}}_i}{s^{\text{ONEI}}_i}}_{\text{reshape}}\]

where \(s_i\) is province \(i\)’s share of the national total.

Code
rescale = total_wp / total_off
decomp = merged.assign(
    total_pct=lambda d: 100 * (d["pop_worldpop2026"] / d["pop_official"] - 1),
    rescale_pct=100 * (rescale - 1),
    reshape_pct=lambda d: 100
    * (
        (d["pop_worldpop2026"] / total_wp)
        / (d["pop_official"] / total_off)
        - 1
    ),
)
decomp[
    ["ADM1_PCODE", "adm1_name", "total_pct", "rescale_pct", "reshape_pct"]
].round(2)
ADM1_PCODE adm1_name total_pct rescale_pct reshape_pct
0 21 Pinar del Río 10.98 11.29 -0.28
1 22 Artemisa 14.84 11.29 3.19
2 23 La Habana 17.96 11.29 5.99
3 24 Mayabeque 13.63 11.29 2.10
4 25 Matanzas 10.21 11.29 -0.97
5 26 Villa Clara 13.34 11.29 1.84
6 27 Cienfuegos 14.40 11.29 2.80
7 28 Sancti Spíritus 11.27 11.29 -0.02
8 29 Ciego de Ávila 11.80 11.29 0.45
9 30 Camagüey 13.90 11.29 2.35
10 31 Las Tunas 8.82 11.29 -2.23
11 32 Holguín 7.26 11.29 -3.62
12 33 Granma 5.58 11.29 -5.13
13 34 Santiago de Cuba 6.05 11.29 -4.71
14 35 Guantánamo 3.94 11.29 -6.61
15 40 Isla de la Juventud 12.35 11.29 0.95

The rescale is +11.3% for every province (UN WPP 2024 national total vs. ONEI’s 2025 internal projection). The reshape varies from −6.6% (Guantánamo) to +6.0% (La Habana), with a clear east–west tilt:

Direction Provinces (reshape sign)
Western (gainers) La Habana, Artemisa, Cienfuegos, Camagüey, Mayabeque, Villa Clara
Eastern (losers) Guantánamo, Granma, Santiago de Cuba, Holguín, Las Tunas

B.0.5 Rank check

For applications that rank provinces by exposure, the question is whether the ordering is preserved. 13 of 16 provinces have identical rank between WorldPop and ONEI; the three that swap are a single trio in the middle of the distribution:

Code
ranked = merged.assign(
    rank_wp=lambda d: d["pop_worldpop2026"].rank(ascending=False, method="min").astype(int),
    rank_onei=lambda d: d["pop_official"].rank(ascending=False, method="min").astype(int),
)
ranked["rank_diff"] = ranked["rank_wp"] - ranked["rank_onei"]
ranked.sort_values("rank_onei")[
    ["adm1_name", "rank_wp", "rank_onei", "rank_diff"]
]
adm1_name rank_wp rank_onei rank_diff
2 La Habana 1 1 0
13 Santiago de Cuba 2 2 0
11 Holguín 3 3 0
12 Granma 4 4 0
5 Villa Clara 5 5 0
9 Camagüey 6 6 0
4 Matanzas 7 7 0
0 Pinar del Río 8 8 0
10 Las Tunas 10 9 1
14 Guantánamo 11 10 1
1 Artemisa 9 11 -2
7 Sancti Spíritus 12 12 0
8 Ciego de Ávila 13 13 0
6 Cienfuegos 14 14 0
3 Mayabeque 15 15 0
15 Isla de la Juventud 16 16 0

Spearman ρ on admin1 totals:

Code
r_wp = merged["pop_worldpop2026"].rank()
r_on = merged["pop_official"].rank()
rho = np.corrcoef(r_wp, r_on)[0, 1]
print(f"Spearman rho: {rho:.4f}")
Spearman rho: 0.9912

The only meaningful rank swap is Artemisa rising 2 places (9th → 11th in ONEI, but 9th in WorldPop), pushing Las Tunas and Guantánamo down one slot each. That is the same east-west reshape made visible on the rank scale.

B.0.6 Cross-checking at admin2

If the shared-lineage story is right, we should see the same close agreement at admin2 (municipio). Both sources publish at this scale: CODAB has 168 admin2 polygons, ONEI’s CUB_ADM2_POP_2024.xlsx has 168 rows.

The pcode systems do not match (CODAB uses CU0201-style, ONEI’s adm2 pcode column is NaN), so the join is on (adm1_name, adm2_name) after Unicode normalisation. The compound key also disambiguates two municipios called “San Luis” (one in Pinar del Río, one in Santiago de Cuba).

A handful of municipio names have typos or spelling variants on one side (e.g. ONEI’s "aguada de pasajajeros" vs. CODAB’s "aguada de pasajeros"; ONEI’s "la habana del este" vs. CODAB’s "habana del este"). Rather than build a manual fix-up map and risk silently mis-matching, we apply a stricter quality filter: keep an admin1 only if every one of its admin2 names matches exactly between CODAB and ONEI under basic Unicode normalisation. If even one municipio in a province has uncertain name alignment, drop the whole province — because if names are uncertain, boundary alignment between the two sources is uncertain too.

Code
def norm_basic(s: str) -> str:
    return (
        unicodedata.normalize("NFKD", str(s))
        .encode("ascii", "ignore")
        .decode("ascii")
        .strip()
        .lower()
    )


adm2 = stratus.load_shp_from_blob(
    blob_name=f"{PROJECT_PREFIX}/raw/codab/cub.shp.zip",
    shapefile="cub_adm2.shp",
    stage="dev",
)
adm2["a1_k"] = adm2["ADM1_ES"].map(norm_basic)
adm2["a2_k"] = adm2["ADM2_ES"].map(norm_basic)

xlsx2_bytes = stratus.load_blob_data(
    f"{PROJECT_PREFIX}/raw/unfpa/CUB_ADM2_POP_2024.xlsx",
    container_name="projects",
    stage="dev",
)
official2 = (
    pd.read_excel(io.BytesIO(xlsx2_bytes), sheet_name="CUB_ADM2_POP_2024")[
        ["ADM1_NAME", "ADM2_NAME", "T_TL"]
    ]
    .rename(columns={"T_TL": "pop_official"})
    .assign(
        a1_k=lambda d: d["ADM1_NAME"].map(norm_basic),
        a2_k=lambda d: d["ADM2_NAME"].map(norm_basic),
    )
)

# Keep admin1s where the set of adm2 names is identical in both sources
def is_clean(a1_key: str) -> bool:
    g = set(adm2.loc[adm2["a1_k"] == a1_key, "a2_k"])
    o = set(official2.loc[official2["a1_k"] == a1_key, "a2_k"])
    return bool(g) and g == o


clean_a1 = sorted(
    {a1 for a1 in set(adm2["a1_k"]) | set(official2["a1_k"]) if is_clean(a1)}
)
dropped_a1 = sorted(
    {a1 for a1 in set(adm2["a1_k"]) | set(official2["a1_k"]) if not is_clean(a1)}
)
print(f"clean admin1s ({len(clean_a1)}/16):  {clean_a1}")
print(f"dropped admin1s ({len(dropped_a1)}/16): {dropped_a1}")
clean admin1s (9/16):  ['artemisa', 'granma', 'isla de la juventud', 'las tunas', 'matanzas', 'mayabeque', 'pinar del rio', 'sancti spiritus', 'villa clara']
dropped admin1s (7/16): ['camaguey', 'ciego de avila', 'cienfuegos', 'guantanamo', 'holguin', 'la habana', 'santiago de cuba']
Code
adm2_clean = adm2[adm2["a1_k"].isin(clean_a1)].merge(
    official2.loc[official2["a1_k"].isin(clean_a1), ["a1_k", "a2_k", "pop_official"]],
    on=["a1_k", "a2_k"],
    validate="one_to_one",
)
if str(adm2_clean.crs) != str(da.rio.crs):
    adm2_clean = adm2_clean.to_crs(da.rio.crs)

stats2 = exact_extract(
    da,
    adm2_clean,
    ops=["sum"],
    output="pandas",
    include_cols=["ADM2_PCODE", "ADM2_ES", "ADM1_ES", "pop_official"],
)
sum2_col = next(
    c for c in stats2.columns if c == "sum" or c.endswith("_sum")
)
stats2 = stats2.rename(columns={sum2_col: "pop_worldpop2026"})
stats2["pct_diff"] = (
    100 * (stats2["pop_worldpop2026"] / stats2["pop_official"] - 1)
)

print(f"adm2 polygons in clean subset: {len(stats2)} / 168")
print(
    f"WP total: {stats2['pop_worldpop2026'].sum():>11,.0f}   "
    f"ONEI total: {stats2['pop_official'].sum():>11,.0f}   "
    f"diff: {100*(stats2['pop_worldpop2026'].sum()/stats2['pop_official'].sum()-1):+.2f}%"
)
r2 = np.corrcoef(stats2["pop_worldpop2026"], stats2["pop_official"])[0, 1]
rho2 = np.corrcoef(
    stats2["pop_worldpop2026"].rank(), stats2["pop_official"].rank()
)[0, 1]
print(f"Pearson r:    {r2:.4f}   (vs 0.997 at adm1)")
print(f"Spearman rho: {rho2:.4f}   (vs 0.991 at adm1)")
adm2 polygons in clean subset: 89 / 168
WP total:   4,743,825   ONEI total:   4,284,194   diff: +10.73%
Pearson r:    0.9786   (vs 0.997 at adm1)
Spearman rho: 0.9105   (vs 0.991 at adm1)

After filtering, 9 of 16 admin1s qualify, leaving 89 municipios. A naive Pearson r is 0.98, but that statistic is dominated by the largest municipios (each province’s capital) and hides what is happening to the small ones. The more honest test is whether the per-adm2 pct_diff distribution within each clean province clusters tightly around that province’s adm1-level pct_diff (which we already know — it sits on the +11.3% rescale plus the east-west reshape).

Code
adm1_summary = (
    stats2.groupby("ADM1_ES")
    .agg(
        adm1_wp=("pop_worldpop2026", "sum"),
        adm1_onei=("pop_official", "sum"),
        n_adm2=("pop_worldpop2026", "size"),
    )
    .assign(adm1_pct=lambda d: 100 * (d["adm1_wp"] / d["adm1_onei"] - 1))
)
adm2_dist = stats2.groupby("ADM1_ES")["pct_diff"].agg(
    ["min", "median", "max", "std"]
)
combined = (
    adm1_summary.join(adm2_dist)
    .sort_values("adm1_pct")[
        ["n_adm2", "adm1_pct", "min", "median", "max", "std"]
    ]
    .round(1)
)
combined.columns = [
    "n_adm2",
    "adm1_pct",
    "adm2_min",
    "adm2_median",
    "adm2_max",
    "adm2_std",
]
combined
n_adm2 adm1_pct adm2_min adm2_median adm2_max adm2_std
ADM1_ES
Granma 13 5.7 -21.3 3.8 25.7 11.7
Las Tunas 8 8.8 -0.5 8.0 14.2 5.5
Matanzas 13 10.1 -17.6 3.6 40.1 15.7
Pinar del Río 11 11.0 -5.8 8.6 81.2 23.4
Sancti Spíritus 8 11.3 -27.8 11.4 32.6 17.5
Isla de la Juventud 1 12.4 12.4 12.4 12.4 NaN
Villa Clara 13 13.3 -66.4 11.9 92.6 37.3
Mayabeque 11 13.6 -10.9 13.7 39.8 16.6
Artemisa 11 15.0 -7.9 10.3 77.7 23.4

Two things to notice:

  1. The median per-adm2 pct_diff closely tracks the adm1 pct_diff in each province (e.g. Mayabeque adm1 = +13.6%, adm2 median = +13.7%; Sancti Spíritus adm1 = +11.3%, adm2 median = +11.4%). There is no systematic within-province bias — the per-province mass is in roughly the right place.
  2. But the spread is large. Villa Clara’s individual municipios range from −66% to +93% (std 37 pp); Pinar del Río and Artemisa span ~80 pp; Matanzas and Mayabeque ~50–60 pp. Even in the cleanest provinces, per-municipio agreement is far worse than per-province.

This is consistent with the dasymetric preservation property of popRF: it preserves the total inside each WorldPop-input admin unit but redistributes population within that unit according to the random forest density × built-settlement mask. When CODAB’s adm2 polygons happen to match WorldPop’s internal nested adm2 boundaries, the per-municipio sums recover ONEI’s values. When they don’t, the within- unit allocation determines the per-CODAB-adm2 result, and that allocation depends on RF/settlement signal rather than on the actual ONEI municipal census.

To make the within-province scatter explicit, the residual of each adm2’s pct_diff from its province’s pct_diff should be centred near zero with most weight inside ±10–15 pp if dasymetric preservation behaved cleanly. In practice the residuals reach ±80 pp:

Code
stats2 = stats2.merge(
    adm1_summary["adm1_pct"].rename("province_pct"),
    left_on="ADM1_ES",
    right_index=True,
)
stats2["residual_pp"] = stats2["pct_diff"] - stats2["province_pct"]
print("=== Top 5 residuals (WP allocates more than its province average) ===")
print(
    stats2.nlargest(5, "residual_pp")[
        ["ADM2_ES", "ADM1_ES", "pop_worldpop2026", "pop_official", "residual_pp"]
    ]
    .round(0)
    .to_string(index=False)
)
print("=== Bottom 5 residuals (WP allocates less than its province average) ===")
print(
    stats2.nsmallest(5, "residual_pp")[
        ["ADM2_ES", "ADM1_ES", "pop_worldpop2026", "pop_official", "residual_pp"]
    ]
    .round(0)
    .to_string(index=False)
)
=== Top 5 residuals (WP allocates more than its province average) ===
            ADM2_ES       ADM1_ES  pop_worldpop2026  pop_official  residual_pp
          Caibarién   Villa Clara           67771.0         35191         79.0
San Juan y Martínez Pinar del Río           69201.0         38194         70.0
              Bauta      Artemisa           77968.0         43887         63.0
          Cifuentes   Villa Clara           37301.0         23243         47.0
           Remedios   Villa Clara           56242.0         37250         38.0
=== Bottom 5 residuals (WP allocates less than its province average) ===
     ADM2_ES         ADM1_ES  pop_worldpop2026  pop_official  residual_pp
    Placetas     Villa Clara           19009.0         56514        -80.0
   La Sierpe Sancti Spíritus           11326.0         15685        -39.0
  Jovellanos        Matanzas           42232.0         51241        -28.0
Cauto Cristo          Granma           15678.0         19921        -27.0
   Nueva Paz       Mayabeque           18007.0         20211        -24.0

So even after the strict admin1 filter:

  • Province-level totals (adm1) are preserved. Per-adm2 medians track per-adm1 pct_diff closely, so there is no systematic within-province bias.
  • Individual municipio counts are not reliable. The within-province spread is ±20–80 pp. The raster is a province-level estimator, not a municipio-level one — even in provinces where names and boundaries align cleanly.
  • The 7 dropped provinces include exactly the densest urban areas (La Habana, Santiago de Cuba, Cienfuegos). We can say nothing empirically validated about adm2 accuracy there; a polygon-level ONEI–CODAB crosswalk would be needed for a true test.

B.0.7 Methodology context

ONEI projection. The xlsx is ONEI’s projection from the 2012 Censo de Población y Viviendas (Cuba’s last completed census) using a deterministic cohort-component method via CEPDE under a UNFPA collaboration. The xlsx’s “Methods” cell does not specify the technique explicitly. The known methodological weakness, documented by independent demographers (notably Albizu-Campos, CEDEM/UH), is in emigration accounting:

  • Since 2013, ONEI does not classify as emigrants persons who hold a valid Cuban passport and re-enter the country at least once every 24 months, regardless of where they actually live.
  • The external migration balance is reportedly dominated by US arrivals data, undercounting flows to Spain, Mexico, Russia, Uruguay, etc.
  • Independent reconstruction from electoral rolls + birth/death + destination data places end-2024 population at roughly 8.0M vs. ONEI’s 9.75M — a ~1.7M (~18%) gap that ONEI’s published projection does not see.

WorldPop pipeline. The R2025A raster is produced by random-forest dasymetric disaggregation (Stevens et al. 2007), implemented via the popRF R package. For Cuba specifically (WorldPop’s census-sources sheet, row 52):

Stage Input for Cuba
Timepoint 1 ONEI 2012 census, 168 municipios
Timepoint 2 ONEI Anuario Estadístico 2022, 170 municipios
Annual interpolation Modelled per municipio, 2010–2030
National constraint UN WPP 2024 Revision, January-1st estimates
Settlement mask GHSL BUILT-S/V + Google Open Buildings v3 + Microsoft Building Footprints + World Settlement Footprint

Both WorldPop timepoints are ONEI products, and the UN WPP 2024 national control itself draws on ONEI. So WorldPop and our reference xlsx share the same 2012-census-rooted lineage and the same emigration blind-spot. R2025A is marked as an alpha release by WorldPop, with refinements expected.

B.0.8 What this means for the trigger framework

  • At admin1, rank- and share-based comparisons are robust. 13/16 ranks match exactly, all 16 shares match within 1.1 pp, and r ≈ 0.997. Exposure rankings used for provincial prioritisation are insensitive to the choice between WorldPop and ONEI.
  • At admin2, the raster is not a reliable per-municipio estimator — not even in provinces where names and boundaries align cleanly. After restricting to the 9 admin1s with perfect adm2-name matches between CODAB and ONEI (89 municipios), the per-adm2 pct_diff median tracks each province’s adm1 pct_diff closely, but the spread is ±20–80 percentage points within province. The dasymetric preservation property holds only at WorldPop’s internal nested input units (~161 for Cuba), not at CODAB’s published 168 — so within-province allocation is driven by the random-forest density
    • built-settlement mask, not by ONEI’s municipal census. For municipio-level exposure numbers, use ONEI’s adm2 directly rather than zonal sums from this raster.
  • Absolute counts are not robust at any level. Both datasets descend from the 2012 census via ONEI projections that do not reflect post-2020 emigration. Independent estimates suggest the true end-2024 population is ~15–18% lower than either number. If absolute “people affected” figures are needed, they should be flagged with that uncertainty.
  • The +11% gap between WorldPop and the xlsx is not evidence about real population. It is mostly a difference between UN WPP 2024 (WorldPop’s national control) and ONEI’s later 2025 projection (the xlsx). The ~6% east-west reshape on top is consistent with WorldPop’s 2022 input pre-dating ONEI’s 2025 redistribution of share away from eastern Cuba.

B.0.9 References