Skip to content

Evaluation Measures

API Description Example
ComparisonResult Result of compare_to: an overall value and, if grouped, a per-group breakdown.
common_part_of_commuters Return the common part of commuters (CPC) between observed and predicted flows. Common part of commuters and links
common_part_of_commuters_distance Compare the trip-distance distributions of two sets of trips (CPCD). Common part of commuters and links
common_part_of_links Return the common part of links (CPL) between observed and predicted flows. Common part of commuters and links
compare_flow_models Score one or several flow models against observed origin-destination flows. Agreement with the reference
compare_to Compare value_col between two dataframes, optionally grouped by group_col. Compare by group
information_gain Return the information gain (KL divergence variant) of true over pred. Information and distribution metrics
jensen_shannon_divergence Return Jensen-Shannon divergence between two distributions. Information and distribution metrics
kullback_leibler_divergence Return the Kullback–Leibler divergence (D_\mathrm{KL}(\mathrm{true}\,\ \,\mathrm{pred})).
max_error Return the maximum signed error max(true_i - pred_i). Error metrics on matched flows
mse Return the mean squared error between true and predicted values. Error metrics on matched flows
nrmse Return the normalized root mean squared error (RMSE / sum(true)). Error metrics on matched flows
r_squared Return the coefficient of determination R². Error metrics on matched flows
rmse Return the root mean squared error between true and predicted values. Error metrics on matched flows
stvd_emd Compute the spatio-temporal Wasserstein distance between two distributions. Compare when things happen
wasserstein_distance Return the 1-D Wasserstein (earth mover's) distance between two empirical samples. Information and distribution metrics

evaluation

fastmob.measures.evaluation — generic comparison metrics.

For grouped/dataframe-level comparisons, use BaseDataFrame.compare_to() (inherited by Staypoints, Trips, Triplegs, Locations, TrajDataFrame, FlowDataFrame, Tours) instead of hand-coding a grouping around these primitives.

ComparisonResult dataclass

Result of compare_to: an overall value and, if grouped, a per-group breakdown.

common_part_of_commuters(observed, predicted)

Return the common part of commuters (CPC) between observed and predicted flows.

CPC is 2 * sum(min(observed, predicted)) / (sum(observed) + sum(predicted)) over origin-destination pairs: 1 when the two flow tables are identical, 0 when they share no trips. Self-loops and missing endpoints are ignored.

Parameters:

Name Type Description Default
observed Trips or FlowDataFrame

Two Trips tables (each trip counts 1) or two sparse FlowDataFrame tables (weighted by flow). Both must be the same type.

required
predicted Trips or FlowDataFrame

Two Trips tables (each trip counts 1) or two sparse FlowDataFrame tables (weighted by flow). Both must be the same type.

required

Returns:

Type Description
float

CPC between 0 and 1 (higher is more similar). It depends on the zone size: finer zones give lower values.

common_part_of_commuters_distance(observed, predicted)

Compare the trip-distance distributions of two sets of trips (CPCD).

Distances are grouped in 2 km bins and the result is sum(min(count_observed, count_predicted)) / number of observed trips: 1 for identical distance distributions, 0 for none in common.

Parameters:

Name Type Description Default
observed Trips

Two Trips tables, each with a distance_km column (kilometres).

required
predicted Trips

Two Trips tables, each with a distance_km column (kilometres).

required

Returns:

Type Description
float

CPCD between 0 and 1 (higher is more similar when the two sets have similar numbers of trips).

Return the common part of links (CPL) between observed and predicted flows.

CPL is 2 * |links in both| / (|links observed| + |links predicted|): the share of origin-destination links that exist in both tables, whatever their weight. It is 1 when both have exactly the same links.

Parameters:

Name Type Description Default
observed Trips or FlowDataFrame

Two Trips tables or two FlowDataFrame tables of the same type.

required
predicted Trips or FlowDataFrame

Two Trips tables or two FlowDataFrame tables of the same type.

required

Returns:

Type Description
float

CPL between 0 and 1 (higher is more similar).

compare_flow_models(observed, models, pairs='catalogue')

Score one or several flow models against observed origin-destination flows.

Every model is scored on the same set of pairs, leaving out self-loops, and a pair that is missing from a table has flow 0 there. By default (pairs="catalogue") the pairs are all the pairs of the locations of observed, so the score of a model does not depend on which other models are scored with it; report the n_pairs column with the scores. With pairs="union" they are the pairs that have flow in the observed table or in any model, which is the only choice when observed has no locations, but then a model's score changes when another model is added. The Poisson deviance and pseudo R-squared treat the flows as counts, so compare models whose total flow is on the scale of the observed one (for example generated with the observed outflows).

Two cautions. The common part of links (CPL) compares which pairs have flow. Expected flows (out_format="flows") are positive on every pair, so their CPL is fixed by the number of observed pairs and says nothing about the model; use sampled (integer) flows for CPL. And a model that was fitted to the observed flows is scored in-sample: compare it with a parameter-free model, or score it on flows that were held out, before concluding that it is better.

Parameters:

Name Type Description Default
observed FlowDataFrame

Observed flows with origin, destination and flow columns.

required
models FlowDataFrame or mapping of str to FlowDataFrame

The model flows to score, by name. A single FlowDataFrame is named "model".

required
pairs ('catalogue', 'union')

The pairs on which the scores are computed, as above. Default "catalogue".

"catalogue"

Returns:

Type Description
Table

One row per model with model, n_pairs (pairs in the union), observed_flow and model_flow (totals), cpc (common part of commuters, 0 to 1), cpl (common part of links), r_squared (squared Pearson correlation of the pair flows), spearman (rank correlation), poisson_deviance and pseudo_r_squared (one minus the deviance over the deviance of a model that predicts the mean observed flow for every pair).

Raises:

Type Description
TypeError

If observed or a model is not a FlowDataFrame.

ValueError

If pairs is not "catalogue" or "union", or "catalogue" is asked for and observed has no locations.

See Also

fastmob.models.Gravity.fit : Fit a gravity model and get confidence intervals. common_part_of_commuters : The CPC on its own.

Examples:

>>> import pandas as pd
>>> from fastmob import FlowDataFrame
>>> from fastmob.measures.evaluation import compare_flow_models
>>> table = pd.DataFrame({"origin": ["a", "a", "b"], "destination": ["b", "c", "c"], "flow": [10, 5, 7]})
>>> flows = FlowDataFrame(table, tile_id="tile_id")
>>> compare_flow_models(flows, flows, pairs="union").column("cpc").to_pylist()
[1.0]

compare_to(df1, df2, value_col, *, group_col=None, metric=None)

Compare value_col between two dataframes, optionally grouped by group_col.

metric defaults to wasserstein_distance and accepts any (array, array) -> float callable, e.g. jensen_shannon_divergence. When group_col is given, the comparison runs once per label present in both dataframes (sorted for determinism) and overall is the mean across groups.

information_gain(true, pred)

Return the information gain (KL divergence variant) of true over pred.

\[ \mathrm{IG} = \sum_i \frac{y_i}{N} \log\left(\frac{y_i}{\hat{y}_i}\right) \]

where \(N = \sum_i y_i\).

Parameters:

Name Type Description Default
true

Ground truth target values (must be positive).

required
pred

Estimated target values (must be positive).

required

Returns:

Type Description
float

Information gain value; 0.0 when the distributions are identical.

Examples:

>>> from fastmob import information_gain
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(information_gain(observed, predicted), 3))
0.005

