I created an interactive dashboard to visualize and analyze the performance of the package locker pilot program in New York City. The goal was to build a tool to monitor the operation of these lockers in a simple and visual way.
Key Performance Indicators (KPIs): At a glance, you can see important data like the total number of reservations, the delivery success rate, and the average time it takes to pick up or deliver a package.
Interactive Map: The NYC map, which shows all the locker locations. If you click on any of them, a side panel appears with detailed information on that specific locker, such as how many reservations it’s had or its individual performance.
Trends Analysis: I also included a section for viewing trends. You can switch between different charts to better understand reservation behavior, for example, how reservations vary by the day of the week or time of day.
Filters: You can filter all the information by different Boroughs, locker sizes, or location types to see only what’s relevant to you.
I designed it with a fluid user experience and a visual style that evokes the city of New York. I hope you like the result.
The code
import pandas as pd
from datetime import datetime, timedelta
import dash
from dash import dcc, html, callback_context
from dash.dependencies import Input, Output, State
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import numpy as np
import dash_bootstrap_components as dbc
try:
df = pd.read_csv(“LockerNYC_Reservations_20250903.csv”)
print(“Archivo ‘LockerNYC_Reservations_20250903.csv’ cargado exitosamente.”)
except Exception as e:
print(f"Error al cargar el archivo CSV: {e}")
exit()
def parse_duration_to_minutes(duration_str):
“”“Convierte una cadena de duración (ej: ‘0.1.2’) a minutos.”“”
if pd.isna(duration_str):
return np.nan
try:
parts = str(duration_str).split(‘.’)
hours = int(parts[0]) if len(parts) > 0 else 0
minutes = int(parts[1]) if len(parts) > 1 else 0
seconds = int(parts[2]) if len(parts) > 2 else 0
total_minutes = hours * 60 + minutes + seconds / 60
return total_minutes
except (ValueError, IndexError):
return np.nan
Convertir duraciones
df[‘Pickup Duration (min)’] = df[‘Pickup Duration’].apply(parse_duration_to_minutes)
df[‘Delivery Duration (min)’] = df[‘Delivery Duration’].apply(parse_duration_to_minutes)
df[‘Pickup Duration (min)’] = df[‘Pickup Duration (min)’].fillna(0)
df[‘Delivery Duration (min)’] = df[‘Delivery Duration (min)’].fillna(0)
Procesar fechas con formato explícito para evitar advertencias
date_columns = [‘Created Date’, ‘Delivery Date’, ‘Receive Date’, ‘Withdraw Date’, ‘Expire Date’]
for col in date_columns:
df[col] = pd.to_datetime(df[col], errors=‘coerce’, format=‘mixed’)
Limpiar datos
df.dropna(subset=[‘Locker Size’, ‘Location Type’, ‘Borough’, ‘Latitude’, ‘longitude’, ‘Locker Name’], inplace=True)
df[‘Borough’] = df[‘Borough’].str.capitalize()
df = df[df[‘Borough’] != ‘nan’]
Crear métricas adicionales
df[‘Day_of_Week’] = df[‘Created Date’].dt.day_name()
df[‘Hour_Created’] = df[‘Created Date’].dt.hour
df[‘Dwell_Time’] = (df[‘Receive Date’] - df[‘Delivery Date’]).dt.total_seconds() / 3600
— NYC Styled Dashboard —
app = dash.Dash(name, external_stylesheets=[dbc.themes.DARKLY], suppress_callback_exceptions=True)
app.title = “Lockers NY Dashboard”
Paleta de colores de NYC
nyc_colors = {
‘bg’: ‘#0f0f0f’,
‘surface’: ‘#1a1a1a’,
‘card’: ‘#262626’,
‘taxi’: ‘#ffd500’,
‘subway’: ‘#0099d8’,
‘central_park’: ‘#228b22’,
‘danger’: ‘#ff6b35’,
‘text’: ‘#ffffff’,
‘text_muted’: ‘#b0b0b0’,
‘text_dark’: ‘#666666’,
‘success’: ‘#00ff7f’,
‘warning’: ‘#ffd500’,
‘error’: ‘#ff3030’
}
Funciones para generar contenido del dashboard
def generate_kpi_card(title, value, unit=“”, color_accent=nyc_colors[‘subway’]):
“”“Genera una tarjeta de KPI con un estilo de NYC.”“”
return dbc.Card(style={
‘backgroundColor’: nyc_colors[‘card’],
‘borderLeft’: f’4px solid {color_accent}',
‘flex’: ‘1’
}, children=dbc.CardBody(
[
html.P(title, style={
‘margin’: ‘0’,
‘fontSize’: ‘14px’, # Aumentado el tamaño de la fuente para el título del KPI
‘fontWeight’: ‘700’,
‘color’: nyc_colors[‘text_muted’]
}),
html.H2(f"{value}{unit}", style={
‘margin’: ‘5px 0 0 0’,
‘fontSize’: ‘28px’,
‘fontWeight’: ‘bold’,
‘color’: nyc_colors[‘text’]
})
]
))
def generate_recommendations(df_filtered, selected_locker_df):
“”“Genera recomendaciones basadas en el rendimiento del casillero vs. el promedio del distrito.”“”
recommendations =
# Calcular promedios del distrito
if not selected_locker_df.empty:
borough = selected_locker_df['Borough'].iloc[0]
df_borough = df_filtered[df_filtered['Borough'] == borough]
delivery_time = selected_locker_df['Delivery Duration (min)'].mean()
pickup_time = selected_locker_df['Pickup Duration (min)'].mean()
success_rate = (selected_locker_df['Received'].sum() / len(selected_locker_df)) * 100
# Lógica de recomendaciones basada en percentiles o desviación estándar
if delivery_time > df_borough['Delivery Duration (min)'].quantile(0.75):
recommendations.append(
html.P("⚠️ High delivery duration. This could be due to operational bottlenecks.",
style={'color': nyc_colors['warning'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
elif delivery_time < df_borough['Delivery Duration (min)'].quantile(0.25):
recommendations.append(
html.P("✅ Excellent delivery performance! A model for best practices.",
style={'color': nyc_colors['success'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
if pickup_time > df_borough['Pickup Duration (min)'].quantile(0.75):
recommendations.append(
html.P("⚠️ High pickup duration. Consider optimizing pickup routes.",
style={'color': nyc_colors['warning'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
elif pickup_time < df_borough['Pickup Duration (min)'].quantile(0.25):
recommendations.append(
html.P("✅ Efficient pickup operations. A best practice model for this route.",
style={'color': nyc_colors['success'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
if success_rate < df_borough['Received'].mean() * 100 * 0.9: # 10% por debajo del promedio del distrito
recommendations.append(
html.P("🔄 Success rate is below borough average. Review notification processes.",
style={'color': nyc_colors['error'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
elif success_rate > df_borough['Received'].mean() * 100 * 1.1: # 10% por encima del promedio del distrito
recommendations.append(
html.P("🏆 Outstanding success rate! Consider increasing capacity if demand allows.",
style={'color': nyc_colors['success'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
if not recommendations:
recommendations.append(
html.P("📊 Performance is within expected parameters. Continue monitoring trends.",
style={'color': nyc_colors['text_muted'], 'fontSize': '16px', 'marginBottom': '10px'}) # Aumentado el tamaño de la fuente
)
return recommendations
— Layout de la aplicación —
app.layout = dbc.Container(fluid=True, style={
‘backgroundColor’: nyc_colors[‘bg’],
‘color’: nyc_colors[‘text’],
‘fontFamily’: ‘Helvetica Neue, Arial, sans-serif’
}, children=[
dbc.Row(className="mb-4", children=[
dbc.Col(
html.Div(
style={'display': 'flex', 'alignItems': 'center', 'padding': '20px', 'borderLeft': f'5px solid {nyc_colors["taxi"]}'},
children=[
html.Div(style={'flex': 1}, children=[
html.H1("LockerNYC", style={'margin': '0', 'fontSize': '36px', 'fontWeight': 'bold'}),
# Aumentado el tamaño de la fuente para los párrafos de contexto
html.P("LockerNYC is a pilot program that allows New Yorkers to receive and send packages using secure lockers on public sidewalks.",
style={'margin': '0', 'color': nyc_colors['text_muted'], 'fontSize': '16px'}),
html.P("Real-Time Operations Dashboard", style={'margin': '0', 'color': nyc_colors['text_muted'], 'fontSize': '16px'})
]),
]
)
)
]),
dbc.Row(id='kpis-container', className="g-4 mb-4", style={'padding': '20px'}),
dbc.Row(className="g-4", children=[
# Ajuste de las columnas a 2, 8, 2
dbc.Col(width=2, children=[
dbc.Card(style={
'backgroundColor': nyc_colors['card'],
'borderLeft': f'4px solid {nyc_colors["subway"]}'
}, children=dbc.CardBody(
[
html.H3("Filters & View", style={'marginTop': '0', 'fontSize': '18px'}),
html.Label("Filter by Borough:", style={'color': nyc_colors['text_muted'], 'fontSize': '14px'}),
dcc.Dropdown(
id='borough-dropdown',
options=[{'label': b, 'value': b} for b in df['Borough'].unique()],
value=None,
multi=True,
placeholder="Select Boroughs",
className="Selectable-dropdown",
style={'backgroundColor': nyc_colors['surface'], 'color': nyc_colors['text_dark']}
),
html.Br(),
html.Label("Filter by Locker Size:", style={'color': nyc_colors['text_muted'], 'fontSize': '14px'}),
dcc.Dropdown(
id='size-dropdown',
options=[{'label': s, 'value': s} for s in df['Locker Size'].unique()],
value=None,
multi=True,
placeholder="Select Locker Sizes",
className="Selectable-dropdown",
style={'backgroundColor': nyc_colors['surface'], 'color': nyc_colors['text_dark']}
),
html.Br(),
html.Label("Filter by Location Type:", style={'color': nyc_colors['text_dark'], 'fontSize': '14px'}),
dcc.Dropdown(
id='location-type-dropdown',
options=[{'label': lt, 'value': lt} for lt in df['Location Type'].unique()],
value=None,
multi=True,
placeholder="Select Location Types",
className="Selectable-dropdown",
style={'backgroundColor': nyc_colors['surface'], 'color': nyc_colors['text_dark']}
),
html.Br(),
html.Label("Filter by Created Date:", style={'color': nyc_colors['text_muted'], 'fontSize': '14px'}),
dcc.DatePickerRange(
id='date-range-picker',
start_date=df['Created Date'].min().date(),
end_date=df['Created Date'].max().date(),
display_format='YYYY-MM-DD',
style={'backgroundColor': nyc_colors['surface'], 'color': nyc_colors['text']}
),
dbc.Row(className="g-2 mt-4",
children=[
dbc.Col(dbc.Button('MAP VIEW', id='map-view-button', n_clicks=0,
style={'width': '100%', 'fontWeight': 'bold', 'backgroundColor': nyc_colors['subway'],
'color': nyc_colors['text']})),
dbc.Col(dbc.Button('TRENDS ANALYSIS', id='trends-analysis-button', n_clicks=0,
style={'width': '100%', 'fontWeight': 'bold', 'backgroundColor': nyc_colors['surface'],
'color': nyc_colors['text_muted']})),
]),
]
))
]),
dbc.Col(width=8, children=[
dbc.Card(style={
'backgroundColor': nyc_colors['card']
}, children=dbc.CardBody(
dcc.Loading(id="loading-spinner", children=[
html.Div(id='main-content', style={'width': '100%', 'height': '100%'})
], type="default")
))
]),
dbc.Col(width=2, children=[
dbc.Card(id='details-panel', style={
'backgroundColor': nyc_colors['card'],
'borderRight': f'4px solid {nyc_colors["taxi"]}'
})
])
]),
# dcc.Store para guardar la vista actual y evitar que los clics del mapa se pierdan
dcc.Store(id='view-store', data='map'),
# Footer
dbc.Row(className="mt-5", children=[
dbc.Col(
html.Footer(
"Dashboard developed using Python | Plotly | Dash ",
className="text-center py-3",
style={'color': nyc_colors['text_muted'], 'fontSize': '16px'} # Aumentado el tamaño de la fuente para el footer
)
)
])
])
— Callbacks —
Callback para alternar entre las vistas y manejar los filtros
@app.callback(
Output(‘kpis-container’, ‘children’),
Output(‘main-content’, ‘children’),
Output(‘map-view-button’, ‘style’),
Output(‘trends-analysis-button’, ‘style’),
Output(‘view-store’, ‘data’),
Input(‘borough-dropdown’, ‘value’),
Input(‘size-dropdown’, ‘value’),
Input(‘location-type-dropdown’, ‘value’),
Input(‘date-range-picker’, ‘start_date’),
Input(‘date-range-picker’, ‘end_date’),
Input(‘map-view-button’, ‘n_clicks’),
Input(‘trends-analysis-button’, ‘n_clicks’),
State(‘view-store’, ‘data’)
)
def update_dashboard(selected_boroughs, selected_sizes, selected_types, start_date, end_date, map_clicks, trends_clicks, current_view):
“”"
Callback principal que actualiza el dashboard según los filtros y la vista seleccionada.
Se ha reestructurado para que los botones de vista no interfieran con la selección del mapa.
“”"
ctx = callback_context
view = current_view
if ctx.triggered:
triggered_id = ctx.triggered[0]['prop_id'].split('.')[0]
if triggered_id == 'trends-analysis-button':
view = 'trends'
elif triggered_id == 'map-view-button':
view = 'map'
# Filtrar datos
df_filtered = df.copy()
if selected_boroughs:
df_filtered = df_filtered[df_filtered['Borough'].isin(selected_boroughs)]
if selected_sizes:
df_filtered = df_filtered[df_filtered['Locker Size'].isin(selected_sizes)]
if selected_types:
df_filtered = df_filtered[df_filtered['Location Type'].isin(selected_types)]
if start_date and end_date:
df_filtered = df_filtered[
(df_filtered['Created Date'].dt.date >= pd.to_datetime(start_date).date()) &
(df_filtered['Created Date'].dt.date <= pd.to_datetime(end_date).date())
]
# Calcular KPIs
total_reservations = len(df_filtered)
total_received = df_filtered['Received'].sum()
success_rate = (total_received / total_reservations) * 100 if total_reservations > 0 else 0
avg_pickup = df_filtered['Pickup Duration (min)'].mean() if not df_filtered.empty else 0
avg_delivery = df_filtered['Delivery Duration (min)'].mean() if not df_filtered.empty else 0
kpis = [
generate_kpi_card("Total Reservations", f"{total_reservations:,}", color_accent=nyc_colors['subway']),
generate_kpi_card("Total Received", f"{total_received:,}", color_accent=nyc_colors['success']),
generate_kpi_card("Success Rate", f"{success_rate:.2f}", "%", color_accent=nyc_colors['warning']),
generate_kpi_card("Avg. Delivery Duration", f"{avg_delivery:.2f}", " min", color_accent=nyc_colors['danger']),
generate_kpi_card("Avg. Pickup Duration", f"{avg_pickup:.2f}", " min", color_accent=nyc_colors['taxi']),
]
# Estilos de los botones
map_button_style = {'width': '100%', 'fontWeight': 'bold'}
trends_button_style = {'width': '100%', 'fontWeight': 'bold'}
if view == 'map':
map_button_style['backgroundColor'] = nyc_colors['subway']
map_button_style['color'] = nyc_colors['text']
trends_button_style['backgroundColor'] = nyc_colors['surface']
trends_button_style['color'] = nyc_colors['text_muted']
fig = px.scatter_map(df_filtered,
lat="Latitude",
lon="longitude",
color="Borough",
# Aumentar el tamaño de los puntos para que los más pequeños sean visibles
size=df_filtered['Delivery Duration (min)'] + 2,
size_max=30,
hover_name="Locker Name",
color_discrete_map={
'Manhattan': '#ff7f0e',
'Brooklyn': '#1f77b4',
'Queens': '#2ca02c',
'Bronx': '#d62728',
'Staten Island': '#9467bd'
},
map_style="carto-positron",
zoom=10,
# Altura del mapa ajustada a 500
height=500)
fig.update_layout(
map_center={"lat": 40.7128, "lon": -74.0060},
paper_bgcolor=nyc_colors['bg'],
plot_bgcolor=nyc_colors['bg'],
font_color=nyc_colors['text'],
margin={"r":0, "t":0, "l":0, "b":0},
clickmode='event+select',
legend_title_text='Borough',
# Leyenda en la parte superior
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)
content = dcc.Graph(id='map-section', figure=fig)
else: # Vista de tendencias
map_button_style['backgroundColor'] = nyc_colors['surface']
map_button_style['color'] = nyc_colors['text_muted']
trends_button_style['backgroundColor'] = nyc_colors['subway']
trends_button_style['color'] = nyc_colors['text']
content = dbc.CardBody(
[
html.H3("Trends Analysis", className="mb-4", style={'textAlign': 'center'}),
dcc.RadioItems(
id='trends-radio',
options=[
{'label': 'Reservations by Day of Week', 'value': 'day'},
{'label': 'Reservations by Hour', 'value': 'hour'},
{'label': 'Dwell Time Distribution', 'value': 'dwell'},
{'label': 'Delivery vs. Pickup Duration', 'value': 'scatter'}
],
value='day',
className="mb-4",
# Ajuste de estilo para que los radio items se vean mejor
style={
'display': 'flex',
'justifyContent': 'space-between', # Ajusta el espacio entre elementos
'flexWrap': 'wrap', # Permite que los elementos se envuelvan en la siguiente línea si no hay espacio
'color': nyc_colors['text']
},
inputStyle={"marginRight": "5px"}, # Espacio entre el radio y el texto
labelStyle={"marginRight": "15px", "flex": "1", "textAlign": "center"} # Espacio entre los labels
),
dcc.Loading(
id="loading-trends",
children=html.Div(id='trends-content'),
type="default"
)
]
)
return kpis, content, map_button_style, trends_button_style, view
@app.callback(
Output(‘trends-content’, ‘children’),
Input(‘trends-radio’, ‘value’),
State(‘borough-dropdown’, ‘value’),
State(‘size-dropdown’, ‘value’),
State(‘location-type-dropdown’, ‘value’),
State(‘date-range-picker’, ‘start_date’),
State(‘date-range-picker’, ‘end_date’)
)
def update_trends_graph(selected_trend, selected_boroughs, selected_sizes, selected_types, start_date, end_date):
df_filtered = df.copy()
if selected_boroughs:
df_filtered = df_filtered[df_filtered[‘Borough’].isin(selected_boroughs)]
if selected_sizes:
df_filtered = df_filtered[df_filtered[‘Locker Size’].isin(selected_sizes)]
if selected_types:
df_filtered = df_filtered[df_filtered[‘Location Type’].isin(selected_types)]
if start_date and end_date:
df_filtered = df_filtered[
(df_filtered['Created Date'].dt.date >= pd.to_datetime(start_date).date()) &
(df_filtered['Created Date'].dt.date <= pd.to_datetime(end_date).date())
]
# Generación de los gráficos
if selected_trend == 'day':
fig = px.bar(df_filtered.groupby('Day_of_Week').size().reindex(
['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']).reset_index(name='Count'),
x='Day_of_Week', y='Count',
labels={'Day_of_Week': 'Day of Week', 'Count': 'Number of Reservations'},
title="Reservations by Day of Week",
color_discrete_sequence=[nyc_colors['subway']])
elif selected_trend == 'hour':
fig = px.bar(df_filtered.groupby('Hour_Created').size().reset_index(name='Count'),
x='Hour_Created', y='Count',
labels={'Hour_Created': 'Hour of Day', 'Count': 'Number of Reservations'},
title="Reservations by Hour",
color_discrete_sequence=[nyc_colors['taxi']])
elif selected_trend == 'dwell':
fig = px.histogram(df_filtered, x='Dwell_Time', nbins=50,
labels={'Dwell_Time': 'Dwell Time (hours)'},
title="Dwell Time Distribution",
color_discrete_sequence=[nyc_colors['danger']])
elif selected_trend == 'scatter':
fig = px.scatter(df_filtered, x='Delivery Duration (min)', y='Pickup Duration (min)',
color='Borough',
title="Delivery vs. Pickup Duration",
color_discrete_map={
'Manhattan': '#ff7f0e',
'Brooklyn': '#1f77b4',
'Queens': '#2ca02c',
'Bronx': '#d62728',
'Staten Island': '#9467bd'
})
fig.update_layout(
paper_bgcolor=nyc_colors['card'],
plot_bgcolor=nyc_colors['card'],
font_color=nyc_colors['text'],
margin=dict(l=20, r=20, t=40, b=20)
)
return dcc.Graph(figure=fig)
@app.callback(
Output(‘details-panel’, ‘children’),
Input(‘map-section’, ‘clickData’),
State(‘borough-dropdown’, ‘value’),
State(‘size-dropdown’, ‘value’),
State(‘location-type-dropdown’, ‘value’),
State(‘date-range-picker’, ‘start_date’),
State(‘date-range-picker’, ‘end_date’)
)
def update_details_panel(clickData, selected_boroughs, selected_sizes, selected_types, start_date, end_date):
“”"
Actualiza el panel de detalles al hacer clic en un punto del mapa.
Ahora calcula las métricas para todas las reservas asociadas con el casillero clicado.
“”"
if clickData is None:
return dbc.CardBody(children=[
html.H3(“Select a Locker”, style={‘color’: nyc_colors[‘text’]}),
html.P(“Click on a locker on the map to see its detailed performance metrics and recommendations.”,
style={‘color’: nyc_colors[‘text_muted’], ‘fontSize’: ‘16px’}) # Aumentado el tamaño de la fuente
])
# Obtener el nombre del casillero del punto en el que se hizo clic.
point_data = clickData['points'][0]
latitude = point_data['lat']
longitude = point_data['lon']
# Encontrar la fila en el DataFrame original que coincide con la latitud y longitud
# Esto asegura que obtenemos todas las reservas para ese casillero, no solo el punto del gráfico.
selected_locker_df = df[(df['Latitude'] == latitude) & (df['longitude'] == longitude)]
if selected_locker_df.empty:
return dbc.CardBody(children=[
html.H3("Error", style={'color': nyc_colors['error']}),
html.P("Could not find locker details for the selected point.", style={'color': nyc_colors['text_muted'], 'fontSize': '16px'})
])
locker_name = selected_locker_df['Locker Name'].iloc[0]
# Aplicar filtros adicionales de la UI a los datos del casillero seleccionado.
if selected_boroughs:
selected_locker_df = selected_locker_df[selected_locker_df['Borough'].isin(selected_boroughs)]
if selected_sizes:
selected_locker_df = selected_locker_df[selected_locker_df['Locker Size'].isin(selected_sizes)]
if selected_types:
selected_locker_df = selected_locker_df[selected_locker_df['Location Type'].isin(selected_types)]
if start_date and end_date:
selected_locker_df = selected_locker_df[
(selected_locker_df['Created Date'].dt.date >= pd.to_datetime(start_date).date()) &
(selected_locker_df['Created Date'].dt.date <= pd.to_datetime(end_date).date())
]
# Calcular las métricas agregadas para el casillero.
total_reservations = len(selected_locker_df)
total_received = selected_locker_df['Received'].sum()
success_rate = (total_received / total_reservations) * 100 if total_reservations > 0 else 0
avg_delivery = selected_locker_df['Delivery Duration (min)'].mean() if not selected_locker_df.empty else 0
avg_pickup = selected_locker_df['Pickup Duration (min)'].mean() if not selected_locker_df.empty else 0
# Generar recomendaciones basadas en el rendimiento del casillero.
recommendations = generate_recommendations(df, selected_locker_df)
return dbc.CardBody(children=[
html.H3("Locker Details", style={'color': nyc_colors['text'], 'marginBottom': '10px'}),
html.H4(locker_name, style={'color': nyc_colors['taxi'], 'marginTop': '0'}),
html.P(f"Reservations: {total_reservations}", style={'color': nyc_colors['text'], 'fontSize': '16px'}), # Aumentado el tamaño de la fuente
html.P(f"Received: {total_received}", style={'color': nyc_colors['text'], 'fontSize': '16px'}), # Aumentado el tamaño de la fuente
html.P(f"Success Rate: {success_rate:.2f}%", style={'color': nyc_colors['text'], 'fontSize': '16px'}), # Aumentado el tamaño de la fuente
html.P(f"Avg. Delivery Time: {avg_delivery:.2f} min", style={'color': nyc_colors['text'], 'fontSize': '16px'}), # Aumentado el tamaño de la fuente
html.P(f"Avg. Pickup Time: {avg_pickup:.2f} min", style={'color': nyc_colors['text'], 'fontSize': '16px'}), # Aumentado el tamaño de la fuente
html.Hr(style={'borderColor': nyc_colors['text_muted'], 'marginTop': '20px', 'marginBottom': '20px'}),
html.H4("Recommendations", style={'color': nyc_colors['text']}),
*recommendations
])
server = app.server