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 |
required |
family
|
str
|
One of |
'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
|
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 |
None
|
bounds
|
dict
|
Search range |
None
|
likelihood
|
('grouped', 'exact')
|
|
"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 |
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 |
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 |
{}
|
Returns:
| Type | Description |
|---|---|
Table
|
One row per family with |
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:
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
|
|
required |
parameters
|
dict
|
Maximum-likelihood estimates by name. |
required |
std_errors
|
dict
|
Bootstrap standard errors (NaN when |
required |
ci_low
|
dict
|
Lower bounds of the percentile bootstrap confidence intervals (NaN as for |
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 |
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, |
required |
bic
|
float
|
Bayesian information criterion, |
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 |
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 |
required |
edges
|
ndarray
|
The bin edges. |
required |
counts
|
ndarray
|
The observed counts per bin. |
required |
likelihood
|
str
|
|
'grouped'
|
bounds
|
dict
|
The search range |
dict()
|
values
|
ndarray or None
|
The fitted values inside the range, kept only for |
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 |
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 |
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 |
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 |
summary()
The estimates as a table.
Returns:
| Type | Description |
|---|---|
Table
|
One row per parameter with |
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 |
required |
high
|
float
|
Upper end of the interval. When |
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 |
required |
confidence
|
float
|
Confidence level. |
required |
table
|
Table
|
The profile curve at the points that were evaluated: |
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 |
required |
family
|
str
|
Family to fit. Default |
'truncated_powerlaw'
|
candidates
|
array - like
|
The |
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 |
{}
|
Returns:
| Type | Description |
|---|---|
Table
|
One row per candidate with |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
See Also
fit_distance_distribution : Fit with the selected xmin.
Examples:
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 |
required |
n_bootstrap
|
int
|
Parametric-bootstrap replicates for the p-value; 0 (default) uses the asymptotic mixture. Needs
|
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
|
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 |
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 |
required |
correction
|
('aic', 'bic', None)
|
Penalty for the number of parameters subtracted from the log-likelihood ratio ( |
"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 |
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
|
|
required |
statistic
|
float
|
The likelihood-ratio statistic |
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 |
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
|
|
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