How Jane Jacobs-y is Your Neighborhood? Analysis with Pandas, OpenStreetMap, and the US Census — Part I
By: Brian Parker
Tags: Pandas, Python, Urban Planning, Jane Jacobs, Geopandas

How Jane Jacobs-y is Your Neighborhood? Analysis with Pandas, OpenStreetMap, and the US Census — Part I

An analysis of urban design based on Death and Life of Great American Cities using Pandas, OpenStreetMap, and the US Census

In Death and Life of Great American Cities, the great Jane Jacobs lays out four essential characteristics of a great neighborhood:

Density

A mix of uses

A mix of building ages, types and conditions

A street network of short, connected blocks

Of course, she goes into much greater detail on all of these, but I’m not going to get into all the eyes-on-the-street level stuff. Instead, I’m going to find neighborhoods with the right “bones” to build great urbanism onto. The caveat to this, as with most geospatial planning tools, is that it is not to be blindly trusted. There are a lot of details that need on the ground attention.

On to the data.

Tools

For this project, I’m going to use the following import statement:

To start a session with the Census API, you need to give it a key (get one here). I’m also going to start up my OSM tools, define a couple projections, and create some dictionaries for the locations I’m interested in, for convenience:

These two are interesting because they have both seen significant post-war growth and have a broad spectrum of development patterns. As a former Boise resident, I know Ada County well, and can provide on-the-ground insights. King County has a robust data platform that will allow for a different set of insights in part II of this analysis.

Density

We’ll start off easy. The U.S. Census Bureau publishes population estimates regularly, so we just need to put those to some geometry and see how many people live in different areas. The smallest geography available for all the data that I’m going to use is the tract, so that’s what we’ll get.

Now that I have the geography, I just need to get the population to calculate the density. The Census table for that is ‘B01003_001E,’ obviously. Here’s the function for querying that table by county:

Now that we have a dataframe with population, and a geodataframe with tracts, we just need to merge them together:

This function is a little more generalized so that we can add geometries to other data besides population, as we’ll see later.

Now we can simply call our function and plot the results:

Mix of Building Ages

The next most complicated search is to find a variety of building ages within each tract. Luckily, the Census has some data that’s close enough. They track the age of housing within tracts by decade of construction. To start, we’ll make a dictionary out of these table names:

Next, create a function to combine all of these into a single dataframe. Since the Jane-Jacobsy-est tracts will be closest to equal across each decade, the easy metric for this is going to be the standard deviation, with the lowest being best:

Again, we simply call our function and plot the results:

A Network of short, interconnected blocks

Now we start getting complicated. Luckily, we can get a head-start thanks to the osmnx Python package. We’ll use the graph_from_polygon function to get the street network within each Census tract, then the basic_stats package to get the average street length and average number of streets per intersection, or “nodes” in network analysis terms. However, before we do that, we need to fix one problem with our networks: OpenStreetMap counts parking lot drive-aisles as part of the street network, which is going to skew our results, as these tend to be relatively short, and at least connect in the interior of surface parking lots. To fix this, we’ll query all the parking lots in the county, then exclude them from our tracts to get some Swiss cheesy tracts. First, the function to query OSM for stuff, generalized as we’ll be using it heavily in the next section:

To get parking-less tracts:

This isn’t going to be a perfect solution as a lot of parking lots aren’t tagged as such, but it will at least exclude a lot of them. Now we can create a function to iterate over each tract and get a “street score” that I’m defining as the average length of streets within the tract divided by the number of streets per intersection:

This one takes a while, so I included a progress bar and map output to keep me entertained while I wait There are also some tracts with no streets (I would assume the Puget Sound), hence the try/except. Now we call the function:

A mix of Uses

Now for the most complicated portion of the analysis. Here’s my general plan:

Query OpenStreetMap for all the components of necessary for a “15 minute neighborhood”:

Office

Park

Bar

Restaurant

Coffee shop

Library

School

Bank

Doctor’s office

Pharmacy

Post office

Grocery store

Hardware store

2. Get a sample of points within each Census Tract

3. Count all the neighborhood essentials within walking distance of each sample point

4. Get an average of the number of essentials within walking distance for all the points in the tract.

We’ll start with the osm_query function that I used to find parking lots above to get all the neighborhood essentials in a given geography. Since OSM is open source and editable, there are a few quirks to the data to work out. First, some people put point geographies for some things, while others put areas of the buildings. That’s why there’s the poly_to_point option in the function to standardize all of these to points if we want. The raw output of the Overpass API geometry is a dictionary of coordinates, so we need to convert those to shapely geometries in order to get fed into GeoPandas:

We want these to come out in a single column, so we combine the outputs:

Now we’re finally ready to get our neighborhood essentials:

Next, we need to get a bunch of random points to search from:

Next, we’ll buffer our points by our walkable distance, which I set at 1 km. If we wanted to get really fancy, we’d use walksheds instead, but this analysis is processor heavy enough as it is, so I’m going to opt to stick with euclidian distances. We’ll then grab all the neighborhood essentials within the buffer area, and calculate a percentage of the essentials that are within walking distance:

That gets us the “nearbyness” of one tract. We now need to iterate over all the tracts in the county:

Now we can call our functions to get our analysis:

Putting it all Together

We now have a score for each of Jane Jacobs’ factors for a quality neighborhood. I’m more interested in comparing tracts within counties than comparing the counties themselves, so I’m going to simply rank each tract on their scores and take an average to get to the “Jane Jacobs Index” (JJI):

To see what we’ve made, we’ll call the function using the four dataframes we made earlier:

Finally, for cool points, we’ll use Folium to create an interactive map:

Here are links to the two newly created maps:

Ada County

King County

What’s it all mean? We’ll dive into that in Part II…