Day 1: Points ¶
This is my first post for the 2019 30 Day Map Challenge. It's November 4th, so clearly I'm a little bit behind, but I hope to catch up throughout the rest of the month.
I've been contemplating what it is I want to get out of this exercise. When I was in school, I was often concerned with meeting the requirements of the assignment. It served me well in terms of grades, but when I look back at how I approached my formal education, I have some regret that I didn't allow myself to stray outside the lines a little bit more. I get the sense that this month is more intended as a cartographic exercise, but I am going to use the prompts for each day more generally as means for focusing my own exploration of new (to me at least) tools and techniques for working with spatial data. In other words, I'm not promising pretty or otherwise remarkable maps. The product of this for me will hopefully be growth and I will use this blog and a catalog of how I produced my daily maps.
So for the first daya of the 2019 30 Day Map Challenge, I'm going to be mapping GoRaleigh bus stops and bus shelters. Recently it was announced that Raleigh's local transit system would be dramtically expanding the number of bus shelters. As I read about this exciting development, I got to wondering about the distribution of bus shelters generally throughout Raleigh. As I was looking around the GoRaleigh's services on ArcGIS Online I found two datasets that I figured would help me interrogate this curiosity:
While on face, it might seem I should be able to get everything I need from the GoRaleigh Bus Stop data, the GoRaleigh Shelters data contains more detailed information about the type of shelter and whether it is existing or planned. So for this first challenge, my goal was to combine these datasets in such a way that I could make a map of GoRaleigh Bus Stops with detailed information about shelters.
I also wanted to try out a few different tools. Recordings from the 2019 NACIS Conference were recently released and I've been working my way through those. One presentation that caught my attention was by Mamata Akella from CARTO regarding their Python package, CARTOframes . I had tried out this package a couple years ago and thought it showed a lot of potential, but when CARTO stopped providing accounts priced for tinkering, I decided to focus on other tools. As I watched the presentation I was really impressed to see how far the package has come. But perhaps the biggest takeaway was that you don't need a CARTO account to use CARTOframes.
As I was thinking through how to approach today's challenge, I decided this would be a great chance to also work with CARTOframes. What follows below is my Python processing of the GoRaleigh shelters and bus stops data using a variety of Python packages and eventually visualized using CARTOframes.
Note: I started down the path of lots of narrative around the various elements of the analysis, but I'm already 4 days behind in the challenge so I making an editorial decision to just let the code do the talking. Apologies if anything is unclear. Hit me up on twitter (@maptastik) if you want to chat more about this notebook.
Libraries ¶
This notebook was produced using Google Colaboratory , which is basically Jupyter Notebooks meets Google Docs. A nice thing about Colaboratory is each notebook includes a Python 3 environment loaded with lots of libraries commonly used for data science type work. Pretty nice! And while most of the libraries we need come with the Colaboratory environment, there are couple we'll need to install:
- CARTOframes (beta
- geopandas
Fortunately, we can use
pip
to install these libraries and their dependencies.
! pip install cartoframes==1.0b4 geopandas
With CARTOframes and geopandas installed we can get down to business. It's somewhat a matter of style in notebooks, but I like to import all the libraries and classes at the beginning. Some folks prefer to import them when they first use them within the flow of the notebook. I think there are merits to both approaches.
import requests
from io import BytesIO
import pandas as pd
import geopandas as gpd
from cartoframes.viz import Map, Layer, Popup, Legend, Layout, basemaps
from cartoframes.viz.widgets import category_widget, formula_widget
from cartoframes.viz.helpers import color_category_layer
Functions ¶
Data loader from ArcGIS REST Service ¶
I know I'm going to be pulling in some data from a couple ArcGIS REST services so to prevent myself from repeating the somewhat verbose procedure for querying those and dumping the results into a GeoDataFrame, I've put together a function that in one line will carry out that process.
def arcgis_rest_to_gdf(url, layer_id):
url = f'{url}/{layer_id}/query'
params = {
'f': 'geojson',
'where': '1=1',
'outFields': '*',
'outSR': 4326
}
r = requests.get(url, params = params)
return gpd.read_file(BytesIO(r.content))
Loading and Examining the Data ¶
Shelters ¶
shelters_gdf = arcgis_rest_to_gdf("https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/GoRaleigh_Shelters/FeatureServer", 0)
display(shelters_gdf.head(), shelters_gdf.info())
display(sorted(shelters_gdf["Shelter"].unique()))
color_category_layer(shelters_gdf, value = 'Shelter', title = 'Shelter Type', top = 5)
Stops ¶
stops_gdf = arcgis_rest_to_gdf("https://services.arcgis.com/v400IkDOw1ad7Yad/ArcGIS/rest/services/GoRaleigh_Stops/FeatureServer", 0)
display(stops_gdf.head(), stops_gdf.info())
Map(Layer(stops_gdf))
Combine datasets ¶
shelters_reduced_gdf = shelters_gdf[["Stop_ID", "Stop_Name", "Shelter", "geometry"]]
shelters_reduced_gdf["Stop_ID"] = shelters_reduced_gdf.apply(lambda x: str(x["Stop_ID"]), axis = 1)
shelters_reduced_gdf["Status"] = shelters_reduced_gdf.apply(lambda x: "Planned" if "Planned" in x["Shelter"] else "Existing", axis = 1)
shelters_reduced_gdf["Shelter"] = shelters_reduced_gdf.apply(lambda x: x["Shelter"].split(' - ')[0], axis = 1)
display(shelters_reduced_gdf.head(), shelters_reduced_gdf.info())
stops_reduced_gdf = stops_gdf[["StopAbbr", "StopName", "geometry"]]
stops_reduced_gdf = stops_reduced_gdf.groupby("StopAbbr").first().reset_index()[["StopAbbr", "StopName", "geometry"]]
display(stops_reduced_gdf.info(), stops_reduced_gdf.head())
stops_shelters_gdf = stops_reduced_gdf.merge(shelters_reduced_gdf, how = 'outer', left_on = 'StopAbbr', right_on = 'Stop_ID', suffixes = ('', '_shelters'), sort = True)
# Clean up some of the fields and pivot Status field for use with formula widget
stops_shelters_gdf['StopAbbr'].fillna('0', inplace = True)
stops_shelters_gdf["StopName"].fillna("Unnamed Stop", inplace = True)
stops_shelters_gdf["Shelter"].fillna("No Shelter", inplace = True)
stops_shelters_gdf["Status"].fillna('No Shelter Planned', inplace = True)
stops_shelters_gdf["Existing"] = stops_shelters_gdf.apply(lambda x: 1 if x["Status"] == "Existing" else 0, axis = 1)
stops_shelters_gdf["Planned"] = stops_shelters_gdf.apply(lambda x: 1 if x["Status"] == "Planned" else 0, axis = 1)
stops_shelters_gdf["No_Shelter_Planned"] = stops_shelters_gdf.apply(lambda x: 1 if x["Status"] == "No Shelter Planned" else 0, axis = 1)
stops_shelters_gdf["geometry"] = stops_shelters_gdf.apply(lambda x: x["geometry_shelters"] if x["geometry"] is None else x["geometry"], axis = 1)
stops_shelters_gdf = stops_shelters_gdf[["StopAbbr", "StopName", "Shelter", "Status", "Existing", "Planned", "No_Shelter_Planned", "geometry"]]
stops_shelters_gdf = gpd.GeoDataFrame(stops_shelters_gdf, crs = {"init":"epsg:4326"}, geometry = "geometry")
stops_shelters_gdf.head()
Final Map ¶
Map(
Layer(
stops_shelters_gdf,
'''
color: ramp(buckets($Status, ["Existing", "Planned", "No Shelter Planned"]), [#4CAF50, #FFC107, #B0BEC533]),
width: 5
''',
legend = Legend(
'color-category',
title = "GoRaleigh Status of Bus Stop Shelters",
footer = "Data: GoRaleigh, City of Raleigh"
),
popup = Popup({
'click': [{
'title': 'Stop',
'value':'$StopName'
}, {
'title': 'Shelter',
'value': '$Shelter'
}, {
'title': 'Shelter Status',
'value': '$Status'
}
]
}),
widgets = [
formula_widget(
'Existing',
'sum',
title = "Existing Bus Stop Shelters"
),
formula_widget(
'Planned',
'sum',
title = "Planned Bus Stop Shelters"
),
formula_widget(
'No_Shelter_Planned',
'sum',
title = "No Planned Bus Stop Shelters"
),
category_widget(
'Status',
title = "Shelter Status",
description = "Click to filter by shelter status"
),
category_widget(
'Shelter',
title = "Shelter Type",
description = "Click to filter by shelter Type"
)
]
)
)
stops_shelters_gdf.to_file("goraleigh_stops_shelters.geojson", driver = "GeoJSON")