| import dash |
| from dash import dcc, html, Input, Output, State, callback, ctx, dash_table |
| import dash_bootstrap_components as dbc |
| import duckdb |
|
|
| from dashboard_helpers import ( |
| get_neighborhood_geojson, |
| get_all_neighborhoods, |
| build_trip_choropleth, |
| build_demo_choropleth, |
| build_rr_bar_chart, |
| build_rr_heatmap, |
| build_neighborhood_profile, |
| build_comparison_map, |
| get_trip_stats_df, |
| get_download_csv, |
| _GRAPH_CONFIG, |
| ) |
|
|
| DB_PATH = "sf_dashboard.db" |
|
|
| def get_con(): |
| c = duckdb.connect(DB_PATH, read_only=True) |
| c.install_extension("spatial") |
| c.load_extension("spatial") |
| return c |
|
|
| _init_con = get_con() |
| GEOJSON = get_neighborhood_geojson(_init_con) |
| NEIGHBORHOODS = get_all_neighborhoods(_init_con) |
| MONTHS = _init_con.sql( |
| "SELECT DISTINCT month FROM trip_counts_pu ORDER BY month" |
| ).df()["month"].tolist() |
|
|
| baseline_df = _init_con.sql("SELECT * FROM city_baselines").df() |
| BASELINE_WHITE = float(baseline_df["baseline_white_pct"].iloc[0]) |
| BASELINE_ASIAN = float(baseline_df["baseline_asian_pct"].iloc[0]) |
| _init_con.close() |
|
|
| app = dash.Dash( |
| __name__, |
| external_stylesheets=[dbc.themes.DARKLY], |
| meta_tags=[ |
| {"name": "viewport", "content": "width=device-width, initial-scale=1"} |
| ], |
| title="SF Taxi Mobility Equity Dashboard", |
| ) |
| server = app.server |
|
|
| sidebar = dbc.Card( |
| [ |
| html.H4("Controls", className="text-center mb-3"), |
| html.Hr(), |
| dbc.Label("Month"), |
| dcc.Dropdown( |
| id="month-selector", |
| options=[{"label": m, "value": m} for m in MONTHS], |
| value="Jan2024", |
| clearable=False, |
| className="mb-3", |
| ), |
| dbc.Label("Hail Type"), |
| dbc.Checklist( |
| id="hail-type-filter", |
| options=[ |
| {"label": " Street-Hail", "value": "Street"}, |
| {"label": " App-Based", "value": "App"}, |
| ], |
| value=["Street", "App"], |
| className="mb-3", |
| ), |
| html.Hr(), |
| html.Div(id="selected-nhood-display", className="mb-3"), |
| dbc.Button( |
| "Reset Selection", |
| id="reset-selection-btn", |
| color="secondary", |
| size="sm", |
| className="w-100 mb-2", |
| ), |
| dbc.Button( |
| "Download CSV", |
| id="download-btn", |
| color="info", |
| size="sm", |
| className="w-100 mb-2", |
| ), |
| dbc.Button( |
| "Download GeoJSON", |
| id="download-geojson-btn", |
| color="success", |
| size="sm", |
| className="w-100 mb-2", |
| ), |
| dcc.Download(id="csv-download"), |
| dcc.Download(id="geojson-download"), |
| ], |
| body=True, |
| id="sidebar", |
| className="bg-dark", |
| style={ |
| "position": "sticky", |
| "top": "10px", |
| "height": "calc(100vh - 20px)", |
| "overflowY": "auto", |
| }, |
| ) |
|
|
| _STAT_CARD_STYLE = {"height": "100%"} |
|
|
| insights_banner = dbc.Row( |
| [ |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H3(id="stat-total-trips", className="text-center mb-0"), |
| html.P("Total Trips", className="text-center text-muted small"), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| style=_STAT_CARD_STYLE, |
| ), |
| md=3, |
| ), |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H3(id="stat-top-nhood", className="text-center mb-0", |
| style={"fontSize": "1.6rem"}), |
| html.P("Most Served", className="text-center text-muted small"), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| style=_STAT_CARD_STYLE, |
| ), |
| md=3, |
| ), |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H3(id="stat-rr-white", className="text-center mb-0"), |
| html.P("White RR", className="text-center text-muted small"), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| style=_STAT_CARD_STYLE, |
| ), |
| md=3, |
| ), |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H3(id="stat-rr-asian", className="text-center mb-0"), |
| html.P("Asian RR", className="text-center text-muted small"), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| style=_STAT_CARD_STYLE, |
| ), |
| md=3, |
| ), |
| ], |
| id="insights-banner", |
| className="mb-2 g-2", |
| ) |
|
|
| narrative_row = dbc.Row( |
| dbc.Col( |
| html.P( |
| id="narrative-text", |
| className="text-center fst-italic", |
| style={"color": "#adb5bd", "fontSize": "0.95rem"}, |
| ), |
| ), |
| className="mb-3", |
| ) |
|
|
| trip_maps = dbc.Row( |
| [ |
| dbc.Col( |
| dcc.Graph(id="street-pu-map", config=_GRAPH_CONFIG), |
| md=6, |
| ), |
| dbc.Col( |
| dcc.Graph(id="app-pu-map", config=_GRAPH_CONFIG), |
| md=6, |
| ), |
| dbc.Col( |
| dcc.Graph(id="street-do-map", config=_GRAPH_CONFIG), |
| md=6, |
| className="mt-2", |
| ), |
| dbc.Col( |
| dcc.Graph(id="app-do-map", config=_GRAPH_CONFIG), |
| md=6, |
| className="mt-2", |
| ), |
| ], |
| className="mb-3", |
| ) |
|
|
| demographics_tab = dbc.Row( |
| [ |
| dbc.Col(dcc.Graph(id="white-deviation-map", config=_GRAPH_CONFIG), md=6), |
| dbc.Col(dcc.Graph(id="asian-deviation-map", config=_GRAPH_CONFIG), md=6), |
| ] |
| ) |
|
|
| rr_tab = html.Div( |
| [ |
| dbc.Row( |
| [ |
| dbc.Col(dcc.Graph(id="rr-bar-chart", config=_GRAPH_CONFIG), md=7), |
| dbc.Col(dcc.Graph(id="rr-heatmap", config=_GRAPH_CONFIG), md=5), |
| ] |
| ), |
| ], |
| id="rr-section", |
| ) |
|
|
| comparison_tab = html.Div( |
| [ |
| dbc.Row( |
| [ |
| dbc.Col( |
| [ |
| dbc.Label("Month A"), |
| dcc.Dropdown( |
| id="comp-month-a", |
| options=[{"label": m, "value": m} for m in MONTHS], |
| value="Jan2024", |
| clearable=False, |
| ), |
| ], |
| md=3, |
| ), |
| dbc.Col( |
| [ |
| dbc.Label("Month B"), |
| dcc.Dropdown( |
| id="comp-month-b", |
| options=[{"label": m, "value": m} for m in MONTHS], |
| value="Mar2024", |
| clearable=False, |
| ), |
| ], |
| md=3, |
| ), |
| dbc.Col( |
| [ |
| dbc.Label("Hail Type"), |
| dcc.Dropdown( |
| id="comp-hail", |
| options=[ |
| {"label": "Street-Hail", "value": "Street"}, |
| {"label": "App-Based", "value": "App"}, |
| ], |
| value="Street", |
| clearable=False, |
| ), |
| ], |
| md=3, |
| ), |
| dbc.Col( |
| [ |
| dbc.Label("Metric"), |
| dcc.Dropdown( |
| id="comp-metric", |
| options=[ |
| {"label": "Pickups", "value": "pu"}, |
| {"label": "Drop-offs", "value": "do"}, |
| ], |
| value="pu", |
| clearable=False, |
| ), |
| ], |
| md=3, |
| ), |
| ], |
| className="mb-3", |
| ), |
| dbc.Row(dbc.Col(dcc.Graph(id="comparison-map", config=_GRAPH_CONFIG))), |
| ] |
| ) |
|
|
| top10_section = dbc.Row( |
| [ |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H5( |
| id="street-stats-title", |
| className="text-center", |
| ), |
| dbc.Row( |
| [ |
| dbc.Col( |
| [ |
| html.H6("Pickups", className="text-center text-muted"), |
| html.Div(id="street-pu-table"), |
| ], |
| md=6, |
| ), |
| dbc.Col( |
| [ |
| html.H6("Drop-offs", className="text-center text-muted"), |
| html.Div(id="street-do-table"), |
| ], |
| md=6, |
| ), |
| ] |
| ), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| ), |
| md=6, |
| ), |
| dbc.Col( |
| dbc.Card( |
| [ |
| html.H5( |
| id="app-stats-title", |
| className="text-center", |
| ), |
| dbc.Row( |
| [ |
| dbc.Col( |
| [ |
| html.H6("Pickups", className="text-center text-muted"), |
| html.Div(id="app-pu-table"), |
| ], |
| md=6, |
| ), |
| dbc.Col( |
| [ |
| html.H6("Drop-offs", className="text-center text-muted"), |
| html.Div(id="app-do-table"), |
| ], |
| md=6, |
| ), |
| ] |
| ), |
| ], |
| body=True, |
| className="bg-dark border-secondary", |
| ), |
| md=6, |
| ), |
| ], |
| className="mt-3", |
| ) |
|
|
| analysis_tabs = dbc.Tabs( |
| [ |
| dbc.Tab(demographics_tab, label="Demographics", tab_id="tab-demo"), |
| dbc.Tab(rr_tab, label="Representative Ratios", tab_id="tab-rr"), |
| dbc.Tab(comparison_tab, label="Monthly Comparison", tab_id="tab-comp"), |
| ], |
| id="analysis-tabs", |
| active_tab="tab-demo", |
| className="mb-3", |
| ) |
|
|
| nhood_offcanvas = dbc.Offcanvas( |
| html.Div(id="nhood-detail-content"), |
| id="nhood-offcanvas", |
| title="Neighborhood Profile", |
| placement="end", |
| is_open=False, |
| style={"width": "400px", "backgroundColor": "#303030"}, |
| ) |
|
|
| app.layout = dbc.Container( |
| [ |
| dcc.Store(id="selected-neighborhood", data=None), |
| nhood_offcanvas, |
| |
| dbc.Row( |
| dbc.Col( |
| [ |
| html.H2( |
| "SF Taxi Mobility Equity Dashboard", |
| className="text-center mt-3 mb-1", |
| ), |
| html.P( |
| "Analyzing whether Street-Hail and App-Based taxi services " |
| "in San Francisco are equitably distributed across " |
| "neighborhoods with different demographic compositions.", |
| className="text-center text-muted mb-3", |
| style={"maxWidth": "700px", "margin": "0 auto"}, |
| ), |
| ] |
| ) |
| ), |
| |
| dbc.Row( |
| [ |
| dbc.Col(sidebar, md=2, className="pe-1"), |
| dbc.Col( |
| [ |
| insights_banner, |
| narrative_row, |
| html.H5("Trip Distribution Maps", className="mb-2"), |
| trip_maps, |
| html.H5("Analysis", className="mb-2"), |
| analysis_tabs, |
| html.H5("Top 10 Neighborhoods", className="mb-2"), |
| top10_section, |
| ], |
| md=10, |
| ), |
| ] |
| ), |
| |
| dbc.Row( |
| dbc.Col( |
| html.P( |
| [ |
| "Data: ", |
| html.A("DataSF Taxi Trips", href="https://data.sfgov.org/Transportation/Taxi-Trips/m8hk-2ipk/", target="_blank"), |
| " | ", |
| html.A("ACS 2022", href="https://api.census.gov/data/2022/acs/acs5.html", target="_blank"), |
| " | Debayan Mandal", |
| ], |
| className="text-center text-muted small mt-4 mb-3", |
| ) |
| ) |
| ), |
| ], |
| fluid=True, |
| ) |
|
|
| @callback( |
| Output("street-pu-map", "figure"), |
| Output("app-pu-map", "figure"), |
| Output("street-do-map", "figure"), |
| Output("app-do-map", "figure"), |
| Input("month-selector", "value"), |
| Input("hail-type-filter", "value"), |
| Input("selected-neighborhood", "data"), |
| ) |
| def update_trip_maps(month, hail_types, sel_nhood): |
| hail_types = hail_types or ["Street", "App"] |
| con = get_con() |
| figs = [] |
| for ht, metric in [ |
| ("Street", "pu"), |
| ("App", "pu"), |
| ("Street", "do"), |
| ("App", "do"), |
| ]: |
| if ht in hail_types: |
| figs.append( |
| build_trip_choropleth(con, GEOJSON, ht, month, metric, sel_nhood) |
| ) |
| else: |
| import plotly.graph_objects as go |
| fig = go.Figure() |
| fig.update_layout( |
| title=f"{ht} {'Pickups' if metric == 'pu' else 'Drop-offs'} (filtered out)", |
| paper_bgcolor="rgba(0,0,0,0)", |
| font_color="#e0e0e0", |
| height=420, |
| ) |
| figs.append(fig) |
| return figs[0], figs[1], figs[2], figs[3] |
|
|
| @callback( |
| Output("selected-neighborhood", "data"), |
| Input("street-pu-map", "clickData"), |
| Input("app-pu-map", "clickData"), |
| Input("street-do-map", "clickData"), |
| Input("app-do-map", "clickData"), |
| Input("white-deviation-map", "clickData"), |
| Input("asian-deviation-map", "clickData"), |
| Input("reset-selection-btn", "n_clicks"), |
| prevent_initial_call=True, |
| ) |
| def sync_map_selection(c1, c2, c3, c4, c5, c6, reset): |
| trigger = ctx.triggered_id |
| if trigger == "reset-selection-btn": |
| return None |
| for click in [c1, c2, c3, c4, c5, c6]: |
| if click and trigger in [ |
| "street-pu-map", "app-pu-map", "street-do-map", "app-do-map", |
| "white-deviation-map", "asian-deviation-map", |
| ]: |
| try: |
| return click["points"][0]["customdata"][0] |
| except (KeyError, IndexError, TypeError): |
| try: |
| return click["points"][0]["location"] |
| except (KeyError, IndexError, TypeError): |
| pass |
| return dash.no_update |
|
|
| @callback( |
| Output("selected-nhood-display", "children"), |
| Input("selected-neighborhood", "data"), |
| ) |
| def display_selected_nhood(nhood): |
| if nhood: |
| return dbc.Alert( |
| [html.Strong("Selected: "), nhood], |
| color="info", |
| className="py-2 mb-0", |
| ) |
| return html.P("Click a neighborhood on any map", className="text-muted small") |
|
|
| @callback( |
| Output("stat-total-trips", "children"), |
| Output("stat-top-nhood", "children"), |
| Output("stat-rr-white", "children"), |
| Output("stat-rr-asian", "children"), |
| Output("narrative-text", "children"), |
| Input("month-selector", "value"), |
| Input("hail-type-filter", "value"), |
| ) |
| def update_insights(month, hail_types): |
| hail_types = hail_types or ["Street", "App"] |
| ht_filter = ", ".join(f"'{h}'" for h in hail_types) |
| con = get_con() |
|
|
| total = con.sql(f""" |
| SELECT SUM(trips_pu) AS total |
| FROM trip_counts_pu |
| WHERE month = '{month}' AND hail_type IN ({ht_filter}) |
| """).df()["total"].iloc[0] |
| total = int(total) if total else 0 |
|
|
| top = con.sql(f""" |
| SELECT nhood, SUM(trips_pu) AS t |
| FROM trip_counts_pu |
| WHERE month = '{month}' AND hail_type IN ({ht_filter}) |
| GROUP BY nhood ORDER BY t DESC LIMIT 1 |
| """).df() |
| top_nhood = top["nhood"].iloc[0] if not top.empty else "N/A" |
|
|
| rr = con.sql(f""" |
| SELECT AVG(RR_white_PU) AS rr_w, AVG(RR_asian_PU) AS rr_a |
| FROM representative_ratios |
| WHERE month = '{month}' AND hail_type IN ({ht_filter}) |
| """).df() |
| rr_w = round(float(rr["rr_w"].iloc[0]), 2) if not rr.empty and rr["rr_w"].iloc[0] else 0 |
| rr_a = round(float(rr["rr_a"].iloc[0]), 2) if not rr.empty and rr["rr_a"].iloc[0] else 0 |
|
|
| |
| parts = [f"In {month}, {total:,} taxi trips were recorded across SF."] |
| parts.append(f"{top_nhood} was the most served neighborhood.") |
| if rr_w > 1.0: |
| parts.append( |
| f"White-majority neighborhoods received {rr_w:.2f}x their expected " |
| f"share of service," |
| ) |
| if rr_a < 1.0: |
| parts.append( |
| f"and Asian-majority neighborhoods received {rr_a:.2f}x." |
| ) |
| narrative = " ".join(parts) |
|
|
| return f"{total:,}", top_nhood, f"{rr_w:.2f}x", f"{rr_a:.2f}x", narrative |
|
|
| @callback( |
| Output("white-deviation-map", "figure"), |
| Output("asian-deviation-map", "figure"), |
| Input("selected-neighborhood", "data"), |
| ) |
| def update_demo_maps(sel_nhood): |
| con = get_con() |
| w = build_demo_choropleth( |
| con, GEOJSON, "white_pct", BASELINE_WHITE, |
| f"White Pop. Deviation ({BASELINE_WHITE:.1f}% baseline)", |
| sel_nhood, |
| ) |
| a = build_demo_choropleth( |
| con, GEOJSON, "asian_pct", BASELINE_ASIAN, |
| f"Asian Pop. Deviation ({BASELINE_ASIAN:.1f}% baseline)", |
| sel_nhood, |
| ) |
| return w, a |
|
|
| @callback( |
| Output("rr-bar-chart", "figure"), |
| Output("rr-heatmap", "figure"), |
| Input("month-selector", "value"), |
| ) |
| def update_rr(month): |
| con = get_con() |
| return build_rr_bar_chart(con, month), build_rr_heatmap(con) |
|
|
| @callback( |
| Output("comparison-map", "figure"), |
| Input("comp-month-a", "value"), |
| Input("comp-month-b", "value"), |
| Input("comp-hail", "value"), |
| Input("comp-metric", "value"), |
| ) |
| def update_comparison(month_a, month_b, hail, metric): |
| con = get_con() |
| return build_comparison_map(con, GEOJSON, hail, metric, month_a, month_b) |
|
|
| @callback( |
| Output("nhood-offcanvas", "is_open"), |
| Output("nhood-detail-content", "children"), |
| Input("selected-neighborhood", "data"), |
| State("month-selector", "value"), |
| ) |
| def update_nhood_panel(nhood, month): |
| if not nhood: |
| return False, [] |
|
|
| con = get_con() |
| profile = build_neighborhood_profile(con, nhood, month) |
| demo = profile["demographics"] |
|
|
| children = [ |
| html.H4(profile["name"]), |
| html.Hr(), |
| ] |
|
|
| if demo: |
| children.extend( |
| [ |
| html.H6("Demographics"), |
| html.P(f"Population: {demo['total_pop']:,}"), |
| html.P(f"White: {demo['white_pct']}% (deviation: {demo['white_dev']:+.1f} pp)"), |
| html.P(f"Black: {demo['black_pct']}%"), |
| html.P(f"Asian: {demo['asian_pct']}% (deviation: {demo['asian_dev']:+.1f} pp)"), |
| html.Hr(), |
| ] |
| ) |
|
|
| if profile["trips"]: |
| children.append(html.H6(f"Trips ({month})")) |
| for key, val in sorted(profile["trips"].items()): |
| if month in key: |
| label = key.replace(f"_{month}", "").replace("_", " ") |
| children.append(html.P(f"{label}: {val:,}")) |
| children.append(html.Hr()) |
|
|
| if profile["trend_fig"]: |
| children.append( |
| dcc.Graph( |
| figure=profile["trend_fig"], |
| config={"displayModeBar": False}, |
| style={"height": "280px"}, |
| ) |
| ) |
|
|
| return True, children |
|
|
| def _make_table(df): |
| if df.empty: |
| return html.P("No data", className="text-muted") |
| return dash_table.DataTable( |
| data=df.to_dict("records"), |
| columns=[{"name": c, "id": c} for c in df.columns], |
| style_table={"overflowX": "auto"}, |
| style_header={ |
| "backgroundColor": "#375a7f", |
| "color": "white", |
| "fontWeight": "bold", |
| "textAlign": "center", |
| }, |
| style_cell={ |
| "backgroundColor": "#303030", |
| "color": "#e0e0e0", |
| "textAlign": "center", |
| "padding": "6px", |
| "fontSize": "0.85rem", |
| }, |
| style_data_conditional=[ |
| { |
| "if": {"row_index": 0}, |
| "backgroundColor": "#3a506b", |
| "fontWeight": "bold", |
| } |
| ], |
| page_size=10, |
| ) |
|
|
|
|
| @callback( |
| Output("street-stats-title", "children"), |
| Output("street-pu-table", "children"), |
| Output("street-do-table", "children"), |
| Output("app-stats-title", "children"), |
| Output("app-pu-table", "children"), |
| Output("app-do-table", "children"), |
| Input("month-selector", "value"), |
| ) |
| def update_top10(month): |
| con = get_con() |
| s_pu = get_trip_stats_df(con, "Street", month, "pu") |
| s_do = get_trip_stats_df(con, "Street", month, "do") |
| a_pu = get_trip_stats_df(con, "App", month, "pu") |
| a_do = get_trip_stats_df(con, "App", month, "do") |
|
|
| return ( |
| f"Street-Hail Top 10 ({month})", |
| _make_table(s_pu), |
| _make_table(s_do), |
| f"App-Based Top 10 ({month})", |
| _make_table(a_pu), |
| _make_table(a_do), |
| ) |
|
|
| @callback( |
| Output("csv-download", "data"), |
| Input("download-btn", "n_clicks"), |
| State("month-selector", "value"), |
| State("hail-type-filter", "value"), |
| State("selected-neighborhood", "data"), |
| prevent_initial_call=True, |
| ) |
| def trigger_download(n_clicks, month, hail_types, nhood): |
| con = get_con() |
| df = get_download_csv(con, month, hail_types, nhood) |
| filename = f"sf_taxi_data_{month}" |
| if nhood: |
| filename += f"_{nhood.replace(' ', '_').replace('/', '_')}" |
| return dcc.send_data_frame(df.to_csv, f"{filename}.csv", index=False) |
|
|
| @callback( |
| Output("geojson-download", "data"), |
| Input("download-geojson-btn", "n_clicks"), |
| State("month-selector", "value"), |
| State("hail-type-filter", "value"), |
| State("selected-neighborhood", "data"), |
| prevent_initial_call=True, |
| ) |
| def trigger_geojson_download(n_clicks, month, hail_types, nhood): |
| import json |
| import copy |
| con = get_con() |
| hail_types = hail_types or ["Street", "App"] |
| ht_filter = ", ".join(f"'{h}'" for h in hail_types) |
| nhood_clause = f"AND n.nhood = '{nhood}'" if nhood else "" |
|
|
| |
| data_df = con.sql(f""" |
| SELECT n.nhood, |
| COALESCE(SUM(pu.trips_pu), 0) AS total_pickups, |
| COALESCE(SUM(td.trips_do), 0) AS total_dropoffs, |
| nd.total_pop, nd.white_pct, nd.black_pct, nd.asian_pct |
| FROM neighborhoods n |
| LEFT JOIN trip_counts_pu pu |
| ON pu.nhood = n.nhood AND pu.month = '{month}' |
| AND pu.hail_type IN ({ht_filter}) |
| LEFT JOIN trip_counts_do td |
| ON td.nhood = n.nhood AND td.month = '{month}' |
| AND td.hail_type IN ({ht_filter}) |
| LEFT JOIN nhood_demographics nd ON nd.nhood = n.nhood |
| WHERE 1=1 {nhood_clause} |
| GROUP BY n.nhood, nd.total_pop, nd.white_pct, nd.black_pct, nd.asian_pct |
| """).df() |
| data_map = {row["nhood"]: row.to_dict() for _, row in data_df.iterrows()} |
|
|
| geojson = copy.deepcopy(GEOJSON) |
| |
| if nhood: |
| geojson["features"] = [ |
| f for f in geojson["features"] |
| if f["properties"]["nhood"] == nhood |
| ] |
| |
| for feat in geojson["features"]: |
| name = feat["properties"]["nhood"] |
| if name in data_map: |
| d = data_map[name] |
| feat["properties"]["month"] = month |
| feat["properties"]["total_pickups"] = int(d["total_pickups"]) |
| feat["properties"]["total_dropoffs"] = int(d["total_dropoffs"]) |
| feat["properties"]["total_pop"] = int(d["total_pop"]) if d["total_pop"] else 0 |
| feat["properties"]["white_pct"] = round(float(d["white_pct"]), 1) if d["white_pct"] else 0 |
| feat["properties"]["black_pct"] = round(float(d["black_pct"]), 1) if d["black_pct"] else 0 |
| feat["properties"]["asian_pct"] = round(float(d["asian_pct"]), 1) if d["asian_pct"] else 0 |
|
|
| filename = f"sf_taxi_{month}" |
| if nhood: |
| filename += f"_{nhood.replace(' ', '_').replace('/', '_')}" |
| return dict( |
| content=json.dumps(geojson, indent=2), |
| filename=f"{filename}.geojson", |
| type="application/geo+json", |
| ) |
|
|
| if __name__ == "__main__": |
| app.run(host="0.0.0.0", port=7860, debug=False) |
|
|