idiouf/senegal-socioeconomic-observatory
Senegal Socio-economic Observatory
What Senegal's Agence nationale de la statistique et de la démographie (ANSD) publishes about the country's regions and departments, gathered in one place: population and urbanisation by department; literacy, schooling, activity, employment, poverty, housing, access to services and health staffing by region.
58 indicators · 1,124 observations · 46 departments · 14 regions · 18 published tables. The dashboard produces no data of its own: every figure comes from a published table, identified by report, table number and year.
It is maintained as the reference socio-economic layer for climate-health vulnerability and early-warning work in West Africa — the same territories, the same vintages, the same provenance rules.
Three guarantees hold across every page:
- Provenance is never inferred. Every number carries its report, table and year, with the link.
- Every figure carries a caption — a number, a title, a one-to-two sentence description of what it shows and how to read it, and its source — so that a figure can be read alone and cited as it stands.
- The method is written out, in thirteen numbered sections on the Data & methods page; figure captions refer to them as §5, §8.
The interface is fully bilingual and opens in English, in the light palette. Both defaults are declared in app.py (DEFAULT_LANGUAGE, DEFAULT_THEME) and enforced by a test, so a deployment that passes no environment variable opens the same way. Set SN_ANSD_LANG=fr to open in French instead; the switch at the top of the page moves between the two at any time.
English means English throughout — including place names. Senegal's regions and departments have no English translation; the anglophone convention (World Bank, DHS, the literature) simply drops the diacritics, so Thiès reads Thies and Kédougou reads Kedougou in English mode, on every chart, table, selector and export. A test checks that the 46 department and 14 region names stay distinct once deaccented, and another that no source-status line reaches the English interface untranslated.
Light and dark, light by default. The sidebar carries a visible ☀ Light / ☾ Dark switch, so the reader never has to find the ⋮ menu. Set SN_ANSD_THEME=dark to open dark, or auto to follow the browser.
The pages
Every figure on those pages closes with a caption of the same shape — FIGURE n · title — one or two sentences on what it shows and how to read it, then the source line. The caption is built on the selection actually displayed: it names the number of units on screen, says whether the colour scale is continuous or broken into quantile classes, and prints the published national figure the benchmark line marks. Figures are renumbered from 1 on each page, so "Figure 3" always means the third figure of the page in front of you.
On every thematic page each indicator gets a map and a ranking, side by side, on the same selection: the map gives the geography, the ranking the order and the gap to Senegal, and neither is enough alone — a ranking of forty-six departments without a map is a list of names. Under the map sits the spread of that same selection: the two extremes, the median, the max-to-min ratio, the national figure.
Where a department-level indicator is filtered to one region, the map narrows to that region's departments and its colour scale is recomputed on them — printing national breaks under a map of five departments would misdescribe the colours — and the caption says so. A region-level indicator ignores the department filter and says that too, rather than implying a departmental map that does not exist.
The data
Everything comes from ANSD. The tables are transcribed in full in ansd_tables.py, each with its reference:
58 indicators, 1,124 observations, 46 departments, 14 regions.
What is missing, and stays missing
- Water (Table VIII-12): read for 10 of 14 regions. Matam, Kaffrine, Kédougou and Sédhiou are absent; every view of this indicator says so.
- Area and population density (Table I-22): not transcribed — density is computed nowhere.
- Maternal and child health (continuous DHS: fertility, under-five mortality, contraceptive prevalence, skilled birth attendance, stunting): published by region, not transcribed.
- Unemployment by region: ANSD does not publish it at this level.
- The protected-area inventory — name, type, area, year established — and protected-area extent by region: ANSD publishes neither. The environment chapter of the national SES stops at land occupation and climate; forestry appears there only as a national time series. The Conservation page therefore shows the land take and rainfall ANSD does publish, borrows the one national coverage series that exists from UNEP-WCMC (labelled not ANSD wherever it appears), and says what is absent rather than reconstructing it.
- Classified forests and bush fires: published in the regional SES, not the national one. Only Fatick and Kolda could be read, so only those two appear.
- Education and employment by department: those chapters stop at the region.
Nothing is imputed to fill these gaps. The Data & methods page lists them and shows the coverage of every series.
Deploying to a Hugging Face Space
- Create a Space with SDK = Docker. The
Dockerfilein this folder replaces whatever the template generates. - Upload every file to the repository root. There are no subfolders: the theme that would normally live in
.streamlit/config.tomlis passed as flags, and the cache goes to/tmp. - Wait for the build. Nothing is downloaded at start-up — the ANSD figures are in the repository, so the page paints immediately.
The map
boundaries_ADM1.json and boundaries_ADM2.json ship with the repository, so the map is a real choropleth from the first paint, with nothing to download. They were built from GADM 4.1 — Senegal at commune level, dissolved up to departments and regions — by:
python boundaries_from_gadm.py gadm41_SEN_4.shpRe-run that after any boundary revision. prefetch.py remains as a fallback: it pulls geoBoundaries outlines if you would rather use those. The app prefers the committed files, and redoes the name matching on every read — so fixing sources.NAME_ALIASES is enough, with no rebuild.
Names are matched automatically: accents, punctuation and a letter more or less are absorbed (GADM writes "Birkilane", "Koupentoum", "Malème Hodar", "Médina Yoro Foula"), with a threshold strict enough that no outline lands on the wrong department. 45 of 45 GADM departments and 14 of 14 regions match without a manual entry.
Keur Massar has no outline, and none was invented. Split off from Pikine in 2021, it postdates both GADM 4.1 and geoBoundaries. It could have been assembled from GADM's "Niayes" arrondissement, which carries four of its communes — but that union covers about 40 km² against roughly 90 km² for the real department. A wrong outline colours in as convincingly as a right one, so the department is declared without one and the map caption says why.
The outlines and the regional-capital coordinates are not ANSD data. They draw, they never compute — and the map caption names whichever producer supplied the outlines actually loaded.
Protected areas: what is here, and what one command adds
The Conservation page asks Li et al. (2024)'s question of Senegal — are the territories holding the protected areas the ones where human conditions are best, or worst? — with what actually exists.
The inventory is the three UNESCO World Heritage properties: Niokolo-Koba (913,000 ha, inscribed 1981), the Saloum Delta (145,811 ha inscribed plus a 78,842 ha buffer zone, 2011) and Djoudj (16,000 ha, 1981), each with its reference number, inscribed area, years and published point. It is labelled not ANSD throughout. Two things it is not: it is not the country's 145 protected areas — Protected Planet's count — and the Saloum Delta property is not the national park of the same name, which is smaller. Only figures a primary source publishes line by line are here.
The attribution is computed, not asserted. Each property's published point is run through the committed GADM outlines by ray casting, in fifteen lines of Python rather than a forty-megabyte geometry dependency the Space would otherwise carry. Niokolo-Koba's point falls in Kédougou, the Saloum's in Foundiougne (Fatick), Djoudj's in Dagana (Saint-Louis) — each checked by a test. A point locates; it does not delimit, and for a 913,000 ha property the department holding the point is plainly not the only one it crosses. The page says so where the attribution appears.
The finding it supports is stated in standard deviations across the fourteen regions, favourable-signed, not as a percentage gap: Kédougou is 487% above the national rate for thatch roofs, whose base is tiny, and 19% below on literacy, whose base is wide — plotted as percentages the first bar buries the second, when the second is the point. On the σ scale Kédougou, which holds Senegal's largest protected area, sits one to three standard deviations below the country on essentially every human-development indicator ANSD publishes.
The outlines and their 10 km rings are not in the repository, and nothing was invented to stand in for them. Protected-area geometry comes from the WDPA, which requires a manual download. The computation is here in full:
Download it from protectedplanet.net/country/SEN — Download → SHP → accept the terms — and pass the zip straight in, unopened:
python build_parks.py WDPA_WDOECM_*_Public_SEN_shp.zipProtected Planet ships a zip of three zips, split by geometry type. The script opens them itself, keeps only the polygon layers (a point has no outline, so no ring), and de-duplicates on WDPAID, since one protected area appears once per designation it carries.
It projects to UTM 28N — a degree of longitude is 107 km at Saint-Louis and 109 km at Ziguinchor, so a ring buffered in degrees would stretch north to south — draws the 0–10 km ring in metres, intersects it with the 45 departments, and writes parks_boundaries.json with each ring's share of area per department. The page displays it as soon as the file is there, with nothing else to change. The shares are shares of area, never values: saying a ring is 65% in Tambacounda and 21% in Kédougou is a fact, while averaging those departments' indicators into one number for the ring would assume population spread evenly across a department — false precisely where the parks are. When gridded population (WorldPop) is available, area weighting becomes population weighting, and that is the only change needed.
The geometry is proven offline: a test builds a 0.4° square at Niokolo-Koba's published point, checks its area against the ~1,920 km² the latitude implies and its 10 km ring against the ~2,070 km² the perimeter implies, and checks the department shares sum to one. No synthetic file is ever written into the repository.
The one non-ANSD figure
Protected-area coverage (% of land area, 2013-2025) comes from UNEP-WCMC's World Database on Protected Areas, distributed by the World Bank as ER.LND.PTLD.ZS. It is national-only — no regional split exists in that source either, and splitting a national total across regions would be an invention. It is marked "not ANSD" everywhere it appears, feeds no indicator in the registry, and a test enforces both of those things.
The visual language
Flat institutional grammar, in the manner of the IPCC site: a plain blue frame, a white card inset on it, one large blue headline over a thin rule, small letter-spaced uppercase labels, hairline separators — no shadows, no gradients, no rounded corners. Typeface: Jost, loaded from Google Fonts with a system fallback of the same metric, so a reader with no network sees the same layout.
The chrome follows that charter; the series colours deliberately leave it. A blue map on a blue frame beside blue bars stops the colour saying anything: everything blue reads as decoration, the data included. So the frame keeps the blue, and the series take three families that cannot be confused with it or with each other:
A test enforces this: the sequential ramp, the first categorical colour and both poles of the diverging pair must all sit outside the 195°–255° hue band that holds the chrome blue, and each pole must reach 3:1 against the neutral centre in both themes. In dark mode the diverging pair is inverted — on a navy ground it is the light tints that carry the gap, and transposing the light-mode ramp as-is would sink both extremes into the background.
The files
Code comments and docstrings are in French, the author's working language; the interface, this README and the Data & methods chapter are in both.
Correcting or adding a figure
One place only: the relevant block in ansd_tables.py. Everything else — registry, panel, maps, rankings, profiles — follows from it. Then run:
python sources.py # rebuilds the panel and prints the state of each table
python test_app.py # 278 checks, including the cross-checks on printed totalsTo add a series, transcribe its table into ansd_tables.py, declare its indicators in sources.INDICATORS, and wire it into _build_panel. Add its published national row to the NATIONAL block: without it the indicator still works, but carries no comparison to Senegal — which is better than an invented average.
The rules this dashboard keeps
- Provenance is never inferred. Every chart prints its report, table and year, with the link.
- The benchmark is the published figure for Senegal, never the mean of fourteen regions: that would give Dakar and Kédougou the same weight.
- The two geographies are not mixed. On a department profile a regional indicator is marked "scope: region", and the scatter plot refuses to cross two different levels.
- Missing stays missing. No value is imputed, smoothed or extrapolated. The coverage of every series is shown.
- A correlation over fourteen regions describes; it does not demonstrate. Spearman sits beside Pearson, the count is printed, and no significance test is produced.
Verification
python test_app.py278 checks, including the cross-checks against ANSD's own printed totals: the 46 departments sum to 18,126,388 inhabitants, the 14 regions to 13,108,118 people aged 10 and over, 2,045,436 households and 2,057 doctors; urban land to 3,038.4 and 4,010.0 km²; Fatick's classified forests to 14 and 87,577 ha — all matching the published total rows. A failing test here signals a transcription error.
The suite also exercises every page in both languages, offline — and runs the Relationships page with scipy removed from the import path, because pandas delegates Spearman's rho to scipy and the Space image does not ship it. Spearman is computed here as the Pearson correlation of ranks, which is its definition, gives identical values including ties, and removes a 40 MB dependency.
Three checks guard what this revision adds: every page that carries figures captions all of them, the captions are numbered from 1 with no gap, and the method chapter holds thirteen numbered sections in both languages with a contents list that matches them — so a caption that refers to §7 can never point into an empty section.
Citation
ANSD (2023). Recensement général de la population et de l'habitat (RGPH-5) — final reports, chapters 1, 2, 3 and 8. Dakar.
ANSD (2022). Enquête harmonisée sur les conditions de vie des ménages (EHCVM-II), 2021/2022 — final report. Dakar.
ANSD (2025). Situation économique et sociale du Sénégal, 2022-2023 — social conditions. Dakar.
Dashboard: Diouf, I. (2026). Senegal Socio-economic Observatory — ANSD data.
Dr. Ibrahima Diouf — UCAD–ESP/LPAO-SF, Dakar · Université de Labé–FST, Guinea
Climate-health researcher · Lead Author, Africa chapter of the IPCC Seventh Assessment Report (AR7) · 2026 resident, The Rockefeller Foundation Bellagio Center
Site · Hugging Face · ORCID · LinkedIn · Google Scholar
