In [2]:
import json

from branca.colormap import linear
import folium
from folium import Map, Marker, GeoJson, LayerControl
import pandas as pd
import geopandas as gpd

%matplotlib inline

Load and explore geojson

In [3]:
gj_path = "geojson/anc.geojson"
In [4]:
anc_df = gpd.read_file(gj_path)
In [5]:
anc_df.head()
Out[5]:
OBJECTID ANC_ID WEB_URL NAME Shape_Leng Shape_Area geometry
0 1 1C http://anc.dc.gov/page/advisory-neighborhood-c... ANC 1C 5218.954361 1.285112e+06 POLYGON ((-77.0464219248981 38.92597950466725,...
1 2 1D http://anc.dc.gov/page/advisory-neighborhood-c... ANC 1D 4224.010068 9.475922e+05 POLYGON ((-77.03645123520528 38.93638367371454...
2 3 2A http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2A 12477.943204 7.065358e+06 POLYGON ((-77.05445304334567 38.90725341205063...
3 4 2B http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2B 7712.504785 2.160620e+06 POLYGON ((-77.0412259402753 38.91701561959232,...
4 5 2C http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2C 7811.084627 2.861750e+06 POLYGON ((-77.02404971019487 38.90293630338282...
In [6]:
# Tried this to reduce the map complexity for Chrome, didn't work
# anc_df.geometry = anc_df.geometry.simplify(500, preserve_topology=False)
In [18]:
def embed_map(m):
    from IPython.display import IFrame

    m.save('inline_map.html')
    return IFrame('inline_map.html', width='100%', height='750px')

Preprocess data and join with election CSV using pandas

In [7]:
election_df = pd.read_csv("election_data_for_anc_map.csv")
election_df.head()
Out[7]:
ward year anc num_candidates votes vote_norm engagement prop_uncontested prop_empty
0 1 2012 A 1.583333 481.500000 NaN NaN 0.500000 0.0
1 1 2012 B 1.250000 544.333333 NaN NaN 0.916667 0.0
2 1 2012 C 1.500000 641.875000 NaN NaN 0.625000 0.0
3 1 2012 D 1.400000 559.800000 NaN NaN 0.600000 0.0
4 1 2014 A 1.333333 296.250000 0.529246 0.63618 0.666667 0.0
In [8]:
election_df["ANC_ID"] = election_df["ward"].map(str) + election_df["anc"]
election_df.drop(columns = ["ward", "anc"], inplace=True)
print(election_df.shape)
election_df.head()
(160, 8)
Out[8]:
year num_candidates votes vote_norm engagement prop_uncontested prop_empty ANC_ID
0 2012 1.583333 481.500000 NaN NaN 0.500000 0.0 1A
1 2012 1.250000 544.333333 NaN NaN 0.916667 0.0 1B
2 2012 1.500000 641.875000 NaN NaN 0.625000 0.0 1C
3 2012 1.400000 559.800000 NaN NaN 0.600000 0.0 1D
4 2014 1.333333 296.250000 0.529246 0.63618 0.666667 0.0 1A
In [9]:
election_df_2018 = election_df[election_df.year == 2018]
print(election_df_2018.shape)
(40, 8)
In [10]:
joined_df = anc_df.merge(election_df_2018, how="left", on="ANC_ID")
joined_df.head()
Out[10]:
OBJECTID ANC_ID WEB_URL NAME Shape_Leng Shape_Area geometry year num_candidates votes vote_norm engagement prop_uncontested prop_empty
0 1 1C http://anc.dc.gov/page/advisory-neighborhood-c... ANC 1C 5218.954361 1.285112e+06 POLYGON EMPTY 2018 1.375000 690.500000 0.695144 0.818804 0.625000 0.0
1 2 1D http://anc.dc.gov/page/advisory-neighborhood-c... ANC 1D 4224.010068 9.475922e+05 POLYGON EMPTY 2018 1.600000 513.200000 0.608100 0.818987 0.400000 0.0
2 3 2A http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2A 12477.943204 7.065358e+06 POLYGON EMPTY 2018 1.125000 254.375000 0.688037 0.770138 0.875000 0.0
3 4 2B http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2B 7712.504785 2.160620e+06 POLYGON EMPTY 2018 1.222222 574.666667 0.698349 0.840883 0.777778 0.0
4 5 2C http://anc.dc.gov/page/advisory-neighborhood-c... ANC 2C 7811.084627 2.861750e+06 POLYGON EMPTY 2018 1.333333 417.000000 0.686430 0.806862 0.666667 0.0

Update geojson features from dataframe values

In [11]:
""" No longer necessary
for anc in gjdata['features']:
    anc_id = anc['properties']['ANC_ID']
    features = election_df.columns.tolist()
    features.remove("ANC_ID")
    for feature in features:
        anc['properties'][feature] = joined_df.loc[joined_df['ANC_ID'] == anc_id, feature].item()
"""
Out[11]:
' No longer necessary\nfor anc in gjdata[\'features\']:\n    anc_id = anc[\'properties\'][\'ANC_ID\']\n    features = election_df.columns.tolist()\n    features.remove("ANC_ID")\n    for feature in features:\n        anc[\'properties\'][feature] = joined_df.loc[joined_df[\'ANC_ID\'] == anc_id, feature].item()\n'

Construct map

In [12]:
anc_map = Map(location = (38.8899, -77.0091),
              zoom_start = 12,
              tiles = 'Stamen Toner')
In [13]:
folium.Choropleth(
    geo_data=gj_path,
    data=joined_df,
    columns=["ANC_ID", "votes"],
    key_on='feature.properties.ANC_ID',
    fill_color='GnBu',
    fill_opacity=0.5,
    line_weight=1,  
    highlight=True,
    overlay=True,
    name="average votes",
    legend_name="average # votes for winning candidates",
).add_to(anc_map)
Out[13]:
<folium.features.Choropleth at 0x11b601c18>
In [14]:
folium.Choropleth(
    geo_data=gj_path,
    data=joined_df,
    columns=["ANC_ID", "engagement"],
    key_on='feature.properties.ANC_ID',
    fill_color='PuRd',
    fill_opacity=0.5,
    line_weight=1,  
    highlight=True,
    overlay=True,
    name="engagement",
    legend_name="percentage of ballots where ANC candidate was marked",
).add_to(anc_map)
Out[14]:
<folium.features.Choropleth at 0x11be971d0>
In [15]:
LayerControl().add_to(anc_map)
Out[15]:
<folium.map.LayerControl at 0x11b6011d0>
In [19]:
embed_map(anc_map)
Out[19]:
In [15]:
anc_map.save("anc_map.html")
In [ ]: