An analysis of the rideshare marketplace in NYC at airports
0
stars
10
commits
Python
primary language
Sep 10, 2026
updated
Anyone who wants to know how the airport ride-hail market in New York City (NYC) really works runs into the same wall: the regulator publishes about 40 gigabytes of raw trip records, and nobody publishes an airport summary. This project turns those records into an interactive notebook. It keeps the 122,806,218 trips that touch John F. Kennedy International Airport (JFK), LaGuardia Airport (LGA) or Newark Liberty International Airport (EWR) between February 2019 and June 2026, and it answers the market questions from the trips themselves rather than from a press release.
One marimo notebook holds the whole pipeline: the download, the transform, the storage layer, the analysis and the conclusions.
You can see the static html site here: https://nyc-rideshare-static.omar-irfan.workers.dev/


uv run marimo edit nyc_airports.py
The notebook carries PEP 723 inline metadata, so uv builds the environment
from the file itself. Without uv:
pip install -e ".[dev]"
marimo edit nyc_airports.py
The New York City Taxi and Limousine Commission (TLC) publishes one Apache Parquet file for each month on a public host. The pipeline downloads a month, keeps the airport trips, writes a slice, and deletes the download. Six small tables come out of the slices, and the four blue boxes on the second row are what the notebook does with them.
flowchart LR
cdn([TLC open data]) -->|downloads| extract[Extraction pipeline]
extract -->|writes| slices[(Monthly slices)]
slices -->|sums| tables[(Six tables)]
tables -->|reads| load[Load and profile]
load --> clean[Clean and scope]
clean --> measure[Charts and scorecards]
measure --> answers[Answers and targets]
answers --> analyst([Analyst])
Inside one month, the filter runs at scan time so that about 18 million rows become about 1.5 million before anything reaches memory. The surviving rows get the derived timing, money and flag columns. The notebook then derives which platforms and which airports it may compare, and draws the charts from that.
flowchart TD
scan[scan monthly file] --> gate{airport trip?}
gate -->|no| drop[drop row]
gate -->|yes| derive[derive metrics]
derive --> slices[(processed parquet)]
slices --> exports[export aggregates]
exports --> tables[(six tables)]
tables --> platforms[derive platforms]
platforms --> charts[charts and tables]
charts --> reader([Reader])
Section 0.10 has a button that fetches the archive and builds the aggregates. Defaults to the full history: 89 monthly files, about 40 GB of download. One raw file exists on disk at a time — fetch, filter, write, delete — so peak disk is about 450 MB plus 36 MB per month kept. Interrupting is safe; completed months are skipped on the next run.
If you already have the six aggregate tables, drop them in data/exports/ and
the notebook loads them without downloading anything. They total 164 KB.
Section 2 also loads one month at trip level to demonstrate the cleaning. That
needs data/processed/airport_2025-01.parquet, which the extraction produces.
The notebook exists to answer five questions. Section 8 computes each answer for whichever platform the selector holds, so the numbers below are one reading of it: Uber and Lyft, airport pickups only, JFK and LaGuardia, over the ten months from September 2025 to June 2026. Two of the five have no answer in this data. The section says so and names the missing field, because a proxy that looks like an answer is worse than a gap.
| Measurement | JFK Uber | JFK Lyft | LGA Uber | LGA Lyft |
|---|---|---|---|---|
| Median rider fare | $68.08 | $70.53 | $56.26 | $48.81 |
| Rider fare per mile | $4.29 | $4.18 | $5.47 | $4.90 |
| Median driver gross | $57.82 | $58.99 | $43.82 | $39.36 |
| Driver gross over rider payment | 67.0% | 67.6% | 60.1% | 62.7% |
| Driver gross per engaged hour | $65.01 | $66.86 | $62.16 | $60.90 |
| Minimum-pay binding rate | 4.0% | 2.2% | 2.6% | 1.3% |
Read the rows across, not down. Fare per mile runs $4.18 to $4.29 at JFK and $4.90 to $5.47 at LaGuardia, so the airports differ by more than the platforms do at either airport. Price each one as its own market.
The driver side is not set by a pricing team alone. The NYC minimum-pay standard computes a floor for each trip from the miles, the minutes and the utilization rate, and the fare has no part in that formula. The binding rate is the share of pickups where that floor pays more than the rider paid in total. Where it is high, regulation sets the price of supply. The archive also holds the ceiling case: Juno paid a median 79.7% of the rider payment and stopped trading in 2021.
What this data cannot give. No surge multiplier, no rider-side experiment, no bonus ledger and no cost line. Inside a platform, model the gap between the fare-derived pay and the formula floor on each trip, then measure the share of airport supply the floor holds.
Not answerable from this data. The trip file carries no driver identifier, no tenure, no home borough and no bonus record. Any segment described from it is an invention.
What the file does give is supply by hour. The hours from midnight to 05:00 carry 11.2% of Uber's JFK pickups and 12.7% of Lyft's, against 5.3% and 7.4% at LaGuardia. Those are also the hours where the pay floor binds hardest: Uber's binding rate at LaGuardia peaks at 9.3% in the 01:00 hour against 2.6% across the whole day. Supply is hardest to hold overnight, which is where a bonus lands.
A bonus and the pay floor also pay for the same thing here, and this data cannot separate them, because both show up as driver gross above rider payment. Read the binding rate as the floor, since the formula produces that result with no bonus at all.
What to instrument. Driver identifier, tenure band, acceptance rate, and bonus payments as their own field. Then segment on what share of a driver's week is airport work, and test a bonus against the hours above.
Set every target at the best value another live platform reaches now, at the same airport, in the same window, under the same regulation and the same weather. A target set that way is demonstrated rather than aspirational.
On that basis Lyft holds the JFK targets for driver gross, payout share, gross per engaged hour and mean tip, and Uber trails on all four while holding the shorter curb wait. LaGuardia splits: Uber leads on gross per engaged hour ($62.16 against $60.90) and on mean tip ($5.17 against $4.58), and trails on payout share by 2.6 points.
Rank the airports by the size of the gaps rather than the level of the measurements, because the airports differ by more than the platforms do. One network-wide target reads one of the two airports wrong.
Watch the monitoring more than the measurements. The last months of the archive are thin while filings still arrive, so a dashboard that reads the last two months as a trend is reporting publication lag. Compare each month against the same month a year earlier: the seasonal swing is larger than most of the gaps in the table.
Not answerable at all. Every public trip file holds completed trips only. There is no cancelled-trip record, no rider identifier and no driver identifier. Nothing in this dataset separates a cancellation from a trip that never happened.
The nearest legal signal is unmet demand. The request-to-pickup delay is in the file, and an arrival bank with no matching rise in pickups shows in the hourly profile. Both point at friction. Neither is a cancellation rate.
What to instrument. A cancellation event with five fields: the timestamp, the side that cancelled, the seconds since the request, the stated reason, and the queue position at the airport. The queue position is the one that shows whether the cancellation follows the wait or the fare.
Shared rides have gone from airport pickups. Uber's shared-ride request rate is 4.6% at JFK and 4.1% at LaGuardia. Lyft's is 0.03% and 0.01%, which is a closed product rather than a small one.
The archive holds the counterexample. Via ran a pooling-first service and reached a 94.7% shared-ride request rate on its JFK pickups in November 2019, so the collapse is a product decision and not a limit on what riders will accept.
Curb wait is comparable between platforms only from March 2025, when the reporting rule took effect. Uber's median airport curb wait is 46 seconds at JFK and 35.5 seconds at LaGuardia. Lyft's is exactly 60 seconds at both, and a median that lands on an exact minute is a sign of minute-level rounding, so read the direction of that difference and not its size.
What this data cannot give. No premium tier, no vehicle class beyond the wheelchair flag, and no seat count. Any mode analysis past shared against standard needs the product tier on each trip.
The trip records are published by TLC as open data. This repository holds trip data from 2019 - June 2026. The extraction also downloads it from TLC's CDN at run time if required.
MIT for the code. The trip records carry TLC's own terms.
10 commits
Python
100.0%
An analysis of the rideshare marketplace in NYC at airports
0
stars
10
commits
Python
primary language
Sep 10, 2026
updated
Anyone who wants to know how the airport ride-hail market in New York City (NYC) really works runs into the same wall: the regulator publishes about 40 gigabytes of raw trip records, and nobody publishes an airport summary. This project turns those records into an interactive notebook. It keeps the 122,806,218 trips that touch John F. Kennedy International Airport (JFK), LaGuardia Airport (LGA) or Newark Liberty International Airport (EWR) between February 2019 and June 2026, and it answers the market questions from the trips themselves rather than from a press release.
One marimo notebook holds the whole pipeline: the download, the transform, the storage layer, the analysis and the conclusions.
You can see the static html site here: https://nyc-rideshare-static.omar-irfan.workers.dev/


