📈 Downloading WorldPop Population Projections¶
Overview¶
WorldPop Global Projections (R2025A) provides gridded population estimates for 2015-2030, enabling future scenario planning for disease burden, climate adaptation, and development. This tutorial shows how to download projected population and convert to density for modeling applications.
-
Dataset
WorldPop Global 2015-2030
Version: R2025A
Resolution: 1 km
Coverage: Ethiopia (ETH)
Format: GeoTIFF -
Temporal
Range: 2015–2030
Frequency: Annual
Type: Projections
Method: Constrained -
Spatial
CRS: WGS84 (EPSG:4326)
Grid: ~1 km × 1 km
Units: Persons per cell
Constrained: Yes -
Access
Source: WorldPop
Method: HTTP download
Auth: None required
Size: ~50 MB per file
🎯 What This Script Does¶
graph LR
A[Select Year<br/>2015-2030] --> B[Download GeoTIFF]
B --> C[Compute Cell Area]
C --> D[Calculate Density]
D --> E[Save NetCDF]
style A fill:#e8f5e9
style E fill:#c8e6c9 The script performs the following operations:
- Downloads projected population GeoTIFF from WorldPop
- Reads population counts per grid cell
- Computes cell area using spherical geometry
- Calculates population density (persons/km²)
- Saves as NetCDF for model input
📊 Understanding Population Projections¶
What is WorldPop R2025A?¶
WorldPop R2025A provides constrained population projections:
graph TB
subgraph Inputs
A[Base Population<br/>2015/2020]
B[UN Population<br/>Projections]
C[Urbanization<br/>Trends]
end
subgraph Processing
D[Spatial<br/>Disaggregation]
end
A --> D
B --> D
C --> D
D --> E[Annual Gridded<br/>Projections<br/>2015-2030]
style E fill:#c8e6c9 Constrained vs Unconstrained¶
| Type | Description | Use Case |
|---|---|---|
| Constrained | Population within settlement areas only | More realistic spatial distribution |
| Unconstrained | Population spread across all land | Broader coverage, less realistic |
Recommendation
Use constrained projections (default) for most applications as they better represent actual population distribution.
Available Years¶
| Year Range | Description |
|---|---|
| 2015 | Base year (observed) |
| 2016-2019 | Near-term projections |
| 2020 | Reference year |
| 2021-2025 | Medium-term projections |
| 2026-2030 | Long-term projections |
🚀 Quick Start Guide¶
Prerequisites¶
Basic Usage¶
📋 The Complete Script¶
Python Download Script¶
Save this as download_worldpop_projections.py:
#!/usr/bin/env python
"""
Download WorldPop Global_2015_2030 / R2025A (ETH, 1km constrained)
and convert to population density (persons/km²) in NetCDF format.
Example:
python download_worldpop_projections.py \
--year 2030 \
--out-nc data/pop_eth_worldpop_R2025A_2030_km2.nc
"""
import argparse
from pathlib import Path
import numpy as np
import xarray as xr
import rioxarray as rxr
import requests
EARTH_RADIUS_KM = 6371.0088
def download_file(url: str, out_path: Path, chunk_size: int = 2**20) -> Path:
"""
Download file from URL if not already present.
Parameters
----------
url : str
URL to download
out_path : Path
Local file path
chunk_size : int
Download chunk size in bytes
Returns
-------
Path
Path to downloaded file
"""
out_path = Path(out_path)
out_path.parent.mkdir(parents=True, exist_ok=True)
if out_path.exists():
print(f"[info] File already exists, skipping download: {out_path}")
return out_path
print(f"[info] Downloading from:\n {url}")
with requests.get(url, stream=True, timeout=300) as r:
r.raise_for_status()
total = int(r.headers.get("Content-Length", 0) or 0)
downloaded = 0
with open(out_path, "wb") as f:
for chunk in r.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total:
pct = 100 * downloaded / total
print(
f"\r[info] Downloaded {downloaded/1e6:.1f}/{total/1e6:.1f} MB ({pct:.1f}%)",
end=""
)
print(f"\n[info] Saved to {out_path}")
return out_path
def build_worldpop_R2025A_url(year: int, country: str = "ETH") -> str:
"""
Build download URL for WorldPop R2025A 1km constrained GeoTIFF.
Parameters
----------
year : int
Projection year (2015-2030)
country : str
ISO3 country code (default: ETH for Ethiopia)
Returns
-------
str
Download URL
Example URL (2030):
https://data.worldpop.org/GIS/Population/Global_2015_2030/R2025A/2030/ETH/v1/1km_ua/constrained/eth_pop_2030_CN_1km_R2025A_UA_v1.tif
"""
if year < 2015 or year > 2030:
raise ValueError(f"Year must be between 2015 and 2030, got {year}")
base = "https://data.worldpop.org/GIS/Population/Global_2015_2030/R2025A"
iso3 = country.upper()
iso3_lower = country.lower()
fname = f"{iso3_lower}_pop_{year}_CN_1km_R2025A_UA_v1.tif"
return f"{base}/{year}/{iso3}/v1/1km_ua/constrained/{fname}"
def compute_cell_area_km2(lat: np.ndarray, lon: np.ndarray) -> xr.DataArray:
"""
Compute cell area in km² for each grid cell using spherical geometry.
Parameters
----------
lat : np.ndarray
1D array of latitude values
lon : np.ndarray
1D array of longitude values
Returns
-------
xr.DataArray
2D array of cell areas in km²
"""
if lat.size < 2 or lon.size < 2:
raise ValueError("Need at least 2 lat and 2 lon points to compute cell area.")
lat_rad = np.deg2rad(lat)
dlat = np.abs(lat_rad[1] - lat_rad[0])
dlon = np.abs(np.deg2rad(lon[1] - lon[0]))
phi1 = lat_rad - 0.5 * dlat
phi2 = lat_rad + 0.5 * dlat
row_areas = (EARTH_RADIUS_KM ** 2) * dlon * (np.sin(phi2) - np.sin(phi1))
area2d = np.repeat(row_areas[:, np.newaxis], lon.size, axis=1)
return xr.DataArray(
area2d,
coords={"lat": lat, "lon": lon},
dims=("lat", "lon"),
name="cell_area",
attrs={"units": "km2", "long_name": "grid-cell area"},
)
def tif_to_density_nc(
tif_path: Path,
out_nc: Path,
var_name: str = "pop_density",
year: int = None,
) -> None:
"""
Convert GeoTIFF population counts to density NetCDF.
Parameters
----------
tif_path : Path
Input GeoTIFF path
out_nc : Path
Output NetCDF path
var_name : str
Output variable name
year : int
Projection year (for metadata)
"""
tif_path = Path(tif_path)
out_nc = Path(out_nc)
out_nc.parent.mkdir(parents=True, exist_ok=True)
print(f"[info] Reading GeoTIFF: {tif_path}")
da = rxr.open_rasterio(tif_path, masked=True).squeeze(drop=True)
da = da.rename({"x": "lon", "y": "lat"})
# Ensure latitude is ascending
if float(da.lat[0]) > float(da.lat[-1]):
da = da.sortby("lat")
# Set CRS if missing
if not da.rio.crs:
da = da.rio.write_crs("EPSG:4326", inplace=True)
print(f"[info] Grid size: {da.lat.size} x {da.lon.size}")
print(f"[info] Lat range: {float(da.lat.min()):.3f} to {float(da.lat.max()):.3f}")
print(f"[info] Lon range: {float(da.lon.min()):.3f} to {float(da.lon.max()):.3f}")
# Compute cell areas
print("[info] Computing cell areas...")
area = compute_cell_area_km2(da["lat"].values, da["lon"].values)
# Calculate density
print("[info] Computing population density...")
density = (da / area).astype("float32")
density.name = var_name
density.attrs.update({
"units": "persons km-2",
"long_name": "Projected population density",
"source": "WorldPop Global_2015_2030 R2025A (country=ETH, 1km_ua/constrained)",
"projection_year": year if year else "unknown",
})
# Create dataset
ds_out = density.to_dataset()
ds_out["lat"].attrs.update({
"units": "degrees_north",
"standard_name": "latitude"
})
ds_out["lon"].attrs.update({
"units": "degrees_east",
"standard_name": "longitude"
})
# Global attributes
ds_out.attrs["title"] = f"WorldPop Population Projection {year}"
ds_out.attrs["source"] = "WorldPop Global_2015_2030 R2025A"
ds_out.attrs["institution"] = "WorldPop, University of Southampton"
ds_out.attrs["references"] = "https://www.worldpop.org/"
# Encoding
encoding = {
var_name: {
"_FillValue": np.float32(np.nan),
"zlib": True,
"complevel": 4,
}
}
print(f"[info] Writing NetCDF: {out_nc}")
ds_out.to_netcdf(out_nc, encoding=encoding)
# Summary statistics
valid_density = density.values[np.isfinite(density.values)]
if valid_density.size > 0:
print(f"[info] Density statistics:")
print(f" Min: {valid_density.min():.2f} persons/km²")
print(f" Max: {valid_density.max():.2f} persons/km²")
print(f" Mean: {valid_density.mean():.2f} persons/km²")
print("[info] Done.")
def main():
parser = argparse.ArgumentParser(
description="Download WorldPop Global_2015_2030 / R2025A and convert to persons/km² NetCDF",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Download 2030 projection
python download_worldpop_projections.py \\
--year 2030 \\
--out-nc data/pop_ethiopia_2030_km2.nc
# Download 2025 projection
python download_worldpop_projections.py \\
--year 2025 \\
--out-nc data/pop_ethiopia_2025_km2.nc
# Download with custom variable name
python download_worldpop_projections.py \\
--year 2030 \\
--out-nc data/pop_projection.nc \\
--var-name population
"""
)
parser.add_argument(
"--year",
type=int,
default=2030,
help="Projection year between 2015 and 2030 (default: 2030)",
)
parser.add_argument(
"--cache-dir",
type=str,
default="data/worldpop_projections",
help="Directory to cache downloaded GeoTIFF (default: data/worldpop_projections)",
)
parser.add_argument(
"--out-nc",
type=str,
required=True,
help="Output NetCDF path (e.g. data/pop_ethiopia_2030_km2.nc)",
)
parser.add_argument(
"--var-name",
type=str,
default="pop_density",
help="Name of output variable (default: pop_density)",
)
args = parser.parse_args()
# Validate year
if args.year < 2015 or args.year > 2030:
raise SystemExit(f"Error: Year must be between 2015 and 2030, got {args.year}")
print(f"\n{'#'*60}")
print(f"# WorldPop Population Projection Download")
print(f"# Year: {args.year}")
print(f"# Version: R2025A (1km constrained)")
print(f"{'#'*60}\n")
url = build_worldpop_R2025A_url(args.year)
cache_dir = Path(args.cache_dir)
tif_path = cache_dir / Path(url).name
download_file(url, tif_path)
tif_to_density_nc(tif_path, Path(args.out_nc), var_name=args.var_name, year=args.year)
print(f"\n{'#'*60}")
print(f"# Download and processing complete!")
print(f"# Output: {args.out_nc}")
print(f"{'#'*60}\n")
if __name__ == "__main__":
main()
🔧 Command-Line Arguments¶
Required Arguments¶
| Argument | Type | Description | Example |
|---|---|---|---|
--out-nc | String | Output NetCDF file path | data/pop_2030.nc |
Optional Arguments¶
| Argument | Type | Description | Default |
|---|---|---|---|
--year | Integer | Projection year (2015-2030) | 2030 |
--cache-dir | String | Directory for GeoTIFF cache | data/worldpop_projections |
--var-name | String | Output variable name | pop_density |
📊 Available Projection Years¶
Download Multiple Years¶
#!/bin/bash
# download_all_projections.sh
OUTDIR="data/worldpop_projections"
for YEAR in 2015 2020 2025 2030; do
echo "Downloading $YEAR projection..."
python download_worldpop_projections.py \
--year $YEAR \
--out-nc "$OUTDIR/pop_ethiopia_${YEAR}_km2.nc"
done
echo "All projections downloaded!"
Year-by-Year Download¶
#!/bin/bash
# download_annual_projections.sh
OUTDIR="data/worldpop_projections"
for YEAR in $(seq 2015 2030); do
echo "Downloading $YEAR..."
python download_worldpop_projections.py \
--year $YEAR \
--out-nc "$OUTDIR/pop_ethiopia_${YEAR}_km2.nc"
done
echo "All annual projections downloaded!"
💡 Usage Examples¶
Example 1: Single Year (2030)¶
What it does:
- Downloads 2030 projection GeoTIFF
- Converts to population density
- Saves as compressed NetCDF
- ~2-5 minutes
Example 2: Compare 2020 vs 2030¶
# Download both years
python download_worldpop_projections.py \
--year 2020 \
--out-nc data/pop_ethiopia_2020_km2.nc
python download_worldpop_projections.py \
--year 2030 \
--out-nc data/pop_ethiopia_2030_km2.nc
Example 3: Custom Variable Name¶
python download_worldpop_projections.py \
--year 2030 \
--out-nc data/population_projection.nc \
--var-name population
Example 4: Different Cache Directory¶
python download_worldpop_projections.py \
--year 2030 \
--cache-dir /data/worldpop_cache \
--out-nc data/pop_ethiopia_2030_km2.nc
📂 Output Directory Structure¶
After running the script, your output directory will contain:
data/
├── worldpop_projections/
│ ├── eth_pop_2020_CN_1km_R2025A_UA_v1.tif # Cached GeoTIFF
│ ├── eth_pop_2025_CN_1km_R2025A_UA_v1.tif
│ └── eth_pop_2030_CN_1km_R2025A_UA_v1.tif
├── pop_ethiopia_2020_km2.nc # Output NetCDF
├── pop_ethiopia_2025_km2.nc
└── pop_ethiopia_2030_km2.nc
🔍 Verifying Your Download¶
After downloading, verify your data using Python:
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np
# Open projection file
ds = xr.open_dataset('data/pop_ethiopia_2030_km2.nc')
# Display dataset information
print(ds)
print(f"\nDimensions: {dict(ds.dims)}")
print(f"Units: {ds.pop_density.attrs.get('units', 'unknown')}")
print(f"Year: {ds.pop_density.attrs.get('projection_year', 'unknown')}")
# Statistics
pop = ds.pop_density
valid = pop.values[np.isfinite(pop.values)]
print(f"\nPopulation density statistics:")
print(f" Min: {valid.min():.2f} persons/km²")
print(f" Max: {valid.max():.2f} persons/km²")
print(f" Mean: {valid.mean():.2f} persons/km²")
# Plot population density
fig, ax = plt.subplots(figsize=(12, 10))
pop.plot(
ax=ax,
cmap='YlOrRd',
norm=plt.matplotlib.colors.LogNorm(vmin=1, vmax=10000),
cbar_kwargs={'label': 'Population density (persons/km²)'}
)
ax.set_title('WorldPop Population Projection - Ethiopia 2030')
ax.set_xlabel('Longitude')
ax.set_ylabel('Latitude')
plt.savefig('worldpop_projection_2030.png', dpi=150, bbox_inches='tight')
plt.show()
📈 Analyzing Population Growth¶
Compare Years¶
import xarray as xr
import matplotlib.pyplot as plt
import numpy as np
# Load multiple years
ds_2020 = xr.open_dataset('data/pop_ethiopia_2020_km2.nc')
ds_2030 = xr.open_dataset('data/pop_ethiopia_2030_km2.nc')
# Compute change
pop_2020 = ds_2020.pop_density
pop_2030 = ds_2030.pop_density
# Absolute change
change = pop_2030 - pop_2020
# Percent change (where 2020 > 0)
pct_change = ((pop_2030 - pop_2020) / pop_2020) * 100
pct_change = pct_change.where(pop_2020 > 0)
# Plot comparison
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
# 2020
pop_2020.plot(ax=axes[0], cmap='YlOrRd',
norm=plt.matplotlib.colors.LogNorm(vmin=1, vmax=10000))
axes[0].set_title('2020 Population Density')
# 2030
pop_2030.plot(ax=axes[1], cmap='YlOrRd',
norm=plt.matplotlib.colors.LogNorm(vmin=1, vmax=10000))
axes[1].set_title('2030 Population Density')
# Change
change.plot(ax=axes[2], cmap='RdYlGn_r', center=0, vmin=-100, vmax=500,
cbar_kwargs={'label': 'Change (persons/km²)'})
axes[2].set_title('Population Change (2030 - 2020)')
plt.tight_layout()
plt.savefig('population_change_2020_2030.png', dpi=150, bbox_inches='tight')
plt.show()
# Statistics
print(f"2020 total (approx): {float(pop_2020.sum()):.0f}")
print(f"2030 total (approx): {float(pop_2030.sum()):.0f}")
print(f"Mean change: {float(change.mean()):.2f} persons/km²")
Population Growth Rate¶
import xarray as xr
import numpy as np
# Load years
years = [2015, 2020, 2025, 2030]
totals = []
for year in years:
ds = xr.open_dataset(f'data/pop_ethiopia_{year}_km2.nc')
total = float(ds.pop_density.sum())
totals.append(total)
print(f"{year}: {total/1e6:.2f} million (density sum)")
# Plot trend
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(years, [t/1e6 for t in totals], 'o-', linewidth=2, markersize=10)
plt.xlabel('Year')
plt.ylabel('Population (millions, density sum)')
plt.title('Ethiopia Population Growth Projection')
plt.grid(True, alpha=0.3)
plt.savefig('population_trend.png', dpi=150, bbox_inches='tight')
plt.show()
🔄 Combining with Climate Projections¶
Future Exposure Analysis¶
import xarray as xr
# Load population projection
ds_pop = xr.open_dataset('data/pop_ethiopia_2030_km2.nc')
pop = ds_pop.pop_density
# Load future temperature (e.g., CHC-CMIP6)
ds_temp = xr.open_dataset('data/CHC_CMIP6/2030_SSP245/temperature/2030_SSP245_Tavg_2030_daily.nc')
temp_mean = ds_temp.tavg.mean(dim='time')
# Regrid population to temperature grid
pop_regrid = pop.interp(lat=temp_mean.lat, lon=temp_mean.lon)
# Compute population-weighted temperature
weighted_temp = (temp_mean * pop_regrid).sum() / pop_regrid.sum()
print(f"Population-weighted mean temperature: {float(weighted_temp):.1f}°C")
# Population exposed to high temperatures
hot_threshold = 30 # °C
pop_exposed = pop_regrid.where(temp_mean > hot_threshold).sum()
total_pop = pop_regrid.sum()
pct_exposed = 100 * pop_exposed / total_pop
print(f"Population exposed to >{hot_threshold}°C: {float(pct_exposed):.1f}%")
⚠️ Troubleshooting¶
Common Issues and Solutions¶
Problem: HTTP error during download
Solutions:
- Check year: Must be 2015-2030
- Check URL: Verify file exists on WorldPop
- Try browser: Download manually and place in cache-dir
Problem: Invalid year specified
Solution: Use a year between 2015 and 2030 inclusive.
Problem: Out of memory reading GeoTIFF
Solutions:
- Close other applications
- Process smaller regions
- Use chunked processing
Problem: GeoTIFF has no CRS
Solution: Script automatically sets EPSG:4326 (WGS84).
🎓 Data Quality Notes¶
Strengths
- Annual projections - 2015 to 2030
- 1 km resolution - Good for regional analysis
- Constrained - Realistic spatial distribution
- UN-aligned - Consistent with official projections
- Free access - No registration required
Limitations
- Projections - Not observations
- Uncertainty - Increases with projection horizon
- Single scenario - No alternative pathways
- Country-specific - Need separate downloads per country
Best Practices
- Compare with historical - Validate against 2015/2020
- Consider uncertainty - Use for scenarios, not predictions
- Document version - R2025A in publications
- Combine with climate - For future impact studies
📖 Additional Resources¶
Official Documentation¶
- WorldPop: https://www.worldpop.org/
- Global 2015-2030: https://hub.worldpop.org/geodata/listing?id=77
- Methods: Tatem (2017) - Scientific Data
Related Datasets¶
- WorldPop Historical: 2000-2020 estimates
- AfriPop: Higher resolution for Africa
- GPW v4: NASA alternative
Related Tutorials¶
- WorldPop Population - Historical data
- CHC-CMIP6 Temperature - Future climate
- Climate Data Access - Overview
🚀 Next Steps¶
-
Trend Analysis
Population growth rates
Urban expansion -
Visualize Projections
Future population maps
Change detection -
Climate Exposure
Future temperature exposure
Population at risk -
VECTRI Scenarios
Future disease burden
Climate-population interactions
Need Help?
If you encounter issues or have questions:
- Check the Troubleshooting section
- Review WorldPop Documentation
- Contact workshop instructors
📈 Ready for Future Population Analysis!
You now have everything you need to download WorldPop population projections for future scenario planning and climate-population impact studies.