How Pawa Switch turns Kenya Power’s notice images into the numbers on this site, and what those numbers can and cannot say.
Kenya Power publishes planned interruption notices as images on X (@KenyaPower_Care) and, since ~2025, weekly PDFs on kplc.co.ke. Notices follow a stable grammar: region/county headers → AREA: blocks → date/time → a free-text locality list ending “& adjacent customers”. Planned only: absence of a notice never means the power stayed on.
Each post’s images are processed together by a vision language model bound to a strict JSON schema, so text that flows across images (headers, even mid-word list splits) is reassembled. Extraction is verbatim: KPLC’s wording, including their errors, is preserved, with the reassembled source text kept on every event. A hand-labeled golden set gates any bulk run at ≥98% event recall and ≥99% exact date+time accuracy; failures go to a human review queue, not into the dataset.
Locality mentions are normalized (Mkt→market, S/Centre→shopping centre, C/Fact→coffee factory…) and matched against a gazetteer built from OpenStreetMap, GeoNames, the 2017 school census and the Kenya Master Health Facility List. The waterfall: curated alias (0.99) → exact (0.95) → school/health anchor (0.90) → trigram fuzzy (0.50 to 0.85) → base-name stem (0.75) → unmatched. An event’s geo-confidence is the confidence-weighted share of its localities that matched. It is shown, not hidden. Events attribute to wards through matched localities; failing that, to the area’s historically-known wards; failing that, county-only.
County cross-validation: KPLC’s printed labels contain real errors (verified: “Maungu Town” filed under Tana River, but Maungu is in Taita-Taveta). When geocoded localities disagree with the printed county, the event is flagged, never silently “fixed”.
events: interruptions touching the geography (deduplicated per event on roll-up). outage-hours: scheduled duration summed over touching events: scheduled interruption affecting some part of the geography, not homes-without-power hours. customer-hours (est.): Σ duration × population of touched wards, an upper-bound exposure index. PawaScore (0 to 100, higher = more reliable): within a level and period, over entities with ≥1 event, burden = 0.5·z(hours/100k) + 0.3·z(events) + 0.2·z(avg duration), score = 100·(1−percentile rank). Zero-event entities show “no recorded outages”, not a score.
No official ward-level census table exists. Ward population = WorldPop 100 m (2020, constrained, UNadj) summed per ward, scaled so each county matches its KNBS 2019 census total. Treat ward per-capita figures as estimates.
Planned interruptions only · “adjacent customers” is inherently fuzzy (areas are matched points and hulls, never exact service polygons) · locality coverage is partial and visible per event · the grid layer is indicative (KPLC ~2017 + OSM), no feeder topology is public · KPLC typos are preserved verbatim and flagged when contradicted · boundaries are the 2013 IEBC delimitation.
Every chart and ranking is reproducible from the open exports on the Data page: events with provenance, event→ward attributions, locality matches with confidence, monthly aggregates. The pipeline, golden set, and evaluation harness are in the repository.