The eleven habits of a cycling city
Urban planning, like any other data-driven field, benefits from a readable representation of the data that supports its decisions. Transportation planners are interested in the handful of meaningful, recurring flows behind the raw log numbers, ideally associated with a place and a time that a human can understand (“the morning rush down to the harbour”), and a transparent measure of the trip volume that is not covered by that readable representation. If a dozen habits can carry nearly all traffic and what they miss is mostly chance, we’ve won our bet and the data takes a form that can be easily discussed and understood.
But bike-share operators publish where their riders go as trip logs: Oslo Bysykkel’s open data publishes trip history, featuring where and when each trip started and ended. These logs contain enough information to produce an hourly origin-destination matrix: how many trips ran from each place to each other, hour by hour. For one summer of Oslo’s city bikes, cut into 66 neighbourhood-sized cells and the 168 hours of a week, that’s 66 × 66 × 168 = 731,808 numbers. Any insight hidden inside is hard to extract without specific knowledge and know-how.
Can we use machine learning and signal decomposition techniques to extract meaningful representations of that trip dataset; in other words, can we discover what the cycling habits of Oslo are?
Half a million trips
Oslo Bysykkel’s open trip data for June to August 2023 contains 496,073 trips. For easier spatial treatment, we can group stations into H3 cells (resolution 8), and keep the 66 cells where trips actually start or end. Map 1 denotes trip activity across the grid.
Drag the slider from 07:00 to 17:00 on a weekday and you can already see the city’s pulse: in the morning Grünerløkka, Frogner and Majorstuen empty out while Sentrum, Bjørvika and Aker Brygge fill up, and in the late afternoon it goes the other way. Fig 1 shows the same thing in time.
A weekday peaks at 576 departures per hour at 08:00 and 622 at 16:00; a weekend day climbs to a plateau of about 300 from noon to 18:00. Between 01:00 and 05:00 nothing moves since the bikes can only be unlocked from 5 am to 1 am.
Splitting the traffic into habits
To extract a readable/understandable representation of the dataset above, we set out to define habits. Intuitively, a habit corresponds to a sentence like “people from these cells ride to those cells at these hours”. Our goal is to summarize the city’s traffic well with a short list of such sentences: we want to find them by finding which small set of origin groups, destination groups and weekly rhythms rebuilds the table best.
In signal decomposition, a technique called Tucker decomposition does pretty much exactly this:
\[X \approx G \times_1 A \times_2 B \times_3 C\]- $X$ is the table: trips from each of the 66 origin cells to each of the 66 destination cells, in each of the 168 hours of the week, summed over the summer.
- $A$ holds groups of origin cells, each a weighting over the 66 cells; $B$ holds groups of destination cells.
- $C$ holds rhythms: each a curve over the 168 hours of the week.
- $G$, the core, says how many trips flow from origin group $i$ to destination group $j$ in each rhythm.
Each pair (origin group + destination group) is a candidate habit, with its own places and its own mix of rhythms. We’ll see below how we can find the right number of groups- for Oslo, 8 departure and 8 arrivals groups gave a good representation: that’s 64 habit candidates. We only keep those carrying more than 1% of the model’s trips, and pick the smallest set of habits summing up to 95% of all trips: that leaves eleven habits. We now have a decent representation of the data that fits in 1,984 numbers, against the original table’s 731,808.
If you care about modelling details: because we are modelling counts, we fit using a Poisson loss rather than plain squared error, and we use a tensor rather than a flat decomposition like NMF because flattening throws away the fact that the same origin appears in many pairs. In a test on synthetic data where true patterns were planted, Tucker recovered nine in ten of them while flat NMF found at most a third.
Eleven habits
Habit 4 is a convenient introduction to the model’s output: it describes the morning commute from Grünerløkka and Tøyen down to Bjørvika, Sentrum and Aker Brygge, the offices along the harbour: those three cells rank 1st, 2nd and 9th of the 66 for business listings in Overture’s places data. It carries 10% of the summer trips, is a very strong weekday pattern (91% of habit 4 trips take place Mon-Fri), and peaks at 233 trips per hour at 08:00. Habit 10 is the west side’s version, from Majorstuen and Frogner to Aker Brygge and Skøyen. Habits 7 and 5 are their respective pendulum counterpart at 16:00!
The three morning commutes (4, 8 and 10) carry 21% of all trips between them- but some habits aren’t commutes at all. The biggest, habit 1 (18.5%), starts and ends around Grünerløkka and Tøyen: the east side getting around its own neighbourhood, busiest at 16:00 and steady through weekend afternoons. Habit 6 is the same for Majorstuen and Frogner, and habit 9 takes people from Skøyen and Aker Brygge out to Bygdøy, with a quarter of its trips at weekends.
We have to be careful when labelling a habit as “weekday” vs “weekend”: 72% of the summer’s days are weekdays, so a habit with 76% of its trips on weekdays is about neutral. Only the morning commutes are clearly weekday habits, and no habit is weekend-only: on Saturdays and Sundays, Oslovians ride to and from the same places, just later and less.
How many habits?
The data does not tell us how many habits Oslo has: our model needs us to give it a resolution, i.e a number of origin groups, destination groups and rhythms (8 · 8 · 4 for the eleven habits above), and a higher resolution always produces more habits. To choose one, we need to know what each additional habit is large enough to add meaningful information to the description of the data.
We measure our model’s performance on data it has not seen: we fit it on the odd weeks of the summer, use it to predict the even weeks slot by slot (origin cell, destination cell, hour of the week), and count the missing/extra trips. Most of these slots are almost empty: over the seven test weeks, a slot holds 0.33 trips on average, and 87% of slots hold none. Below one expected trip, the model cannot say more than “at most one trip”, so that is all we ask of it: a prediction under one trip counts as correct whether the slot holds 0 or 1 trip. Under this rule, the model misplaces 24.7% of the test weeks’ trips at 8 · 8 · 4. Part of that error cannot be removed by any model, because trips vary at random around their average rates: an ideal model that knew those rates exactly would still misplace 17.3%. Only the gap between the two, 7.4 percentage points, reflects structure that a finer model could still capture. As a reference, using the odd weeks as prediction misplaces 28.5% of all trips.
Panel A shows how the error responds to the resolution. Between 2 · 2 · 1 and 8 · 8 · 4, we add 9 habits and the gap to the ideal model shrinks by 4.9 percentage points, from 12.3 to 7.4; it then stays at about 7.4 up to 12 · 12 · 6, and adding 18 habits beyond 8 · 8 · 4 (up to 24 · 24 · 12) only reduces it by a further 1.3 points. The error keeps decreasing, but adding more habits yields diminishing returns, and there is no resolution at which it clearly stops: we did not find a “true” number of habits in the data. From 4 · 4 · 2 onwards, the model also misplaces fewer trips than the raw counts of past weeks.
Panel B shows what those additional habits look like. The smallest habit carries 4.1% of all trips at 8 · 8 · 4. Beyond that resolution, the model mostly adds small habits that improve the fit marginally and that we would find harder to name and describe. This is why we kept 8 · 8 · 4: it is a choice based on readability, and a different use case could justify a finer resolution.
The city’s pulse
Press play and watch the week unfold: habits 4, 8 and 10 peak around 08:00, hand over to the neighbourhood habits through the day, and come back reversed as 7 and 5 at 16:00. At the weekend the commute panels stay low and everything else starts later. The last panel, all trips as observed, is there for comparison.
What’s left over
The real test of a model is data it has never seen. Let’s pick one week at random and refit the model on the rest of the summer. Drawing outside the holidays, we get 3–9 July as our test week.
The week came in 8% below the model: 34,843 trips against 37,880. The time plot shows how that error is distributed throughout the week. Thursday 6 July had 18.4 mm of rain and cycling volume was 32% lower than our prediction; Friday 7 July was nearly dry but as the last working day before fellesferie, ran 14% lower; the warm, dry weekend (22–24 °C) was busier than we predicted. Across the summer’s 92 days, a wet day (5 mm of rain or more between 07:00 and 21:00) cuts trips by about a fifth, every extra degree adds about 2%, and the three fellesferie weeks run about a quarter below the average week. All this information is unavailable to the habits!
It’s important to mention that the time plot does not tell us about the quality of the individual habits. Summed over all cells, a model fitted with a Poisson loss reproduces the average hourly totals of the weeks it was trained on, whatever its resolution, so our aggregated prediction (the dashed line) is close to an average summer week. The habits are really tested on the map, where the model has to place each trip in a specific cell. The map displays where they got it wrong, with two different lenses:
- In absolute error, the shortfall sits in the busy core: cells in Grünerløkka and Sentrum are each 200 to 300 departures short, because that’s where there are the most trips to lose.
- In relative error, the same cells are only 11–16% off, and the colour moves to quieter cells at the edges, where a few dozen trips either way make a big percentage. Blindern, the university campus, is a third down, most likely the summer break.
Under the same rule as in Fig 2, the model misplaces 17.2% of this week’s trips, against 11.7% for an ideal model. Both numbers are lower than over seven weeks because a single week is sparser: more of its slots fall under the one-trip rule, where a prediction is easier to get right. The flip side is that there are more slots for which we do not know the actual number of trips- we accepted some uncertainty from our model, and we got served more of it because a single week is too short to smooth randomness in trip choices.
One thing the residual does show: weeks aren’t alike. Across the summer, weekly totals ran from 31% below the average to 26% above it, busy in June and quiet in the holidays, with the 08:00 commute at about half its usual size in fellesferie. The habits describe a typical week, not a particular one.
So what would you do with eleven habits?
Eleven habits turn a very large number table into a page: where people ride from, where to, when, and how much each habit weighs. Good enough to talk about a city’s cycling in human terms (the morning rush to the harbour, the east side getting around), to compare neighbourhoods and to identify which flows a new lane or a station move would impact. Not good enough to forecast a particular week: weather and holidays move the volumes, and the habits don’t know about either. But the habits could serve as input to a more precise algorithm, along with calendar, cultural events, weather forecast etc!
Oslo is already investing in its cycle network. Since its cycling strategy was adopted in 2015, the city has built or upgraded 155 km of cycle paths, and an evaluation for the city found commuting by bike up 62% (Oslo kommune, 2024). Habits like the morning ride down to the harbour show which flows that network has to serve. Whether they actually ride on it is a question for a future post…
If this were a real product, next steps would probably include:
- Weekly volumes: keep the habits’ places and rhythms fixed and fit only their volumes, week by week;
- Weather: fine-tune our trip counts with rain and temperature;
- Other cities: the same pipeline can run on any city with an open trip log, and comparing their habits is where it gets interesting;
If you’re a planner, an operator, or anyone working in transportation, I’d love to talk to you. Get in touch!
Data: trips from Oslo Bysykkel’s open data (NLOD 2.0); basemap © OpenStreetMap contributors (ODbL), via Protomaps; weather from ERA5 (Open-Meteo); hexagons from Uber’s H3.