In this project, a comprehensive two-part analysis is conducted to understand the impact of storm-surge inundation:
For Planners / Government Agencies: The first part focuses on providing spatial information for planners and government agencies. It utilizes choropleth maps to visually depict the distribution of populations affected by storm surges across various census tracts of Washington, DC. The data mapped includes key demographic, economic and housing characteristics such as total population, median age, population segments over 65 and under 10 years of age, total housing units, housing units built before 1950, median income, and the percentage of the population living below the poverty level. These visualizations are designed to help decision-makers prioritize resources and tailor their response strategies for different categories of storms, thus enabling a more effective, data-driven approach to disaster preparedness and mitigation.
For Citizens: The second part adopts a citizen-centric approach, aiming to empower individuals with actionable information in the event of a storm. By allowing users to input a specific point location, the analysis identifies the category of storm that would affect that area. It also shows the pharmacies within a one-mile radius, highlighting those that would be inundated by storm surge waters. This information can be vital for individuals needing access to essential services like pharmacies during emergencies.
#Import required libraries
import matplotlib.pyplot as plt
import pandas as pd
from geopandas import read_file, gpd
import folium
import ipywidgets as widgets
from IPython.display import display
input_file = './Data/StormSurgeRiskAreas/Storm_Surge_Risk_Areas.shp'
surge = gpd.read_file(input_file)
# surge.tail()
surge['HES_ZONE'].replace(99, 5, inplace=True)
print(surge.crs)
surge.tail()
center_lat, center_lon = 38.905996,-77.043686
m1 = folium.Map(location=[center_lat, center_lon], tiles="CartoDB positron", zoom_start=12)
for i in range(1,6):
m1 = surge[surge['HES_ZONE']==i].explore(m=m1, name=f"category {i}")
folium.LayerControl().add_to(m1)
m1
#Demographic
input_file_1 = './Data/ACS_Demographic/ACS_5-Year_Demographic_Characteristics_DC_Census_Tract.shp'
acs_demographic = gpd.read_file(input_file_1)
#Economic
input_file_2 = './Data/ACS_Economic/ACS_5-Year_Economic_Characteristics_DC_Census_Tract.shp'
acs_economic = gpd.read_file(input_file_2)
#Housing
input_file_3 = './Data/ACS_Housing/ACS_5-Year_Housing_Characteristics_DC_Census_Tract.shp'
acs_housing = gpd.read_file(input_file_3)
# Check projections
print('Demographic:', acs_demographic.crs)
print('Economic:', acs_economic.crs)
print('Housing:', acs_housing.crs)
# Inspect the demographic data
acs_demographic.head()
demographic = acs_demographic[['GEOID', 'geometry', 'DP05_0001E', 'DP05_0018E', 'DP05_0005E', 'DP05_0006E', 'DP05_0024E']]
demographic = demographic.rename(columns ={'DP05_0001E':'total_pop', 'DP05_0018E':'median_age', 'DP05_0005E':'pop_under_5', 'DP05_0006E':'pop_5_9', 'DP05_0024E':'pop_over_65'})
demographic['pop_under_10'] = demographic['pop_under_5'] + demographic['pop_5_9']
demographic.drop(columns=['pop_under_5', 'pop_5_9'], inplace=True)
demographic
# acs_economic.head()
economic = acs_economic[['GEOID', 'geometry', 'DP03_0062E', 'DP03_0119P']]
economic = economic.rename(columns ={'DP03_0062E':'median_income', 'DP03_0119P':'percent_below_poverty', 'DP05_0005E':'pop_under_5', 'DP05_0006E':'pop_5_9', 'DP05_0024E':'pop_over_65'})
economic
# acs_housing.head()
housing = acs_housing[['GEOID', 'geometry', 'DP04_0001E', 'DP04_0025E', 'DP04_0026E']]
housing = housing.rename(columns ={'DP04_0001E':'total_units', 'DP04_0025E':'built_1949_1940', 'DP04_0026E':'built_before_1940'})
housing['built_before_1950'] = housing['built_1949_1940'] + housing['built_before_1940']
housing.drop(columns=['built_1949_1940', 'built_before_1940'], inplace=True)
housing
# Combine the ACS datasets
merged_data = demographic.merge(economic, on=['GEOID', 'geometry'])
acs_data = merged_data.merge(housing, on=['GEOID', 'geometry'])
acs_data.dropna(inplace=True)
acs_data.head()
# Visualize the total population in each DC census tract
ax = acs_data.plot(figsize=(15, 10), column='total_pop', legend='true')
def get_category_surge(category):
surge_category = surge[surge['HES_ZONE']<=category]
return surge_category
def get_category_acs(category):
acs_data_category = gpd.clip(acs_data, get_category_surge(category))
return acs_data_category
Further, we create visualizations to help understand the spatial distribution of various attributes of interest. These attributes indicate the impact of the storm of people and infrastructure. Visualization are enabled using choropleth maps for each storm category to help planners understand the resources required for evacuation and preparedness.
# Create two dropdowns for storm category and attribute to be visulaized
storm_category = widgets.Dropdown(
options=['1', '2', '3', '4', '5'],
value='5',
description='Category of storm',
disabled=False,
)
quantity = widgets.Dropdown(
options=[('Total population', 'total_pop'), ('Median age', 'median_age'),
('Population over 65', 'pop_over_65'), ('Population under 10', 'pop_under_10'),
('Median income', 'median_income'), ('Percentage below poverty', 'percent_below_poverty'),
('Total housing units', 'total_units'), ('Units built before 1950', 'built_before_1950')],
value='total_pop',
description='Quantity to plot',
)
# Function to get the maps and display them
def get_category_map(button):
with output:
output.clear_output() # Clear previous output
surge_category = get_category_surge(int(storm_category.value))
acs_data_category = get_category_acs(int(storm_category.value))
m = surge_category.explore(name="storm surge areas", tiles="CartoDB positron")
cols = acs_data_category.columns[2:]
acs_data_category.explore(m=m, name=quantity.value, column=quantity.value,
cmap="Reds", scheme="naturalbreaks", k=5,
style_kwds=dict(color="black", weight=1, opacity=0.5, fillOpacity=1.0),
legend=True, legend_kwds=dict(colorbar=True),
popup=list(cols), tooltip=False,
)
folium.TileLayer("openstreetmap", control=True, opacity=0.5, name="openstreetmap").add_to(m)
folium.LayerControl().add_to(m)
display(m)
# Arrange the dropdowns in the same line using HBox
dropdown_box = widgets.HBox([storm_category, quantity])
# Display the dropdowns
display(dropdown_box)
output = widgets.Output()
# Create the button for generating maps
button = widgets.Button(description='Get Plot')
# Set up the button click event
button.on_click(get_category_map)
# Display the button and output area
display(button)
display(output)
# Plot the total population and housing units impacted by each category of storm
affected_cols = ['total_pop', 'pop_over_65', 'pop_under_10', 'total_units', 'built_before_1950']
affected={}
for category in range(1,6):
acs_category = get_category_acs(category)
affected[category] = {}
for col in affected_cols:
affected[category][col] = sum(acs_category[col])
affected_df = pd.DataFrame(affected).T
axes = affected_df.plot.bar(rot=0, subplots=True, figsize=(10,20))
affected_df = pd.DataFrame(affected)
axes = affected_df.plot.bar(rot=0, figsize=(12,10))
plt.ylabel('Number affected', size=24);
plt.legend(title="Category of storm", fontsize=20, fancybox=True)
axes.tick_params(axis='x', labelsize=20) # Increase x-axis tick font size
axes.tick_params(axis='y', labelsize=20) # Increase y-axis tick font size
-
As the category of storm increases, the impact is higher. There is a significantly higher rise in numbers between category 2 to category 3 storms.
Certain areas may require additional resources and time for evacuation: