Bike-Share Trip Data Explained: One Schema, Five Cities

On this page

Problem statement

Lyft runs bike-share systems in several American cities and publishes their trips in one format: thirteen columns, one row per rental, a CSV per month. Code written for Washington's files runs unchanged on Chicago's or Boston's. That is convenient and slightly dangerous, because the columns being the same does not mean the rules behind them are. Whether an e-bike can end a trip away from a dock, how its location is recorded, which short trips are deleted before publication and when a rental is closed all differ between systems, and none of it is in the file.

This page reads the June 2025 file of five systems β€” Washington, Chicago, the San Francisco Bay Area, Boston and Jersey City β€” and walks through the schema column by column, measuring what each column means in each city.

Quick answer

Read each month into a DataFrame and compare a few shares side by side; the columns match exactly, the systems do not:

print(f"{'':16s}" + "".join(f"{c:>14s}" for c in CITIES))
print(f"{'columns':16s}" + "".join(f"{len(t[c].columns):>14d}" for c in CITIES))
print(f"{'same columns':16s}" + "".join(f"{str(list(t[c].columns) == list(t['Washington'].columns)):>14s}" for c in CITIES))
print(f"{'trips, June':16s}" + "".join(f"{len(t[c]):>14,}" for c in CITIES))
print(f"{'e-bike share':16s}" + "".join(f"{(t[c].rideable_type == 'electric_bike').mean():>14.1%}" for c in CITIES))
print(f"{'member share':16s}" + "".join(f"{(t[c].member_casual == 'member').mean():>14.1%}" for c in CITIES))
                    Washington       Chicago      Bay Area        Boston   Jersey City
columns                     13            13            13            13            13
same columns              True          True          True          True          True
trips, June            691,620       678,904       378,745       488,202        97,124
e-bike share             60.4%         62.6%         78.4%         28.3%         61.5%
member share             69.4%         57.0%         76.9%         67.8%         74.9%

All five have the same thirteen columns in the same order. Within them, the e-bike share runs from 28.3% of trips in Boston to 78.4% in the Bay Area, and members take from 57.0% of trips in Chicago to 76.9% in the Bay Area. Those are differences in the systems. The differences below are in the records.

Four small bar charts comparing Washington, Chicago, Bay Area, Boston and Jersey City in June 2025: e-bike share of trips from 28.3 to 78.4 percent; member share from 57.0 to 76.9 percent; e-bike trips with no start station from 48.1 percent in Washington to 0.0 in Jersey City; e-bike trips under a minute from 4.6 percent in Chicago to 0.0 in Boston and Jersey City. Four small bar charts comparing Washington, Chicago, Bay Area, Boston and Jersey City in June 2025: e-bike share of trips from 28.3 to 78.4 percent; member share from 57.0 to 76.9 percent; e-bike trips with no start station from 48.1 percent in Washington to 0.0 in Jersey City; e-bike trips under a minute from 4.6 percent in Chicago to 0.0 in Boston and Jersey City.
Five systems, one file format, June 2025. The top row describes the systems; the bottom row describes how their trips were recorded.

The thirteen columns, city by city

1. A row is a rental

row = t["Chicago"].iloc[0]
print(row.to_string())
print({c: t[c].ride_id.is_unique for c in CITIES})
ride_id                          0E43949BF937C7DA
rideable_type                       electric_bike
started_at             2025-06-05 21:24:56.104000
ended_at               2025-06-05 21:38:44.446000
start_station_name        Clark St & Armitage Ave
start_station_id                         CHI00474
end_station_name      Dearborn Pkwy & Delaware Pl
end_station_id                           CHI00306
start_lat                               41.918306
start_lng                              -87.636282
end_lat                                 41.898969
end_lng                                -87.629912
member_casual                              member
{'Washington': True, 'Chicago': True, 'Bay Area': True, 'Boston': True, 'Jersey City': True}

A row is one rental: an anonymous ride_id, unique within every file; the bike type; local start and end times to the millisecond; a start and end station (name and id) when there was one; start and end coordinates; and whether the rider had a membership. There is no rider id, no bike id and no route. Nothing links two trips by the same person or the same bike, by design.

2. Bike types and what each implies

for c in CITIES:
    print(f"{c:12s}", t[c].rideable_type.value_counts().to_dict())
