Taxi Zone IDs That Do Not Join: Zones 264 and 265 and Repeated Names
On this page
Problem statement
You join a year of taxi trips to the TLC's zone shapefile and the totals no longer match: some trips have vanished. Or you join on zone names and one neighbourhood suddenly has more pickups than it should. Both come from the way the zone tables are built. The trip records use 265 location IDs, the shapefile has 263 polygons, and the lookup table repeats some names across several IDs. This page measures each problem on the 2025 yellow-taxi records and shows the joins that keep every trip where it belongs.
Quick answer
Join trips to zones on LocationID, never on the zone name, and keep IDs 264 ("Unknown") and 265 ("Outside of NYC") in your totals as their own category, because they have no polygon and an inner join to the shapefile drops them.
pu = con.sql("""select PULocationID as LocationID, count(*) as n from trips group by 1""").df()
dropoff = con.sql("""select DOLocationID as LocationID, count(*) as n from trips group by 1""").df()
missing = sorted((set(pu.LocationID) | set(dropoff.LocationID)) - set(zones.LocationID))
print(f"zone IDs in the trips with no shape: {missing}")
for name, df in [("pickups", pu), ("drop-offs", dropoff)]:
lost = df[~df.LocationID.isin(zones.LocationID)].n.sum()
print(f"{name} lost in a join to the shapefile: {lost:,} ({lost / df.n.sum():.2%})")
zone IDs in the trips with no shape: [264, 265]
pickups lost in a join to the shapefile: 103,043 (0.21%)
drop-offs lost in a join to the shapefile: 324,266 (0.67%)
Exactly two IDs used in the trips have no shape: 264 and 265. An inner join to the shapefile loses 103,043 pickups (0.21%) and 324,266 drop-offs (0.67%). Small shares, but they are not random: they are the trips leaving the city and those whose location the meter did not know.
Where the zone tables disagree
1. What 264 and 265 mean
print(lookup[lookup.LocationID >= 264].to_string(index=False))
print(f"shapefile: {len(zones)} polygons, IDs {zones.LocationID.min()}-{zones.LocationID.max()}; lookup: {len(lookup)} rows")
LocationID Borough Zone service_zone
264 Unknown NaN NaN
265 NaN Outside of NYC NaN
shapefile: 263 polygons, IDs 1-263; lookup: 265 rows
The lookup table has 265 rows and the shapefile 263 polygons, numbered 1 to 263. ID 264 is "Unknown", with the borough "Unknown" and no zone name; 265 is "Outside of NYC", with no borough. Neither can be drawn, but both are legitimate values that every count should keep.
2. Names that repeat
rep = lookup[lookup.Zone.duplicated(keep=False)].sort_values("Zone")
print(rep[["LocationID", "Borough", "Zone"]].to_string(index=False))
LocationID Borough Zone
56 Queens Corona
57 Queens Corona
103 Manhattan Governor's Island/Ellis Island/Liberty Island
104 Manhattan Governor's Island/Ellis Island/Liberty Island
105 Manhattan Governor's Island/Ellis Island/Liberty Island
Two names repeat. "Corona" is both zone 56 and zone 57 in Queens, and three IDs, 103 to 105, share the name "Governor's Island/Ellis Island/Liberty Island". A name is a label, not a key.
3. Join on the ID, never the name
by_name = pu.merge(lookup, on="LocationID").groupby("Zone").n.sum()
by_id = pu.merge(lookup, on="LocationID").set_index("LocationID").n
print(f"'Corona' by name: {by_name.get('Corona', 0):,} pickups; by ID: zone 56 {by_id.get(56, 0):,}, zone 57 {by_id.get(57, 0):,}")
g = zones.merge(by_name.rename("n_by_name"), left_on="zone", right_index=True, how="left")
print(f"pickups on the map if joined by name: {g.n_by_name.sum():,.0f} against {by_id[by_id.index < 264].sum():,} by ID")
'Corona' by name: 14,694 pickups; by ID: zone 56 13,609, zone 57 1,085
pickups on the map if joined by name: 48,634,267 against 48,619,559 by ID
Grouping by name merges the two Coronas into 14,694 pickups; by ID they are 13,609 in zone 56 and 1,085 in zone 57. Joined back to the map by name, both Corona polygons receive the combined count, and the map shows 48,634,267 pickups where there were 48,619,559 β the merged total counted twice, invisible on a map that has no total on it.
4. Keep the unknowns in the totals
od = con.sql("""select PULocationID in (264, 265) as pu_unknown, DOLocationID in (264, 265) as do_unknown, count(*) as n
from trips group by 1, 2 order by 1, 2""").df()
print(od.to_string(index=False))
print(f"265 ('Outside of NYC') as a drop-off: {dropoff.set_index('LocationID').n.get(265, 0):,} trips; as a pickup: {pu.set_index('LocationID').n.get(265, 0):,}")
pu_unknown do_unknown n
False False 48350945
False True 268614
True False 47391
True True 55652
265 ('Outside of NYC') as a drop-off: 219,073 trips; as a pickup: 22,170
In 268,614 trips only the drop-off is unknown or outside the city, in 47,391 only the pickup, and in 55,652 both. Zone 265 is overwhelmingly a destination: 219,073 drop-offs against 22,170 pickups, the trips that leave New York for New Jersey, Long Island and Westchester.
The islands, and older copies of the file
The data used on this page
The NYC Taxi & Limousine Commission's yellow-taxi trip records for 2025, and its taxi zones shapefile and zone lookup table as issued on 18 February 2026 (saved copies).
from pathlib import Path
import duckdb
import geopandas as gpd
import numpy as np
import pandas as pd
import requests
Path("data").mkdir(exist_ok=True)
TLC = "https://d37ci6vzurychx.cloudfront.net/trip-data/"
FILES = [f"yellow_tripdata_2025-{m:02d}.parquet" for m in range(1, 13)]
for name in FILES:
if not Path(f"data/{name}").exists():
with requests.get(TLC + name, stream=True, timeout=600) as r: # 50-80 MB a month
r.raise_for_status()
with open(f"data/{name}", "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
# zones and lookup as the TLC served them on 15 October 2026 (it reissues them; the live copies are
# https://d37ci6vzurychx.cloudfront.net/misc/taxi_zones.zip and .../misc/taxi_zone_lookup.csv)
for name in ["nyc-taxi-zones-2026-10-15.zip", "nyc-taxi-zone-lookup-2026-10-15.csv"]:
if not Path(f"data/{name}").exists():
Path(f"data/{name}").write_bytes(requests.get(f"https://data.spatialworkflow.io/{name}", timeout=300).content)
zones = gpd.read_file("zip://data/nyc-taxi-zones-2026-10-15.zip!taxi_zones/taxi_zones.shp") # EPSG:2263, feet
lookup = pd.read_csv("data/nyc-taxi-zone-lookup-2026-10-15.csv")
con = duckdb.connect()
con.sql("create view trips as select * from read_parquet('data/yellow_tripdata_2025-*.parquet', union_by_name = true)")
print(f"{con.sql('select count(*) from trips').fetchone()[0]:,} rows in trips; {len(zones)} zone polygons, {len(lookup)} lookup rows")
48,722,602 rows in trips; 263 zone polygons, 265 lookup rows
Example 1: a zone with three IDs in the lookup
isl = lookup[lookup.LocationID.isin([103, 104, 105])]
print(isl[["LocationID", "Zone"]].to_string(index=False))
print("their pickups in 2025:", pu.set_index("LocationID").n.reindex([103, 104, 105]).fillna(0).astype(int).to_dict())
print("polygons in the shapefile:", zones[zones.LocationID.isin([103, 104, 105])][["LocationID", "zone"]].values.tolist())
LocationID Zone
103 Governor's Island/Ellis Island/Liberty Island
104 Governor's Island/Ellis Island/Liberty Island
105 Governor's Island/Ellis Island/Liberty Island
their pickups in 2025: {103: 0, 104: 0, 105: 7}
polygons in the shapefile: [[103, "Governor's Island/Ellis Island/Liberty Island"], [104, "Governor's Island/Ellis Island/Liberty Island"], [105, "Governor's Island/Ellis Island/Liberty Island"]]
The three island IDs each have a polygon in this copy of the shapefile, and between them 7 pickups in 2025, all in zone 105. Joined by name, the 7 would be repeated on all three islands.
Example 2: older copies of the shapefile
dup = zones.LocationID.duplicated().sum()
print(f"duplicated LocationID values in this copy: {dup}; distinct IDs {zones.LocationID.nunique()}")
print(f"the CRS is {zones.crs.to_string()} (feet); area of zone 1, Newark Airport: {zones.loc[zones.LocationID == 1].area.iloc[0] / 10.7639 / 1e6:.2f} km2")
duplicated LocationID values in this copy: 0; distinct IDs 263
the CRS is EPSG:2263 (feet); area of zone 1, Newark Airport: 7.34 km2
This copy, issued in February 2026, has no duplicated IDs. Older copies of taxi_zones.shp that circulate in tutorials and repositories have had repeated LocationID values; check for duplicates before dissolving or joining, whichever copy you have. The shapefile is in EPSG:2263 with units of feet, so areas must be converted: zone 1, Newark Airport, is 7.34 kmΒ².
Why a taxi zone is an ID
The TLC's zones are a coding of the city drawn for taxi use, roughly at neighbourhood scale. Each pickup and drop-off is assigned the ID of the zone its position falls in; a location that cannot be placed gets 264, and one outside every mapped zone gets 265. The lookup table attaches readable names to the IDs, which is why some names repeat: they were written for people, not for joins.
Treating the ID as the key and the name as a label avoids every problem on this page. Treating the shapefile as the list of zones, rather than the lookup table, is what makes trips disappear.
Related join problems
- Left join, then count. Join trips to zones with
how="left"and check that the trip total is unchanged before mapping. - Borough "Unknown" and NaN. Grouping by borough puts 264 under "Unknown" and 265 under a missing borough;
groupby(dropna=False)keeps 265. - Shapefile versions. The TLC reissues the zones without notice; save the copy you used and its date, as this page does.
- Unknown is not a place. Zone 264 can look like a real zone in a ranking of pickup zones; filter it out of any list of places.
Internal links
- How to analyse NYC taxi trip records β zones and pickups
- How to find the busiest taxi routes between NYC zones β pairs without the unknown zones
- NTA codes do not match the boundaries β another New York zone system that changed
FAQ
What are taxi zones 264 and 265?
264 is "Unknown" and 265 is "Outside of NYC". Neither has a polygon in the TLC's shapefile.
Why do some taxi zone names appear twice?
Corona is both zone 56 and 57, and Governor's, Ellis and Liberty Islands share one name across IDs 103 to 105. Join on LocationID, not on the name.