uv run marimo edit nyc_airports.py
The notebook carries PEP 723 inline metadata, so uv builds the environment
from the file itself. Without uv:
pip install -e ".[dev]"
marimo edit nyc_airports.py
The New York City Taxi and Limousine Commission (TLC) publishes one Apache Parquet file for each month on a public host. The pipeline downloads a month, keeps the airport trips, writes a slice, and deletes the download. Six small tables come out of the slices, and the four blue boxes on the second row are what the notebook does with them.
flowchart LR
cdn([TLC open data]) -->|downloads| extract[Extraction pipeline]
extract -->|writes| slices[(Monthly slices)]
slices -->|sums| tables[(Six tables)]
tables -->|reads| load[Load and profile]
load --> clean[Clean and scope]
clean --> measure[Charts and scorecards]
measure --> answers[Answers and targets]
answers --> analyst([Analyst])
Inside one month, the filter runs at scan time so that about 18 million rows become about 1.5 million before anything reaches memory. The surviving rows get the derived timing, money and flag columns. The notebook then derives which platforms and which airports it may compare, and draws the charts from that.
flowchart TD
scan[scan monthly file] --> gate{airport trip?}
gate -->|no| drop[drop row]
gate -->|yes| derive[derive metrics]
derive --> slices[(processed parquet)]
slices --> exports[export aggregates]
exports --> tables[(six tables)]
tables --> platforms[derive platforms]
platforms --> charts[charts and tables]
charts --> reader([Reader])
Section 0.10 has a button that fetches the archive and builds the aggregates. Defaults to the full history: 89 monthly files, about 40 GB of download. One raw file exists on disk at a time — fetch, filter, write, delete — so peak disk is about 450 MB plus 36 MB per month kept. Interrupting is safe; completed months are skipped on the next run.
If you already have the six aggregate tables, drop them in data/exports/ and
the notebook loads them without downloading anything. They total 164 KB.
Section 2 also loads one month at trip level to demonstrate the cleaning. That
needs data/processed/airport_2025-01.parquet, which the extraction produces.
The notebook exists to answer five questions. Section 8 computes each answer for whichever platform the selector holds, so the numbers below are one reading of it: Uber and Lyft, airport pickups only, JFK and LaGuardia, over the ten months from September 2025 to June 2026. Two of the five have no answer in this data. The section says so and names the missing field, because a proxy that looks like an answer is worse than a gap.
| Measurement | JFK Uber | JFK Lyft | LGA Uber | LGA Lyft |
|---|---|---|---|---|
| Median rider fare | $68.08 | $70.53 | $56.26 | $48.81 |
| Rider fare per mile | $4.29 | $4.18 | $5.47 | $4.90 |
| Median driver gross | $57.82 | $58.99 | $43.82 | $39.36 |
| Driver gross over rider payment | 67.0% | 67.6% | 60.1% | 62.7% |
| Driver gross per engaged hour | $65.01 | $66.86 | $62.16 | $60.90 |
| Minimum-pay binding rate | 4.0% | 2.2% | 2.6% | 1.3% |
Read the rows across, not down. Fare per mile runs $4.18 to $4.29 at JFK and $4.90 to $5.47 at LaGuardia, so the airports differ by more than the platforms do at either airport. Price each one as its own market.
The driver side is not set by a pricing team alone. The NYC minimum-pay standard computes a floor for each trip from the miles, the minutes and the utilization rate, and the fare has no part in that formula. The binding rate is the share of pickups where that floor pays more than the rider paid in total. Where it is high, regulation sets the price of supply. The archive also holds the ceiling case: Juno paid a median 79.7% of the rider payment and stopped trading in 2021.
What this data cannot give. No surge multiplier, no rider-side experiment, no bonus ledger and no cost line. Inside a platform, model the gap between the fare-derived pay and the formula floor on each trip, then measure the share of airport supply the floor holds.
Not answerable from this data. The trip file carries no driver identifier, no tenure, no home borough and no bonus record. Any segment described from it is an invention.
What the file does give is supply by hour. The hours from midnight to 05:00 carry 11.2% of Uber's JFK pickups and 12.7% of Lyft's, against 5.3% and 7.4% at LaGuardia. Those are also the hours where the pay floor binds hardest: Uber's binding rate at LaGuardia peaks at 9.3% in the 01:00 hour against 2.6% across the whole day. Supply is hardest to hold overnight, which is where a bonus lands.
A bonus and the pay floor also pay for the same thing here, and this data cannot separate them, because both show up as driver gross above rider payment. Read the binding rate as the floor, since the formula produces that result with no bonus at all.
What to instrument. Driver identifier, tenure band, acceptance rate, and bonus payments as their own field. Then segment on what share of a driver's week is airport work, and test a bonus against the hours above.
Set every target at the best value another live platform reaches now, at the same airport, in the same window, under the same regulation and the same weather. A target set that way is demonstrated rather than aspirational.
On that basis Lyft holds the JFK targets for driver gross, payout share, gross per engaged hour and mean tip, and Uber trails on all four while holding the shorter curb wait. LaGuardia splits: Uber leads on gross per engaged hour ($62.16 against $60.90) and on mean tip ($5.17 against $4.58), and trails on payout share by 2.6 points.
Rank the airports by the size of the gaps rather than the level of the measurements, because the airports differ by more than the platforms do. One network-wide target reads one of the two airports wrong.
Watch the monitoring more than the measurements. The last months of the archive are thin while filings still arrive, so a dashboard that reads the last two months as a trend is reporting publication lag. Compare each month against the same month a year earlier: the seasonal swing is larger than most of the gaps in the table.
Not answerable at all. Every public trip file holds completed trips only. There is no cancelled-trip record, no rider identifier and no driver identifier. Nothing in this dataset separates a cancellation from a trip that never happened.
The nearest legal signal is unmet demand. The request-to-pickup delay is in the file, and an arrival bank with no matching rise in pickups shows in the hourly profile. Both point at friction. Neither is a cancellation rate.
What to instrument. A cancellation event with five fields: the timestamp, the side that cancelled, the seconds since the request, the stated reason, and the queue position at the airport. The queue position is the one that shows whether the cancellation follows the wait or the fare.
Shared rides have gone from airport pickups. Uber's shared-ride request rate is 4.6% at JFK and 4.1% at LaGuardia. Lyft's is 0.03% and 0.01%, which is a closed product rather than a small one.
The archive holds the counterexample. Via ran a pooling-first service and reached a 94.7% shared-ride request rate on its JFK pickups in November 2019, so the collapse is a product decision and not a limit on what riders will accept.
Curb wait is comparable between platforms only from March 2025, when the reporting rule took effect. Uber's median airport curb wait is 46 seconds at JFK and 35.5 seconds at LaGuardia. Lyft's is exactly 60 seconds at both, and a median that lands on an exact minute is a sign of minute-level rounding, so read the direction of that difference and not its size.
What this data cannot give. No premium tier, no vehicle class beyond the wheelchair flag, and no seat count. Any mode analysis past shared against standard needs the product tier on each trip.
The trip records are published by TLC as open data. This repository holds trip data from 2019 - June 2026. The extraction also downloads it from TLC's CDN at run time if required.
MIT for the code. The trip records carry TLC's own terms.
10 commits
Python
100.0%