DraftCalgary · 2026 tax year · numbers not final← Back to the map

Methods

How the numbers are made: the formulas, the data behind them and what we assumed, in plain language with enough detail to reproduce them.

Overview

We split Calgary into equal hexagons and, for each one, compare what its properties pay in municipal property tax with what it costs to look after its roads. The roads are measured against mobility's share of that tax, 6.62 cents of every dollar, because that is the part of the tax bill meant for streets, sidewalks, lights and signals.

The method has four steps:

  1. Grid. Cover the city in 800 m hexagons and tag each with its community, era and ward.
  2. Revenue. Spread every property's assessed value into the hexagons it sits in, then apply the 2026 municipal tax rates.
  3. Roads. Measure the length of each class of road in each hexagon and turn it into square metres of pavement.
  4. Cost. Price that pavement under each pavement plan and compare it with the hexagon's mobility share.

All figures are for the 2026 tax year unless noted. The 2025 roll was used to check the method against the City's published totals.

1. The hex grid

Each hexagon is 800 m across, about a ten-minute walk, and covers 55.4 hectares (0.55 km²). Equal-sized cells let us compare places fairly; community boundaries vary too much in size to do that.

MeasureValue
Width, flat side to flat side800 m
Width, corner to corner924 m
Side length462 m
Area of a full hexagon55.4 ha
Hexagons in the grid1,683 (1,411 full, 272 cut by the city limit)
Total area covered853 km²
Ordinary neighbourhood hexagons used in the counts1,236

The grid was built in QGIS and clipped to the city boundary, so hexagons along the edge are smaller. It is stored in NAD83 / UTM zone 12N (EPSG:26912), so all lengths and areas are in true metres.

Tags. Each hexagon takes the community, community structure (era, such as "1960s/1970s" or "Inner city") and ward that its centre point falls in. Three edge hexagons whose centres fall outside every community take the nearest one instead.

Special areas. Hexagons tagged Parks, Undeveloped or Other (special districts such as the airport) stay in every citywide total but are greyed out on the map and left out of the neighbourhood counts. There are 435 of them. They are not expected to pay for their own roads.

2. Revenue: what each hexagon pays

Each property's assessed value is shared among the hexagons it overlaps, in proportion to how much of its land falls in each, and then taxed at the City's 2026 municipal rates. In 2026 the model raises $2,548M in municipal property tax from $421B of taxable assessed value.

Step 1: one value per parcel. The 2026 assessment roll lists each account (roll number) with its land parcel. Where one roll covers several parcels, the roll repeats its full value on each, so we split it by land area:

