Exposure in Python
The examples below assume an open connection, stored in the variable db, and the same building entity_id and tile quadkey used in Entities, Assets and Taxonomy.
from exposurelib import SpatiaLiteExposure
from taxonomylib import Taxonomy
from collections import defaultdict
db = SpatiaLiteExposure('data/CDMX_2501_sample.db')
db.connect(init_spatial_metadata=False)
quadkey = "023310033220023233"
entity_id = 17255
Population and structural value of a building¶
To get the total population and structural value of the building defined earlier, sum the relevant attributes across all its possible assets:
population, structural_value = db.get_sum_entity_asset_attributes(
entity_id=entity_id,
attribute_keys=["OPN", "CST"],
)
print(f"Expected population: {population:,.2f}")
print(f"Expected structural value: {structural_value:,.2f}")
Expected population: 21.49 Expected structural value: 1,619,960.64
get_sum_entity_asset_attributes() already accounts for number and relative_size, so no manual join is needed. Leave attribute_keys empty to sum every exposure attribute at once. Currency for CST depends on the dataset's CUR field — this sample does not set one, so no currency symbol is shown above.
Daytime versus night-time population¶
The same function works for any combination of attribute keys:
population_day, population_night = db.get_sum_entity_asset_attributes(
entity_id=entity_id,
attribute_keys=["OPD", "OPN"],
)
print(f"Daytime population: {population_day:,.2f}")
print(f"Night-time population: {population_night:,.2f}")
Daytime population: 236.56 Night-time population: 21.49
The population attributes are OPA, OPD, OPN, OPT and OAV; the cost attributes are TRC, CST, CNO and CCE; the dwelling attribute is DPB. See the Database Schema page for the full description of each.
Building taxonomies in a tile¶
In a tile-resolution database, one entity carries the whole tile's expected building counts. In this by-building export there is no such single entity — instead, we collect every building sharing the tile's Quadkey and pass all of their IDs to get_entity_asset_pairs() at once, which sums correctly across buildings because each building's own number values are probabilities that add to 1:
db.cursor.execute(
"SELECT id FROM entities WHERE quadkey = ? AND category = 1", (quadkey,)
)
building_ids = [row[0] for row in db.cursor.fetchall()]
entity_asset_pairs = db.get_entity_asset_pairs(building_ids)
buildings_per_material = defaultdict(float)
total_buildings = 0.0
for _, asset_id, number, relative_size in entity_asset_pairs:
_, taxonomy_id, *_ = db.get_asset(asset_id)
_, taxonomy_dict, _ = db.get_taxonomy(taxonomy_id)
material = Taxonomy(db.json_to_dict(taxonomy_dict)).get_attribute("MAT") or "unknown"
buildings_per_material[material] += number
total_buildings += number
print(f"In total, {total_buildings:.1f} buildings are expected in the tile.")
for material, count in sorted(buildings_per_material.items(), key=lambda x: -x[1]):
print(f"{material}: {count:.2f} buildings ({count / total_buildings:.1%})")
In total, 44.0 buildings are expected in the tile. CR: 19.73 buildings (44.8%) MUR: 11.09 buildings (25.2%) MR: 8.89 buildings (20.2%) unknown: 1.81 buildings (4.1%) W: 1.10 buildings (2.5%) E: 0.99 buildings (2.3%) S: 0.39 buildings (0.9%)
The same pattern works for any other taxonomy attribute, for example ROF (roof shape), WAL (exterior wall material) or LLR (lateral load-resisting system), by replacing "MAT" above and looking up the relevant codes in the Database Schema.
Energy performance with the TABULA taxonomy¶
Besides the OXM Taxonomy, some assets also carry a TABULA taxonomy (assets.tabula_taxonomy_id), describing residential energy performance under different refurbishment scenarios. This sample is Mexican, and TABULA only covers European building stock, so no asset here has TABULA data — the code below is real and correct, but every asset is skipped and the totals come out at zero. Try it against a European country's database to see it populated.
entity_asset_pairs = db.get_entity_asset_pairs(building_ids)
total_energy_current = 0.0
total_energy_advanced = 0.0
for _, asset_id, number, relative_size in entity_asset_pairs:
_, taxonomy_id, tabula_taxonomy_id, average_net_floor_area, *_ = db.get_asset(asset_id)
if tabula_taxonomy_id is None or average_net_floor_area is None:
continue # no TABULA (energy) data for this asset type
_, _, tabula_attributes = db.get_taxonomy(tabula_taxonomy_id)
tabula_attributes = db.json_to_dict(tabula_attributes)
energy_current = tabula_attributes.get("energy_heating_current_state")
energy_advanced = tabula_attributes.get("energy_heating_advanced_refurbishment")
if energy_current is None or energy_advanced is None:
continue
building_count = number * relative_size
total_energy_current += building_count * energy_current * average_net_floor_area
total_energy_advanced += building_count * energy_advanced * average_net_floor_area
print(f"Current heating demand: {total_energy_current:,.0f} kWh/year")
print(f"After advanced refurbishment: {total_energy_advanced:,.0f} kWh/year")
Current heating demand: 0 kWh/year After advanced refurbishment: 0 kWh/year
Exploring a whole country¶
For a country total, summing per building in Python (as above) does not scale well — with ~26,000 buildings, query entity_assets and assets directly in one aggregate SQL statement instead, exactly as the manual suggests for bulk processing:
db.cursor.execute(
"""
SELECT
SUM(ea.number * ea.relative_size * CAST(json_extract(a.attributes, '$.OPN') AS REAL)) AS total_population,
SUM(ea.number * ea.relative_size * CAST(json_extract(a.attributes, '$.CST') AS REAL)) AS total_structural_value
FROM entities e
INNER JOIN entity_assets ea ON ea.entity_id = e.id
INNER JOIN assets a ON a.id = ea.asset_id
WHERE e.category = 1 AND e.iso_3166 = 'MEX'
"""
)
total_population, total_structural_value = db.cursor.fetchone()
print(f"Estimated population: {total_population:,.0f}")
print(f"Estimated total structural value: {total_structural_value:,.2f}")
Estimated population: 64,557 Estimated total structural value: 4,793,848,013.24
Visualising results¶
Lastly, we can visualise the model with geopandas, for example population per tile:
import geopandas
from shapely import from_wkt
db.cursor.execute(
"""
SELECT
t.id, t.quadkey, ST_AsText(t.geometry),
SUM(ea.number * ea.relative_size * CAST(json_extract(a.attributes, '$.OPN') AS REAL)) AS population
FROM entities t
INNER JOIN entities b ON b.quadkey = t.quadkey AND b.category = 1
INNER JOIN entity_assets ea ON ea.entity_id = b.id
INNER JOIN assets a ON a.id = ea.asset_id
WHERE t.category = 0
GROUP BY t.id, t.quadkey, t.geometry
"""
)
rows = [
{"quadkey": quadkey, "geometry": from_wkt(geom_wkt), "population": population}
for _, quadkey, geom_wkt, population in db.cursor.fetchall()
]
gdf = geopandas.GeoDataFrame(rows, geometry='geometry', crs='EPSG:4326')
gdf.explore('population')
Calling get_sum_entity_asset_attributes() once per entity is easiest to follow for a handful of buildings, but the direct SQL joins used above are the way to go for anything country-sized.
Close the connection¶
db.close()