dd-l3-lagos-overture-buildings
Map (reference ← swipe → agent)
0:00
Updating the flood-risk model before rainy season. Need every building footprint exceeding 1000 m² across Lagos State (Nigeria) from Overture.
Also need a per-LGA roll-up: for each Lagos Local Government Area, the building count, total footprint area, count of buildings with a non-null Overture height, and the median height where available (null otherwise).
Write two output files. `lagos_buildings.geoparquet` should be EPSG:4326 GeoParquet with columns `id`, `height`, `footprint_area_m2`, `lga`, and `geometry`. `lagos_building_summary.parquet` should be a plain Parquet with no geometry column and columns `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, and `p50_height_m`.
0:00
System info
{
"adapter": "openrouter",
"model": "deepseek/deepseek-v4-flash",
"base_url": "https://openrouter.ai/api/v1"
}
0:16
Let me break this down: 1. Get Overture Maps data for Lagos State, Nigeria 2. Filter to buildings with footprint > 1000 m² 3. For each building: id, height, footprint_area_m2, lga, geometry 4. Per-LGA rollup: building count, total footprint area, count with non-null height, median height 5. Output: `lagos_buildings.geoparquet` (EPSG:4326) and `lagos_building_summary.parquet` Let me first understand the Lagos State geography. Lagos State is in Nigeria. I need to get the LGA boundaries and the buildings. Let me use overturemaps to download the building data for Lagos. I'll need to figure out the bounding box for Lagos State, Nigeria. Actually, let me start by exploring what data is available and figure out the approach. Lagos State, Nigeria roughly spans from about 2.6°E to 3.8°E, and 6.2°N to 6.7°N. But I should be more precise. Let me use overturemaps to get the buildings data. The overturemaps library can query by bounding box or by administrative region. Actually, let me think about this more carefully. Overture Maps has a `places` theme with `division` area type. I could look up the division for Lagos State to get its geometry, then use that to query buildings. Or I could use a combination approach - use overturemaps to get buildings in the bounding box of Lagos, then do a spatial join with LGA boundaries. Let me start by writing a script that: 1. Gets Lagos State boundary from Overture divisions 2. Gets buildings for that area 3. Gets LGA boundaries 4. Does the analysis Actually, Lagos State LGAs - there are 20 LGAs in Lagos State. Let me get the Overture division data for Lagos State and its LGAs. Let me write a Python script step by step. First, let me see what data is available from overturemaps.
0:16
Bash
cd /work && python -c "import overturemaps; help(overturemaps)" 2>&1 | head -40
Help on package overturemaps:
NAME
overturemaps
PACKAGE CONTENTS
__main__
changelog
cli
core
models
releases
state
writers
FILE
/usr/local/lib/python3.14/site-packages/overturemaps/__init__.py
0:21
Let me explore how overturemaps works and what functions are available.
0:21
Bash
cd /work && python -c "import overturemaps; print(dir(overturemaps))"
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'core', 'geodataframe', 'get_all_overture_types', 'models', 'record_batch_reader']
0:30
Let me explore the overturemaps API more to understand how to query building footprints.
0:30
Bash
cd /work && python -c "import overturemaps; help(overturemaps.record_batch_reade...
Help on function record_batch_reader in module overturemaps.core:
record_batch_reader(
overture_type,
bbox=None,
release=None,
connect_timeout=None,
request_timeout=None,
stac=False
) -> Optional[pa.RecordBatchReader]
Return a pyarrow RecordBatchReader for the desired bounding box and s3 path, or None on error.
0:32
Bash
cd /work && python -c "import overturemaps; print(overturemaps.get_all_overture_...
['address', 'bathymetry', 'building', 'building_part', 'division', 'division_area', 'division_boundary', 'place', 'segment', 'connector', 'infrastructure', 'land', 'land_cover', 'land_use', 'water']
0:40
Let me figure out how to use overturemaps to get building data. I need to: 1. Get the bounding box for Lagos State, Nigeria 2. Query Overture for buildings in that area 3. Filter to buildings with footprint area > 1000 m² 4. Join with LGA (Local Government Area) boundaries from Overture's division data 5. Compute the summary statistics per LGA 6. Write both output files Let me first explore how to use overturemaps.record_batch_reader and the division data. For Lagos State, I need its geometry. I can get that from Overture's division data. Let me start by exploring.
0:40
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Check what the building schema looks like
reader = overturemaps.record_batch_reader('building', bbox=(-180, -90, 180, 90))
if reader:
schema = reader.schema
print('Building schema:')
print(schema)
else:
print('No reader returned')
" 2>&1 | head -40
Building schema:
id: string
names: struct<primary: string, common: map<string, string ('common')>, rules: list<element: struct<variant: (... 159 chars omitted)
child 0, primary: string
child 1, common: map<string, string ('common')>
child 0, common: struct<key: string not null, value: string> not null
child 0, key: string not null
child 1, value: string
child 2, rules: list<element: struct<variant: string, language: string, perspectives: struct<mode: string, countries (... 87 chars omitted)
child 0, element: struct<variant: string, language: string, perspectives: struct<mode: string, countries: list<element (... 72 chars omitted)
child 0, variant: string
child 1, language: string
child 2, perspectives: struct<mode: string, countries: list<element: string>>
child 0, mode: string
child 1, countries: list<element: string>
child 0, element: string
child 3, value: string
child 4, between: list<element: double>
child 0, element: double
child 5, side: string
sources: list<element: struct<property: string, dataset: string, license: string, record_id: string, update_t (... 65 chars omitted)
child 0, element: struct<property: string, dataset: string, license: string, record_id: string, update_time: string, c (... 50 chars omitted)
child 0, property: string
child 1, dataset: string
child 2, license: string
child 3, record_id: string
child 4, update_time: string
child 5, confidence: double
child 6, between: list<element: double>
child 0, element: double
level: int32
height: double
min_height: double
is_underground: bool
num_floors: int32
num_floors_underground: int32
min_floor: int32
subtype: string
class: string
facade_color: string
1:33
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Check what the division schema looks like
reader = overturemaps.record_batch_reader('division', bbox=(-180, -90, 180, 90))
if reader:
schema = reader.schema
print('Division schema:')
print(schema)
else:
print('No reader returned')
" 2>&1 | head -40
Division schema:
id: string
geometry: binary
-- field metadata --
ARROW:extension:name: 'geoarrow.wkb'
country: string
sources: list<element: struct<property: string, dataset: string, license: string, record_id: string, update_t (... 65 chars omitted)
child 0, element: struct<property: string, dataset: string, license: string, record_id: string, update_time: string, c (... 50 chars omitted)
child 0, property: string
child 1, dataset: string
child 2, license: string
child 3, record_id: string
child 4, update_time: string
child 5, confidence: double
child 6, between: list<element: double>
child 0, element: double
subtype: string
admin_level: int32
class: string
names: struct<primary: string, common: map<string, string ('common')>, rules: list<element: struct<variant: (... 159 chars omitted)
child 0, primary: string
child 1, common: map<string, string ('common')>
child 0, common: struct<key: string not null, value: string> not null
child 0, key: string not null
child 1, value: string
child 2, rules: list<element: struct<variant: string, language: string, perspectives: struct<mode: string, countries (... 87 chars omitted)
child 0, element: struct<variant: string, language: string, perspectives: struct<mode: string, countries: list<element (... 72 chars omitted)
child 0, variant: string
child 1, language: string
child 2, perspectives: struct<mode: string, countries: list<element: string>>
child 0, mode: string
child 1, countries: list<element: string>
child 0, element: string
child 3, value: string
child 4, between: list<element: double>
child 0, element: double
child 5, side: string
wikidata: string
perspectives: struct<mode: string, countries: list<element: string>>
child 0, mode: string
1:57
I need to get buildings for Lagos State, Nigeria. Let me understand the workflow: 1. Find the bounding box for Lagos State, Nigeria using the `division` type. 2. Get all buildings within that bounding box (or directly filter for Lagos State). 3. Filter buildings with area > 1000 m². 4. Join with LGA boundaries to assign each building to an LGA. 5. Compute per-LGA summary. For division data, I see there's a `subtype` field - Lagos State is a `region` and LGAs are `locality`. Actually, let me check what divisions look like for Nigeria. Let me think about the approach: 1. Get the Lagos State boundary from Overture divisions (admin_level=4 or something appropriate for a state in Nigeria) 2. Get all LGAs in Lagos State from Overture divisions 3. Get all buildings within the Lagos State bounding box 4. Spatial join buildings to LGAs 5. Filter buildings with area > 1000 m² 6. Compute summaries Actually, let me think about this more carefully. Overture buildings have geometry and height fields. The building footprint area needs to be computed. Let me check what geometry format the buildings are in. Let me first figure out Nigeria's divisions and then get the Lagos boundary.
1:57
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Get division data for Nigeria to see admin levels
nigeria_bbox = (2.0, 4.0, 15.0, 14.0)
reader = overturemaps.record_batch_reader('division', bbox=nigeria_bbox)
if reader:
table = reader.read_all()
df = table.to_pandas()
# Filter for Nigeria
nigeria = df[df['country'] == 'NG']
print('Columns:', list(nigeria.columns))
print('admin_level values:', sorted(nigeria['admin_level'].dropna().unique()))
print('subtype values:', sorted(nigeria['subtype'].dropna().unique()))
# Look at some rows
for _, row in nigeria.head(20).iterrows():
print(row['names'], row['admin_level'], row['subtype'], row['country'])
" 2>&1
Columns: ['id', 'geometry', 'country', 'sources', 'subtype', 'admin_level', 'class', 'names', 'wikidata', 'perspectives', 'local_type', 'region', 'hierarchies', 'parent_division_id', 'norms', 'population', 'capital_division_ids', 'capital_of_divisions', 'cartography', 'version', 'bbox']
admin_level values: [np.float64(0.0), np.float64(1.0), np.float64(2.0)]
subtype values: ['country', 'county', 'locality', 'macrohood', 'microhood', 'neighborhood', 'region']
{'primary': 'Ogidigoi', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ogidigben', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogidigbe', 'between': None, 'side': None}],
dtype=object)} nan locality NG
{'primary': 'Akpakpa', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere-Odo', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere-Oke', 'common': None, 'rules': None} nan locality NG
{'primary': 'Okerenkokogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Opuede', 'common': None, 'rules': None} nan locality NG
{'primary': 'Arogbogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ufuoma', 'common': None, 'rules': None} nan locality NG
{'primary': 'Tomagbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Olusumere', 'common': None, 'rules': None} nan locality NG
{'primary': 'Agbobogbene', 'common': None, 'rules': None} nan locality NG
{'primary': 'Sokebolou', 'common': None, 'rules': None} nan locality NG
{'primary': 'Yokri Sobo', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Yoki Village', 'between': None, 'side': None}],
dtype=object)} nan locality NG
{'primary': 'Beniboye', 'common': None, 'rules': None} nan locality NG
{'primary': 'Gulani', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Gula', 'between': None, 'side': None},
{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogulagha II', 'between': None, 'side': None},
{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Ogula', 'between': None, 'side': None}],
dtype=object)} nan locality NG
{'primary': 'Guoghene', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Guogbene', 'between': None, 'side': None},
{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Oguogbene', 'between': None, 'side': None}],
dtype=object)} nan locality NG
{'primary': 'Sobolo-Obotobo', 'common': None, 'rules': array([{'variant': 'alternate', 'language': None, 'perspectives': None, 'value': 'Sokolobo-Obotobo', 'between': None, 'side': None}],
dtype=object)} nan locality NG
{'primary': 'Ijaw Fishing Camp', 'common': None, 'rules': None} nan locality NG
{'primary': 'Ogoba', 'common': None, 'rules': None} nan locality NG
2:11
I can see division data. I need to understand how to get Lagos State and its LGAs. Let me look for the division_area or division_boundary subtype for admin_level, and specifically for Lagos State. Let me look at division_area and division_boundary types, and also find the bbox for Lagos.
2:11
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Try division_area
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
print('division_area columns:', list(nigeria.columns))
print('admin_level values:', sorted(nigeria['admin_level'].dropna().unique()))
print('subtype values:', sorted(nigeria['subtype'].dropna().unique()))
# Find Lagos
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') and 'Lagos' in name['primary']:
print('Found:', name['primary'], 'admin_level:', row['admin_level'], 'subtype:', row['subtype'])
" 2>&1
division_area columns: ['id', 'geometry', 'country', 'sources', 'subtype', 'admin_level', 'class', 'names', 'is_land', 'is_territorial', 'region', 'division_id', 'version', 'bbox'] admin_level values: [np.float64(0.0), np.float64(1.0), np.float64(2.0)] subtype values: ['country', 'county', 'locality', 'macrohood', 'microhood', 'neighborhood', 'region'] Found: Lagos Mainland admin_level: 2.0 subtype: county Found: Lagos admin_level: 1.0 subtype: region Found: Lagos Island admin_level: 2.0 subtype: county
2:40
Let me find the Lagos State area and its LGAs. I need to understand the hierarchical setup. In Nigeria, "Lagos State" would be at admin_level 1 (region), and LGAs (Local Government Areas) would be at admin_level 2 (county/subtype). Let me dig deeper.
2:40
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Find all Lagos LGAs (county-level divisions)
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
# Find the Lagos region to get its ID
lagos_ids = []
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') and 'Lagos' in name['primary'] and row['subtype'] == 'region':
print('Region:', name['primary'], 'ID:', row['id'], 'admin_level:', row['admin_level'])
lagos_ids.append(row['id'])
# Find all counties with Lagos in name
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') and ('Lagos' in name['primary'] or 'Ikeja' in name['primary'] or 'Eti-Osa' in name['primary']):
print(name['primary'], 'admin_level:', row['admin_level'], 'subtype:', row['subtype'], 'id:', row['id'])
" 2>&1
Region: Lagos ID: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f admin_level: 1.0 Lagos Mainland admin_level: 2.0 subtype: county id: 639f85d6-f581-4f31-b52b-98da63a57271 Ikeja admin_level: 2.0 subtype: county id: c540f662-44c9-498d-8f3d-2a41a039a29f Lagos admin_level: 1.0 subtype: region id: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f Lagos Island admin_level: 2.0 subtype: county id: c58fd335-e502-421c-b8e3-f263c5fb0ff7
3:15
Let me understand the data better. The Overture divisions have a hierarchy. I need to get all LGA-level divisions (county subtype with admin_level 2) within the Lagos State region. Let me check if there's a `region` or `parent_division_id` field that links them. Looking at division_area schema, there's a `division_id` field. And for division (not division_area), there's `parent_division_id` and `hierarchies`. Let me find all counties in the Lagos region by checking all counties within the bounding box of Lagos, and then use the region/hierarchy info to filter. Or I can just use a bounding box approach - Lagos state is roughly bounded.
3:15
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow.compute as pc
import pyarrow as pa
# Read the Lagos region area geometry
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
# Find Lagos region
lagos_region = None
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
lagos_region = row
if lagos_region is not None:
print('Lagos region id:', lagos_region['id'])
print('Lagos region division_id:', lagos_region.get('division_id'))
print('Lagos region geometry type:', type(lagos_region['geometry']))
# Now get all counties (LGAs) with parent_division_id matching Lagos
# Let's check the division table for parent_division_id
" 2>&1
Lagos region id: 5fb3f6cb-3993-4bb7-8a53-73e775feb14f Lagos region division_id: 246ecf78-8622-4d7d-8d12-102e73a67cda Lagos region geometry type: <class 'bytes'>
3:40
Let me take a different approach. I'll use the `division_boundary` type to get all LGAs in Lagos state, or I'll use DuckDB to query the Overture data with spatial filtering. Actually, the simplest approach is: 1. Get the Lagos state boundary geometry from `division_area` 2. Use that bounding box to fetch all buildings 3. Spatially filter buildings to those within Lagos state 4. Use `division_area` with subtype `county` to get LGA boundaries and assign LGAs to buildings via spatial join 5. Compute the per-LGA roll-up Let me think about this more carefully. The Overture divisions for Lagos State (Nigeria) - I found that "Lagos" is a region (admin_level=1), and there are subdivisions with subtype="county" (admin_level=2) like "Lagos Mainland", "Ikeja", "Lagos Island". But these are only 3 LGAs - there are actually 20 LGAs in Lagos State. Let me look for more counties within Lagos state area.
3:40
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Get ALL counties (LGAs) - wider bbox for all of Nigeria
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
# List all counties
counties = nigeria[nigeria['subtype'] == 'county']
print(f'Found {len(counties)} counties')
for _, row in counties.iterrows():
name = row['names']
if name:
print(f\" {name.get('primary', 'N/A')}\")
print()
# Show regions
regions = nigeria[nigeria['subtype'] == 'region']
for _, row in regions.iterrows():
name = row['names']
if name:
print(f\"Region: {name.get('primary', 'N/A')} id={row['id']}\")
" 2>&1
Found 773 counties Yewa North Imeko Afon Ipokia Badagry Yewa South Ado Odo/Ota Amuwo Odofin Ifako/Ijaye Ifo Alimosho Ojo Ibarapa North Abeokuta North Ewekoro Ibarapa Central Atisbo Saki West Iwajowa Baruten Saki East Shagamu Obafemi Owode Ibarapa East Ìsẹ́yìn Itesiwaju Kajola Abeokuta South Odeda Kosofe Shomolu Lagos Mainland Apapa Mushin Ikeja Agege Oshodi/Isolo Ajeromi/Ifelodun Surulere Lagos Island Eti Osa Ìkòròdú Epe Ibeju Lekki Oluyole Ibadan South West Ibadan North West Ibadan South East Afijio Ibadan North Ibadan North East Ijebu North Ìjẹ̀bú Òde Akinyele Atiba Orelope Oyo West Odogbolu Ido Remo North Ikenne Dandi Bunza Bagudo Irepo Borgu Suru Kalgo Arewa Dandi Birnin Kebbi Maiyama Kaiama Ijebu East Aiyedade Aiyedire Irewole Ejigbo Ola Oluwa Ogbomosho South Ogo Oluwa Ogbomosho North Ori Ire Olorunsogo Oyo East Egbeda Lagelu Ona Ara Ijebu North East Iwo Isokan Ogun Waterside Osogbo Ife East Ife South Atakunmosa West Ifelodun Oyun Odo Otin Boripe Offa Ilorin South Olorunda Moro Ilorin West Orolu Irepodun Ife Central Ife North Ede North Ede South Egbedore Surulere Asa Agwara Koko-Besse Jega Aleiro Argungu Gwandu Augie Kebbe Shanga Ngaski Yauri Tambuwal Silame Fakai Yabo Rijau Magama Ifelodun Ifedayo Isin Ijero Ekiti South-West Ekiti West Efon Ondo East Irepodun Ila Ilorin East Boluwaduro Atakunmosa East Ilesha East Obokun Ilesha West Ondo West Ile Oluji/Okeigbo Oriade Odigbo Okitipupa Ilaje Ese Odo Irele Warri North Ovia South-West Idanre Adó-Èkìtì Irepodun/Ifelodun Akure North Ekiti Oye Oke Ero Edu Ido-Osi Ilejemeje Ikere Akure South Ifedore Moba Gummi Shagari Wamako Sokoto North Bodinga Zuru Sokoto South Mashegu Mokwa Kontagora Dange-Shuni Tureta Bukkuyum Wasagu-Danko Sakaba Rabah Mariga Lavun Ose Akoko South West Ekiti East Ikole Yagba West Ise/Orun Gbonyin Emure Ọ̀wọ̀ Ovia North-East Egor Oredo Warri South-West Burutu Warri South Ikpoba-Okha Sapele Ethiope West Ekeremor Udu Okpe Uvwie Uhunmwonde Bomadi Ughelli South Southern Ijaw Ethiope East Patani Ughelli North Orhionmwon Owan East Mopa-Muro Ijumu Akoko South East Owan West Akoko North East Akoko North West Yagba East Pategi Edati Wushishi Bakura Anka Talata Mafara Gbako Bida Rafi Maru Maradun Shinkafi Katcha Bosso Agaie Lokoja Adavi Esan North-East Okehi Etsako West Okene Kabba/Bunu Ogori/Magongo Akoko-Edo Esan Central Ika North East Ukwuani Ika South Iguegben Esan West Isoko South Isoko North Kolokuma/Opokuma Sagbama Ogbia Yenegoa Ndokwa West Nembe Brass Degema Akuku Toru Ahoada West Ndokwa East Aniocha South Aniocha North Esan South-East Ogba/Egbema/Ndoni Abua/Odual Ahoada East Emuoha Oguta Ogbaru Onitsha North Onitsha South Anambra West Oshimili South Oshimili North Idah Ibaji Lapai Ajaokuta Etsako Central Etsako East Chanchaga Shiroro Birnin Gwari Kaura Namoda Bungudu Zurmi Paikoro Gusau Birnin Magaji-Kiyaw Tsafe Sabuwa Faskari Munya Gurara Gwagwalada Dekina Ofu Bassa Igalamela-Odolu Kwali Abaji Kogi Orsu Nnewi South Dunukofia Nnewi North Anambra East Ekwusigo Idemili South Idemili North Oyi Oru West Ihiala Ohaji/Egbema Asari-Toru Ikwerre Oru East Owerri West Obio/Akpor Port-Harcourt Isu Mbaitoli Njaba Owerri Municipal Anaocha Njikoka Ayamelum Awka North Orlu Aguata Nkwerre Awka South Uzo-Uwani Ideato North Ideato South Ikeduru Ngor-Okpala Nwangele Owerri North Etche Okrika Eleme Ogu Bolo Ukwa West Omumma Aboh-Mbaise Isiala Mbano Orumba North Ezeagu Unuimo Orumba South Oji-River Udi Okigwe Ezinihitte Ehime Mbano Ahiazu-Mbaise Tai Osisioma Ngwa Oyigbo Aba North Aba South Ugwunagbo Obowo Ihitte/Uboma Isiala-Ngwa North Ukwa East Obi Ngwa Umuahia North Umuahia South Isiala-Ngwa South Awgu Isuikwuato Igbo-Etiti Umu-Nneochi Igbo-Eze-South Nsukka Igbo-Eze-North Kuje Toto Suleja Tafa Chikun Dandume Batsari Safana Dan Musa Funtua Giwa Kankara Jibia Kurfi Dutsin Ma Bakori Kaduna North Kaduna South Municipal Area Council Bwari Kagarko Kachia Igabi Danja Batagarawa Muhammadu Dikko Stadium Charanchi Matazu Malumfashi Zaria Sabon Gari Kudan Kafur Musawa Rimi Kankia Rogo Kajuru Karu Keffi Nasarawa Omala Olamaboro Udenu Ankpa Ogbadibo Isi-Uzo Ivo Aninri Nkanu East Enugu East Enugu South Nkanu West Enugu North Bende Ikwuano Ika Etim Ekpo Ukanafun Obot Akara Essien Udim Oruk Anam Ikot Ekpene Mkpat Enin Abak Ikono Ini Edda Ohaozara Ishielu Arochukwu Ohafia Etinan Eastern Obolo Ikot Abasi Opobo/Nkoro Khana Andoni Gokana Bonny Gudu Binji Tangaza Gwadabawa Illela Kware Gada Wurno Goronyo Sabon Birni Isa Kaita Onna Ibiono Ibom Nsit Ibom Uyo Onicha Afikpo North Ezza North Ibesikpo Asutan Eket Nsit Ubium Itu Uruan Nsit Atai Esit Eket Biase Abi Ezza South Ohaukwu Ado Ebonyi Odukpani Okobo Ibeno Urue Offong/Oruko Ikwo Yakurr Abakaliki Oron Mbo Udung Uko Calabar South Calabar Municipal Izzi Oju Obubra Boki Vandeikya Bekwarra Etung Ogoja Yala Ikom Akpabuyo Akamkpa Markurdi Nasarawa Egon Kaura Tudun Wada Rano Lere Bunkure Doguwa Kibiya Riyom Wamba Lafia Konshisha Guma Obi Keana Tarka Gboko Bokkos Barkin Ladi Jos North Sumaila Garko Jos South Bassa Warawa Baure Babura Gabasawa Wudil Gezawa Dawakin Kudu Minjibir Dambatta Zango Nassarawa Tarauni Kumbotso Ungongo Yankwashi Kazaure Makoda Fagge Kano Municipal Kura Dala Gwale Daura Mai'Adua Dutsi Bichi Kunchi Gwiwa Rimin Gado Tofa Dawakin Tofa Roni Sandamu Madobi Garum Mallam Bebeji Kubau Sanga Gwer East Akwanga Doma Obi Gwer West Zangon Kataf Jema'A Kauru Ikara Kiru Soba Jaba Oturkpo Kokona Ohimini Agatu Apa Okpokwu Makarfi Kusada Bindawa Mani Mashi Tsanyawa Gwarzo Shanono Karaye Kabo Bagwai Ingawa Machina Malam Maduri Kafin Hausa Auyo Hadejia Itas/Gadau Biriniwa Kanam Ussa Takum Bauchi Ganjuwa Shira Gwaram Jama'Are Ibi Langtang South Langtang North Wukari Kanke Warji Buji Kiyawa Kaugama Miga Jahun Maigatari Gagarawa Gumel Taura Ajingi Gaya Ringim Garki Sule Tankakar Dutse Toro Ningi Takai Albasu Jos East Mangu Qua'An Pan Buruku Ushongo Logo Awe Birnin Kudu Dass Tafawa-Balewa Bogoro Mikang Pankshin Shendam Ukum Katsina-Ala Kwande Obanliku Obudu Sardauna Kurmi Wase Donga Alkaleri Giade Katagum Kiri Kasama Nguru Zaki Misau Guri Gamawa Darazo Kirfi Gassol Dukku Karim Lamido Bali Billiri Shongom Gombe Akko Nangere Potiskum Nafada Barde Jakusko Damban Karasuwa Yusufari Borsari Fika Funakaye Fune Yamaltu/Deba Kaltungo Lau Kwami Ardo-Kola Gashaka Jalingo Yorro Balanga Bayo Larmurde Zing Teungo Mayo-Belwa Guyuk Kwaya Kusar Numan Gulani Tarmua Yunusari Gujba Biu Ngelzarma Geidam Shelleng Shani Demsa Ganye Jada Yola North Yola South Hawul Kaga Gubio Magumeri Damboa Song Fufore Gombi Girei Askira-Uba Hong Maiha Konduga Jere Nganzai Maiduguri Chibok Mobbar Monguno Mafa Gwoza Guzamala Abadam Michika Mubi South Mubi North Madagali Marte Kukawa Bama Dikwa Ngala Kala-Balge Region: Oyo id=1e6cd123-0b89-4cf6-b84a-6fe0dec9d6b3 Region: Ogun id=d8e3535a-c797-4f52-a361-6f2c5116018b Region: Lagos id=5fb3f6cb-3993-4bb7-8a53-73e775feb14f Region: Osun id=5691ecd9-8b1c-4858-98c0-87cff32222f9 Region: Kwara id=7c895e09-4f5c-498b-8078-8d206c2ffeb6 Region: Kebbi id=e69e0ecd-90b9-4996-b993-ff4072821ace Region: Ekiti id=769badad-946c-45f1-8761-62489c8ffd1c Region: Ondo id=833b2001-6dde-4e77-8b40-3faadf0c9ce7 Region: Niger id=1ac1cbf8-15bc-4fb9-ae15-abc107d591de Region: Sokoto id=ab126ab5-8213-4242-800e-cd453cdae1d8 Region: Delta id=57e7821f-04a3-4a12-baa6-80fc930bc131 Region: Edo id=d5a8660c-7516-4157-a6a2-4b3f518d9880 Region: Bayelsa id=4698eac8-abcd-4a96-b672-7162af3f6d1a Region: Zamfara id=63dcf9f1-e17c-4391-a98e-1a5f964f0928 Region: Kogi id=77e6ff83-0ba8-432e-9a76-fa204f688ecf Region: Rivers id=1167611b-4414-4c14-adc3-df3d9b118c93 Region: Imo id=79da8210-61a0-4aa3-89cc-053a93bc2f5a Region: Anambra id=665cfea3-803d-4834-8175-4f21419c9bdd Region: Enugu id=e6503528-9360-4c7e-9a18-3ff07c5e699c Region: Federal Capital Territory id=91838175-4269-499c-afee-8a2930129d5a Region: Kaduna id=e1a9fcd7-cce2-42bf-95da-4b18b5d20545 Region: Abia id=5224273a-e4f3-4c21-9fc9-6db1bec82325 Region: Akwa Ibom id=0a8848e8-192c-4c61-a560-0a2a9ca10563 Region: Ebonyi id=6f901e2b-f10d-4916-b609-481ad0cf66de Region: Cross River id=155c73e5-9f36-4693-b259-d50e88a753d3 Region: Kano id=2f12c73f-b617-495f-9423-047cf513d93d Region: Benue id=1afecc55-c117-473e-ad6b-c72a5ff19758 Region: Nasarawa id=769cca77-e3fb-4e27-aa79-ab5d0aa8ba7d Region: Katsina id=73b76fc0-d464-4915-ae63-e02a69b8ae6d Region: Bauchi id=4a0a1c2c-5100-483c-b613-8bd34bf0b5f8 Region: Jigawa id=a1cfd87a-38b7-492e-aaed-83c55665aed1 Region: Plateau id=9a52a2bc-bc4d-4d49-8378-0776b94d6c46 Region: Taraba id=5f6d19c5-9a0d-49e3-97f4-33158e69b688 Region: Gombe id=d277207a-67cd-43ad-8d38-25bc20066e0f Region: Yobe id=d496707d-35eb-4f0c-985b-2658a9cc28d3 Region: Adamawa id=57b46577-6cb1-456a-8c4a-42e36b37cbc9 Region: Borno id=8567bc8d-45b2-4648-a3b5-780fbddf04c9
4:25
Now I can see the Lagos LGAs (counties). Let me get the Lagos-specific ones. From the output, the Lagos LGAs are: 1. Badagry 2. Amuwo Odofin 3. Ifako/Ijaye (actually, this might not be Lagos, let me check) 4. Alimosho 5. Ojo 6. Kosofe 7. Shomolu 8. Lagos Mainland 9. Apapa 10. Mushin 11. Ikeja 12. Agege 13. Oshodi/Isolo 14. Ajeromi/Ifelodun 15. Surulere 16. Lagos Island 17. Eti Osa 18. Ìkòròdú (Ikorodu) 19. Epe 20. Ibeju Lekki Let me get the ones specifically under Lagos region by checking parent_division_id or using the division hierarchy.
4:25
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
# Let's check the division table (not division_area) for parent_division_id
reader = overturemaps.record_batch_reader('division', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
# Find Lagos region
lagos_region = None
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
lagos_region = row
break
if lagos_region is not None:
print('Lagos region id:', lagos_region['id'])
lagos_id = lagos_region['id']
# Find counties with parent_division_id = lagos_id
lagos_counties = nigeria[(nigeria['subtype'] == 'county') & (nigeria['parent_division_id'] == lagos_id)]
print(f'Found {len(lagos_counties)} counties under Lagos region')
for _, row in lagos_counties.iterrows():
name = row['names']
if name:
print(f\" {name.get('primary', 'N/A')} - id: {row['id']}\")
" 2>&1
Lagos region id: 246ecf78-8622-4d7d-8d12-102e73a67cda Found 20 counties under Lagos region Amuwo Odofin - id: 7583d723-71b9-4f18-9fff-bff98df8322d Badagry - id: 202749c9-2473-4ecb-8528-72dc7a1278ee Ojo - id: 2c66b8ba-4745-4334-b145-ae00b1a6f604 Ajeromi/Ifelodun - id: b9ecb255-fc97-4e0a-8bc7-9dd0588f7a21 Eti Osa - id: 4d40b68b-a144-47cc-9345-4604adc7fb60 Surulere - id: 22ef6348-2be5-4543-8f1d-6e9faf24e126 Lagos Mainland - id: 4f1aabae-5a08-4179-bdfb-f0e9d71d2ca5 Lagos Island - id: 65a83885-eae9-4a94-8663-f6917cc9a04e Ibeju Lekki - id: 343b4b84-9150-4f3b-937f-c58ca20db27f Apapa - id: dc5547ff-cb10-4818-8e9b-363c14daa13b Epe - id: 053c8979-6fdf-4078-a9d1-3b545a06d72d Agege - id: 24e04d44-9e6e-47ba-a71c-737793381785 Ikeja - id: af88a8c6-f84f-4629-a25b-1e79a9ccbc3d Ìkòròdú - id: 634b4f3c-a5c4-42d2-b18b-1432b67373b8 Shomolu - id: 1b64c604-707e-4a65-a114-75f0f1abb3c5 Mushin - id: fe1ca044-b547-49be-9883-2c8ffa3f04d5 Oshodi/Isolo - id: e199f023-49ae-46ff-92a4-580c4fe1346d Kosofe - id: 9713c2c3-7416-41b8-9912-6e1450d3347d Alimosho - id: b4bd3fef-918f-4a60-bfd3-3569ed991562 Ifako/Ijaye - id: a9df031a-eaa0-4b74-b42c-5bf303feb0e1
4:43
I found the 20 LGAs of Lagos State. Now I need to: 1. Read the building data for the Lagos bounding box 2. Filter buildings with area > 1000 m² 3. Assign each building to an LGA using spatial join 4. Compute the per-LGA summary Let me get the bounding box for Lagos from the division_area geometry, then get buildings. Actually, let me think about the approach more carefully: 1. Get the Lagos region geometry (from division_area) to define the bounding box 2. Read buildings within that bbox 3. Also read the division_area for the 20 LGAs (counties) in Lagos 4. Spatial join to assign LGA names to buildings 5. Filter by area > 1000 m² 6. Compute summary stats Let me first get the bbox of Lagos.
4:43
Bash
cd /work && python -c "
cd /work && python -c "
import overturemaps
import pyarrow as pa
from shapely import wkb
import geopandas as gpd
# Read division_area for the region geometry
reader = overturemaps.record_batch_reader('division_area', bbox=(2.0, 4.0, 15.0, 14.0))
if reader:
table = reader.read_all()
df = table.to_pandas()
nigeria = df[df['country'] == 'NG']
# Find Lagos region area
lagos_area = None
for _, row in nigeria.iterrows():
name = row['names']
if name and name.get('primary') == 'Lagos' and row['subtype'] == 'region':
lagos_area = row
break
if lagos_area is not None:
geom = wkb.loads(lagos_area['geometry'])
bounds = geom.bounds
print('Lagos bbox:', bounds)
# Make a slightly padded bbox for building queries
margin = 0.05
padded = (bounds[0]-margin, bounds[1]-margin, bounds[2]+margin, bounds[3]+margin)
print('Padded bbox:', padded)
" 2>&1
Lagos bbox: (2.7059846, 6.366684, 4.3509056, 6.6967964) Padded bbox: (2.6559846, 6.316684, 4.4009056, 6.7467964)
5:33
Now I have all the pieces. Let me write the complete solution: 1. Get the Lagos region boundary from `division_area` 2. Get the Lagos LGA boundaries from `division` (parent_division_id = Lagos region's id) 3. Use the division_area geometries for each LGA to clip buildings 4. Query Overture buildings for the Lagos bbox 5. Compute footprint area from geometry 6. Filter buildings > 1000 m² 7. Spatial join with LGA boundaries 8. Compute per-LGA summary stats 9. Write both output files Let me write this as a comprehensive script.
5:33
5:33
Write
/work/solve.py
content · 255 lines · py
#!/usr/bin/env python3
"""
Extract Lagos State buildings > 1000 m² from Overture Maps,
spatially join to LGAs, and produce per-LGA summary.
"""
import sys
import time
import numpy as np
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
import geopandas as gpd
from shapely import wkb
from overturemaps import record_batch_reader
# ── 1. Lagos region bbox & geometry ──────────────────────────────────────
print("Fetching Lagos region boundary ...")
for attempt in range(3):
try:
reader = record_batch_reader("division_area", bbox=(2.0, 4.0, 15.0, 14.0))
break
except Exception as exc:
print(f" attempt {attempt+1} failed: {exc}")
time.sleep(2 ** attempt)
else:
sys.exit("Could not read division_area after 3 attempts")
df_areas = reader.read_all().to_pandas()
ng_areas = df_areas[df_areas["country"] == "NG"]
lagos_region_row = None
for _, row in ng_areas.iterrows():
name = row.get("names", {})
if name and name.get("primary") == "Lagos" and row["subtype"] == "region":
lagos_region_row = row
break
if lagos_region_row is None:
sys.exit("Could not find Lagos region in division_area")
lagos_geom = wkb.loads(lagos_region_row["geometry"])
bounds = lagos_geom.bounds
margin = 0.05
padded_bbox = (
bounds[0] - margin,
bounds[1] - margin,
bounds[2] + margin,
bounds[3] + margin,
)
print(f" Lagos bbox: {bounds}")
print(f" Padded bbox: {padded_bbox}")
# ── 2. Lagos LGA boundaries from division table ─────────────────────────
print("Fetching Lagos LGA boundaries ...")
for attempt in range(3):
try:
reader2 = record_batch_reader("division", bbox=(2.0, 4.0, 15.0, 14.0))
break
except Exception as exc:
print(f" attempt {attempt+1} failed: {exc}")
time.sleep(2 ** attempt)
else:
sys.exit("Could not read division after 3 attempts")
df_divisions = reader2.read_all().to_pandas()
ng_divisions = df_divisions[df_divisions["country"] == "NG"]
# Find Lagos region in division table (different ID than division_area!)
lagos_div_row = None
for _, row in ng_divisions.iterrows():
name = row.get("names", {})
if name and name.get("primary") == "Lagos" and row["subtype"] == "region":
lagos_div_row = row
break
if lagos_div_row is None:
sys.exit("Could not find Lagos region in division table")
lagos_region_id = lagos_div_row["id"]
print(f" Lagos region ID (division): {lagos_region_id}")
lagos_lgas = ng_divisions[
(ng_divisions["subtype"] == "county")
& (ng_divisions["parent_division_id"] == lagos_region_id)
].copy()
print(f" Found {len(lagos_lgas)} LGAs:")
for _, row in lagos_lgas.iterrows():
nm = row["names"]
print(f" {nm.get('primary', '?') if isinstance(nm, dict) else nm}")
# ── 3. Create a GeoDataFrame of LGA boundaries ──────────────────────────
# We need geometry from division_area, joined by id (division_id field)
print("Building LGA geometry from division_area ...")
lga_geoms = {}
for _, row in ng_areas.iterrows():
if row["subtype"] == "county":
lga_geoms[row["division_id"]] = wkb.loads(row["geometry"])
lga_data = []
for _, row in lagos_lgas.iterrows():
nm = row["names"]
lga_name = nm.get("primary", "Unknown") if isinstance(nm, dict) else str(nm)
geom = lga_geoms.get(row["id"])
if geom is None:
print(f" WARNING: no geometry for LGA {lga_name}, skipping")
continue
lga_data.append({"lga": lga_name, "geometry": geom})
lga_gdf = gpd.GeoDataFrame(lga_data, crs="EPSG:4326")
print(f" {len(lga_gdf)} LGAs with geometry loaded")
# ── 4. Fetch Overture buildings in padded bbox ──────────────────────────
print("Fetching Overture buildings ...")
for attempt in range(3):
try:
bldg_reader = record_batch_reader("building", bbox=padded_bbox)
break
except Exception as exc:
print(f" attempt {attempt+1} failed: {exc}")
time.sleep(2 ** attempt)
else:
sys.exit("Could not read buildings after 3 attempts")
print(" Reading building batches ...")
batches = []
while True:
try:
batch = bldg_reader.read_next_batch()
except StopIteration:
break
batches.append(batch)
bldg_table = pa.Table.from_batches(batches)
bldg_df = bldg_table.to_pandas()
print(f" Total buildings fetched: {len(bldg_df)}")
# ── 5. Filter to geometry within Lagos boundary & compute area ──────────
print("Processing building geometries ...")
bldg_geoms = []
valid_mask = []
for idx, row in bldg_df.iterrows():
try:
geom = wkb.loads(row["geometry"])
bldg_geoms.append(geom)
valid_mask.append(True)
except Exception:
valid_mask.append(False)
bldg_df = bldg_df[valid_mask].copy()
print(f" Valid geometries: {len(bldg_df)}")
bldg_gdf = gpd.GeoDataFrame(
bldg_df[["id", "height"]],
geometry=bldg_geoms,
crs="EPSG:4326",
)
# Clip to Lagos region boundary
bldg_lagos = bldg_gdf[bldg_gdf.within(lagos_geom)].copy()
print(f" Buildings within Lagos boundary: {len(bldg_lagos)}")
# Compute area in sq meters (project to UTM 32N — Lagos is ~3-4°E)
bldg_lagos_utm = bldg_lagos.to_crs("EPSG:32632") # UTM zone 32N
bldg_lagos_utm["footprint_area_m2"] = bldg_lagos_utm.geometry.area
# Filter > 1000 m²
bldg_big = bldg_lagos_utm[bldg_lagos_utm["footprint_area_m2"] > 1000].copy()
print(f" Buildings > 1000 m²: {len(bldg_big)}")
# Convert back to 4326
bldg_big = bldg_big.to_crs("EPSG:4326")
# ── 6. Spatial join to LGAs ─────────────────────────────────────────────
print("Spatially joining to LGAs ...")
bldg_with_lga = gpd.sjoin(
bldg_big,
lga_gdf[["lga", "geometry"]],
how="left",
predicate="intersects",
)
# Some buildings may intersect multiple LGAs — keep first match
bldg_with_lga = bldg_with_lga.drop_duplicates(subset="id")
# Filter to only buildings that actually matched an LGA
bldg_with_lga = bldg_with_lga[bldg_with_lga["lga"].notna()].copy()
print(f" Buildings with LGA assignment: {len(bldg_with_lga)}")
# ── 7. Build output GeoParquet ──────────────────────────────────────────
out_cols = ["id", "height", "footprint_area_m2", "lga", "geometry"]
out_gdf = bldg_with_lga[out_cols].copy()
out_gdf["footprint_area_m2"] = out_gdf["footprint_area_m2"].round(2)
out_gdf.to_parquet(
"/work/lagos_buildings.geoparquet",
index=False,
compression="zstd",
)
print(f" Written lagos_buildings.geoparquet: {len(out_gdf)} rows")
# ── 8. Per-LGA summary ──────────────────────────────────────────────────
print("Computing per-LGA summary ...")
summary_rows = []
for lga_name, grp in bldg_with_lga.groupby("lga"):
n_buildings = len(grp)
total_footprint = grp["footprint_area_m2"].sum()
n_with_height = grp["height"].notna().sum()
if n_with_height > 0:
p50_height = float(grp["height"].median())
else:
p50_height = None
summary_rows.append(
{
"lga": lga_name,
"n_buildings": n_buildings,
"total_footprint_m2": round(total_footprint, 2),
"n_with_height": n_with_height,
"p50_height_m": p50_height,
}
)
summary_df = pd.DataFrame(summary_rows)
# Ensure all 20 LGAs appear (even if no qualifying buildings)
all_lga_names = set(
nm.get("primary", "") if isinstance(nm, dict) else str(nm)
for nm in lagos_lgas["names"]
)
existing_lgas = set(summary_df["lga"])
for lga_name in all_lga_names:
if lga_name not in existing_lgas:
summary_rows.append(
{
"lga": lga_name,
"n_buildings": 0,
"total_footprint_m2": 0.0,
"n_with_height": 0,
"p50_height_m": None,
}
)
summary_df = pd.DataFrame(summary_rows)
summary_df = summary_df.sort_values("lga").reset_index(drop=True)
summary_df.to_parquet(
"/work/lagos_building_summary.parquet",
index=False,
compression="zstd",
)
print(f" Written lagos_building_summary.parquet: {len(summary_df)} rows")
print("Done.")
Wrote /work/solve.py (8011 chars).
5:38
Wait, there's an issue with the `within` filtering of buildings against the Lagos geometry. The `lagos_geom` is from `division_area` which is a polygon geometry. But `bldg_gdf` is in EPSG:4326 and `lagos_geom` is also in 4326, so that should work. But the building geometries from Overture are multipolygons (footprints), so `.within()` should work. However, there's a subtle issue: the division_area table gives us the region area polygon, while the division table gives us the hierarchical relationship. The `division_id` field in division_area refers to the division table's ID. Let me verify this mapping works correctly. Also, I need to be more careful about the bbox passed to the building reader. The Overture bbox format is (west, south, east, north) which matches what I have. Let me also consider: the buildings in the padded bbox will include some outside Lagos boundary. I'm filtering with `.within(lagos_geom)` which will keep only those fully inside. Some buildings on the boundary might be partially overlapping and could get missed - but for this use case it's acceptable. Let me run the script and see if it works.
5:38
Bash
cd /work && python solve.py 2>&1
Fetching Lagos region boundary ...
Lagos bbox: (2.7059846, 6.366684, 4.3509056, 6.6967964)
Padded bbox: (2.6559846, 6.316684, 4.4009056, 6.7467964)
Fetching Lagos LGA boundaries ...
Lagos region ID (division): 246ecf78-8622-4d7d-8d12-102e73a67cda
Found 20 LGAs:
Amuwo Odofin
Badagry
Ojo
Ajeromi/Ifelodun
Eti Osa
Surulere
Lagos Mainland
Lagos Island
Ibeju Lekki
Apapa
Epe
Agege
Ikeja
Ìkòròdú
Shomolu
Mushin
Oshodi/Isolo
Kosofe
Alimosho
Ifako/Ijaye
Building LGA geometry from division_area ...
20 LGAs with geometry loaded
Fetching Overture buildings ...
Reading building batches ...
Total buildings fetched: 3687595
Processing building geometries ...
Valid geometries: 3687595
Buildings within Lagos boundary: 2241855
Buildings > 1000 m²: 7375
Spatially joining to LGAs ...
Buildings with LGA assignment: 7375
Written lagos_buildings.geoparquet: 7375 rows
Computing per-LGA summary ...
Written lagos_building_summary.parquet: 20 rows
Done.
12:26
Let me verify the output files to make sure they're correct.
12:26
Bash
cd /work && python -c "
cd /work && python -c "
import geopandas as gpd
import pandas as pd
# Check buildings geoparquet
gdf = gpd.read_parquet('lagos_buildings.geoparquet')
print('=== lagos_buildings.geoparquet ===')
print(f'Rows: {len(gdf)}')
print(f'Columns: {list(gdf.columns)}')
print(f'CRS: {gdf.crs}')
print(gdf.head())
print()
# Check summary parquet
df = pd.read_parquet('lagos_building_summary.parquet')
print('=== lagos_building_summary.parquet ===')
print(f'Rows: {len(df)}')
print(f'Columns: {list(df.columns)}')
print(df.to_string())
"
=== lagos_buildings.geoparquet ===
Rows: 7375
Columns: ['id', 'height', 'footprint_area_m2', 'lga', 'geometry']
CRS: {"$schema": "https://proj.org/schemas/v0.7/projjson.schema.json", "type": "GeographicCRS", "name": "WGS 84", "datum_ensemble": {"name": "World Geodetic System 1984 ensemble", "members": [{"name": "World Geodetic System 1984 (Transit)"}, {"name": "World Geodetic System 1984 (G730)"}, {"name": "World Geodetic System 1984 (G873)"}, {"name": "World Geodetic System 1984 (G1150)"}, {"name": "World Geodetic System 1984 (G1674)"}, {"name": "World Geodetic System 1984 (G1762)"}, {"name": "World Geodetic System 1984 (G2139)"}, {"name": "World Geodetic System 1984 (G2296)"}], "ellipsoid": {"name": "WGS 84", "semi_major_axis": 6378137, "inverse_flattening": 298.257223563}, "accuracy": "2.0", "id": {"authority": "EPSG", "code": 6326}}, "coordinate_system": {"subtype": "ellipsoidal", "axis": [{"name": "Geodetic latitude", "abbreviation": "Lat", "direction": "north", "unit": "degree"}, {"name": "Geodetic longitude", "abbreviation": "Lon", "direction": "east", "unit": "degree"}]}, "scope": "Horizontal component of 3D system.", "area": "World.", "bbox": {"south_latitude": -90, "west_longitude": -180, "north_latitude": 90, "east_longitude": 180}, "id": {"authority": "EPSG", "code": 4326}}
id ... geometry
0 e24f1bf5-e957-4867-859b-a6a578fd9b06 ... POLYGON ((2.70836 6.37804, 2.70816 6.37804, 2....
1 5df57f70-f431-495f-98cb-eff96e1ca151 ... POLYGON ((2.72675 6.37881, 2.72672 6.37907, 2....
2 cfb03953-1851-4ec2-a087-3283cd510753 ... POLYGON ((2.7137 6.37984, 2.7136 6.37984, 2.71...
3 51f6d045-e5ae-4537-8eab-73c366a133bc ... POLYGON ((2.70833 6.38159, 2.70834 6.38147, 2....
4 a98aa38c-6427-44a7-b421-4afe08a12736 ... POLYGON ((2.70892 6.38282, 2.70897 6.38283, 2....
[5 rows x 5 columns]
=== lagos_building_summary.parquet ===
Rows: 20
Columns: ['lga', 'n_buildings', 'total_footprint_m2', 'n_with_height', 'p50_height_m']
lga n_buildings total_footprint_m2 n_with_height p50_height_m
0 Agege 48 95798.10 0 NaN
1 Ajeromi/Ifelodun 63 109524.81 0 NaN
2 Alimosho 359 658967.58 0 NaN
3 Amuwo Odofin 846 1778624.33 0 NaN
4 Apapa 288 616237.75 0 NaN
5 Badagry 126 212527.44 0 NaN
6 Epe 345 820969.67 0 NaN
7 Eti Osa 1088 1895665.61 241 20.0
8 Ibeju Lekki 606 1817607.97 0 NaN
9 Ifako/Ijaye 117 245337.51 0 NaN
10 Ikeja 928 3203954.37 47 15.0
11 Kosofe 167 349037.56 0 NaN
12 Lagos Island 121 232289.12 9 20.0
13 Lagos Mainland 123 231525.70 0 NaN
14 Mushin 468 1075539.67 1 35.0
15 Ojo 237 417723.04 0 NaN
16 Oshodi/Isolo 428 810638.07 3 27.0
17 Shomolu 127 205595.37 3 17.0
18 Surulere 346 791894.28 0 NaN
19 Ìkòròdú 544 1884575.21 4 7.5
12:36
Let me verify the outputs match the requirements: 1. `lagos_buildings.geoparquet` - EPSG:4326 GeoParquet with columns `id`, `height`, `footprint_area_m2`, `lga`, and `geometry` ✅ 2. `lagos_building_summary.parquet` - plain Parquet with no geometry, columns `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, and `p50_height_m` ✅ Let me also verify: - `p50_height_m` should be null where no height data available - shows NaN which is fine in pandas/parquet - All 20 LGAs appear ✅ - Building count vs n_with_height makes sense Wait, I need to double-check the `p50_height_m` column. The spec says "null otherwise" (when no heights). The NaN values will be stored as null in parquet, which is correct. Let me verify CRS is EPSG:4326 and the file structure:
12:36
Bash
cd /work && python -c "
cd /work && python -c "
import pyarrow.parquet as pq
# Verify GeoParquet metadata
meta = pq.read_schema('lagos_buildings.geoparquet')
print('Buildings schema:')
print(meta)
print()
# Verify summary parquet
meta2 = pq.read_schema('lagos_building_summary.parquet')
print('Summary schema:')
print(meta2)
print()
# Check geometry column type
print('Buildings field names:', [f.name for f in meta])
print('Summary field names:', [f.name for f in meta2])
"
Buildings schema:
id: large_string
height: double
footprint_area_m2: double
lga: large_string
geometry: binary
-- field metadata --
ARROW:extension:name: 'geoarrow.wkb'
ARROW:extension:metadata: '{"crs": {"$schema": "https://proj.org/schema' + 1498
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [], "columns": [{"name":' + 657
geo: '{"primary_column": "geometry", "columns": {"geometry": {"encoding":' + 1409
Summary schema:
lga: large_string
n_buildings: int64
total_footprint_m2: double
n_with_height: int64
p50_height_m: double
-- schema metadata --
pandas: '{"index_columns": [], "column_indexes": [], "columns": [{"name":' + 699
Buildings field names: ['id', 'height', 'footprint_area_m2', 'lga', 'geometry']
Summary field names: ['lga', 'n_buildings', 'total_footprint_m2', 'n_with_height', 'p50_height_m']
12:37
Bash
ls -lh /work/lagos_buildings.geoparquet /work/lagos_building_summary.parquet
-rw-r--r-- 1 agent agent 4.1K Jun 18 14:15 /work/lagos_building_summary.parquet -rw-r--r-- 1 agent agent 1.2M Jun 18 14:15 /work/lagos_buildings.geoparquet
12:46
Let me check: the user wanted columns in `lagos_building_summary.parquet` to be `lga`, `n_buildings`, `total_footprint_m2`, `n_with_height`, `p50_height_m`. The summary has `p50_height_m` showing NaN for some LGAs. But the spec says "null otherwise" for median height. NaN is a float null-equivalent in Parquet, so that's fine — it will be stored as null. Wait, I notice the `p50_height_m` column in the schema shows `double` type, and for LGAs where there are no buildings with height, it's `NaN` (which in Parquet becomes null). That's correct. But I should check: the `p50_height_m` value for LGAs with no heights at all — I'm setting `None` in Python which becomes NaN in pandas float, which is the correct representation for null in a double column. Let me also verify the `lga` column values. I noticed the user says "per-LGA roll-up: for each Lagos Local Government Area". So the LGAs should be the standard Lagos LGAs. Let me check them all: 1. Agege ✓ 2. Ajeromi/Ifelodun ✓ 3. Alimosho ✓ 4. Amuwo Odofin ✓ 5. Apapa ✓ 6. Badagry ✓ 7. Epe ✓ 8. Eti Osa ✓ 9. Ibeju Lekki ✓ 10. Ifako/Ijaye ✓ 11. Ikeja ✓ 12. Kosofe ✓ 13. Lagos Island ✓ 14. Lagos Mainland ✓ 15. Mushin ✓ 16. Ojo ✓ 17. Oshodi/Isolo ✓ 18. Shomolu ✓ 19. Surulere ✓ 20. Ìkòròdú ✓ That's 20 LGAs, which matches the official 20 LGAs of Lagos State. Good. The output files look correct. Both have the right columns, CRS for geoparquet is EPSG:4326, summary has no geometry column. Both written with zstd compression. Let me verify the geometry column is properly stored as GeoParquet — the metadata shows `geoarrow.wkb` extension type, so it's proper GeoParquet. Good. Everything looks good. Let me provide the final answer.
12:46