Skip to content

Fitting

Fastkit-Mobility includes fitting utilities for common empirical mobility laws: distributions of distances (maximum-likelihood fits with confidence intervals, profile-likelihood and likelihood-ratio tests, automatic choice of the range and a comparison of alternative forms), visitation laws, and daily-location distributions. Use these functions after preparing the corresponding trajectory-derived data. Gravity models are fitted with Gravity.fit (see Models) and flows are scored with compare_flow_models (see Evaluation).

For how the estimators work and what to report, see How the fits work. For end-to-end examples, see Compare to mobility laws and the fit and evaluate guide; the whole cycle on GPS tracks is in Validate mobility with GeoLife.

Catalogue

Distributions of distances

API Description Example
fit_distance_distribution Fit a distribution to positive values by maximum likelihood, with confidence intervals. How sure is the fit? Range, uncertainty and alternatives
compare_distance_distributions Fit several families to the same values and rank them by AIC. How sure is the fit? Range, uncertainty and alternatives
DistributionFit A fitted distribution of positive values, with its uncertainty and fit quality. Step 1
DistributionFit.cdf Cumulative distribution function at x of the fitted distribution. Step 1
DistributionFit.pdf Probability density at x of the fitted distribution, normalised on [xmin, xmax].
DistributionFit.profile Profile-likelihood interval of one parameter. Does the data need a cutoff, and does a different family fit better?
DistributionFit.summary The estimates as a table. How sure is the fit? Range, uncertainty and alternatives
ProfileInterval Profile-likelihood interval of one parameter, from DistributionFit.profile.
select_xmin Pick xmin as the lower end of the range that makes the fit closest to the data. Does the data need a cutoff, and does a different family fit better?
likelihood_ratio_test Test a family against the family that extends it. Does the data need a cutoff, and does a different family fit better?
vuong_test Vuong test of two families that are not nested. Does the data need a cutoff, and does a different family fit better?
ModelTest Result of likelihood_ratio_test or vuong_test.
fit_values_to_truncated_powerlaw Fit a truncated power law to positive values using a log histogram. Law 1
log_truncated_powerlaw Evaluate log(c * (x + r0)**(-beta) * exp(-x / kappa)). Law 1

Other laws

API Description Example
fit_visitation_law Fit the universal visitation law from staypoints. Law 2
VisitationLawFit Fitted universal visitation law and the data used to estimate it.
fit_daily_location_lognormal Fit a lognormal distribution to per-user daily distinct-location counts. Law 3

API

fit_distance_distribution(values, family='truncated_powerlaw', *, xmin=None, xmax=None, bins=200, n_bootstrap=100, confidence=0.95, n_gof=0, groups=None, max_cutoff=None, bounds=None, likelihood='grouped', random_state=0)

Fit a distribution to positive values by maximum likelihood, with confidence intervals.

The values are grouped into geometric bins on [xmin, xmax] and the family is fitted by maximising the multinomial log-likelihood of the bin counts. The confidence intervals are percentile intervals of a bootstrap of the bin counts, which treats the values as independent: when they come from a limited number of people pass groups so that whole people are resampled. When n_gof is positive, a parametric bootstrap gives a p-value for the Kolmogorov-Smirnov distance. Values that are zero, negative, not finite or outside the range are left out and counted in n_excluded, so state the range when you report a fit: the estimates depend on it.

The families are f(x) ∝ x^-beta ("powerlaw"), exp(-x / kappa) ("exponential"), a log-normal with parameters mu and sigma ("lognormal"), (x + r0)^-beta ("shifted_powerlaw") and (x + r0)^-beta exp(-x / kappa) ("truncated_powerlaw", as in González et al., 2008). Comparing the last two shows whether the cutoff adds anything beyond the shift r0. A parameter that ends on the edge of its search range is listed in at_bound; for the truncated power law this typically means that the data have no cutoff.

Parameters:

Name Type Description Default
values array - like

Positive values such as jump lengths in kilometres. Pass a flat array; use numpy.concatenate on the lists returned by jump_lengths.

required
family str

One of "truncated_powerlaw" (default), "shifted_powerlaw", "powerlaw", "exponential" or "lognormal".

