Synthetic Mobility Data Generation

Synthetic mobility data generation produces realistic-but-artificial trajectory datasets by fitting a generative model to real movement and then sampling entirely new agents — preserving aggregate statistics such as trip-length and radius-of-gyration distributions while breaking the one-to-one link between any published trace and a real person.

When to Generate Synthetic Data vs. Anonymize Real Traces permalink

Synthetic generation is the right tool when downstream users need realistic movement structure — plausible routines, hotspots, flows — but do not need any specific individual’s real path. When users must retain real records (for billing reconciliation, incident replay, or ground-truth evaluation), perturbing the originals with trajectory anonymization techniques is more appropriate. The pipeline below shows the decision.

Synthetic mobility generation pipeline Real GPS traces are fitted to a Markov and EPR generative model, synthetic agents are sampled, then utility and privacy are validated. A decision node asks whether a formal differential privacy guarantee is needed: if yes, the model is trained with a spent epsilon budget; if no, the validated synthetic dataset is released. Real GPS traces (consented input) Fit model Markov / EPR Sample agents synthetic traces Validate utility + privacy Formal DP guarantee? Yes DP-synthetic spend budget epsilon No Release dataset + privacy report
Fit a generative model, sample synthetic agents, then validate. If a formal guarantee is required, train the model under a differential privacy budget; otherwise release the validated set. When users need the real records rather than artificial ones, prefer trajectory anonymization techniques instead.

Algorithmic Specification permalink

Three model families dominate practical mobility synthesis: discrete Markov chains over a tessellation, the exploration-and-preferential-return (EPR) family, and differentially private variants of either.

Markov transition model permalink

A first-order Markov model treats each visited location (a cell of a spatial tessellation) as a state sis_i and estimates the probability of moving to sjs_j next as the normalized transition count:

Pij=Pr(Xt+1=sjXt=si)=nijknikP_{ij} = \Pr(X_{t+1} = s_j \mid X_t = s_i) = \frac{n_{ij}}{\sum_{k} n_{ik}}

where nijn_{ij} is the number of observed transitions from cell ii to cell jj across the whole population. Sampling a synthetic sequence is then a walk on this chain. Because a raw count of nij=1n_{ij} = 1 encodes exactly one person’s move, transition estimation must be smoothed — the mechanics of that smoothing, plus stationary-distribution analysis, are covered in the deep-dive on generating synthetic GPS traces with Markov models.

Exploration and preferential return (EPR) permalink

The EPR model reproduces the empirical law that people mostly revisit familiar places but occasionally explore new ones. If an agent has visited SS distinct locations, the probability that its next move is to a new location is:

Pnew=ρSγP_{\text{new}} = \rho\, S^{-\gamma}

With probability 1Pnew1 - P_{\text{new}} the agent instead returns to a previously visited location ii, chosen by preferential return — proportional to how often it has been there:

Pr(return to i)=fikfk\Pr(\text{return to } i) = \frac{f_i}{\sum_{k} f_k}

Jump lengths Δr\Delta r and waiting times Δt\Delta t follow heavy-tailed distributions:

P(Δr)Δr(1+β),P(Δt)Δt(1+α)P(\Delta r) \sim \Delta r^{-(1+\beta)}, \qquad P(\Delta t) \sim \Delta t^{-(1+\alpha)}

Radius of gyration permalink

The primary utility invariant is the radius of gyration, which summarizes the spatial spread of an individual’s trajectory around its center of mass rcm\mathbf{r}_{cm}:

rg=1Ni=1Nrircm2r_g = \sqrt{\frac{1}{N}\sum_{i=1}^{N} \lVert \mathbf{r}_i - \mathbf{r}_{cm} \rVert^2}

A synthetic dataset is only useful if the distribution of rgr_g across synthetic agents matches the real population.

Parameter reference permalink

Parameter Symbol Typical value Role
Exploration coefficient ρ\rho 0.6 Scales the new-location probability
Exploration exponent γ\gamma 0.21 Decay of exploration as SS grows
Jump-length exponent β\beta 0.75 Heaviness of trip-length tail
Waiting-time exponent α\alpha 0.8 Heaviness of stop-duration tail
Markov order mm 1 Memory depth of the diary/transition model
Tessellation resolution 500 m – 2 km cells Spatial granularity of states
Smoothing strength λ\lambda 0.5 – 1.0 Laplace pseudocount on transitions

Prerequisites & Data Requirements permalink

Before fitting a generator, confirm the following:

  • Input trace schema: a long-format table with columns uid, datetime, lat, lng (WGS84 / EPSG:4326, the CRS scikit-mobility expects). Reproject to a metric CRS only for distance measurements, not for the model itself.
  • Stop detection and clustering: raw GPS pings must be reduced to stops and clustered into stable locations, producing a cluster (or location) column that becomes the Markov/EPR state space.
  • Spatial tessellation: a GeoDataFrame of polygons (a uniform grid, H3 hexagons, or administrative units) with a relevance column — usually population or historical visit counts — that seeds the density-EPR location choice.
  • Minimum dataset size: enough users that no diary state or transition is supported by a single individual. Fewer than a few hundred users invites memorization; prune or smooth any state with support below a threshold (e.g. fewer than k=5k = 5 contributing users).
  • Dependencies: scikit-mobility (skmob) for the EPR and Markov diary generators, numpy, pandas, geopandas, and h3 if you build a hexagonal tessellation. No network calls are required at generation time.