Washington   {'electric_bike': 417566, 'classic_bike': 274054}
Chicago      {'electric_bike': 424789, 'classic_bike': 254115}
Bay Area     {'electric_bike': 296885, 'classic_bike': 81860}
Boston       {'classic_bike': 349936, 'electric_bike': 138266}
Jersey City  {'electric_bike': 59763, 'classic_bike': 37361}

Every system has two types, classic_bike and electric_bike (older files also had docked_bike). The type matters for more than speed: as the next steps show, in three of the five cities it decides whether a trip has a station at all.

3. Stations: ids, names and trips without one

for c in CITIES:
    d = t[c]
    e = d[d.rideable_type == "electric_bike"]
    print(f"{c:12s} id like {str(d.start_station_id.dropna().iloc[0]):>14s}   stations {d.start_station_id.nunique():>5,}   "
          f"e-bike trips with no start station {e.start_station_id.isna().mean():6.1%}")
Washington   id like          32046   stations   806   e-bike trips with no start station  48.1%
Chicago      id like       CHI00474   stations 1,376   e-bike trips with no start station  35.5%
Bay Area     id like          OK-A4   stations   584   e-bike trips with no start station   5.7%
Boston       id like         D32043   stations   554   e-bike trips with no start station   0.1%
Jersey City  id like          JC099   stations    85   e-bike trips with no start station   0.0%

Station ids are strings in four cities and numbers in one: Washington's five-digit codes, Chicago's CHI00474, the Bay Area's city-prefixed OK-A4 (Oakland), Boston's D32043 and Jersey City's JC099. Reading them as integers, as is natural in Washington, breaks everywhere else, so read them as strings. The last column is the largest difference in the schema. In Washington 48.1% of e-bike trips had no start station in June 2025, in Chicago 35.5%; in the Bay Area 5.7%; in Boston and Jersey City practically none. The same empty field means "a dockless trip" in one city and "a data error" in another.

4. Coordinates: station positions and rounded ones

Count the decimal places of the start latitude, separately for trips with and without a station. Using repr of the float, rather than formatting, shows what the file actually held:

def decimals(s):
    return s.dropna().map(lambda v: len(repr(v).split(".")[1]) if "." in repr(v) else 0)


for c in CITIES:
    d = t[c]
    free = d[d.start_station_id.isna()]
    print(f"{c:12s} no-station starts: {decimals(free.start_lat).value_counts(normalize=True).round(2).head(2).to_dict()}   "
          f"station starts, median decimals: {decimals(d[d.start_station_id.notna()].start_lat).median():.0f}")
lat = t["Washington"].start_lat.median()
print(f"a 0.01-degree cell at {lat:.1f}Β°N: {0.01 * 111.13:.2f} km north-south, "
      f"{0.01 * 111.32 * np.cos(np.radians(lat)):.2f} km east-west")
Washington   no-station starts: {2: 0.86, 1: 0.14}   station starts, median decimals: 6
Chicago      no-station starts: {2: 0.89, 1: 0.11}   station starts, median decimals: 6
Bay Area     no-station starts: {2: 0.89, 1: 0.11}   station starts, median decimals: 7
Boston       no-station starts: {2: 0.97, 1: 0.03}   station starts, median decimals: 13
Jersey City  no-station starts: {}   station starts, median decimals: 13
a 0.01-degree cell at 38.9Β°N: 1.11 km north-south, 0.87 km east-west

Every trip without a station has its coordinates rounded to two decimal places (one, when the second is a zero): a grid of cells 1.11 km tall and 0.87 km wide at Washington's latitude. Trips at a station carry the station's own coordinates, with six or seven decimals in three cities β€” and thirteen in Boston and Jersey City, which is a floating-point artefact of how those positions were stored, not centimetre precision. Neither number describes where a rider was; one is a dock, the other a kilometre-wide cell.

Grid of five cities by four recording rules. Washington, Chicago and the Bay Area have dockless e-bike trips, keep sub-minute e-bike trips and cap e-bike rentals near 8 hours; Boston and Jersey City do none of these. Classic rentals are capped near 25 hours everywhere except Jersey City, whose longest classic trip lasted 5,656 minutes. Grid of five cities by four recording rules. Washington, Chicago and the Bay Area have dockless e-bike trips, keep sub-minute e-bike trips and cap e-bike rentals near 8 hours; Boston and Jersey City do none of these. Classic rentals are capped near 25 hours everywhere except Jersey City, whose longest classic trip lasted 5,656 minutes.
What each city's file implies about its rules, measured from the June 2025 trips rather than taken from documentation.