V_{\text{parcel}} = V_{\text{roll}} \times \frac{\text{area of the parcel}}{\text{total area of all the roll's parcels}}

Without this step, about $25B of value was counted twice.

Step 2: from parcels to hexagons. A parcel that straddles a hexagon edge is split by area:

\text{share}_{p,h} = \frac{\text{area}(p \cap h)}{\sum_{h'} \text{area}(p \cap h')} \qquad V_{h} = \sum_{p} \text{share}_{p,h} \times V_{p}

The denominator is the parcel's area inside the whole grid, so every dollar of a parcel's value lands in some hexagon. This is done separately for residential, non-residential and farm value.

Step 3: tax. Revenue is the value of each class times its 2026 municipal rate:

R_h = V_{h,\text{res}} \times 0.0038906 + V_{h,\text{nonres}} \times 0.0179986 + V_{h,\text{farm}} \times 0.0339557

The provincial education tax (0.0027593 residential and farm, 0.0041502 non-residential) is calculated too, but only to check totals. It is never counted as money available for roads.

Exempt property. Property in two groups of the roll's sub-type codes pays no tax in the model: institutional (codes starting with S: hospitals, schools, universities) and recreation (codes starting with P: parks, recreation centres, the Stampede grounds). That is $27.8B of value in 2026. Its land still counts toward the hexagon it sits in.

Calibration. The same method applied to the 2025 roll and 2025 rates (where the exempt codes are IS and RC) gives $3,473M of total property tax, municipal plus provincial. The City reported $3,471M for 2025, a difference of under 0.1%.

3. Roads: how much pavement each hexagon has

We measure the length of City-maintained, paved road in each hexagon and multiply it by a typical width to get square metres of pavement. Citywide that comes to 6,288 km of road and 62.1 km² of pavement.

Which roads count. Roads come from OpenStreetMap (2025 extract). We keep five classes of OSM highway tag and drop the rest:

ClassOSM tags keptLengthWidth usedPavement
Highwaymotorway, trunk and their links335 km11.0 m3.7 km²
Primary (arterial)primary, primary_link226 km11.0 m2.5 km²
Secondary (arterial)secondary, secondary_link909 km11.0 m10.0 km²
Tertiary (collector)tertiary, tertiary_link1,162 km11.0 m12.8 km²
Residential (local)residential, living_street3,656 km9.1 m on average33.1 km²

Left out: lanes and alleys, service roads, unclassified roads, private roads, paths and tracks. Also left out:

Width. Divided roads are drawn in OSM as two separate lines, one per direction, so the bigger classes use the width of one carriageway, 11 m. That figure is a placeholder, not a measured value. Residential widths come from 110 sampled streets, 10 in each community structure. Within each structure, 10 random points were placed and snapped to the nearest residential road, and the road width at each point was measured with the Google Maps "Measure distance" tool. Each hexagon uses the median of its community structure's samples, from about 8.8 m in post-2000 suburbs to 10.3 m in Centre City. Where a structure has no samples, the 9.0 m default applies.

\text{pavement}_h = \sum_{\text{class}} \text{length}_{h,\text{class}} \times \text{width}_{\text{class}}

Shared roads. Highways and arterials (primary and secondary) are used by everyone, not just the people who live beside them. So their 16.2 km² of pavement is pooled and split equally across all 1,683 hexagons, about 9,600 m² each, instead of being charged to whichever hexagon they run through. Collectors and local streets stay with the hexagon they are in.

\text{pavement}_h = \text{local pavement}_h + \frac{\text{shared pavement, citywide}}{1{,}683}

4. Cost: pricing the pavement

Each pavement plan turns square metres of pavement into dollars a year, in one of three ways. Where a plan gives a low and a high figure, the headline uses the midpoint.

City budget plans are a single citywide amount a year. Each hexagon gets the share that matches its share of the city's pavement:

C_h = B \times \frac{\text{pavement}_h}{\text{pavement, citywide}}

That works out to the same dollars per square metre everywhere, whatever the road class.

Maintenance-cycle plans give a cost per square metre per year, applied to all pavement:

C_h = \text{rate} \times \text{pavement}_h

Full replacement is a one-off rebuild cost per square metre, spread over each class's life. Local streets last 40 years; every other class lasts 21.

C_h = \text{unit cost} \times \sum_{\text{class}} \frac{\text{pavement}_{h,\text{class}}}{\text{life}_{\text{class}}}
PlanTypeInputCitywide, a yearSource
Historical spendingBudget$40M a year$40MPavement Quality report, p. 7
2026 budgetBudget$64M a year$64MBudget 2026, pp. 76–78
Break evenBudget$88M a year$88MPavement Quality presentation, p. 7
50% of roads in good conditionBudget$116M a year$116MPavement Quality presentation, pp. 7–8
60% of roads in good conditionBudget$132M a year$132MPavement Quality presentation, p. 7
Seal cracks every 10 yearsCycle$1.00–2.50 per m² a year$109MPavement Quality report, p. 4
Repave every 12–17 yearsCycle$2.92–4.17 per m² a year$220MPavement Quality report, p. 4
Full replacementRebuild$300–350 per m², over 21 or 40 years$717MPavement Quality Level of Service report, 2024

The 2026 budget is the current scenario: it is what the City spends now, and the yardstick for the tax increase in section 5.

Per-square-metre costs are in 2024 dollars. The pipeline can inflate them to 2026, but the inflation rate is set to zero, so no adjustment is applied.

5. Comparing with the mobility budget

Each hexagon's road cost is measured against the part of its own tax bill that goes to mobility: 6.62 cents of every municipal tax dollar, or $169M citywide.

Where 6.62% comes from. The City's property tax calculator breaks the full bill into services. Mobility is 3.87% of the whole bill, which includes the Province's 41.49%. As a share of the City's portion alone, that is 3.87 ÷ 58.51 = 6.62%. The City's full Mobility budget is larger ($377M in 2026) because it also draws on fees, grants and other revenue. Those are left out here, because property tax is the lever the City and each neighbourhood control.

The hexagon's mobility budget and the overrun:

M_h = 0.0662 \times R_h \qquad \text{ratio}_h = \frac{C_h}{M_h} \qquad \text{overrun}_h = \text{ratio}_h - 1 \quad (\text{when ratio}_h > 1)

A ratio of 0.4 means roads use 40% of the area's mobility budget, leaving 60% for sidewalks, bike lanes, streetlights, signals and snow clearing. A ratio of 3 is shown as a 200% overrun. The map colours hexagons at ratios of 0.5, 1, 2, 4 and 8.

Cents per tax dollar is the road cost over all municipal revenue, Ch ÷ Rh, set against the 6.62¢ line. For a group of hexagons (a ward, or a type of neighbourhood) it is total cost over total revenue, not an average of the hexagons' ratios.

Tax increase needed. This is how much the municipal tax rate would have to rise to pay for a plan, compared with the 2026 budget, if nothing else changed. It is never shown as negative.

\text{increase} = \max\left(0,\ \frac{C_{\text{plan}} - C_{\text{2026 budget}}}{R_{\text{city}}}\right)

Per median household. The median 2026 assessment of a single residential property (code R110) is $718,000, which pays $2,793 in municipal tax. The extra cost is that bill times the increase:

\text{extra a year} = \text{increase} \times 718{,}000 \times 0.0038906

Overrun counts include only ordinary neighbourhood hexagons that pay some tax: 1,236 in 2026. Parks, undeveloped land, special districts and hexagons with no taxable property are left out.

6. Assumptions and limitations

The model counts only road pavement against only municipal property tax, so it understates what a neighbourhood really costs. Every other piece of infrastructure, and the rest of the mobility budget, comes on top.

What is counted

Road assumptions

Tax assumptions

Grid assumptions

7. Reproducing it

The whole method is a set of SQL scripts run in order by one Python script, with every setting in one file. Code: github.com/wklumpen/citymath.

Inputs

InputSourceNotes
Hex gridProject file citymath.gpkg, layer hex_800m_clipped800 m hexagons built in QGIS, clipped to the city limit
CommunitiesCity of Calgary community boundaries, 2025Name, community structure (era)
WardsCity of Calgary ward boundaries, 2025 (data.calgary.ca, dataset tz8z-hyaz)Current 14 wards
RoadsOpenStreetMap, 2025 extracthighway, surface, ref tags
AssessmentsCity of Calgary property assessments (parcel), 2026 roll; 2025 historical roll for calibrationValue by class, sub-type code, roll number, parcel shape
Street widthsProject sample of 110 residential streets10 random points per community structure, measured in Google Maps
Tax ratesCity of Calgary 2026 (and 2025) municipal and provincial rates

The source data is not in the repository; only the derived per-hexagon files for the website are.

Steps

  1. Load the sources into a PostGIS database and list their table names in config.yaml.
  2. Put the database login in .env.
  3. Run uv run citymath.py. It loads the grid, tags communities and wards, measures roads, allocates assessments, then builds the summary and plan tables (sql/00_staging.sql to sql/05_hex_scenario.sql).
  4. The last step, export, writes the website's data files to docs/data/.

After changing a setting, re-run only the later steps, for example uv run citymath.py roads summary.

Outputs. One row per hexagon in hex_summary, with community, ward, road length by class, pavement, assessed value and revenue by class, road cost and net. hex_scenario adds one row per hexagon per plan. Both open directly in QGIS.

Website and slides. The website recomputes each plan in the browser from the exported files, with the same formulas as above. uv run slides.py draws the presentation charts from the same files.