How to Split Shapefiles. Cutting the western Aleutian Islands... | by Allison Stafford | Towards Data Science

Sometimes you only want part of a shapefile. Depending on what part, and the shapefile source, this can be super simple, or less so. For example, the other week, I was mapping the US states’ relative rates of complaints to the Federal Consumer Financial Protection Bureau (more here), and my choropleth mapped like this:
Although it is very hard to see at this scale, the western Aleutian Islands cross over the 180th Meridian, creating this disjointed map that attempts to flatten a sphere onto a rectangle.
You have several options for tackling this problem:
Separate the unwanted (again, uninhabited) Polygons from their Multipolygon clusterUse the shapely function .intersects() on the entire GeoDataFrameSet the x-limit of your plot within matplotlibShift your positive/east longitude polygons to their negative/west longitude equivalentInstead of splitting your shapes, use Cartopy for a different map projection
Separate the unwanted (again, uninhabited) Polygons from their Multipolygon cluster
Use the shapely function .intersects() on the entire GeoDataFrame
Set the x-limit of your plot within matplotlib
Shift your positive/east longitude polygons to their negative/west longitude equivalent
Instead of splitting your shapes, use Cartopy for a different map projection
Here we go:
I wanted to start with this option because it solves a more universal problem that I have bumped into time and time again. How to split up Multipolygons in shapely and/or geopandas and then put them back together for use in choropleths.
First, I load the shapefiles and select the record I want to work on, in this case, Alaska. (Thank you US Census for the shapefiles! #opendata)
# import necessary packagesimport geopandas as gpdfrom matplotlib import pyplot as plt# load shapefilestates_gdf = gpd.read_file('./data/cb_2018_us_state_5m.shp')# select Alaskaalaska_gdf = states_gdf.loc[states_gdf['STUSPS'] == "AK"]
The ‘geometry’ column returns a single-item DataFrame. You can access a list of the Shapely geometry with .values . In this case we have a one item list for our one-record DataFrame. Accessing the first value gives us Alaska’s Shapely MultiPolygon.
alaska_mp = alaska_gdf['geometry'].values[0]
Alternatively, you could use the original states GeoDataFrame, but I always forget exactly how many layers there are, and find the guessing and checking easier with my one-record DataFrame.
alaska_mp_alt = states_gdf.loc[states_gdf['STUSPS'] == "AK",                                'geometry'].values[0]
With a MultiPolygon, you can access each of the polygons like a list or array.
In its current form, the entire collection of Polygons (the MultiPolygon) are taken as a whole when using the Shapely functions (like .intersects()). To consider each Polygon separately, I converted the MultiPolygon to a GeoDataFrame of its own:
# create the GeoDataFrameak_exp_gdf = gpd.GeoDataFrame(alaska_mp)# set the geometry columnak_exp_gdf.columns = ['geometry']
Now that we have a GeoDataFrame where each row is a Polygon that together make up Alaska, we’re ready to filter these for the locations we want or don’t want. We need to make a Shapely Polygon that we’ll use to judge and filter each piece of Alaska against. We need a Polygon that covers all the area you want (or don’t want) that we will use with .intersects() or another shapely function of your choice to classify your shapes.
Polygons are created with a list of points/vertices. The last point connects to the first automatically, so you don’t need to close the loop yourself.
from shapely.geometry import Polygon# create polygon that covers Alaska up to the 180th Meridiantarget_poly = Polygon([(-180, 50), (-180, 75),                        (-100, 75), (-100, 50)])
Now we can filter using this polygon and shapely/geopandas’ .intersects().
western_isles = ak_exp_gdf[al_exp_gdf.intersects(target_poly) ==                            False].copy()eastern_ak = ak_exp_gdf[al_exp_gdf.intersects(target_poly)].copy()
Awesome!!!
Now that we have our Alaska polygons of interest, we’re ready to put this geometry into our states GeoDataFrame. We use .dissolve() to combine a DataFrame of Polygons into one or more MultiPolygons, using the “by” argument. (I still don’t understand the framework that makes dissolve feel like a synonym for glue or combine — maybe we’re dissolving the borders?)
# add a column to groupbyeastern_ak['STUSPS'] = 'AK'# combine polygons/records into a single multipolygon/recordalaska_trimmed = eastern_ak.dissolve(by='STUSPS')# add this new multipolygon in place of the old alaska multipolygonstates_trimmed = states_gdf.copy()states_trimmed.loc[states_trimmed['STUSPS'] == 'AK', 'geometry']  = alaska_trimmed['geometry'].values
And we’re set!
Here’s a choropleth example that includes Alaska — Water Area to Land Area ratio. The sheer size of Alaska does make it hard to see some of those eastern states, but it’s nice to be in control, and have the freedom to decide whether (and how) to include different shapes by way of trimming.
You can also modify geometries without breaking the MultiPolygon into its separate Polygons. This method does have its own particularities, but has far fewer lines of code. Note that using GeoDataFrame.intersection(shape) returns a series of geometries, not the original GeoDataFrame with this new information added. You’ll need to add or replace a column to store this information in your existing GeoDataFrame.
from shapely.geometry import Polygon# create a polygon with the desired dimensions of the mapdesired_map = Polygon([(-180, 80), (-60, 80), (-60, 15),                        (-180, 15)])# create a new copy of the states GeoDataFrame for this examplestates_trimmed_v3 = states_gdf.copy()# get the intersection of the states geometries with the desired map states_trimmed_v3['geometry'] = states_gdf.intersection(desired_map)
If you try to .plot() this geometry set directly, you may get a TypeError: ‘Polygon’ object is not subscriptable. This happens when you have some records that do not overlap with your intersection geometry, in our case the Pacific Territories. In this case .intersection() returns an empty polygon. My solution was to filter for this case before plotting.
states_trimmed_v3[states_trimmed_v3['geometry'] != Polygon()].plot()
We get the same result as above, without the work needed to find the offending shape, split up the MultiPolygon, and dissolve the Polygons back into a single MultiPolygon. Depending on your use case, this could definitely be a good option.
Another easy alternative is to use matplotlib‘s graphing interface to cut off any unwanted areas of the plot.
fig, ax = plt.subplots(figsize=(15,10))alaska_gdf.plot(ax=ax)ax.set_xlim(-180, -128)
If you want to retain the projection and the western Aleutian Islands, you can shift those Polygons’ x values by -360 using .translate(). This option uses the technique described above of splitting up the MultiPolygon
# shift the western isles by 360 degreeswestern_isles_shifted = western_isles.translate(xoff=-360)# convert to GeoDataFramewest_ak_shift_gdf = gpd.GeoDataFrame(western_isles_shifted)west_ak_shift_gdf.columns = ['geometry']# add 'STUSPS' columnwest_ak_shift_gdf['STUSPS'] = 'AK'# combine the western and eastern parts into the same dataframeak_shifted_actual_gdf = west_ak_shift_gdf.append(eastern_ak,                                                  ignore_index=True)# combine/dissolve into a single MultiPolygonak_shifted_actual_dissolved =                          ak_shifted_actual_gdf.dissolve(by='STUSPS')
This produces an Alaska with x-values of -190 to -125. You might want to adjust the x-tick labels to match the conventional -180 to +180 convention when plotting.
fig, ax = plt.subplots(figsize=(15, 12))ak_shifted_actual_dissolved.plot(ax=ax)# get the locations and labels of the x-tickslocs, labels = plt.xticks()# set the first label in labels to 170 instead of -190labels[1] = '170'# set the x-ticks with new labels (keeping the locs)plt.xticks(locs, labels)
Note: I tried to shift the entire US by +30, but this only changed the x-tick values on the x-axis, without bringing the rightmost part of the map around to the left side.
Alternatively, you can use Cartopy to alter the map projection to your liking. I’m loving this option! It lets you adjusting geospatial data to the view that makes the most sense for your project.
First, install with conda install -c conda-forge cartopy or check out the full installation guide. Note that I have geopandas installed on the conda-forge channel as well — probably good to have all of these packages and their dependancies on the same channel.
Then you’re ready! Basic use with the same projection as above (Plate Carrée):
# set figure sizefig = plt.figure(figsize=(15, 12)# set axis with desired projectionax = plt.axes(projection=ccrs.PlateCarree())# add shapes, indicating their original projectionax.add_geometries(states_gdf['geometry'],                   crs=ccrs.PlateCarree())# add gridlinesgl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True,                  linewidth=.5, color='gray', alpha=0.5)# display mapplt.show();
You can check out all of the projections here. In my case, the projections that allow you to set the center of the view are just what I was looking for.
# set desired projection/viewax = plt.axes(projection = ccrs.Orthographic(central_longitude=-100,                                              central_latitude=40))# add your shapes (indicating their existing projection)ax.add_geometries(states_gdf.geometry, crs=ccrs.PlateCarree())# add coastlines and gridlines for contextax.coastlines(linewidth=.5)ax.gridlines()
You can also focus on a specific area using ax.set_extent(xmin, ymin, xmax, ymax).
To adjust the color of the geometries, add them individually with various facecolors. For example with the same choropleth above:
from matplotlib.colors import Normalize# value normalization for the color mapvalues = list(states_trimmed_v2['water:land'])norm = Normalize(vmin=min(values), vmax=max(values))# select color mapcmap = plt.cm.get_cmap('viridis')#create chorpleth: set fig and ax projectionfig = plt.figure(figsize=(15,15))ax = plt.axes(projection = ccrs.Orthographic(central_longitude=-100, central_latitude=40))# add each state to the ax with its appropriate facecolorfor state_i in states_trimmed_v2.index:    geom = []    geom.append(states_trimmed_v2.loc[state_i, 'geometry'])    facecolor = cmap(norm(states_trimmed_v2.loc[state_i, 'water:land']))    ax.add_geometries(geom,                       ccrs.PlateCarree(),                       facecolor=facecolor,                       edgecolor='grey',                       linewidth=.2)    ax.set_title('States By Water Area to Land Area Ratio', fontsize=20)ax.coastlines(linewidth=.2)ax.gridlines()
For more info, check out these resources:
Working with geospatial data: this blog post (written by yours truly)
An intro to Cartopy: post by geohackweek
Find the GitHub repo here. Happy coding!