Step-by-Step Implementation permalink

The reference pipeline uses scikit-mobility’s DITRAS construction: a MarkovDiaryGenerator for when and what, driving a density-based EPR model for where.

Step 1 — Preprocess Real Traces into Stops and States permalink

import skmob
from skmob.preprocessing import detection, clustering

# Real, consented GPS pings in WGS84 (EPSG:4326) — the CRS skmob assumes.
tdf = skmob.TrajDataFrame(
    raw_df,                      # columns: uid, datetime, lat, lng
    latitude="lat",
    longitude="lng",
    datetime="datetime",
    user_id="uid",
)

# Reduce noisy pings to stationary stops (>= 20 min within a 200 m radius),
# then cluster stops into stable locations that become the model's states.
stops = detection.stay_locations(
    tdf, stop_radius_factor=0.5, minutes_for_a_stop=20.0, spatial_radius_km=0.2
)
stops = clustering.cluster(stops, cluster_radius_km=0.1, min_samples=1)
# `stops` now carries a `cluster` column: the discrete location alphabet.

Working from stops rather than raw pings is a privacy decision as much as a modeling one: it discards the high-frequency detail that makes a raw trace uniquely identifiable, a risk quantified in re-identification risk assessment for geospatial datasets.

Step 2 — Fit the Markov Diary Generator permalink

from skmob.models.markov_diary_generator import MarkovDiaryGenerator

# The diary generator learns the population-level routine: the probability of
# being "at home", "at another frequent location", or "exploring" as a function
# of the hour and the previous state. It aggregates over ALL users, so no single
# person's schedule is memorized — provided support per state stays high.
mdg = MarkovDiaryGenerator()
mdg.fit(stops, n_individuals=stops["uid"].nunique(), lid="cluster")

The diary is intentionally coarse — a handful of abstract states over hourly bins — which limits how much any individual can influence the fitted matrix. Verify that every diary state is supported by many users before trusting it.

Step 3 — Build a Spatial Tessellation with Relevance permalink

import geopandas as gpd

# A tessellation of the study area. `relevance` (e.g. residential population)
# biases the EPR exploration step toward plausible destinations rather than
# revealing which specific cells the real cohort actually visited.
tessellation: gpd.GeoDataFrame = gpd.read_file("study_area_grid.geojson")
tessellation = tessellation.to_crs(epsg=4326)          # skmob expects WGS84
tessellation = tessellation.rename(columns={"pop": "relevance"})
assert (tessellation["relevance"] >= 0).all()

Step 4 — Sample Synthetic Agents permalink

import pandas as pd
from skmob.models.epr import Ditras

# DITRAS = Markov diary (timing) + density-EPR (location choice).
# Each generated agent is a fresh random walk; it does not copy any real uid.
ditras = Ditras(diary_generator=mdg)

synth_tdf = ditras.generate(
    start_date=pd.to_datetime("2026-01-06 00:00:00"),
    end_date=pd.to_datetime("2026-01-13 00:00:00"),
    spatial_tessellation=tessellation,
    relevance_column="relevance",
    n_agents=2000,                 # synthetic population size is your choice
    random_state=42,
    show_progress=True,
)
# synth_tdf has the same schema (uid, datetime, lat, lng) but artificial uids.

Synthetic uids are freshly minted integers with no mapping back to real users. That severed linkage is the intended privacy property — but it is not automatic, which is why Step 5 is mandatory rather than optional.

Step 5 — Validate Before Release permalink

from skmob.measures.individual import radius_of_gyration, jump_lengths

real_rg = radius_of_gyration(stops).set_index("uid")["radius_of_gyration"]
synth_rg = radius_of_gyration(synth_tdf).set_index("uid")["radius_of_gyration"]

# Compare distributions, never individuals: a good synthetic set matches the
# SHAPE of these curves without reproducing any one real trajectory.
print("real   r_g median:", real_rg.median())
print("synth  r_g median:", synth_rg.median())

Validation & Re-identification Testing permalink

Utility and privacy are validated separately — a synthetic set can pass one and fail the other.

Distributional utility checks permalink

Compare the real and synthetic populations on the invariants that mobility models are supposed to preserve, using a two-sample statistic such as the Kolmogorov–Smirnov distance:

import numpy as np
from scipy.stats import ks_2samp
from skmob.measures.individual import jump_lengths

