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.

Bar chart of 2025 pickups and drop-offs coded to zone 264 Unknown and zone 265 Outside NYC, largest for drop-offs outside the city Bar chart of 2025 pickups and drop-offs coded to zone 264 Unknown and zone 265 Outside NYC, largest for drop-offs outside the city
Zone 265 is mostly a destination: 219,073 drop-offs against 22,170 pickups.

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.

Map of two adjoining Queens taxi zones, 56 and 57, both named Corona, coloured separately with their pickup counts Map of two adjoining Queens taxi zones, 56 and 57, both named Corona, coloured separately with their pickup counts
Zones 56 and 57 share a name; grouped by name both would show 14,694 pickups.

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.

Bar chart of trips by which end is unknown or outside NYC: drop-off only, both ends, or pickup only Bar chart of trips by which end is unknown or outside NYC: drop-off only, both ends, or pickup only
In 268,614 trips only the drop-off is unknown or outside the city.

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.

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.