How to Measure Airbnb Prices Against Distance to Transit in Python

On this page

Problem statement

Location is the first thing guests pay for, and in a city with a metro system, distance to the nearest station is one of the clearest measures of location. Testing that in data takes a join between two sources: listing coordinates from Inside Airbnb and station locations from OpenStreetMap, measured in a projected CRS, then binned and compared like for like, since the listings near downtown stations are also different kinds of listing.

This page computes the distance from every entire-home listing in the Inside Airbnb snapshot of Washington, D.C. of 24 June 2026 to the nearest Metro station in OpenStreetMap. It compares median prices by distance band, controls for size, and fits a simple regression of log price on distance.

Quick answer

Project both layers to UTM, find each listing's nearest station with a spatial index, and bin the distances:

E0 = L[(L.room_type == "Entire home/apt") & L.price_usd.notna()]
E = gpd.GeoDataFrame(E0, geometry=gpd.points_from_xy(E0.longitude, E0.latitude), crs=4326).to_crs(32618)
near = gpd.sjoin_nearest(E, S[["name", "geometry"]].rename(columns={"name": "station"}), how="left", distance_col="dist_m")
near = near[~near.index.duplicated()]
near["band"] = pd.cut(near.dist_m, [0, 400, 800, 1600, 20000], labels=["<400 m", "400-800 m", "0.8-1.6 km", ">1.6 km"])
print(f"entire homes: {len(near):,}; median distance to a Metro station {near.dist_m.median():.0f} m")
print(near.groupby("band", observed=True).agg(listings=("id", "size"), median_price=("price_usd", "median")).round(0).to_string())
entire homes: 4,618; median distance to a Metro station 729 m
            listings  median_price
band                              
<400 m           981         258.0
400-800 m       1554         243.0
0.8-1.6 km      1607         217.0
>1.6 km          476         209.0

The 4,618 priced entire homes are a median 729 m from the nearest Metro station. Prices fall with distance, from a median of $258 within 400 m to $209 beyond 1.6 km.

Map of entire-home listings coloured by distance band to the nearest Metro station, with station squares Map of entire-home listings coloured by distance band to the nearest Metro station, with station squares
981 listings lie within 400 m of a station; 476 lie more than 1.6 km away.

Like with like, a simple regression, stations with most listings

1. Compare like with like

Larger homes are farther from the centre. Compare price per guest:

near["per_guest"] = near.price_usd / near.accommodates.clip(lower=1)
print(near.groupby("band", observed=True).agg(median_accommodates=("accommodates", "median"), price_per_guest=("per_guest", "median")).round(1).to_string())
            median_accommodates  price_per_guest
band                                            
<400 m                      4.0             72.9
400-800 m                   4.0             64.2
0.8-1.6 km                  4.0             55.2
>1.6 km                     4.0             53.2

Homes in every band sleep a median of four, so size does not explain the gradient here. Price per guest falls from $72.90 within 400 m to $53.20 beyond 1.6 km.

Entire-home listings and prices by distance to the nearest Metro station. price by distance banddistancelistingsmedianper guestunder 400 m981$258$72.9400-800 m1,554$243$64.20.8-1.6 km1,607$217$55.2over 1.6 km476$209$53.2median party size is 4 guests in every band4,618 entire homes; median distance 729 m spatialworkflow.io
The median falls from $258 within 400 m to $209 beyond 1.6 km.

2. A simple regression

Fit log price on distance and size, so the coefficient reads as a percentage change:

import statsmodels.formula.api as smf
d = near.assign(log_price=np.log(near.price_usd), km=near.dist_m / 1000, bedrooms=near.bedrooms.fillna(1)).query("price_usd > 20 and price_usd < 2000")
m = smf.ols("log_price ~ km + bedrooms + accommodates", data=d).fit()
print(f"n = {int(m.nobs)}; R² = {m.rsquared:.2f}; each km farther from a station: {100 * (np.exp(m.params['km']) - 1):+.1f}% price (95% CI {100 * (np.exp(m.conf_int().loc['km', 0]) - 1):+.1f} to {100 * (np.exp(m.conf_int().loc['km', 1]) - 1):+.1f}%)")
n = 4598; R² = 0.33; each km farther from a station: -19.1% price (95% CI -21.2 to -16.9%)

Controlling for bedrooms and capacity, each kilometre from a station is associated with a 19.1% lower price, with a 95% interval of 16.9% to 21.2%, over 4,598 listings.

3. Which stations have the most listings nearby

top = near[near.dist_m <= 800].groupby("station").agg(listings=("id", "size"), median_price=("price_usd", "median")).sort_values("listings", ascending=False)
print(top.head(5).round(0).to_string())
                         listings  median_price
station                                        
Dupont Circle                 289         216.0
Shaw–Howard University        230         290.0
NoMa–Gallaudet U              220         299.0
Eastern Market                201         232.0
Georgia Avenue–Petworth       177         195.0

Dupont Circle has the most entire homes within 800 m, 289, followed by Shaw-Howard University, NoMa-Gallaudet, Eastern Market and Georgia Avenue-Petworth. Median prices around them range from $195 to $299.

The price gradient in numbers

The data used on this page