5. Times: local, unzoned, and filtered differently

for c in CITIES:
    d = t[c]
    mins = (d.ended_at - d.started_at).dt.total_seconds() / 60
    by = mins.groupby(d.rideable_type)
    print(f"{c:12s} under 1 min: classic {(by.get_group('classic_bike') < 1).mean():.1%}, "
          f"e-bike {(by.get_group('electric_bike') < 1).mean():.1%}   "
          f"longest: classic {by.max()['classic_bike']:,.0f} min, e-bike {by.max()['electric_bike']:,.0f} min")
Washington   under 1 min: classic 0.0%, e-bike 4.1%   longest: classic 1,500 min, e-bike 480 min
Chicago      under 1 min: classic 0.0%, e-bike 4.6%   longest: classic 1,575 min, e-bike 480 min
Bay Area     under 1 min: classic 0.0%, e-bike 1.2%   longest: classic 1,500 min, e-bike 527 min
Boston       under 1 min: classic 0.0%, e-bike 0.0%   longest: classic 1,500 min, e-bike 1,500 min
Jersey City  under 1 min: classic 0.0%, e-bike 0.0%   longest: classic 5,656 min, e-bike 1,500 min

No classic-bike trip under a minute survives in any city: the operators' published rule of removing them is applied. For e-bikes it is applied in Boston and Jersey City and not in the other three, where 1.2–4.6% of e-bike trips last under a minute. The longest trips show the cut-offs at which each system closes a rental: 1,500 minutes (25 hours) for classic bikes in most cities, 480 minutes for e-bikes in Washington and Chicago, 1,500 for e-bikes in Boston and Jersey City β€” and a classic-bike trip of 5,656 minutes in Jersey City that no cap stopped. The timestamps themselves carry no time zone in any file; each is local wall-clock time.

The same questions in five systems

The data used on this page

The June 2025 trip files of five systems that Lyft operates and publishes in the same format: Capital Bikeshare (Washington, D.C.), Divvy (Chicago), Bay Wheels (San Francisco Bay Area), Bluebikes (Boston) and Citi Bike's Jersey City file. Each is published under its operator's data licence agreement.

from pathlib import Path
import zipfile

import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
FILES = {
    "Washington": "https://s3.amazonaws.com/capitalbikeshare-data/202506-capitalbikeshare-tripdata.zip",
    "Chicago": "https://divvy-tripdata.s3.amazonaws.com/202506-divvy-tripdata.zip",
    "Bay Area": "https://s3.amazonaws.com/baywheels-data/202506-baywheels-tripdata.csv.zip",
    "Boston": "https://s3.amazonaws.com/hubway-data/202506-bluebikes-tripdata.zip",
    "Jersey City": "https://s3.amazonaws.com/tripdata/JC-202506-citibike-tripdata.csv.zip",
}
CITIES = list(FILES)
t = {}
for city, url in FILES.items():
    f = Path("data") / url.rsplit("/", 1)[-1]
    if not f.exists():
        f.write_bytes(requests.get(url, timeout=600).content)
    with zipfile.ZipFile(f) as z:
        name = next(n for n in z.namelist() if n.endswith(".csv") and not n.startswith("__MACOSX"))
        t[city] = pd.read_csv(z.open(name), dtype={"start_station_id": str, "end_station_id": str},
                              parse_dates=["started_at", "ended_at"], low_memory=False)
print({c: f"{len(d):,}" for c, d in t.items()})
{'Washington': '691,620', 'Chicago': '678,904', 'Bay Area': '378,745', 'Boston': '488,202', 'Jersey City': '97,124'}

Example 1: when each city rides

hours = pd.DataFrame({c: t[c].started_at.dt.hour.value_counts(normalize=True).sort_index() for c in CITIES})
weekday = {c: t[c][t[c].started_at.dt.dayofweek < 5] for c in CITIES}
peak = {c: weekday[c].started_at.dt.hour.isin([7, 8, 9, 16, 17, 18]).mean() for c in CITIES}
print("share of weekday trips in the six commuting hours:", {c: f"{v:.1%}" for c, v in peak.items()})
print("busiest start hour:", hours.idxmax().to_dict())
share of weekday trips in the six commuting hours: {'Washington': '42.8%', 'Chicago': '45.8%', 'Bay Area': '51.4%', 'Boston': '47.9%', 'Jersey City': '46.4%'}
busiest start hour: {'Washington': 17, 'Chicago': 17, 'Bay Area': 17, 'Boston': 17, 'Jersey City': 17}

