Correcting Panel Bias in Mobility Datasets

This page solves one exact task: converting raw panel visit counts into estimates scaled to the population, by weighting each origin zone according to how well the panel covers it and then post-stratifying on the demographics that drive both device ownership and shopping behaviour.

This is where most of the accuracy in a mobility pipeline is won. An uncorrected panel over-represents younger, urban, higher-income households — precisely the segments whose shopping patterns differ most from the population average — so an uncorrected trade area is a picture of a subset presented as a picture of a market.

Prerequisites

  • Panel device counts per origin zone, supplied by the vendor or derivable from the aggregates.
  • Population denominators per zone from ACS or equivalent estimates, matched to the same geography and vintage.
  • Demographic composition per zone for post-stratification — age bands, income bands, household size.
  • Python packages: pandas and numpy. Install with pip install pandas numpy.
  • The parent context. Mobility and foot traffic data integration explains why levels need correcting and shares do not.

Configuration and execution parameters

Parameter Value for this task Notes
coverage_basis devices ÷ adult population Per zone, per month
min_devices_per_zone 30 Below this the weight is unstable
weight_cap 5× the median weight Prevents one sparse zone dominating
post_strata age × income band Two dimensions is usually enough
smoothing shrink toward the market mean For zones near the device floor
recompute monthly Panels move; a stale weight is a silent bias
report_effective_n yes The honest sample size after weighting

weight_cap is the parameter that separates a working correction from an unstable one. A zone with twelve panel devices and four thousand adults produces a weight in the hundreds, and one visit from that zone then contributes as much as an entire neighbourhood. Capping the weight biases the estimate slightly and stops a single observation from dominating a market — a trade almost always worth making.

The panel is not a random sample of anywhere Panel coverage is 4.4 per cent in zones whose median age is under thirty-five and 1.6 per cent where it is over fifty-five; 4.1 per cent in the highest income quartile and 2.2 per cent in the lowest. Uncorrected counts therefore describe a younger, wealthier population than the one that shops in the store. Coverage tracks exactly the attributes that shape shopping behaviour by median age of the origin zone under 35 4.4% 35–55 2.9% over 55 1.6% by household income quartile highest 4.1% lowest 2.2% A store serving an older, lower-income catchment will look quieter than an identical store elsewhere — which is a statement about phones, not about customers.

Annotated implementation

The correction is two stages: a per-zone weight that scales devices to population, and a post-stratification adjustment that fixes the residual demographic imbalance within zones.

python
from __future__ import annotations

import numpy as np
import pandas as pd

MIN_DEVICES = 30
WEIGHT_CAP_MULTIPLE = 5.0


def zone_weights(panel: pd.DataFrame, population: pd.DataFrame) -> pd.DataFrame:
    """Weight = adults per panel device, capped and smoothed.

    panel: zone_id, devices, month
    population: zone_id, adults
    """
    df = panel.merge(population, on="zone_id", how="inner")
    df["raw_weight"] = df["adults"] / df["devices"].clip(lower=1)

    # Zones below the device floor get the market median rather than their own
    # unstable ratio: one device in a zone of 4,000 adults is not a measurement.
    market_median = df.loc[df["devices"] >= MIN_DEVICES, "raw_weight"].median()
    df["weight"] = np.where(df["devices"] >= MIN_DEVICES,
                            df["raw_weight"], market_median)

    cap = WEIGHT_CAP_MULTIPLE * market_median
    df["capped"] = df["weight"] > cap
    df["weight"] = df["weight"].clip(upper=cap)
    return df[["zone_id", "month", "devices", "adults", "weight", "capped"]]


def post_stratify(visits: pd.DataFrame, weights: pd.DataFrame,
                  strata: pd.DataFrame) -> pd.DataFrame:
    """Adjust so the weighted visitor mix matches the population mix by stratum.

    strata: zone_id, stratum, population_share
    """
    v = visits.merge(weights[["zone_id", "month", "weight"]],
                     on=["zone_id", "month"], how="inner")
    v["weighted_visits"] = v["visits"] * v["weight"]

    joined = v.merge(strata, on="zone_id", how="left")
    observed = (joined.groupby(["store_id", "stratum"])["weighted_visits"].sum()
                / joined.groupby("store_id")["weighted_visits"].transform("sum").iloc[0])
    target = strata.groupby("stratum")["population_share"].mean()

    factor = (target / observed).replace([np.inf, -np.inf], np.nan).fillna(1.0)
    joined["strat_factor"] = joined["stratum"].map(factor).clip(0.5, 2.0)
    joined["adjusted_visits"] = joined["weighted_visits"] * joined["strat_factor"]
    return joined


def effective_sample_size(weights: pd.Series) -> float:
    """Kish's effective n — the honest sample size once weights are unequal."""
    return float(weights.sum() ** 2 / (weights ** 2).sum())

The effective sample size is the diagnostic worth reporting alongside every corrected figure. A store whose weighted estimate rests on 900 raw device-visits with highly unequal weights may have an effective sample of 180, and that number tells a reader far more about how much to trust the estimate than the raw count does.

Failure modes and debugging

