Spaces:
Sleeping
Sleeping
| import os | |
| import pandas as pd | |
| import numpy as np | |
| import geopandas as gpd | |
| import plotly.express as px | |
| from dash import Dash, dcc, html, Input, Output, callback | |
| import dash_bootstrap_components as dbc | |
| external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] | |
| app = Dash(__name__, external_stylesheets=[dbc.themes.SLATE]) | |
| server = app.server # Required for deployment | |
| # Load data using relative paths | |
| flood_risk = gpd.read_file("data/final_flood_risk_scores.geojson") | |
| flood_risk['Census Tract'] = flood_risk['Census Tract'].astype(int) | |
| ami = pd.read_csv("data/acs_2023_ami_cleaned.csv") | |
| age = pd.read_csv("data/acs_2023_age_cleaned.csv") | |
| tenure = pd.read_csv("data/acs_2023_tenure_cleaned.csv") | |
| race = pd.read_csv("data/race_percentages.csv") | |
| # Adjust race dataframe for display | |
| race[['NH White','NH Black or African American','NH Asian','Hispanic or Latino','NH Other']] = race[['NH White','NH Black or African American','NH Asian','Hispanic or Latino','NH Other']] * 100 | |
| race = race.rename(columns={'NH White': 'White', 'NH Black or African American': 'Black or African American','NH Asian' : 'Asian', 'NH Other': 'Other'}) | |
| race = race.set_index("geoid") | |
| race.rename(index={'total': '111'}, inplace=True) | |
| race.index = race.index.astype(int) | |
| race.rename(index={111: 'San Francisco'}, inplace=True) | |
| def normalize_scores(series): | |
| """Takes a series and normalizes values to be on a 0-100 range""" | |
| max_val = series.max() | |
| min_val = series.min() | |
| scores = ((series - min_val) * 100) / (max_val - min_val) | |
| return scores.round(1) | |
| def race_plot(tract_id: str): | |
| rows = race.loc[['San Francisco', tract_id], race.columns[0:5]] | |
| race_fig = px.bar( | |
| rows.T, | |
| y=rows.index, | |
| x=rows.columns, | |
| barmode="group", | |
| labels={'geoid': 'Geographic Area', 'index': 'Race or Ethnicity', 'value': 'Percent of Total Population'}, | |
| color_discrete_sequence=['lightgray', 'black'] | |
| ) | |
| race_fig.update_layout(xaxis={'categoryorder': 'total descending'}) | |
| race_fig.update_traces( | |
| hovertemplate="%{x}<br><b>%{y:,.0f}%<br></b><extra></extra>" | |
| ) | |
| race_fig.update_layout({'plot_bgcolor': 'rgba(0,0,0,0)'}) | |
| return race_fig | |
| def baseline_race_plot(): | |
| rows = race.loc[['San Francisco'], race.columns[0:5]] | |
| race_fig = px.bar( | |
| rows.T, | |
| y=rows.index, | |
| x=rows.columns, | |
| barmode="group", | |
| labels={'geoid': 'Geographic Area', 'index': 'Race or Ethnicity', 'value': 'Percent of Total Population'}, | |
| color_discrete_sequence=['lightgray', 'black'] | |
| ) | |
| race_fig.update_layout(xaxis={'categoryorder': 'total descending'}) | |
| race_fig.update_traces( | |
| hovertemplate="%{x}<br><b>%{y:,.0f}%<br></b><extra></extra>" | |
| ) | |
| race_fig.update_layout({'plot_bgcolor': 'rgba(0,0,0,0)'}) | |
| return race_fig | |
| def plot_data(df: pd.DataFrame, tract_id: str, column: str, x_label: str): | |
| df = df.copy() | |
| df['select'] = 0 | |
| df['opacity'] = 0.5 | |
| df.loc[df['geoid'] == tract_id, 'select'] = 1 | |
| df.loc[df['geoid'] == tract_id, 'opacity'] = 1 | |
| fig = px.strip( | |
| df, | |
| x=column, | |
| color='select', | |
| stripmode='overlay', | |
| color_discrete_map={1: 'black', 0: 'darkgray'}, | |
| labels={column: x_label}, | |
| hover_name='geoid', | |
| hover_data={column: True, 'select': False} | |
| ) | |
| fig.update_layout(showlegend=False) | |
| fig.update_layout({'plot_bgcolor': 'rgba(0,0,0,0)'}) | |
| fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) | |
| fig.update_layout(height=250) | |
| fig.update_traces({'marker': {'size': 15}}) | |
| fig.update_xaxes(gridcolor='#f2f2f2') | |
| fig.update_traces( | |
| hovertemplate=f"Census Tract: {tract_id}<br>{x_label}: <b>%{{x:,.0f}}<br></b><extra></extra>" | |
| ) | |
| fig.data[0].marker.opacity = 0.2 | |
| fig.data[1].marker.opacity = 1.0 | |
| return fig | |
| def baseline_plot(df: pd.DataFrame, column: str, x_label: str): | |
| df = df.copy() | |
| df['select'] = 0 | |
| df['opacity'] = 0.5 | |
| fig = px.strip( | |
| df, | |
| x=column, | |
| color_discrete_sequence=['darkgray'], | |
| stripmode='overlay', | |
| labels={column: x_label}, | |
| hover_name='geoid', | |
| hover_data={column: True, 'select': False}, | |
| custom_data=['geoid'] | |
| ) | |
| fig.update_layout(showlegend=False) | |
| fig.update_layout({'plot_bgcolor': 'rgba(0,0,0,0)'}) | |
| fig.update_layout(margin=dict(l=0, r=0, t=0, b=0)) | |
| fig.update_layout(height=250) | |
| fig.update_traces({'marker': {'size': 15}}) | |
| fig.update_xaxes(gridcolor='#f2f2f2') | |
| fig.update_traces(marker=dict(opacity=0.2)) | |
| fig.update_traces( | |
| hovertemplate="Census Tract: %{customdata[0]}<br>" + x_label + ": <b>%{x:,.0f}<br></b><extra></extra>" | |
| ) | |
| return fig | |
| def update_figure(value1, value2, value3): | |
| updated_flood = flood_risk.copy() | |
| total_value = value1 + value2 + value3 | |
| factor1 = value1 / total_value | |
| factor2 = value2 / total_value | |
| factor3 = value3 / total_value | |
| updated_flood["Weighted flood risk score"] = ( | |
| updated_flood["Floodplain coverage score"] * factor1 + | |
| updated_flood["Drain quality score"] * factor2 + | |
| updated_flood["Green infrastructure score"] * factor3 | |
| ) | |
| updated_flood["Weighted flood risk score"] = normalize_scores(updated_flood["Weighted flood risk score"]) | |
| sf_lat = 37.7749 | |
| sf_lon = -122.4194 | |
| updated_flood = updated_flood.set_index('Census Tract') | |
| updated_flood = updated_flood.to_crs("EPSG:4326") | |
| fig = px.choropleth_map( | |
| updated_flood, | |
| geojson=updated_flood.geometry, | |
| locations=updated_flood.index, | |
| color="Weighted flood risk score", | |
| center={"lat": sf_lat, "lon": sf_lon}, | |
| map_style="carto-positron", | |
| zoom=10, | |
| color_continuous_scale='blues' | |
| ) | |
| fig.update_layout( | |
| transition_duration=200, | |
| uirevision="constant", | |
| clickmode='event+select', | |
| width=800, | |
| height=400, | |
| margin={"r": 0, "t": 0, "l": 0, "b": 0}, | |
| coloraxis_colorbar=dict(orientation='h', len=0.8), | |
| coloraxis_colorbar_title_font=dict(family='sans-serif') | |
| ) | |
| fig.update_traces( | |
| marker_line_width=0.5, | |
| marker_opacity=0.75, | |
| marker_line_color='white' | |
| ) | |
| fig.update_traces( | |
| selected=dict(marker=dict(opacity=1)), | |
| unselected=dict(marker=dict(opacity=0.5)) | |
| ) | |
| fig.update_traces( | |
| hovertemplate="Census Tract: %{location}<br>Weighted flood risk score: <b>%{z:,.0f}</b><extra></extra>" | |
| ) | |
| return fig | |
| def update_graphs(clicked_data): | |
| if clicked_data is not None: | |
| tract_id = clicked_data["points"][0]["location"] | |
| ami_fig = plot_data(ami, tract_id, "median_income", 'Median Income') | |
| tenure_fig = plot_data(tenure, tract_id, "pct_renter", 'Percent Renter Households') | |
| under5_fig = plot_data(age, tract_id, 'Under 5 Years', 'Percent Residents Under 5 Years') | |
| over65_fig = plot_data(age, tract_id, '65 Years and Over', 'Percent Residents 65 Years and Older') | |
| race_fig = race_plot(tract_id) | |
| return ami_fig, tenure_fig, under5_fig, over65_fig, race_fig | |
| else: | |
| ami_fig = baseline_plot(ami, "median_income", "Median Income") | |
| tenure_fig = baseline_plot(tenure, "pct_renter", 'Percent Renter Households') | |
| under5_fig = baseline_plot(age, 'Under 5 Years', 'Percent Residents Under 5 Years') | |
| over65_fig = baseline_plot(age, '65 Years and Over', 'Percent Residents 65 Years and Older') | |
| race_fig = baseline_race_plot() | |
| return ami_fig, tenure_fig, under5_fig, over65_fig, race_fig | |
| # Set initial state | |
| initial_ami_fig, initial_tenure_fig, initial_under5_fig, initial_over65_fig, initial_race_fig = update_graphs(None) | |
| initial_flood_map = update_figure(2, 2, 2) | |
| # Build the app layout | |
| app.layout = html.Div([ | |
| html.Div( | |
| children="Analyzing Flood Risk in San Francisco", | |
| style={'fontFamily': 'Arial', 'fontSize': '40px', "font-weight": "bold", 'textAlign': 'center', 'paddingTop': '30px'} | |
| ), | |
| html.Div([ | |
| # Sliders | |
| html.Div([ | |
| html.Div([ | |
| html.H3("Select a floodplain coverage weight:", style={'fontFamily': 'Arial', 'fontSize': '18px', "font-weight": "normal", 'letterSpacing': '.05px'}), | |
| dcc.Slider(0, 4, step=None, marks={0: 'low priority', 1: '', 2: 'moderate priority', 3: '', 4: 'high priority'}, value=2, id='floodplain_slider') | |
| ]), | |
| html.Div([ | |
| html.H3("Select a drain quality weight:", style={'fontFamily': 'Arial', 'fontSize': '18px', 'letterSpacing': '.05px'}), | |
| dcc.Slider(0, 4, step=None, marks={0: 'low priority', 1: '', 2: 'moderate priority', 3: '', 4: 'high priority'}, value=2, id='drain_slider') | |
| ]), | |
| html.Div([ | |
| html.H3("Select a green infrastructure weight:", style={'fontFamily': 'Arial', 'fontSize': '18px', 'letterSpacing': '.05px'}), | |
| dcc.Slider(0, 4, step=None, marks={0: 'low priority', 1: '', 2: 'moderate priority', 3: '', 4: 'high priority'}, value=2, id='gi_slider') | |
| ]), | |
| ], style={'display': 'flex', 'flexDirection': 'column', 'flex': 1, 'marginTop': '20px', 'paddingRight': '40px', 'padding': '3%', 'justifyContent': 'center', "gap": "25px"}), | |
| html.Div(id='slider-output-container'), | |
| # Flood risk score map | |
| html.Div(dcc.Graph(id='flood-risk-graph', figure=initial_flood_map), style={'padding': '3%', 'flex': 1}) | |
| ], style={'display': 'flex', 'flexDirection': 'row'}), | |
| # Census tract name display | |
| html.Div([ | |
| html.Span("Census Tract:", style={'fontFamily': 'Arial', 'fontSize': '18px', 'marginRight': '5px'}), | |
| html.Span(id='tract_id', style={'fontFamily': 'Arial', 'fontSize': '18px', 'font-weight': 'bold'}) | |
| ], style={'display': 'flex', 'flexDirection': 'row', 'padding': '3%', 'justify-content': 'center'}), | |
| # AMI and tenure charts | |
| html.Div([ | |
| html.Div(dcc.Graph(id='ami_fig', figure=initial_ami_fig), style={'padding': '3%', 'flex': 1}), | |
| html.Div(dcc.Graph(id='tenure_fig', figure=initial_tenure_fig), style={'padding': '3%', 'flex': 1}), | |
| ], style={'display': 'flex', 'flexDirection': 'row'}), | |
| # Age charts | |
| html.Div([ | |
| html.Div(dcc.Graph(id='under5_fig', figure=initial_under5_fig), style={'padding': '3%', 'flex': 1}), | |
| html.Div(dcc.Graph(id='over65_fig', figure=initial_over65_fig), style={'padding': '3%', 'flex': 1}), | |
| ], style={'display': 'flex', 'flexDirection': 'row'}), | |
| # Race bar chart | |
| html.Div([ | |
| html.Div(dcc.Graph(id='race_fig', figure=initial_race_fig), style={'padding': '3%', 'flex': 1}), | |
| ], style={'display': 'flex', 'flexDirection': 'row', 'paddingLeft': '20%', 'paddingRight': '20%'}), | |
| ]) | |
| def update_output(value1, value2, value3, clickData): | |
| flood_risk_fig = update_figure(value1, value2, value3) | |
| ami_fig, tenure_fig, under5_fig, over65_fig, race_fig = update_graphs(clickData) | |
| if clickData is not None: | |
| tract_id = clickData["points"][0]["location"] | |
| else: | |
| tract_id = 'Select a census tract on the map' | |
| return flood_risk_fig, ami_fig, tenure_fig, under5_fig, over65_fig, race_fig, tract_id | |
| if __name__ == '__main__': | |
| app.run(debug=False, host="0.0.0.0", port=7860) | |