The Inside Airbnb snapshot for Washington, D.C. dated 24 June 2026 (CC BY 4.0), and Metro stations (railway=station, station=subway) from the Geofabrik OpenStreetMap extract for D.C. of 1 January 2026 (© OpenStreetMap contributors, ODbL), read with pyosmium.

from pathlib import Path

import geopandas as gpd
import numpy as np
import osmium
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
BASE = "https://data.insideairbnb.com/united-states/dc/washington-dc/2026-06-24"
def cached(path, url):
    p = Path(path)
    if not p.exists():
        r = requests.get(url, timeout=600); r.raise_for_status(); p.write_bytes(r.content)
    return p
L = pd.read_csv(cached("data/listings.csv.gz", f"{BASE}/data/listings.csv.gz"))
L["price_usd"] = pd.to_numeric(L.price.str.replace(r"[$,]", "", regex=True), errors="coerce")
PBF = cached("data/district-of-columbia-260101.osm.pbf", "https://download.geofabrik.de/north-america/us/district-of-columbia-260101.osm.pbf")
fp = (osmium.FileProcessor(str(PBF)).with_areas().with_filter(osmium.filter.EntityFilter(osmium.osm.NODE | osmium.osm.AREA))
      .with_filter(osmium.filter.TagFilter(("railway", "station"))).with_filter(osmium.filter.GeoInterfaceFilter(tags=["name", "station"])))
S = gpd.GeoDataFrame.from_features(fp, crs=4326)
S = S[S.station == "subway"].to_crs(32618)
S["geometry"] = S.representative_point()
print(f"{len(L):,} listings; {len(S)} Metro station features in the extract (D.C. and its margin)")
6,945 listings; 44 Metro station features in the extract (D.C. and its margin)

The OSM extract yields 44 Metro station features, in the District and its margin.

Example 1: the price gradient in numbers

The ratio of the median price within 400 m of a station to the median beyond 1.6 km:

b = near.groupby("band", observed=True).price_usd.median()
print(f"median within 400 m ${b['<400 m']:.0f}; beyond 1.6 km ${b['>1.6 km']:.0f}; ratio {b['<400 m'] / b['>1.6 km']:.2f}")
median within 400 m $258; beyond 1.6 km $209; ratio 1.23

The median within 400 m of a station is 23% above the median beyond 1.6 km.

Scatter of entire-home price on a log scale against kilometres to the nearest station with a fitted line Scatter of entire-home price on a log scale against kilometres to the nearest station with a fitted line
The model gives -19.1% per km, holding bedrooms and guests fixed.

Transit distance as a bundle of things guests value

Measuring listing prices against distance to transit. listings to a distance modelstationsOSM station=subwayprojectUTM 18N, metressjoin_nearestdistance_colbands400 / 800 / 1,600 mlog-pricemodel-19.1% per kmControls for bedrooms and guests; R² 0.33 spatialworkflow.io
Distances are measured in metres in UTM zone 18N.

Distance to transit is a standard variable in housing economics, and for short-term rentals it stands in for a bundle of things guests value: easy access to sights, restaurants and offices, and a central location in general. Computing it needs both layers in a projected CRS, so distances are in metres, and a nearest-neighbour search, which GeoPandas' sjoin_nearest performs with a spatial index. Station locations come from OpenStreetMap, reduced to one point per station, and include stations just outside the city so listings near the border find their true nearest station.

The raw gradient overstates the effect of the station itself. Listings near downtown stations are smaller apartments in denser, more expensive areas, while those far from stations are often larger houses. Comparing price per guest, or controlling for bedrooms and capacity in a regression, separates some of that. Even then, distance to a station is correlated with everything else about a central location, so the coefficient describes how prices vary across the city, not what building a new station would do. Inside Airbnb's location blurring, up to about 150 m, adds noise to distances under a few hundred metres.

Walking distance, entrances and log prices

Distance-to-transit analysis problems and fixes. distance analysis trapsSymptomFixdistances in degreesproject to metres firststations outside the city missedkeep the extract marginnear = central = dearadd neighbourhood controlsduplicate rows after joindrop duplicated index44 station features including the D.C. margin spatialworkflow.io
Proximity and centrality go together, so a distance effect is not purely about the station.
  • Straight-line vs walking distance: network distance along streets is more realistic; OSMnx can compute it for a sample.
  • Station entrances: large stations have several entrances; the station point is an approximation.
  • Log prices: model log price so effects read as percentages and the skew does not dominate.
  • Outliers: trim implausible prices before regression; the result depends on the trimming rule, so state it.

FAQ

How do I calculate the distance from Airbnb listings to the nearest station?

Project listings and stations to a metric CRS and use gpd.sjoin_nearest(listings, stations, distance_col="dist_m").

Are Airbnb listings near metro stations more expensive?

In D.C. the median entire home within 400 m of a Metro station costs more than one beyond 3.2 km; part of that difference is size and central location, not the station itself.

Why use log price in a regression?

Prices are skewed and effects are roughly proportional, so log price gives percentage effects and better-behaved residuals.

Where do I get metro station locations?

From OpenStreetMap (railway=station with station=subway), or from the transit agency's GTFS feed.