jensen_shannon_divergence(distribution1, distribution2)

Return Jensen-Shannon divergence between two distributions.

Inputs are normalised internally (raw counts and probabilities both work) and must have the same length -- unlike wasserstein_distance, the two arrays are paired by index (one entry per category/bin), not independent samples.

kullback_leibler_divergence(true, pred)

Return the Kullback–Leibler divergence \(D_\mathrm{KL}(\mathrm{true}\,\|\,\mathrm{pred})\).

\[ D_\mathrm{KL}(P \,\|\, Q) = \sum_k p_k \log\left(\frac{p_k}{q_k}\right) \]

Inputs are normalized to probability distributions before evaluation, matching the historical scipy.stats.entropy(true, pred) behavior.

Parameters:

Name Type Description Default
true

Probability distribution P (reference).

required
pred

Probability distribution Q (approximation).

required

Returns:

Type Description
float

KL divergence; 0.0 when the distributions are identical.

Examples:

>>> from fastmob import kullback_leibler_divergence
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(kullback_leibler_divergence(observed, predicted), 3))
0.005

max_error(true, pred)

Return the maximum signed error max(true_i - pred_i).

Parameters:

Name Type Description Default
true

Ground truth target values.

required
pred

Estimated target values.

required

Returns:

Type Description
float

Maximum element-wise difference true - pred.

Examples:

>>> from fastmob import max_error
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(max_error(observed, predicted), 3))
3.0

mse(true, pred)

Return the mean squared error between true and predicted values.

Parameters:

Name Type Description Default
true

Ground truth target values.

required
pred

Estimated target values.

required

Returns:

Type Description
float

Non-negative MSE; 0.0 is the best possible value.

Examples:

>>> from fastmob import mse
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(mse(observed, predicted), 3))
6.5

nrmse(true, pred)

Return the normalized root mean squared error (RMSE / sum(true)).

Parameters:

Name Type Description Default
true

Ground truth target values.

required
pred

Estimated target values.

required

Returns:

Type Description
float

