Spaces:
Runtime error
Runtime error
| """ | |
| 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() | |