'truncated_powerlaw'
xmin float

Lower end of the fitted range. Default: the smallest positive value. Raise it to ignore GPS noise.

None
xmax float

Upper end of the fitted range. Default: the largest value, which makes the sample maximum a hard limit of the distribution and can hide a cutoff (the likelihood cannot tell a cutoff from the end of the data). When the study area has a known largest distance (the diagonal of its bounding box, or half the Earth's circumference for global data) pass it here; a warning is issued for the families with a cutoff when xmax is left out.

None
bins int

Number of geometric bins, at least 10. Default 200.

200
n_bootstrap int

Bootstrap replicates for the standard errors and intervals; 0 skips them. Default 100.

100
confidence float

Confidence level of the intervals, between 0 and 1. Default 0.95.

0.95
n_gof int

Parametric-bootstrap replicates for the goodness-of-fit p-value; 0 skips it. Default 0. The test treats the values as independent, so with clustered values (many jumps per user) it is too optimistic.

0
groups array - like

One label per value (for example the user id of each jump). When given, the bootstrap for the standard errors and intervals resamples whole groups instead of single values, which widens the intervals when the values of a group are alike. Use it whenever the values come from a limited number of people.

None
max_cutoff float

Largest value that kappa can take. Default 1,000 times xmax, so that the cutoff factor is practically 1 over the whole range.

None
bounds dict

Search range {parameter: (low, high)} of any parameter, in the units of the values for r0 and kappa. It overrides the defaults (and max_cutoff). Use it to check that a conclusion does not depend on the range; the defaults are beta 0.01 to 10 (6 for the shifted and truncated power laws), r0 0.01 xmin to xmax, kappa 0.01 xmin (xmin for the truncated power law) to 1000 xmax, mu log(xmin) - 5 to log(xmax) + 5 and sigma 0.02 to 10. r0, kappa and sigma are searched on a log scale, so their lower bound must be positive.

None
likelihood ('grouped', 'exact')

"grouped" (default) maximises the likelihood of the bin counts, whose cost does not grow with the number of values. "exact" maximises the likelihood of the individual values (the density normalised on [xmin, xmax]); it removes the approximation of the bins but every evaluation passes over all values, so it is slow beyond about 10^5 values, and n_gof is not available. The two likelihoods are not comparable with each other.

"grouped"
random_state int

Seed of the bootstrap. Default 0.

0

Returns:

Type Description
DistributionFit

The estimates, intervals, likelihood, AIC, BIC, KS distance and the parameters at their bounds.

Raises:

Type Description
ValueError

If family is unknown, fewer than 20 positive values (or 20 inside the range) are given, or the range, bins, confidence, likelihood or bounds is invalid, or n_gof is positive with likelihood="exact".

See Also

DistributionFit.profile : Profile-likelihood interval, also for a parameter at its bound. compare_distance_distributions : Fit several families and rank them by AIC. fit_values_to_truncated_powerlaw : The earlier histogram grid search.

Examples:

>>> import numpy as np
>>> from fastmob.measures.fitting import fit_distance_distribution
>>> rng = np.random.default_rng(1)
>>> jumps = rng.lognormal(mean=1.0, sigma=0.8, size=5000)
>>> fit = fit_distance_distribution(jumps, "lognormal", n_bootstrap=20)
>>> round(fit.parameters["mu"], 1), round(fit.parameters["sigma"], 1)
(1.0, 0.8)
>>> fit.at_bound
()

Worked example: How sure is the fit? Range, uncertainty and alternatives

compare_distance_distributions(values, families=FAMILIES, *, degenerate_last=True, **kwargs)

Fit several families to the same values and rank them by AIC.

Every family is fitted on the same range and bins, so their likelihoods, AIC and BIC are comparable. A small AIC difference (under about 2) means the data cannot tell the families apart.

A family with a parameter on the edge of its search range (at_bound) is a degenerate fit when the edge is not a legitimate limit of the family: the shape it reached is not the one the family is meant to describe (a shifted power law with beta at its upper limit is an exponential in disguise), so its AIC says little. r0 at its lower edge (a power law, or a power law with an exponential cutoff) and kappa at its upper edge (no cutoff) are legitimate limits, so those fits are ranked normally; see DistributionFit.limits. With degenerate_last=True (the default) those families are listed after the others, delta_aic is measured from the best family that is not degenerate, and akaike_weight is computed among the families that are not degenerate only (it is missing for the others). A negative delta_aic on a degenerate row means that its likelihood is higher, which is what the degeneracy buys; do not read it as support.

Parameters:

Name Type Description Default
values array - like

Positive values, as for fit_distance_distribution.

required
families sequence of str

Families to compare. Default: all five.

FAMILIES
degenerate_last bool

Rank families with a parameter at its bound after the others and leave them out of the Akaike weights. With False every family is ranked by AIC alone. Default True.

True
**kwargs Any

Passed to fit_distance_distribution (xmin, xmax, bins, n_bootstrap, n_gof, max_cutoff, bounds, likelihood, random_state). n_bootstrap defaults to 0 here, because only the criteria are needed. bounds may name parameters that some families do not have; each family uses the entries that apply to it.

{}

Returns:

Type Description
Table

One row per family with family, n_parameters, log_likelihood, aic, bic, delta_aic, akaike_weight, ks_statistic, gof_p_value, at_bound (the parameters on the edge of their range) and degenerate (True when one of them is not a legitimate limit). The AIC and the Akaike weights treat the values as independent, so they overstate the evidence when many values come from the same people.

See Also

fit_distance_distribution : Fit one family with confidence intervals. likelihood_ratio_test : Test a family against the one that extends it. vuong_test : Test two families that are not nested.

Examples:

>>> import numpy as np
>>> from fastmob.measures.fitting import compare_distance_distributions
>>> jumps = np.random.default_rng(1).lognormal(1.0, 0.8, size=5000)
>>> table = compare_distance_distributions(jumps, ["exponential", "lognormal"])
>>> table.column("family").to_pylist()[0]
'lognormal'

Worked example: How sure is the fit? Range, uncertainty and alternatives

DistributionFit dataclass

A fitted distribution of positive values, with its uncertainty and fit quality.

Parameters:

Name Type Description Default
family str

"powerlaw", "shifted_powerlaw", "exponential", "lognormal" or "truncated_powerlaw".

required
parameters dict

Maximum-likelihood estimates by name.

required
std_errors dict

Bootstrap standard errors (NaN when n_bootstrap=0 and for parameters at a bound).

required
ci_low dict

Lower bounds of the percentile bootstrap confidence intervals (NaN as for std_errors).

required
ci_high dict

Upper bounds of the percentile bootstrap confidence intervals.

required
confidence float

Confidence level of the intervals.

required
n int

Number of values inside [xmin, xmax] that were fitted.

required
n_excluded int

Number of values left out because they are not positive or fall outside the range.

required
xmin float

Lower end of the fitted range, in the units of the values.

required
xmax float

Upper end of the fitted range.

required
log_likelihood float

Grouped-data log-likelihood. Compare it between families fitted on the same range and bins; its absolute value is not meaningful.

required
n_parameters int

Number of fitted parameters.

required
aic float

Akaike information criterion, 2k - 2 log L.

required
bic float

Bayesian information criterion, k log(n) - 2 log L.

required
ks_statistic float

Largest distance between the empirical and the fitted cumulative distribution at the bin edges.

required
gof_p_value float or None

Parametric-bootstrap p-value of the KS distance, or None when n_gof=0. With many observations every simple model is rejected, so read it together with the KS distance.

required
at_bound tuple of str

Parameters that ended on the edge of their search range, so are not identified by the data (for example a cutoff kappa at its upper bound means that the data show no cutoff).

required
edges ndarray

The bin edges.

required
counts ndarray

The observed counts per bin.

required
likelihood str

"grouped" (the likelihood of the bin counts) or "exact" (the likelihood of the individual values).

'grouped'
bounds dict

The search range (low, high) of each parameter. A parameter that ends on one of these edges is in at_bound.

dict()
values ndarray or None

The fitted values inside the range, kept only for likelihood="exact".

None

degenerate property

Parameters on the edge of their range that are not a legitimate limit (see limits).

These mark a distorted fit, for example a shifted power law whose beta is at its upper limit, which imitates an exponential, so its likelihood and AIC rank say little.

limits property

Parameters on the edge of their range that are a legitimate limit of the family, by the model they give.

r0 at its lower edge turns the shifted power law into the plain power law and the truncated power law into a power law with an exponential cutoff; kappa at its upper edge turns the truncated power law into the shifted one. These are simpler models inside the family, not a distorted shape.

cdf(x)

Cumulative distribution function at x of the fitted distribution.

Parameters:

Name Type Description Default
x array - like

Positive points, in the units of the fitted values.

required

Returns:

Type Description
ndarray

The probability of a value at most x, interpolated between the bin edges.

pdf(x)

Probability density at x of the fitted distribution, normalised on [xmin, xmax].

Parameters:

Name Type Description Default
x array - like

Positive points, in the units of the fitted values.

required

Returns:

Type Description
ndarray

The density at each point; 0 outside [xmin, xmax].

profile(parameter, confidence=None)

Profile-likelihood interval of one parameter.

The parameter is fixed at a value, the other parameters are refitted (from several starting points), and the interval is where the likelihood-ratio statistic 2 (log L_max - log L_profile) stays below the chi-squared quantile with one degree of freedom. Each end is found by bisection from the estimate, so the result does not depend on a grid. Unlike the percentile bootstrap it is defined for a parameter that the data cannot pin down: when the statistic is still below the quantile at the edge of the search range, that end is reported as open (for a cutoff kappa it reads "kappa is at least X km"). The interval treats the values as independent, and it is the contiguous region around the estimate. nuisance_at_bound lists the other parameters that reach the edge of their range at the ends of the interval: then the interval is partly decided by a degenerate shape (for example a steep shifted power law standing in for an exponential), not only by the parameter.

Parameters:

Name Type Description Default
parameter str

Name of a parameter of the fitted family, as in parameters.

required
confidence float

Confidence level. Default: the level of the fit.

None

Returns:

Type Description
ProfileInterval

The interval, which ends are open, the nuisance parameters at a bound, and the profile curve as a table of the points that were evaluated.

Raises:

Type Description
ValueError

If parameter is not a parameter of the family, or confidence is invalid.

summary()

The estimates as a table.

Returns:

Type Description
Table

One row per parameter with parameter, estimate, std_error, ci_low, ci_high and at_bound (True when the estimate is on the edge of its search range).

Worked example: Step 1 · Does the data need a cutoff, and does a different family fit better? · How sure is the fit? Range, uncertainty and alternatives

ProfileInterval dataclass

Profile-likelihood interval of one parameter, from DistributionFit.profile.

Parameters:

Name Type Description Default
parameter str

The parameter name.

required
estimate float

The maximum-likelihood estimate.

required
low float

Lower end of the interval. When low_open this is the lower edge of the search range.

required
high float

Upper end of the interval. When high_open this is the upper edge of the search range.

required
low_open bool

True when the likelihood is still within the interval at the lower edge of the search range, so the data give no lower limit.

required
high_open bool

True when the data give no upper limit (for a cutoff: no evidence of a cutoff below high).

required
confidence float

Confidence level.

required
table Table

The profile curve at the points that were evaluated: value, delta_deviance and inside.

required
nuisance_at_bound tuple of str

Other parameters of the family that reach the edge of their range at the ends of the interval. When there are any, a degenerate shape helps to decide the interval.

()

__str__()

The interval in words, such as kappa >= 1039 (95%; no upper limit).

select_xmin(values, family='truncated_powerlaw', *, candidates=None, n_candidates=30, min_values=100, **kwargs)

Pick xmin as the lower end of the range that makes the fit closest to the data.

For each candidate xmin the family is fitted on [xmin, xmax] and the Kolmogorov-Smirnov distance between the fitted and the empirical distribution is computed; the candidate with the smallest distance is selected (Clauset, Shalizi and Newman, 2009). Fit fit_distance_distribution again with the selected xmin to get the estimates and intervals. The selection uses the data, so the intervals of that final fit do not include the uncertainty about xmin: report the selected value and how the estimate moves over the candidates (the table). The KS distance is computed on the bin edges of each fit, and a lower xmin leaves more values, so a family that does not describe the small values gets a high xmin.

Parameters:

Name Type Description Default
values array - like

Positive values, as for fit_distance_distribution.

required
family str

Family to fit. Default "truncated_powerlaw".

'truncated_powerlaw'
candidates array - like

The xmin values to try. Default: n_candidates points spaced geometrically from the smallest positive value to the value above which min_values observations remain.

None
n_candidates int

Number of default candidates. Default 30.

30
min_values int

Fewest values a candidate range may leave (at least 20). Default 100.

100
**kwargs Any

Passed to fit_distance_distribution (xmax, bins, max_cutoff, bounds, likelihood, random_state). n_bootstrap is 0.

{}

Returns:

Type Description
Table

One row per candidate with xmin, n (values in range), ks_statistic, log_likelihood, one column per parameter, at_bound (parameters on the edge of their range) and selected (True on the row with the smallest KS distance).

Raises:

Type Description
ValueError

If min_values is below 20 or too few values are positive, or no candidate leaves min_values values.

See Also

fit_distance_distribution : Fit with the selected xmin.

Examples:

>>> import numpy as np
>>> from fastmob.measures.fitting import select_xmin
>>> x = np.random.default_rng(2).pareto(1.5, size=3000) + 1.0
>>> table = select_xmin(x, "powerlaw", n_candidates=8)
>>> bool(table.column("selected").to_pylist().count(True) == 1)
True

Worked example: Does the data need a cutoff, and does a different family fit better?

likelihood_ratio_test(restricted, general, n_bootstrap=0, random_state=0)

Test a family against the family that extends it.

The statistic is 2 (log L_general - log L_restricted). The restricted family is a limit of the general one with parameters on the boundary of their range (r0 -> 0, or the cutoff kappa -> infinity), so the null distribution is not chi-squared: it is a mixture of chi-squared distributions (Chernoff, 1954), which is used here (for the pair with two boundary parameters the mixture assumes that they are orthogonal, Self and Liang, 1987). That asymptotic p-value needs a range that does not clip the data: when xmax is the sample maximum the cutoff can hide behind the end of the data and the test has little power, and its size is wrong. With n_bootstrap above 0 the p-value comes instead from a parametric bootstrap: data are simulated from the restricted fit, both families are refitted, and the p-value is the share of statistics at least as large as the observed one. It is slower but does not rely on the asymptotic theory. The pairs that can be tested are powerlaw against shifted_powerlaw, shifted_powerlaw against truncated_powerlaw (does the data support a cutoff at all?) and powerlaw against truncated_powerlaw. The test treats the values as independent, so it overstates the evidence for clustered values.

Parameters:

Name Type Description Default
restricted DistributionFit

The simpler family.

required
general DistributionFit

The family that extends it. Both must be fitted to the same values with the same xmin, xmax, bins and likelihood.

required
n_bootstrap int

Parametric-bootstrap replicates for the p-value; 0 (default) uses the asymptotic mixture. Needs likelihood="grouped".

0
random_state int

Seed of the bootstrap. Default 0.

0

Returns:

Type Description
ModelTest

The statistic, p-value and which family is supported.

Raises:

Type Description
ValueError

If the two families are not a nested pair of the list above, were not fitted to the same data, or n_bootstrap is positive for an exact likelihood.

Warns:

Type Description
UserWarning

If the general family has a lower likelihood than the restricted one, which can only be a search limit (for example the cap on kappa) or a failed optimisation; the statistic is then 0.

See Also

vuong_test : For families that are not nested. compare_distance_distributions : Rank families by AIC.

Examples:

>>> import numpy as np
>>> from fastmob.measures.fitting import fit_distance_distribution, likelihood_ratio_test
>>> x = np.random.default_rng(3).pareto(1.5, size=4000) + 1.0
>>> a = fit_distance_distribution(x, "powerlaw", n_bootstrap=0)
>>> b = fit_distance_distribution(x, "shifted_powerlaw", n_bootstrap=0)
>>> likelihood_ratio_test(a, b).test
'likelihood ratio'

Worked example: Does the data need a cutoff, and does a different family fit better?

vuong_test(first, second, correction='aic')

Vuong test of two families that are not nested.

The test asks whether the two fitted families have the same expected log-likelihood per value. Under that null the standardised difference of the log-likelihoods is approximately standard normal (Vuong, 1989). A significant positive statistic favours first, a negative one second; a small statistic means that the data cannot choose, however large the AIC difference looks. The values are treated as independent, so the evidence is overstated for clustered values.

Parameters:

Name Type Description Default
first DistributionFit

One family.

required
second DistributionFit

The other. Both must be fitted to the same values with the same xmin, xmax, bins and likelihood.

required
correction ('aic', 'bic', None)

Penalty for the number of parameters subtracted from the log-likelihood ratio (k1 - k2 for AIC, (k1 - k2) log(n) / 2 for BIC). Default "aic".

"aic"

Returns:

Type Description
ModelTest

The statistic, two-sided p-value and which family is supported at the 5% level.

Raises:

Type Description
ValueError

If the fits are not of the same data, the families are nested (use likelihood_ratio_test), or correction is invalid.

See Also

likelihood_ratio_test : For nested families. compare_distance_distributions : Rank families by AIC.

Examples:

>>> import numpy as np
>>> from fastmob.measures.fitting import fit_distance_distribution, vuong_test
>>> x = np.random.default_rng(1).lognormal(1.0, 0.8, size=4000)
>>> a = fit_distance_distribution(x, "lognormal", n_bootstrap=0)
>>> b = fit_distance_distribution(x, "exponential", n_bootstrap=0)
>>> vuong_test(a, b).favours
'lognormal'

Worked example: Does the data need a cutoff, and does a different family fit better?

ModelTest dataclass

Result of likelihood_ratio_test or vuong_test.

Parameters:

Name Type Description Default
test str

"likelihood ratio" or "vuong".

required
statistic float

The likelihood-ratio statistic 2 (log L_general - log L_restricted) or the Vuong z statistic (positive when the first family fits better).

required
p_value float

The p-value.

required
favours str or None

The family the test supports at the 5% level, or None when it cannot choose.

required
note str

How to read the result.

required
asymptotic_p_value float or None

For likelihood_ratio_test with n_bootstrap above 0, the p-value of the boundary chi-squared mixture; p_value is then the parametric-bootstrap one. None otherwise.

None

VisitationLawFit dataclass

Fitted universal visitation law and the data used to estimate it.

data contains per-user, per-H3-cell observations. spectrum holds the aggregate rf and rho values that were fitted.

fit_daily_location_lognormal(staypoints, *, locations=None, user_id_col=None, location_id_col=None, timestamp_col=None)

Fit a lognormal distribution to per-user daily distinct-location counts.

staypoints may be a Staypoints object or an eager dataframe. When a Locations catalogue is supplied, its global or user-scoped location identities are validated before fitting. Timezone-aware starts are bucketed by their local wall-clock calendar day.

Worked example: Law 3

fit_values_to_truncated_powerlaw(values, bins=100)

Fit a truncated power law to positive values using a log histogram.

Fitting uses a deterministic, parallel coarse-to-fine grid search over (r0, beta, kappa) implemented in Rust; the optimal c is solved in closed form in log space for every candidate. The log-spaced histogram the search optimizes over is also computed in Rust and returned alongside the fitted parameters.

Worked example: Law 1

fit_visitation_law(staypoints, *, locations=None, h3_resolution=9, user_id_col=None, timestamp_col=None, lat_col=None, lng_col=None, start_night=22, end_night=7, min_rf=None, max_rf=None, n_bins=30, distance_bin_width_km=1.0)

Fit the universal visitation law from staypoints.

Uses supplied global locations directly, or assigns shared H3 cells when no catalogue is supplied; it then fits rho = mu * rf**(-eta).

Returns:

Type Description
VisitationLawFit

data has one row per user and H3 cell with r_km, distinct-day frequency f, rf, and n_staypoints. spectrum contains the fitted rf/rho points; eta, mu, and r2 are the fitted parameters.

Worked example: Law 2

log_truncated_powerlaw(x, c, r0, beta, kappa)

Evaluate log(c * (x + r0)**(-beta) * exp(-x / kappa)).

Worked example: Law 1