Skip to content
CityParquet is v0.1.0-draft — the specification is still changing.
Esc
↑↓navigate↵open⌘Jpreview
On this page

Prepare building-energy inputs with Python

Read a neighbourhood from CityParquet, calculate surface-to-volume ratios, and save a building feature table with Python and DuckDB.

Which buildings have more surface area relative to their volume? This small Python example prepares a feature table for exploring that question in Delft. It adapts the attribute inputs and surface-to-volume metric from the energy use case.

The full use case computes geometry metrics, separates semantic surfaces, and runs an energy-demand screen. Here we use 3DBAG’s recorded attributes instead of recomputing geometry. You need only Python and DuckDB, with no 3D extension, Pandas, or downloaded national dataset.

Set up Python

Create a virtual environment and install DuckDB:

python -m venv .venv
source .venv/bin/activate
python -m pip install duckdb

On Windows, activate it with .venv\Scripts\activate instead.

Extract a neighbourhood and calculate a form metric

Save the following as energy_inputs.py. It selects buildings overlapping a 1 km × 1 km window in Delft from the hosted national 3DBAG file. The window uses the dataset’s Dutch national grid coordinates (EPSG:7415).

import csv
from pathlib import Path

import duckdb

SOURCE = "https://cityparquet.open3d.city/data/3dbag/building.parquet"
OUTPUT = Path("delft_energy_inputs.csv")
# xmin, ymin, xmax, ymax in the dataset's coordinate reference system.
EXTENT = (84000, 446000, 85000, 447000)

with duckdb.connect() as con:
    con.execute("INSTALL httpfs")
    con.execute("LOAD httpfs")
    result = con.execute(
        """
        WITH buildings AS (
          SELECT id AS building_id,
                 oorspronkelijkbouwjaar AS construction_year,
                 b3_volume_lod22 AS volume_m3,
                 b3_opp_dak_plat + b3_opp_dak_schuin AS roof_m2,
                 b3_opp_buitenmuur AS external_wall_m2,
                 b3_opp_scheidingsmuur AS party_wall_m2,
                 b3_opp_grond AS ground_m2
          FROM read_parquet(?)
          WHERE object_type = 'Building'
            AND bbox.xmin < ? AND bbox.xmax > ?
            AND bbox.ymin < ? AND bbox.ymax > ?
        )
        SELECT *,
               (roof_m2 + external_wall_m2 + party_wall_m2 + ground_m2)
                 / NULLIF(volume_m3, 0) AS surface_to_volume_per_m
        FROM buildings
        WHERE volume_m3 > 0
        ORDER BY surface_to_volume_per_m DESC NULLS LAST, building_id
        """,
        [SOURCE, EXTENT[2], EXTENT[0], EXTENT[3], EXTENT[1]],
    )
    columns = [column[0] for column in result.description]
    rows = result.fetchall()

with OUTPUT.open("w", newline="", encoding="utf-8") as stream:
    writer = csv.writer(stream)
    writer.writerow(columns)
    writer.writerows(rows)

print(f"Wrote {len(rows)} buildings to {OUTPUT}")
print("Highest surface-to-volume ratios (building ID, 1/m):")
for row in [row for row in rows if row[-1] is not None][:5]:
    print(f"  {row[0]}: {row[-1]:.3f}")

Run it from the directory where you saved it:

python energy_inputs.py

The script writes delft_energy_inputs.csv in that directory. Change EXTENT to select another area, or replace SOURCE with a local 3DBAG CityParquet file carrying the same attributes. SQL parameters keep paths and bounds separate from the query text.

Interpret the output

Each row represents one Building. In this 3DBAG package, the parent building holds the attributes used here; its BuildingPart children carry the detailed 3D geometry. Filtering to buildings avoids counting those parts again.

The table contains construction year, recorded LoD 2.2 volume, roof, wall and ground areas, and a surface-to-volume ratio in inverse metres. Larger ratios indicate more model surface per unit volume. The sum includes party walls, so it is a geometric form metric, not an exposed heat-loss envelope or an energy demand estimate. Recorded model volume is not necessarily heated volume.

Buildings with missing or non-positive volume are excluded. Missing area values remain unknown: their ratios are empty in the CSV, rather than treating missing surfaces as zero. The spatial selection includes whole buildings whose bounding boxes overlap the window; it does not clip them.

Why CityParquet helps

DuckDB reads the selected attributes and bounding-box fields without fetching the geometry columns. Parquet statistics allow it to skip row groups outside the window. Python receives only the selected building records, ready for a spreadsheet, plot, or further analysis.

For geometry-derived volume and semantic roof/wall/ground areas, continue with the full energy use case. For more SQL examples over the same hosted file, see Working with 3DBAG at cloud scale.

Last updated on September 23, 2026

Was this page helpful?