Skip to content

Open in Colab Launch Binder

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.

%pip install -q 'fastmob[vis]>=0.3.4' pandas

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.

import fastmob

jumps = fastmob.jump_lengths(df)
jumps
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.

rg = fastmob.radius_of_gyration(df)
rg
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.

longest = fastmob.maximum_distance(df)
longest
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.

waits = fastmob.waiting_times(df)
waits
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")

svg

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.

traj.plot_bar(category_col="uid", value_col="jump_km", title="Jump length by person")

svg

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.

traj.plot_boxplot(
    category_col="uid", value_col="jump_km", title="Jump length by person"
)

svg

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.

traj.plot_scatter(
    x_col="lng", y_col="lat", series_col="uid", title="Where each jump ends"
)

svg

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",
)

svg

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

jump_lengths · radius_of_gyration · maximum_distance · waiting_times · TrajDataFrame · plot_ecdf · plot_bar · plot_boxplot · plot_scatter · plot_heatmap