The shared schema does make comparisons quick. All five systems peak at 5 p.m. The share of weekday trips in the six commuting hours ranges from 42.8% in Washington β€” where, as the commuting page shows, much of the system serves visitors to the Mall β€” to 51.4% in the Bay Area.

Example 2: how long a trip lasts

med = {}
for c in CITIES:
    d = t[c]
    m = (d.ended_at - d.started_at).dt.total_seconds() / 60
    ok = (m >= 1) & d.end_lat.notna()
    med[c] = m[ok].groupby([d.rideable_type[ok], d.member_casual[ok]]).median().round(1)
print(pd.DataFrame(med).to_string())
                             Washington  Chicago  Bay Area  Boston  Jersey City
rideable_type member_casual                                                    
classic_bike  casual               14.9     17.5      14.7    16.6         11.1
              member                8.5     10.2       7.8     9.8          5.9
electric_bike casual               10.8     11.5      12.1    13.3          7.6
              member                9.7      9.3       8.8    10.5          5.6

Once trips under a minute and unreturned bikes are removed, the pattern holds in every city: members ride shorter trips than casual riders, and casual riders ride shorter on e-bikes than on classic bikes. Jersey City's trips are the shortest, with a median of 5.6–5.9 minutes for members β€” a small, dense system of 85 stations β€” and Chicago's casual classic-bike trips the longest, at 17.5 minutes.

Line chart of the share of weekday trips by start hour in five cities. All have a morning peak at 8 a.m. and a higher evening peak at 5 p.m.; the Bay Area's morning peak is the highest and Washington's evening peak the lowest. Line chart of the share of weekday trips by start hour in five cities. All have a morning peak at 8 a.m. and a higher evening peak at 5 p.m.; the Bay Area's morning peak is the highest and Washington's evening peak the lowest.
Weekday hourly profiles, June 2025. The shared schema makes this comparison a few lines of code.

A shared format is not a shared definition

The trip file is the end of a pipeline that starts in the bike, the dock and the app, and each system's pipeline makes its own decisions before anything is published. Three of them change what the columns mean.

Where an e-bike trip may end. A docked system records stations at both ends of every trip. A system that lets e-bikes lock to a rack or a post records a station only when the bike happened to end at one; the rest of the trips carry an empty station and a rounded position. That one policy is why "trips per station" covers all but 0.1% of e-bike trips in Boston and about half in Washington.

What is published about location. A station has a fixed position, so publishing it reveals nothing about a rider. A free-floating bike's position could be someone's front door, and the published files round it to two decimal places β€” most likely for that reason, though the system data pages do not say. The rounding is the same in every city; its consequence is that dockless trips can be analysed only at the scale of the rounding, never snapped to a street.

Which trips are published. Trips under a minute and rentals that never ended are handled differently by bike type and by city, and the system data pages do not always say so. Measuring them, as above, is the only way to know which rules apply to a given file β€” and to apply the missing ones yourself so that five cities' numbers can be compared.

Reading a new city's files

  • Read station ids as strings. Only Washington's are numeric.
  • Check the e-bike share with no station before any station-level analysis.
  • Rounded coordinates have one or two decimals, never more; anything with more came from a station.
  • Apply the one-minute rule to every bike type if comparing cities.
  • Look at the longest trips to find each system's rental cap β€” and the trips that escape it.
  • Times are local and unzoned. Comparing cities in different time zones by clock hour is fine; by instant it is not.
  • File names and zip contents differ (.csv.zip against .zip, Jersey City's JC- prefix, macOS __MACOSX entries). Find the CSV inside each zip rather than guessing its name.

FAQ

Do Divvy, Citi Bike, Bluebikes, Bay Wheels and Capital Bikeshare use the same data format?

Their current trip files have the same thirteen columns: ride_id, rideable_type, started_at, ended_at, start and end station name and id, start and end latitude and longitude, and member_casual. The rules behind the columns β€” dockless e-bikes, coordinate rounding, short-trip filtering, rental caps β€” differ.

Why do some bike-share trips have no start station?

Because the system lets e-bikes start or end away from a dock. In June 2025 that was 48.1% of e-bike trips in Washington, 35.5% in Chicago and practically none in Boston and Jersey City, where e-bikes stay docked.