Ravi Shekhar's Technical Blog

A Technical Blog of the Data Science Process

Geospatial Operations at Scale with Dask and Geopandas


Note: This post has interactive Bokeh graphics which may not render well on mobile devices. Try viewing the Jupyter notebook which underlies this post on NBViewer.

Part 1 : A Gentle Introduction to the Spatial Join

One problem I came across when analyzing the New York City Taxi Dataset, is that from 2009 to June 2016, both the starting and stopping locations of taxi trips were given as longitude and latitude points. After July 2016, to provide a degree of anonymity when releasing data to the public, the Taxi and Limousine Commission (TLC) only provides the starting and ending "taxi zones" of a trip, and a shapefile that specifies the boundaries, available here. Let's load this up in Geopandas, and set the coordinate system to 'epsg:4326', which is latitude and longitude coordinates.

In [1]:
Expand Code
Out[1]:
LocationID borough geometry zone
0 1 EWR POLYGON ((-74.18445299999996 40.6949959999999,... Newark Airport
1 2 Queens (POLYGON ((-73.82337597260663 40.6389870471767... Jamaica Bay
2 3 Bronx POLYGON ((-73.84792614099985 40.87134223399993... Allerton/Pelham Gardens
3 4 Manhattan POLYGON ((-73.97177410965318 40.72582128133706... Alphabet City
4 5 Staten Island POLYGON ((-74.17421738099989 40.5625680859999,... Arden Heights

We see that the geometry column consists of polygons (from Shapely) that have vertices defined by longitude and latitude points. Let's plot using bokeh, in order of ascending LocationID.

In [2]:
Expand Code
Loading BokehJS ...

This is a familiar map of New York, with 262 taxi districts shown, colored by the id of the taxi district. I have added a random point (-73.966˚E, 40.78˚N) in magenta, which happens to fall in the middle of Central Park. Assigning a point as within a taxi zone is something humans can do easily, but on a computer it requires solving the point in polygon problem. Luckily the Shapely library provides an easy interface to such geometric operations in Python. But, point in polygon is computationally expensive, and using the Shapely library on 2.4 billion (latitude, longitude) pairs to assign taxi zones as in the NYC Taxi Dataset would take a modern single core cpu about four years. To speed this up, we calculate the bounding boxes for each taxi zone, which looks like:

In [3]:
Expand Code

Now, given a (longitude, latitude) coordinate pair, bounding boxes that contain that pair can be efficiently calculated with an R-tree. You can find an excellent introduction to R-trees here. Only the polygons (taxi zones) that have bounding boxes that contain the coordinate pair need to be examined, and then the point in Polygon is solved for those (hopefully) few taxi zones. This reduces computation by a factor of about 100-1000. This process, assigning coordinate pairs to taxi zones is one example of a spatial join. Geopandas provides a nice interface to efficient spatial joins in Python, and it takes care of calculating bounding boxes and R-trees for you, as this snippet shows.

In [4]:
gpd.sjoin(gpd.GeoDataFrame(crs={'init': 'epsg:4326'}, geometry=[Point(-73.966, 40.78)]), 
          df,
          how='left', op='within')
Out[4]:
geometry index_right LocationID borough zone
0 POINT (-73.96599999999999 40.78) 42 43 Manhattan Central Park

This does the merge for a single point (drawn in magenta) on maps above, and correctly identifies it in Central Park

Part 2 : Spatial Joins at scale using Dask

In my NYC transit project, I download and process the Taxi dataset. Here I load up a single file from the taxi dataset (May 2016) into Dask, and show the first few rows and a few columns. The file is a bit large at 1.8GB, and Dask chooses to divide up the dataframe into 30 partitions for efficient calculations. Each partition is a pandas DataFrame, and dask takes care of all the logic to view the combination as a single DataFrame. Here are a few columns.

In [20]:
Expand Code
There are 30 partitions. 
Out[20]:
tpep_pickup_datetime pickup_longitude pickup_latitude
0 2016-05-01 00:00:00 -73.985901 40.768040
1 2016-05-01 00:00:00 -73.991577 40.744751
2 2016-05-01 00:00:00 -73.993073 40.741573
3 2016-05-01 00:00:00 -73.991943 40.684601
4 2016-05-01 00:00:00 -74.005280 40.740192
In [21]:
Expand Code
Out[21]:
tpep_dropoff_datetime dropoff_longitude dropoff_latitude
0 2016-05-01 00:17:31 -73.983986 40.730099
1 2016-05-01 00:07:31 -73.975700 40.765469
2 2016-05-01 00:07:01 -73.980995 40.744633
3 2016-05-01 00:19:47 -74.002258 40.733002
4 2016-05-01 00:06:39 -73.997498 40.737564

So each trip has pickup and dropoff (longitude, latitude) coordinate pairs. Just to give you a feel for the data, I plot the start and end locations of the first trip, ending in the East Village. Driving directions come with a great deal of additional complexity, so here I just plot an arrow, as the crow flies. A spatial join identifies the taxi zones as Clinton East and East Village.

In [22]:
Expand Code