instawarn / src /data_processing /load_population.py
jubayerahmad's picture
Upload 99 files
d64c823 verified
Raw
History Blame Contribute Delete
5.85 kB
"""
Load population data from WorldPop raster and compute per-union statistics.
Falls back to estimated population figures if raster processing fails.
"""
import geopandas as gpd
import pandas as pd
import numpy as np
import logging
from rasterstats import zonal_stats
from src.config import (
POPULATION_RASTER_FILE, PROCESSED_UNIONS,
POPULATION_BY_UNION, PROCESSED_DIR,
)
import rasterio
logger = logging.getLogger(__name__)
# Approximate upazila populations for Cox's Bazar (2022 census estimates)
FALLBACK_POPULATION = {
"Cox's Bazar Sadar": 620_000,
"Cox'S Bazar Sadar": 620_000,
"Sadar": 620_000,
"Ramu": 320_000,
"Chakaria": 520_000,
"Kutubdia": 125_000,
"Maheshkhali": 330_000,
"Pekua": 220_000,
"Teknaf": 280_000,
"Ukhia": 210_000,
}
def load_population():
"""
Extract population per union from WorldPop raster, or estimate.
Returns:
DataFrame with columns: union_name, upazila_name, population,
pop_density_per_cell, cells_count.
"""
logger.info("Processing population data...")
PROCESSED_DIR.mkdir(parents=True, exist_ok=True)
if not PROCESSED_UNIONS.exists():
raise FileNotFoundError(
f"Union boundaries not found at {PROCESSED_UNIONS}. "
"Run load_boundaries.py first."
)
unions = gpd.read_file(PROCESSED_UNIONS)
if POPULATION_RASTER_FILE.exists():
try:
pop_df = _extract_from_raster(unions)
if pop_df is not None:
zero_frac = (pop_df["population"] == 0).mean()
total_pop = pop_df["population"].sum()
if total_pop == 0 or zero_frac > 0.8:
logger.warning(
f"Raster extraction produced invalid data "
f"(total={total_pop:,}, zero_frac={zero_frac:.1%}). "
"Falling back to estimator."
)
pop_df = _estimate_population(unions)
pop_df.to_csv(POPULATION_BY_UNION, index=False)
logger.info(f"Saved population data → {POPULATION_BY_UNION}")
return pop_df
except Exception as e:
logger.warning(f"Raster extraction failed: {e}")
logger.info("Falling back to estimated population...")
else:
logger.info("WorldPop raster not found — using estimates...")
pop_df = _estimate_population(unions)
pop_df.to_csv(POPULATION_BY_UNION, index=False)
logger.info(f"Saved estimated population → {POPULATION_BY_UNION}")
return pop_df
def _extract_from_raster(unions):
"""Zonal statistics on WorldPop raster per union polygon."""
logger.info(f" Running zonal statistics on {POPULATION_RASTER_FILE.name}...")
logger.info(f" Unions CRS: {unions.crs}")
if unions.crs and unions.crs.to_epsg() != 4326:
unions = unions.to_crs(epsg=4326)
with rasterio.open(str(POPULATION_RASTER_FILE)) as src:
nodata_val = src.nodata
logger.info(f" Raster CRS: {src.crs}")
# Force evaluating to list in case it's a generator
stats = list(zonal_stats(
unions, str(POPULATION_RASTER_FILE),
stats=["sum", "mean", "count"],
nodata=nodata_val,
all_touched=True
))
result = pd.DataFrame()
if "union_name" in unions.columns:
result["union_name"] = unions["union_name"].values
if "upazila_name" in unions.columns:
result["upazila_name"] = unions["upazila_name"].values
result["population"] = [
int(round(s["sum"])) if s["sum"] else 0 for s in stats
]
result["pop_density_per_cell"] = [
round(s["mean"], 2) if s["mean"] else 0 for s in stats
]
result["cells_count"] = [s["count"] if s["count"] else 0 for s in stats]
for col in ("union_name", "upazila_name", "population", "pop_density_per_cell", "cells_count"):
if col not in result.columns:
result[col] = 0 if col != "union_name" and col != "upazila_name" else "Unknown"
total = result["population"].sum()
logger.info(f" Total extracted population: {total:,}")
logger.info(f" Unions: {len(result)}")
logger.info(
f" Range: {result['population'].min():,}{result['population'].max():,}"
)
return result
def _estimate_population(unions):
"""Generate estimated population from known upazila totals."""
logger.info(" Generating estimated population figures...")
np.random.seed(42)
result = pd.DataFrame()
result["union_name"] = (
unions["union_name"].values
if "union_name" in unions.columns
else [f"Union_{i}" for i in range(len(unions))]
)
result["upazila_name"] = (
unions["upazila_name"].values
if "upazila_name" in unions.columns
else "Unknown"
)
populations = []
for _, row in unions.iterrows():
upazila = str(row.get("upazila_name", ""))
# Look up known population
upazila_pop = 300_000 # default
for key, pop in FALLBACK_POPULATION.items():
if key.lower() in upazila.lower():
upazila_pop = pop
break
# Count sibling unions for distribution
n_unions = max(
1,
len(unions[unions["upazila_name"] == upazila])
if "upazila_name" in unions.columns
else 10,
)
base = upazila_pop / n_unions
populations.append(int(base * (0.7 + 0.6 * np.random.random())))
result["population"] = populations
result["pop_density_per_cell"] = 0
result["cells_count"] = 0
logger.info(f" Estimated total population: {sum(populations):,}")
return result
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
load_population()