def distributional_report(real_tdf, synth_tdf) -> dict:
    """KS distance between real and synthetic distributions (lower = better)."""
    real_jl = np.concatenate(jump_lengths(real_tdf)["jump_lengths"].dropna().values)
    synth_jl = np.concatenate(jump_lengths(synth_tdf)["jump_lengths"].dropna().values)
    return {
        "jump_length_ks": ks_2samp(real_jl, synth_jl).statistic,
        # Repeat for radius_of_gyration, distinct-locations, and OD-flow cells.
    }

Also compare the origin–destination (OD) flow matrix: aggregate transitions between tessellation cells for both sets and check that the cell-to-cell flow volumes correlate strongly. A synthetic set that reproduces jump lengths but scrambles OD flows is not useful for transport planning.

Membership-inference and uniqueness checks permalink

The critical privacy test asks whether the synthetic release reveals who was in the training data. Compute, for each real trajectory, the distance to its nearest synthetic trajectory; if some real traces have a synthetic near-duplicate far closer than they have real neighbors, the model has memorized them.

from scipy.spatial import cKDTree
import numpy as np

def nearest_synthetic_distance(real_pts: np.ndarray, synth_pts: np.ndarray) -> np.ndarray:
    """Min distance (deg) from each real location to the synthetic cloud.
    Near-zero distances flag potential memorization / rare-trip leakage."""
    tree = cKDTree(synth_pts)
    d, _ = tree.query(real_pts, k=1)
    return d

# A memorization alarm: real points with an implausibly exact synthetic match.
leak_fraction = np.mean(nearest_synthetic_distance(real_xy, synth_xy) < 1e-4)
print(f"Suspected memorized points: {leak_fraction:.4%}")   # target: ~0

The uniqueness of the real traces sets the risk ceiling; estimating it directly is covered in estimating uniqueness of mobility traces. If real traces are highly unique, even a well-behaved generator can echo them, and you should move to a differentially private training procedure.

Common Failure Modes & Gotchas permalink

Overfitting reproduces real traces. High-order Markov models or EPR agents seeded from real visit histories can regenerate near-verbatim real paths. Keep the model order low, aggregate transitions across the whole cohort, and always run the nearest-synthetic-distance check above.

Rare-trip leakage. A trip taken by exactly one person (a visit to a specialist clinic, a remote address) survives naive fitting because its transition count is nonzero only for that individual. Prune or Laplace-smooth any transition or diary state with single-user support before sampling.

Sparse tessellation cells memorize. If a cell is visited by one user, EPR relevance and Markov transitions for that cell encode that user. Enforce a minimum contributing-user count per state.

Mismatched CRS in distance measures. scikit-mobility models operate in WGS84 degrees, but jump-length and radius-of-gyration comparisons must be computed with a geodesic or projected metric distance. Mixing degree and metre units silently corrupts every utility metric.

Treating “synthetic” as “anonymous” by definition. Synthetic provenance is not a legal safe harbour. Without the membership-inference test — and, for a formal claim, a differentially private fit — a synthetic release can carry the same disclosure risk as the raw data.

Compliance Alignment permalink

Control Satisfied by
GDPR Recital 26 — anonymous data Synthetic records are not personal data only if re-identification is not reasonably likely; the membership-inference test is your evidence
GDPR Art. 25 — data protection by design Fitting on aggregated stops and smoothing single-user states are architectural controls, not post-hoc fixes
GDPR Art. 35 — DPIA Document the generator, smoothing parameters, and validation metrics as the impact assessment for the release
CCPA/CPRA “de-identified” standard Requires a documented process preventing re-identification — the uniqueness and leakage checks supply it
NIST SP 800-188 Synthetic data is listed as a disclosure-limitation technique conditional on a measured re-identification test

For a formal, provable guarantee, train the transition and diary models under a privacy budget (epsilon) for spatial queries: add calibrated noise to the transition counts so the fitted model — and therefore every sampled agent — inherits epsilon-differential privacy. This is the “Yes” branch of the pipeline diagram, and it converts an empirical privacy argument into a mathematical one.

FAQ permalink

Is synthetic mobility data automatically anonymous?

No. A model that overfits can reproduce real trajectories almost verbatim, and rare trips can leak the individuals who took them. Synthetic data is private only if you constrain the model (low order, smoothed transitions, pruned single-user states) and confirm it with a membership-inference test. A formal guarantee requires a differentially private training procedure with a bounded epsilon.

How much real data do I need to fit a mobility generator?

Enough that no transition or diary state is supported by a single individual — in practice at least a few hundred users and several thousand stop events. Any state that only one person contributes to must be smoothed or pruned so it cannot be memorized.

What is the difference between the Markov and EPR models?

The Markov diary model governs when an agent moves and which activity it performs; the EPR model governs where it goes, trading off preferential return to familiar places against exploration of new ones. Combined models like DITRAS use the diary for timing and EPR for location.

Which distributions should I compare to validate utility?

Trip-length (jump-length) distribution, radius of gyration, number of distinct locations visited, and the origin–destination flow matrix, compared real vs. synthetic with a two-sample statistic such as the KS distance.


Back to Trajectory & Mobility Data Privacy