Start with a trajectory
Question. How far does each person move, and how widely do they range?
You will get four results per person that summarise their movement (two of them lists): the length of each trip between consecutive records, how far they range from their centre of activity, the longest single jump they make, and how long they wait between records.
Prerequisites. None beyond Python. This notebook uses a six-row synthetic example, so there is nothing to download.
Data. Two invented people, Alice in Rome and Bob in New York, with three timestamped positions each. The numbers are too few to say anything about real behaviour. The point is to see the input table, the function call and the result side by side.
What a trajectory table looks like
A trajectory is a table with one row per recorded position. Fastkit-Mobility finds the columns by name, so you rarely need to configure anything:
| Role | Accepted column names | Meaning |
|---|---|---|
| person | uid, user, user_id |
optional; without it all rows belong to one person |
| time | datetime, timestamp, time |
when the position was recorded; used as given, with no time-zone conversion |
| latitude, longitude | lat/latitude, lng/lon/longitude |
decimal degrees, WGS 84 |
Distances returned by the measures are in kilometres and times in seconds. Pass uid_col=, lat_col= and similar arguments when your columns are named differently (see Columns and backends).
import pandas as pd
df = pd.DataFrame(
{
"uid": ["alice"] * 3 + ["bob"] * 3,
"datetime": pd.to_datetime(
[
"2020-01-01 08:00",
"2020-01-01 09:00",
"2020-01-01 10:00",
"2020-01-01 08:00",
"2020-01-01 09:00",
"2020-01-01 10:00",
]
),
"lat": [41.8902, 41.9028, 41.9109, 40.7128, 40.7306, 40.7580],
"lng": [12.4922, 12.4964, 12.4818, -74.0060, -73.9352, -73.9855],
}
)
df
| uid | datetime | lat | lng | |
|---|---|---|---|---|
| 0 | alice | 2020-01-01 08:00:00 | 41.8902 | 12.4922 |
| 1 | alice | 2020-01-01 09:00:00 | 41.9028 | 12.4964 |
| 2 | alice | 2020-01-01 10:00:00 | 41.9109 | 12.4818 |
| 3 | bob | 2020-01-01 08:00:00 | 40.7128 | -74.0060 |
| 4 | bob | 2020-01-01 09:00:00 | 40.7306 | -73.9352 |
| 5 | bob | 2020-01-01 10:00:00 | 40.7580 | -73.9855 |
Each row is one position of one person. Alice's three positions are in central Rome and Bob's are in New York City.
Measure jump lengths
A jump is the move between two consecutive records of the same person. jump_lengths returns one list per person, in kilometres, in time order.
| uid | jump_lengths | |
|---|---|---|
| 0 | alice | [1.44354, 1.506992] |
| 1 | bob | [6.286276, 5.219113] |
Alice has two jumps of about 1.4 and 1.5 km; Bob has two of about 5 to 6 km. A jump says nothing about the route taken, only the straight-line (great-circle) distance between the two records, so a person who walked a long loop and came back would show a jump of nearly zero.
Measure the radius of gyration
The radius of gyration is the typical distance of a person's positions from their own centre of mass. It is the standard single-number summary of how widely someone ranges.
| uid | radius_of_gyration | |
|---|---|---|
| 0 | alice | 1.074642 |
| 1 | bob | 3.248943 |
Alice's radius is about 1 km; Bob's is about 3 km. Bob ranges roughly three times more widely. With three points per person this is only an illustration; on real data the radius is computed over hundreds of positions, and it is sensitive to how often positions are recorded.
Find the longest single jump
maximum_distance is the largest distance between two consecutive records of a person, in kilometres: it is the maximum of the jump list above, not the span between the two most distant places.
| uid | maximum_distance | |
|---|---|---|
| 0 | alice | 1.506992 |
| 1 | bob | 6.286276 |
Alice's value, 1.51 km, is the second jump above and Bob's, 6.29 km, is the first, which confirms the definition. The radius describes the typical spread of a person's positions; this value describes only their longest single move. Use it when you care about the extremes of someone's range instead of its typical size.
Measure waiting times
waiting_times returns the time between consecutive records, in seconds.
| uid | waiting_times | |
|---|---|---|
| 0 | alice | [3600.0, 3600.0] |
| 1 | bob | [3600.0, 3600.0] |
Every wait is 3,600 seconds (one hour) because the example was built that way. In real data, waiting times are very uneven: many seconds-long gaps from continuous tracking and a few multi-hour gaps when the device was off. Look at their distribution before trusting any measure that assumes regular sampling.
Chart the results
The optional fastmob-vis package (pip install "fastmob[vis]") turns the columns of a trajectory table into charts. Every typed table has five helpers, plot_ecdf, plot_bar, plot_boxplot, plot_scatter and plot_heatmap; each takes the names of existing columns and does not recompute any measure. They need the trajectory columns, so we first attach each jump to the record where it ends, using jump_lengths(..., merge=True), which returns the jump lengths as one array, ordered by person and then by time (the order of df, which is already sorted that way).
In a live notebook a chart left as the last expression of a cell is interactive, with tooltips; this page shows static pictures of the same charts.
pairs = df[df["uid"].eq(df["uid"].shift())].assign(
jump_km=fastmob.jump_lengths(df, merge=True),
hour=lambda d: d["datetime"].dt.hour.astype(str),
)
traj = fastmob.TrajDataFrame(pairs)
traj.plot_ecdf(value_col="jump_km", x_unit="km", title="Jump lengths")
plot_ecdf draws the share of jumps that are at most each length. The curve starts at 0.25, the share of the four jumps that are as short as the shortest (1.4 km), and reaches 1 at the longest (6.3 km); the line is smoothed, so with four jumps it only shows the interface. Use second= to draw a second result on the same axes.
plot_bar draws one bar per row of its category. Here each person has two jumps, so Alice's bar shows only her first jump and Bob's only his: bars do not add up values, so aggregate first when a category has several rows.
plot_boxplot summarises all the values of a category: the box spans the quartiles with the median inside, and the whiskers reach the minimum and the maximum. With two jumps each, a box is just the range between them.
plot_scatter draws one point per row and one colour per series_col: Alice's points are in Rome and Bob's in New York, far apart on the longitude axis.
traj.plot_heatmap(
x_col="hour",
y_col="uid",
value_col="jump_km",
title="Jump length by person and hour",
)
plot_heatmap takes one row per cell: the colour is the jump that ended in that hour for that person. For all the chart functions and every option, see Chart your results and the Visualization reference.
Interpretation and limits
- Measures are per person and only as good as the sampling. A person recorded every second and one recorded twice a day are not comparable without cleaning and resampling.
- Everything here uses the straight-line distance between records, not road or walking distance.
- Six rows demonstrate the interface. They are not a statistical result.
Next steps
- Use a different dataframe library or your own column names: Columns and backends.
- Run the same measures on thousands of real users: Measure individual mobility.
- Remove noisy points before measuring: Clean noisy GPS data and find stops.
Related API
jump_lengths ·
radius_of_gyration ·
maximum_distance ·
waiting_times ·
TrajDataFrame ·
plot_ecdf ·
plot_bar ·
plot_boxplot ·
plot_scatter ·
plot_heatmap