Weighting a zone with almost no devices. The uncapped weight explodes, and one visitor from that zone becomes hundreds of estimated visitors. The device floor and the cap exist for this, and the count of capped zones belongs in the output — a market where a third of zones are capped is a market where the panel is too thin to support this analysis.

Correcting with a mismatched population vintage. Weights built from last year’s population estimates and this year’s devices drift in the zones where the population grew fastest, which are usually the ones a retailer is most interested in. Pin both to the same vintage and recompute when either changes.

Post-stratifying on too many dimensions. Age by income by household size by tenure produces strata with a handful of devices each, and the resulting factors are noise. Two dimensions with three or four bands each is usually the practical ceiling.

Correcting shares that did not need it. Applying zone weights to a quantity that is already a ratio within a zone double-counts the correction. Weights fix the comparison between zones; they do not belong inside a within-zone proportion.

What the correction does to a store's estimated visitor mix Uncorrected, the panel says 41 per cent of a store's visitors are under thirty-five and 18 per cent over fifty-five. After zone weighting and post-stratification the figures are 29 and 31 per cent, which matches the loyalty-card mix far more closely and changes which products the site's assortment should carry. The correction changes the customer, not just the count uncorrected panel under 35 · 41% 35–55 · 41% 55+ · 18% after weighting and post-stratification under 35 · 29% 35–55 · 40% 55+ · 31% loyalty-card mix for the same store 28% 41% 31% The corrected mix lands within a point of the loyalty data, which is the check that the correction is working.

Verification

  • Compare corrected composition against loyalty data for stores where both exist. This is the only external check available, and agreement within a few points is a strong signal the weights are sensible.
  • Report the effective sample size for every store-level estimate. A figure a tenth of the raw count means the weights are doing a great deal of work and the estimate is correspondingly fragile.
  • Track the share of capped zones by market. Rising caps mean a thinning panel, which invalidates level comparisons across the period.
  • Re-run the correction with the cap removed on a sample and compare. A result that moves substantially is one where a handful of sparse zones are driving the estimate.
What unequal weights cost in effective sample size A store with 1,840 raw device-visits and near-uniform weights retains an effective sample of 1,610. One with the same raw count but weights spanning a factor of twenty retains 214 — an estimate that looks equally precise in the output and is not. Two stores, the same raw count, very different confidence store raw device-visits weight spread effective n urban, dense panel 1,840 1.4× 1,610 suburban, mixed zones 1,790 4.8× 742 rural, sparse panel 1,840 21× 214 Publishing the effective n beside the estimate is what stops the third row being read with the same confidence as the first — and it costs one column. It is also the number that decides whether a rural market can be analysed from panel data at all, which is a better conversation than discovering the answer from a strange result.

Frequently Asked Questions

Should the vendor’s own weights be used instead?

Use them as a starting point and validate them the same way. Vendors weight to national or regional benchmarks, which is appropriate for their general audience and not necessarily for a retailer whose markets are a specific subset. Comparing vendor-weighted output against your loyalty mix, as above, tells you quickly whether their correction suffices; where it does, using it saves effort and is more defensible than a home-made alternative.

How often do weights need recomputing?

Monthly, alongside the data. Panels change composition continuously, and a weight computed once and reused is a bias that grows silently. The computation is cheap — a join and a division — so the only reason it is ever skipped is that nobody built it into the pipeline.

What if population denominators are unavailable at the origin geography?

Aggregate up until they are. Weighting at tract level with reliable denominators beats weighting at block-group level with interpolated ones, because the interpolation error enters every weight and propagates to every estimate. The loss of resolution is visible and bounded; the error from bad denominators is neither.

Does correction fix the level, or only the mix?

Both, but with different confidence. The mix — which zones and which demographics — is corrected well, because that is what the weights are constructed to fix. The absolute level still depends on the vendor’s expansion assumptions and on visit-detection thresholds, so it should be anchored against transactions before it is used as a demand figure. The safe rule remains: trust the corrected shares, calibrate the corrected levels.

Can the correction be validated without loyalty data?

Partially. Two internal checks are available to anyone: the corrected visitor mix for a store should resemble the demographic mix of its own catchment more closely than the uncorrected one does, and the corrected level should scale sensibly with store size within a format. Neither is proof, and both catch a correction that has gone badly wrong. The external check against transaction or loyalty data remains the only one that speaks to accuracy rather than plausibility, which is why it is worth arranging even for a subset of stores.

Does correction remove the need for care about levels?

No, and treating it as if it does is the commonest way a corrected pipeline still misleads. Weighting fixes the composition of the sample; it cannot fix a vendor’s expansion factor, a visit-detection threshold that suits a different format, or a place polygon that includes the shop next door. The corrected level is a better estimate in the same units as before — which are still not transactions — so the calibration step remains necessary and the honest label on a corrected figure is “estimated visits, panel-derived” rather than “visits”.

Should the weights be published with the estimates?

Yes, at least in summary. A consumer of a corrected figure needs to know the median weight, the share of capped zones and the effective sample size to interpret it, and those three numbers travel easily as columns. Publishing the full per-zone weight table alongside is better still, because it lets a sceptical analyst reproduce the correction rather than take it on trust — which is exactly the scrutiny a panel-derived number should be able to withstand.

← Back to Mobility & Foot Traffic Data Integration