Building a Daytime Population Layer from Worker Flows

This page solves one exact task: producing a daytime population estimate per zone by adding inbound commuters to the residents who stay, subtracting the residents who leave, and using the result to score sites whose trade depends on people at work rather than people at home.

Residential population is the default demographic input everywhere in this stack, and for a weekly grocery shop it is the right one. For a lunchtime format, a coffee shop, a convenience store in a business district or a pharmacy near a hospital, it is close to the wrong measurement — the customers are in the zone from nine to five and counted somewhere else entirely.

Prerequisites

  • Residential population by zone from ACS or equivalent estimates.
  • A commuting flow table — origin zone to workplace zone, with worker counts. Published employment-flow statistics are the standard source.
  • Employment counts by workplace zone, used as a control total.
  • Python packages: pandas, geopandas. Install with pip install pandas geopandas.

Configuration and execution parameters

Parameter Value for this task Notes
zone_geography tract Flow tables rarely support finer reliably
at_home_share from the flow table’s non-commuting rows Remote and non-working residents
worker_hours 09:00–17:00 weekday State it; a different daypart is a different layer
student_flows separate layer if available Schools and campuses behave like workplaces
control_to workplace employment totals Flows and totals disagree; reconcile explicitly
flow_vintage recorded Commuting data lags; a five-year-old flow is common
remote_adjustment explicit factor Working patterns have changed; do not use a stale share silently

The remote_adjustment row deserves emphasis. Published commuting tables describe where people worked when the data was collected, and the share of workers who now work from home some or all of the week is both large and market-specific. Applying an unadjusted flow table overstates city-centre daytime population and understates suburban, in a direction that has grown rather than shrunk.

Where the people are at eleven in the morning A central business district holds 3,100 residents and 41,000 people during the working day. A hospital campus zone holds 1,900 residents and 12,400 daytime. A commuter suburb falls from 8,400 residents to 4,600, and a retirement area barely moves, from 6,200 to 5,900. Residential population describes the night, not the trading day central business district 3,100 residents 41,000 daytime hospital campus 1,900 12,400 commuter suburb 8,400 4,600 — half the market leaves retirement area 6,200 5,900 — barely moves Lighter bar: residents. Darker bar: estimated population present on a weekday at 11:00.

Annotated implementation

The calculation is an accounting identity applied per zone, plus a reconciliation against employment totals.

python
from __future__ import annotations

import pandas as pd

REMOTE_SHARE_DEFAULT = 0.22          # market-specific; never leave at a default silently


def daytime_population(residents: pd.DataFrame, flows: pd.DataFrame,
                       employment: pd.DataFrame,
                       remote_share: float = REMOTE_SHARE_DEFAULT) -> pd.DataFrame:
    """Residents who stay + workers who arrive, controlled to employment totals.

    residents: zone_id, population, workers_resident
    flows: home_zone, work_zone, workers
    employment: zone_id, jobs
    """
    # Workers arriving, discounted for those now working remotely.
    inbound = (flows.groupby("work_zone")["workers"].sum()
               .mul(1 - remote_share).rename("inbound"))
    outbound = (flows.loc[flows["home_zone"] != flows["work_zone"]]
                .groupby("home_zone")["workers"].sum()
                .mul(1 - remote_share).rename("outbound"))

    df = (residents.set_index("zone_id")
          .join(inbound, how="left").join(outbound, how="left").fillna(0.0))
    df["daytime"] = df["population"] - df["outbound"] + df["inbound"]

    # Flow tables and employment totals disagree; scale inbound to the control
    # so a zone cannot receive more workers than it has jobs for.
    df = df.join(employment.set_index("zone_id")["jobs"], how="left")
    over = df["inbound"] > df["jobs"] * 1.1
    df.loc[over, "inbound"] = df.loc[over, "jobs"]
    df.loc[over, "daytime"] = (df.loc[over, "population"]
                               - df.loc[over, "outbound"] + df.loc[over, "inbound"])

    df["daytime_ratio"] = df["daytime"] / df["population"].clip(lower=1)
    return df.reset_index()

The reconciliation against employment totals is not cosmetic. Flow tables are surveys with their own error, and a zone whose inbound workers exceed its jobs by fifty per cent is producing a daytime figure that will look impressive in a site score and cannot be true.

Failure modes and debugging

Using a stale remote-working share. The single largest source of error in a contemporary daytime layer. A flow table collected before working patterns changed, applied without adjustment, gives a central business district a daytime population it no longer has. Where a local estimate of remote working exists, use it; where it does not, use a national figure, state it prominently, and re-check annually.

Double-counting the workers who live where they work. A resident who works in the same zone should not be added as an inbound commuter and also retained as a resident. Excluding same-zone flows from the outbound sum, as above, keeps the identity consistent.

Applying the layer to the wrong format. A daytime population is the right denominator for a lunchtime or convenience format and the wrong one for a weekly grocery shop, whose customers really are at home. Using it universally replaces one systematic error with another.

Ignoring the weekend. A daytime layer describes weekdays. A retail park whose trade peaks on Saturday needs a different treatment entirely, and applying the weekday layer to it understates the residential catchment that actually shops there.

Matching the population layer to the format A coffee or lunch format should be scored almost entirely on daytime population; a convenience store on a weighted blend; a supermarket on residents with a small daytime component; and a weekend-led retail park almost entirely on residents. Using one layer for all four mis-scores at least two. The blend is a format decision, made once and recorded lunch / coffee 85% daytime urban convenience 55% daytime supermarket 80% resident weekend retail park 95% resident Lighter segment: residential weight. Darker: daytime weight. The blend belongs in the scoring configuration beside the criterion weights, where it can be reviewed rather than assumed.

Verification

  • Check the national totals balance. Summed across zones, daytime population should approximate residential population; a large discrepancy means flows are being counted in one direction only.
  • Compare the highest daytime-ratio zones against what is actually there. They should be business districts, hospitals, universities and industrial parks. Anything else is a flow-table artifact.
  • Validate against observed footfall where a panel is available: a zone’s daytime ratio should correlate with the weekday-to-weekend visit ratio at stores inside it.
  • Re-run with and without the remote adjustment and compare site rankings. If the ranking is insensitive, the adjustment can be treated as a refinement; if it is not, the adjustment deserves a local estimate rather than a national default.
How much the remote-working assumption moves a shortlist For a lunch-format shortlist, assuming no remote working puts three central sites in the top five; a twenty-two per cent assumption puts two; and a thirty-five per cent assumption puts one, with suburban sites taking their places. The assumption is doing more work than any criterion weight. One assumption, three different shortlists remote-working share central sites in the top five top-ranked site 0% (unadjusted table) 3 CBD tower lobby 22% (national estimate) 2 hospital campus 35% (high estimate) 1 suburban business park When a single assumption changes the top-ranked site, it belongs on the cover of the pack rather than in a configuration file — and it deserves a local measurement rather than a borrowed number. Hospital and industrial zones are the stable ground here: their workers cannot work from home.

Frequently Asked Questions

Where does the flow data come from and how current is it?

From published employment-flow statistics, which are collected on a multi-year cycle and released with a lag — so a current pipeline is typically working with a table several years old. That is tolerable for the structure of commuting, which changes slowly, and intolerable for the level of remote working, which changed abruptly. Treating the flow table as a description of where people commute and applying a separate, current adjustment for whether they commute is the practical split.

Should students be included?

Yes, as their own layer where the data allows. A university campus behaves like a large workplace for weekday trade and unlike one in the vacation, so folding students into worker flows produces a daytime figure that is right in term and badly wrong for a quarter of the year. Modelling them separately also lets a site near a campus be scored on the seasonality that actually governs it.

How does this interact with the drive-time catchment?

It replaces the population attached to zones, not the geometry. The catchment is still a drive-time polygon, and the apportionment still splits partially covered zones by area — what changes is which population figure each zone carries. That means a daytime layer drops into an existing pipeline as an alternative column rather than as a new stage.

Is a blended population ever the right answer?

Frequently, and the blend should be per format rather than global. A convenience store genuinely trades on both populations at different hours, so a weighted combination reflects its business better than either alone. What matters is that the weights are stated in the scoring configuration next to the criterion weights, and that the same blend is applied to every candidate of that format.

Can mobility data replace the flow table entirely?

Increasingly, yes, and with better currency. A panel observes where devices are during working hours directly, so a daytime population can be estimated from observation rather than from a survey plus an assumption about remote working. The catch is the panel’s own bias, which is why the two are best used together: the flow table provides the structure and the control totals, and the panel provides a current correction to the commuting share. Where only one is available, the flow table with a stated remote adjustment is the more defensible starting point.

How should the layer be stored?

As an additional population column on the same zone table rather than as a separate dataset, with the vintage of both the flow table and the remote adjustment recorded alongside. Keeping it beside the residential figure means every downstream join picks up both automatically and the choice of which to use becomes a scoring decision rather than a data-engineering one. A separate table invites two versions of the truth and a quarterly argument about which is current.

Does the layer need its own validation gate?

A light one. Two assertions cover most of the risk: that the national sum of daytime population stays within a percentage point of the residential sum, and that no zone’s inbound workers exceed its published jobs. Both are one-line checks over the finished layer, both fail loudly when a flow table is joined on the wrong key, and neither requires any external data the pipeline does not already hold.

← Back to Mobility & Foot Traffic Data Integration