Non-negative NRMSE; 0.0 is the best possible value.

Examples:

>>> from fastmob import nrmse
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(nrmse(observed, predicted), 3))
0.025

r_squared(true, pred)

Return the coefficient of determination R².

\[ R^2 = 1 - \frac{SS_\mathrm{res}}{SS_\mathrm{tot}} \]

where

\[ SS_\mathrm{tot} = \sum_i (y_i - \bar{y})^2 \]

Parameters:

Name Type Description Default
true

Ground truth target values.

required
pred

Estimated target values.

required

Returns:

Type Description
float

R² score. Best possible value is 1.0; can be negative.

Examples:

>>> from fastmob import r_squared
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(r_squared(observed, predicted), 3))
0.948

rmse(true, pred)

Return the root mean squared error between true and predicted values.

Parameters:

Name Type Description Default
true

Ground truth target values.

required
pred

Estimated target values.

required

Returns:

Type Description
float

Non-negative RMSE; 0.0 is the best possible value.

Examples:

>>> from fastmob import rmse
>>> observed = [10, 20, 30, 40]
>>> predicted = [12, 18, 33, 37]
>>> print(round(rmse(observed, predicted), 3))
2.55

stvd_emd(dist_a, dist_b, alpha=10.0, cyclical_period=1440.0, *, time_col=None, weight_col=None, lat_col=None, lng_col=None, reg=0.01, max_iter=200, tol=None)

Compute the spatio-temporal Wasserstein distance between two distributions.

Each input is a DataFrame representing a spatial-temporal distribution with columns for a time bin (HH:MM string), a weight/volume value, and a WGS84 latitude/longitude centroid — the exact shape produced by fastmob.measures.collective.build_stvd.

Distances are computed as an entropy-regularised (Sinkhorn) Earth Mover's Distance over an explicit cost matrix combining the great-circle (Haversine) distance between centroids with a cyclical-time term: two points at times t1/t2 contribute 2 * r * sin(d_theta / 2) metres, where r = alpha * cyclical_period / (2*pi), theta = 2*pi*(t / cyclical_period), and d_theta is the shortest angular difference between the two times.

Parameters:

Name Type Description Default
dist_a Any

Any Narwhals-compatible eager DataFrame (pandas, polars, …).

required
dist_b Any

Any Narwhals-compatible eager DataFrame (pandas, polars, …).

required
alpha float

Space-time tradeoff: 1 minute equals alpha metres. Default 10.0.

10.0
cyclical_period float

Wrap-around period in minutes. Default 1440 (one day).

1440.0
time_col str | None

Explicit time column name. Auto-detected when None.

None
weight_col str | None

Explicit weight column name. Auto-detected when None.

None
lat_col str | None

Explicit latitude/longitude column names. Auto-detected when None.

None
lng_col str | None

Explicit latitude/longitude column names. Auto-detected when None.

None
reg float

Sinkhorn entropic regularization strength (same units as the cost matrix, metres). Smaller values approximate exact optimal transport more closely but are more prone to numerical underflow. Default 0.01.

0.01
max_iter int

Maximum number of Sinkhorn dual-update iterations. Default 200.

200
tol float | None

Optional marginal-constraint convergence tolerance. When set, the solver stops early once the dual updates converge to within tol instead of always running max_iter rounds. Default None (always run the full max_iter, matching prior behavior).

None

Returns:

Type Description
float

Spatio-temporal Wasserstein distance in metres.

Examples:

>>> import pandas as pd
>>> from fastmob.measures.evaluation import stvd_emd
>>> dist_a = pd.DataFrame(
...     {
...         "time_bin": ["08:00", "08:10"],
...         "mean_volume": [2.0, 1.0],
...         "center_lat": [0.0, 0.0],
...         "center_lng": [0.0, 0.001],
...     }
... )
>>> dist_b = pd.DataFrame(
...     {
...         "time_bin": ["08:00", "08:10"],
...         "mean_volume": [1.0, 2.0],
...         "center_lat": [0.0, 0.0],
...         "center_lng": [0.0, 0.002],
...     }
... )
>>> print(round(stvd_emd(dist_a, dist_b), 3))
74.156

wasserstein_distance(values1, values2)

Return the 1-D Wasserstein (earth mover's) distance between two empirical samples.

The distance is the average amount by which the values of one sample must be moved to turn it into the other, in the units of the values. Non-finite values are dropped; the result is nan if a sample is empty.

Parameters:

Name Type Description Default
values1 array - like

The two samples (they may have different lengths).

required
values2 array - like

The two samples (they may have different lengths).

required

Returns:

Type Description
float

Non-negative distance; 0 for identical samples.

Worked example: Common part of commuters and links · Agreement with the reference · Compare by group · Information and distribution metrics · Error metrics on matched flows · Compare when things happen