--- title: Map Gifs keywords: fastai sidebar: home_sidebar summary: "In this tutorial, the basics of creating map gifs is introduced" description: "In this tutorial, the basics of creating map gifs is introduced" ---
This Coding Notebook is the fourth in a series.
An Interactive version can be found here .
This colab and more can be found on our webpage.
Content covered in previous tutorials will be used in later tutorials.
New code and or information should have explanations and or descriptions attached.
Concepts or code covered in previous tutorials will be used without being explaining in entirety.
If content can not be found in the current tutorial and is not covered in previous tutorials, please let me know.
This notebook has been optimized for Google Colabs ran on a Chrome Browser.
Statements found in the index page on view expressed, responsibility, errors and ommissions, use at risk, and licensing extend throughout the tutorial.
Description: This notebook was made to demonstrate how to make a gif map by merging 2 datasets. The first being a dataset containing mappable coordinates onto which the second dataset may mapping its information of interest.
This lab is split into two sections.
Input(s):
Output: Files, Gif
*please note
Read Make sure the appropriate spatial Coordinate Reference System (CRS) is used when reading in your data!
CRS 4326 is th CRS most people are familar with when refering to latiude and longitudes.
Baltimore's 4326 CRS should be at (39.2, -76.6)
BNIA uses CRS 2248 internally #http://www.spatialreference.org/ref/epsg/2248/
Make sure all your dataset coordinates are using the same CRS
Perform conversions where needed
Additional Information: https://docs.qgis.org/testing/en/docs/gentle_gis_introduction/coordinate_reference_systems.html
Census Geographic Data:
%%capture #supress_output
# Install the Widgets Module.
# Colabs does not locally provide this Python Library
# The '!' is a special prefix used in colabs when talking to the terminal
!pip install geopandas
#@title Run This Cell: Import Modules
# Once installed we need to import and configure the Widgets
import ipywidgets as widgets
!jupyter nbextension enable --py widgetsnbextension
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = 'all'
import ipywidgets as widgets
from ipywidgets import interact, interact_manual
# Used 4 Importing Data
import urllib.request as urllib
from urllib.parse import urlencode
# This Prevents Timeouts when Importing
import socket
socket.setdefaulttimeout(10.0)
# Pandas Data Manipulation Libraries
import pandas as pd
# Show entire column widths
pd.set_option('display.max_colwidth', -1)
# 4 Working with Json Data
import json
# 4 Data Processing
import numpy as np
# 4 Reading Json Data into Pandas
from pandas.io.json import json_normalize
# 4 exporting data as CSV
import csv
# Geo-Formatting
# Postgres-Conversion
import geopandas as gpd
import psycopg2,pandas,numpy
from shapely import wkb
import os
import sys
# In case file is KML
import fiona
fiona.drvsupport.supported_drivers['kml'] = 'rw' # enable KML support which is disabled by default
fiona.drvsupport.supported_drivers['KML'] = 'rw' # enable KML support which is disabled by default
# https://www.census.gov/geographies/mapping-files/time-series/geo/tiger-line-file.2010.html
# https://www.census.gov/cgi-bin/geo/shapefiles/index.php?year=2010&layergroup=Census+Tracts
from geopandas import GeoDataFrame
from shapely.wkt import loads
from pandas import ExcelWriter
from pandas import ExcelFile
# load libraries
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import geopandas as gpd
import glob
# Gif
import imageio
# Pictures
from PIL import Image
import requests
from io import BytesIO
# This will just beautify the output
pd.set_option('display.expand_frame_repr', False)
pd.set_option('display.precision', 2)
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
# pd.set_option('display.expand_frame_repr', False)
# pd.set_option('display.precision', 2)
# pd.reset_option('max_colwidth')
pd.set_option('max_colwidth', 20)
# pd.reset_option('max_colwidth')
# (Optional) Run this cell to gain access to Google Drive (Colabs only)
from google.colab import drive
# Colabs operates in a virtualized enviornment
# Colabs default directory is at ~/content.
# We mount Drive into a temporary folder at '~/content/drive'
drive.mount('/content/drive')
# Find Relative Path to Files
def findFile(root, file):
for d, subD, f in os.walk(root):
if file in f:
return "{1}/{0}".format(file, d)
break
# To 'import' a script you wrote, map its filepath into the sys
def addPath(root, file): sys.path.append(os.path.abspath( findFile( './', file) ))
def getColName (df, col): return df.columns[df.columns.str.contains(pat = col)][0]
def getColByName (df, col): return df[getColName(df, col)]
def addKey(df, fi, col):
key = getColName(df, col)
val = getColByName(df, col)
fi[key] = val
return fi
def nullIfEqual(df, c1, c2):
return df.apply(lambda x:
x[getColName(df, c1)]+x[getColName(df, c2)] if x[getColName(df, c1)]+x[getColName(df, c2)] != 0 else 0, axis=1)
def sumInts(df): return df.sum(numeric_only=True)
This next function was created in previous colabs. We are going to recycle it for use in this lab
cd ../../../../../../../../../content/drive/My Drive/colabs/DATA/
ls
geom = pd.read_csv('boundaries-baltimore-tracts-NoWater-2010.csv')
print("Column headings:")
print(geom.columns)
You will not be able to follow along in this section of the lab as I have not made the data publicly accessible.
I am using function created in earlier tutorials to perfrom this part.
I want to merge a geospatial dataset with 6 years/datasets of longitudinal data
final = ''
cd ../../../../../content/drive/My Drive/colabs/DATA/baltimore-births
# I start by accessing the first file I need
b17 = pd.read_excel('BirthsbyCensus_2017.XLS', sheetname='Births')
# Then I merge the GeoSpatial datset
gdf = mergeDatasets( b17, geom, 'ctract', 'TRACTCE10', 'geometry', True, 'attempt2.csv' )
# I convert the dataset into a geospatial one after processing the geometry columns text as a coordinate
gdf['geometry'] = gdf['geometry'].apply(lambda x: loads(x))
final = GeoDataFrame(gdf, geometry='geometry')
# I store the count for that year as a unique column
final['Total Number of Births By Census Tract 2017'] = final['Count']
# .. And repeat 5 more times
def createFinalDf(filename=False, sheetname=False, left=False, right=False, how=False):
df = pd.read_excel(filename, sheetname=sheetname)
gdf = mergeDatasets( df, geom, left, right, how )
gdf[how] = gdf[how].apply(lambda x: loads(x))
return GeoDataFrame(gdf, geometry=how)
filename, sheetname, left, right, how = ['Jones.BirthsbyCensus2016.XLS', 'Births', 'ctract', 'TRACTCE10', 'geometry']
final['Total Number of Births By Census Tract 2016'] = createFinalDf(filename, sheetname, left, right, how)['Count']
filename, sheetname, left, right, how = ['Jones.BirthsbyCensus2015.XLS', 'Births', 'ctract', 'TRACTCE10', 'geometry']
final['Total Number of Births By Census Tract 2015'] = createFinalDf(filename, sheetname, left, right, how)['Count']
filename, sheetname, left, right, how = ['Births2014.XLS', 'Births', 'ctract', 'TRACTCE10', 'geometry']
final['Total Number of Births By Census Tract 2014'] = createFinalDf(filename, sheetname, left, right, how)['Count']
filename, sheetname, left, right, how = ['Births2013.XLS', 'Births', 'ctract', 'TRACTCE10', 'geometry']
final['Total Number of Births By Census Tract 2013'] = createFinalDf(filename, sheetname, left, right, how)['Count']
filename, sheetname, left, right, how = ['Births_2012.xls', 'Births', 'ctract', 'TRACTCE10', 'geometry']
final['Total Number of Births By Census Tract 2012'] = createFinalDf(filename, sheetname, left, right, how)['Count']
Fantastic!
Your data is all together in a single dataset.
now what?
First lets take the centerpoint of each geometry. This will be where we place labeled text on the map geometry.
#final['centroid'] = final['geometry'].centroid
final['centroid'] = final['geometry'].representative_point()
final.head()
final.plot()
Data was successfully merged across all years and geometry.
Now we want the tractname, geometry, and the specific column we want to make a gif from.
# Get only the results tab
td = final.filter(regex="Births|tract|geometry|centroid")
td = td.reindex(sorted(td.columns), axis=1)
# Coerce columns stored as floats into integers.
# This will ensure numbers are rounded to whole digits when displaying the reults
col1 = 'Total Number of Births By Census Tract 2012'
col2 = 'Total Number of Births By Census Tract 2016'
td[[col1, col2]] = td[[col1, col2]].fillna(-1)
td[[col1, col2]] = td[[col1, col2]].astype('int32')
td.head()
Data exploration is essential! But not covered in this lab.
td.filter(regex="Births").hist()
Everything is almost ready to start making our gifmap!
Lets just get the minimum and maximum values so that our color ramp will have consistent values on each picture.
# Get Min Max
mins = []
maxs = []
for col in td.columns:
if col in ['NAME', 'state', 'county', 'ctract', 'geometry', 'centroid'] :
pass
else:
mins.append(td[col].min())
maxs.append(td[col].max())
print(mins, maxs)
# set the min and max range for the choropleth map
vmin, vmax = min(mins), max(maxs)
print('Smallest Value: ', vmin, ', Max Value:', vmax)
pd.set_option('precision', 0)
merged = td
fileNames = []
saveGifAs = './Births_By_Tract_With_Count_Label.gif'
labelBounds = True
specialLabelCol = False
annotation = 'Source: Maryland Vital Statistics; Analysis by: Baltimore Neighborhood Indicators Alliance'
fontsize='22'
# For each column
for indx, col in enumerate(merged.filter(regex="Births").columns):
if col in ['NAME', 'state', 'county', 'tract', 'geometry'] :
pass
else:
print('INDEX', indx)
print('Col: '+str(col) )
image_name = col+'.jpg'
fileNames.append(image_name)
# create map, UDPATE: added plt.Normalize to keep the legend range the same for all maps
fig = merged.plot(column=col, cmap='Blues', figsize=(10,10),
linewidth=0.8, edgecolor='0.8', vmin=vmin, vmax=vmax,
legend=True, norm=plt.Normalize(vmin=vmin, vmax=vmax)
)
# https://stackoverflow.com/questions/38899190/geopandas-label-polygons
if labelBounds:
labelColumn = col
if specialLabelCol: labelColumn = specialLabelCol
merged.apply(lambda x: fig.annotate(s=x[labelColumn], xy=x.geometry.centroid.coords[0], ha='center'),axis=1);
# remove axis of chart and set title
fig.axis('off')
fig.set_title(str(col.replace("_", " ")), fontdict={'fontsize': fontsize, 'fontweight' : '3'})
# create an annotation for the data source
fig.annotate(annotation,
xy=(0.1, .08), xycoords='figure fraction',
horizontalalignment='left', verticalalignment='top',
fontsize=10, color='#555555')
# this will save the figure as a high-res png in the output path. you can also save as svg if you prefer.
# filepath = os.path.join(output_path, image_name)
chart = fig.get_figure()
# fig.savefig(“map_export.png”, dpi=300)
chart.savefig(image_name, dpi=300)
plt.close(chart)
images = []
for filename in fileNames:
images.append(imageio.imread(filename))
imageio.mimsave(saveGifAs, images, fps=.5)
# This will print out a picture of each picture in the gifmap.
from PIL import Image
import requests
from io import BytesIO
for filename in fileNames:
img = Image.open(filename)
size = 328, 328
img.thumbnail(size, Image.ANTIALIAS)
img
def getMinMax(df):
# Get Min Max
mins = []
maxs = []
for col in td.columns:
if col in ['NAME', 'state', 'county', 'ctract', 'geometry', 'centroid'] :
pass
else:
mins.append(df[col].min())
maxs.append(df[col].max())
print(mins, maxs)
return [mins, maxs]
def getAbsMinMax(df):
# Get Min Max
mins, maxs = getMinMax(df)
return [min(mins), max(maxs)]
def createGif(fileNames, saveGifAs):
# This will print out a picture of each picture in the gifmap as well.
images = []
for filename in fileNames:
images.append(imageio.imread(filename))
img = Image.open(filename)
size = 328, 328
img.thumbnail(size, Image.ANTIALIAS)
print(img)
imageio.mimsave(saveGifAs, images, fps=.5)
def createPicture(merged, labelBounds, specialLabelCol, annotation, fontsize):
fileNames = []
# For each column
for indx, col in enumerate(merged.filter(regex="Births").columns):
if col in ['NAME', 'state', 'county', 'tract', 'geometry'] :
pass
else:
print('INDEX', indx)
print('Col: '+str(col) )
image_name = col+'.jpg'
fileNames.append(image_name)
# create map, UDPATE: added plt.Normalize to keep the legend range the same for all maps
fig = merged.plot(column=col, cmap='Blues', figsize=(10,10),
linewidth=0.8, edgecolor='0.8', vmin=vmin, vmax=vmax,
legend=True, norm=plt.Normalize(vmin=vmin, vmax=vmax)
)
# https://stackoverflow.com/questions/38899190/geopandas-label-polygons
if labelBounds:
labelColumn = col
if specialLabelCol: labelColumn = specialLabelCol
merged.apply(lambda x: fig.annotate(s=x[labelColumn], xy=x.geometry.centroid.coords[0], ha='center'),axis=1);
# remove axis of chart and set title
fig.axis('off')
fig.set_title(str(col.replace("_", " ")), fontdict={'fontsize': fontsize, 'fontweight' : '3'})
# create an annotation for the data source
fig.annotate(annotation,
xy=(0.1, .08), xycoords='figure fraction',
horizontalalignment='left', verticalalignment='top',
fontsize=10, color='#555555')
# this will save the figure as a high-res png in the output path. you can also save as svg if you prefer.
# filepath = os.path.join(output_path, image_name)
chart = fig.get_figure()
# fig.savefig(“map_export.png”, dpi=300)
chart.savefig(image_name, dpi=300)
plt.close(chart)
return fileNames
def createGifMap(df, saveGifAs, labelBounds, specialLabelCol, annotation, fontsize):
# set the min and max range for the choropleth map
vmin, vmax = getAbsMinMax(df)
print('Smallest Value: ', vmin, ', Max Value:', vmax)
fileNames = createPicture(df, labelBounds, specialLabelCol, annotation, fontsize)
print('Pictures were created')
createGif(fileNames, saveGifAs)
pd.set_option('precision', 0)
saveGifAs = './Births_By_Tract_With_Count_Label.gif'
labelBounds = True
specialLabelCol = False
annotation = 'Source: Maryland Vital Statistics; Analysis by: Baltimore Neighborhood Indicators Alliance'
fontsize='22'
createGifMap(td, saveGifAs, labelBounds, specialLabelCol, annotation, fontsize)
ls