"Frase de alguien" - Bejamin Franklin
No sé tú, pero yo a veces encuentro un poco intimidante tener que codificar algo. Esto es doblemente peor cuando estoy construyendo algo parecido al desarrollo web en lugar de hacer algún análisis de datos locales con visualización. Soy un programador de Python competente, pero no me llamaría a mí mismo un desarrollador web en absoluto, incluso después de haber más que chapoteado con Django y Flask.
Puedes leer más artículos de Data Science en español aquí
Aún así, convertir los datos de salida en una aplicación web conlleva algunas mejoras no triviales para tu proyecto.
Es mucho más fácil incorporar una verdadera y poderosa interactividad en una aplicación web. También significa que puedes controlar exactamente cómo se presentan los datos, ya que la aplicación web puede convertirse en el informe de facto, así como en el punto de acceso a tus datos. Por último, y lo más importante, puedes escalar exponencialmente la accesibilidad a tus resultados; haciéndolos disponibles en cualquier lugar y en cualquier momento. Siempre hay un navegador web al alcance de la mano de un usuario.
Build a web data dashboard — in just a few lines of Python code
Así que, empecé a hacer esto con algunos de mis proyectos de ciencia de datos recientemente, con una velocidad y eficiencia sorprendentemente rápida. Convertí uno de mis resultados de este artículo en una aplicación web (que puedes encontrar aqui) en sólo un par de horas.
My NBA analytics web app (link)
Pensé que esto era bastante divertido, y quería compartir cómo esto se unió en sólo unas pocas líneas de código.
Como siempre, incluyo todo lo que necesitas para replicar mis pasos (datos y código), y el artículo no es realmente sobre baloncesto. Así que no os preocupéis si no estáis familiarizados con él, y pongámonos en marcha.
Antes de que empecemos
Data
Incluyo el código y los datos en mi repositorio GitLab aquí (directorio dash_simple_nba). Así que por favor, siéntete libre de jugar con él / mejorarlo.
Paquetes
Asumo que estás familiarizado con Python. Aunque seas relativamente nuevo, este tutorial no debería ser muy difícil.
Necesitarás pandas, plotly y dash. Instala cada uno (en tu entorno virtual) con una simple instalación de pip [NOMBRE_PAQUETE].
Anteriormente, en Python...
Para este tutorial, simplemente voy a saltarme *la mayoría* de los pasos dados para crear la versión local de nuestra visualización. Si estáis interesados en lo que está pasando, echad un vistazo a este artículo
Tendremos una sesión de recapitulación, para que puedan ver lo que sucede entre el trazado del gráfico localmente con Plotly, y cómo portarlo a una aplicación web con Plotly Dash.
Cargar datos
He preprocesado los datos y los he guardado como un archivo CSV. Es una colección de datos de los jugadores de la actual temporada de la NBA (a partir del 26/Feb/2020), que muestra:
- Qué parte de los tiros de su equipo están tomando, y
- Cuán eficientes / eficaces son en hacerlo.
Para esta parte, abra el local_plot.py de mi repositorio.
Cargue los datos con:
all_teams_df = pd.read_csv(‘srcdata/shot_dist_compiled_data_2019_20.csv’)
Inspecciona los datos con all_teams_df.head(), y deberías ver:
>>> all_teams_df.head() player pl_acc pl_pps min_start min_mid min_end shots_count shots_made shots_freq shots_acc group 0 Jahlil Okafor 65.1 132.6 1 0.5 1 3 1 3.7 33.3 NOP 1 Jaxson Hayes 77.8 155.6 1 0.5 1 3 2 3.7 66.7 NOP 2 Nicolo Melli 36.4 88.6 1 0.5 1 1 1 1.2 100.0 NOP 3 Zion Williamson 54.3 111.4 1 0.5 1 2 1 2.5 50.0 NOP 4 Frank Jackson 44.7 105.3 1 0.5 1 0 0 0.0 0.0 NOP
El marco de datos contiene todos los jugadores de la NBA, así que vamos a desglosarlo hasta un tamaño manejable, filtrando por equipo. Por ejemplo, los jugadores de los Pelícanos de Nueva Orleans pueden ser elegidos con:
all_teams_df[all_teams_df.group == 'NOP']
import plotly.express as px fig = px.scatter(all_teams_df[all_teams_df.group == 'NOP'], x='min_mid', y='player', size='shots_freq', color='pl_pps') fig.show()
Visualised player data for New Orlean Pelicans
A riesgo de hacer esto:
How to Draw a Horse — Van Oktop (Tweet)
Añadi algunos pequeños detalles a mi gráfico, para producir esta versión del mismo gráfico.
Same chart, with a few ‘small details’ added (& different team).
Este es el código que usé para hacerlo.
Puedes leer más artículos de Data Science en español aquí
Ahora, aunque es un montón de código de formato, pensé que era útil mostrarles cómo lo hice, porque vamos a reutilizar estas funciones en nuestra versión Dash del código.
def clean_chart_format(fig): import plotly.graph_objects as go fig.update_layout( paper_bgcolor="white", plot_bgcolor="white", annotations=[ go.layout.Annotation( x=0.9, y=1.02, showarrow=False, text="Twitter: @_jphwang", xref="paper", yref="paper", textangle=0 ), ], font=dict( family="Arial, Tahoma, Helvetica", size=10, color="#404040" ), margin=dict( t=20 ) ) fig.update_traces(marker=dict(line=dict(width=1, color='Navy')), selector=dict(mode='markers')) fig.update_coloraxes( colorbar=dict( thicknessmode="pixels", thickness=15, outlinewidth=1, outlinecolor='#909090', lenmode="pixels", len=300, yanchor="top", y=1, )) fig.update_yaxes(showgrid=True, gridwidth=1, tickson='boundaries', gridcolor='LightGray', fixedrange=True) fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='LightGray', fixedrange=True) return True def make_shot_dist_chart(input_df, color_continuous_scale=None, size_col='shots_count', col_col='pl_acc', range_color=None): max_bubble_size = 15 if color_continuous_scale is None: color_continuous_scale = px.colors.diverging.RdYlBu_r if range_color is None: range_color = [min(input_df[col_col]), max(input_df[col_col])] fig = px.scatter( input_df, x='min_mid', y='player', size=size_col, color=col_col, color_continuous_scale=color_continuous_scale, range_color=range_color, range_x=[0, 49], range_y=[-1, len(input_df.player.unique())], hover_name='player', hover_data=['min_start', 'min_end', 'shots_count', 'shots_made', 'shots_freq', 'shots_acc', ], render_mode='svg' ) fig.update_coloraxes(colorbar=dict(title='Points per<BR>100 shots')) fig.update_traces(marker=dict(sizeref=2. * 30 / (max_bubble_size ** 2))) fig.update_yaxes(title="Player") fig.update_xaxes(title='Minute', tickvals=list(range(0, 54, 6))) return fig fig = make_shot_dist_chart( all_teams_df[all_teams_df.group == 'SAS'], col_col='pl_pps', range_color=[90, 120], size_col='shots_freq') clean_chart_format(fig) fig.update_layout(height=500, width=1250) fig.show()
Ahora, vayamos al evento principal: cómo crear una aplicación web a partir de estos graficos.
En la World Wide Web
Funciona con Flask under the hood, y puedes reutilizar felizmente la mayor parte del código que usaste para desarrollar los gráficos en plotly.py.
Esta es la versión simple que he armado:
import pandas as pd import dash import dash_core_components as dcc import dash_html_components as html from dash.dependencies import Input, Output all_teams_df = pd.read_csv('srcdata/shot_dist_compiled_data_2019_20.csv') app = dash.Dash(__name__) server = app.server team_names = all_teams_df.group.unique() team_names.sort() app.layout = html.Div([ html.Div([dcc.Dropdown(id='group-select', options=[{'label': i, 'value': i} for i in team_names], value='TOR', style={'width': '140px'})]), dcc.Graph('shot-dist-graph', config={'displayModeBar': False})]) @app.callback( Output('shot-dist-graph', 'figure'), [Input('group-select', 'value')] ) def update_graph(grpname): import plotly.express as px return px.scatter(all_teams_df[all_teams_df.group == grpname], x='min_mid', y='player', size='shots_freq', color='pl_pps') if __name__ == '__main__': app.run_server(debug=False)
¡Pruébalo! Debería abrir este grafico en tu navegador.
Our first Dash app!
¿Cuál es el tema de todo esto? Bueno, para empezar, es una aplicación web en vivo, en menos de 25 líneas de código. ¿Y notan el menú desplegable en la parte superior izquierda? Intenta cambiar los valores en él, y mira el gráfico cambiar *mágicamente*.
Puedes leer más artículos de Data Science en español aquí
Adelante, esperaré.
¿De acuerdo? Hecho.
Repasemos brevemente el código.
En un nivel alto, lo que estoy haciendo aquí es:
- Iniciar una aplicación Dash;
- Obtener una lista de nombres de equipos disponibles, y proporcionarla a un menú desplegable (con DOM id group-select) con un valor por defecto o 'TOR';
- Instanciar un objeto Graph como el identificador shot-dist-graph dentro de Dash; y
- Crear una función de devolución de llamada en la que, si se modifica alguno de los valores, llamará a la función update_graph y pasará el objeto devuelto a la salida.
Si echas un vistazo al código, muchas de las cosas que probablemente son triviales para los desarrolladores web pero molestas para mí se abstraen.
dcc.Graph envuelve el objeto figurativo de plotly.py en mi aplicación web y los componentes HTML como divs pueden ser llamados y configurados convenientemente con los objetos html.Div.
Lo más gratificante para mí, personalmente, es que los objetos de entrada y las llamadas de esas entradas están configuradas de forma declarativa, y puedo evitar tener que lidiar con cosas como formularios HTML o JavaScript.
Y la aplicación resultante sigue funcionando maravillosamente. El gráfico se actualiza en el momento en que el menú desplegable se utiliza para seleccionar otro valor.
Y hemos hecho todo eso en menos de 25 líneas de código.
¿Por qué Dash?
En este punto, podrías estarte preguntando - ¿por qué Dash? Podemos hacer todo esto con un framework de JS, y Flask, o cualquier otra de las innumerables combinaciones.
Para alguien como yo, que prefiere la comodidad de Python que tratar nativamente con HTML y CSS, el uso de Dash abstrae muchas cosas que no agregan mucho valor al producto final.
Tomemos, por ejemplo, una versión de esta aplicación que incluye más formato y notas para la audiencia:
(Es simple_dash_w_format.py en el git repo)
def clean_chart_format(fig): fig.update_layout( paper_bgcolor="white", plot_bgcolor="white", annotations=[ go.layout.Annotation( x=0.9, y=1.02, showarrow=False, text="Twitter: @_jphwang", xref="paper", yref="paper", textangle=0 ), ], font=dict( family="Arial, Tahoma, Helvetica", size=10, color="#404040" ), margin=dict( t=20 ) ) fig.update_traces(marker=dict(line=dict(width=1, color='Navy')), selector=dict(mode='markers')) fig.update_coloraxes( colorbar=dict( thicknessmode="pixels", thickness=15, outlinewidth=1, outlinecolor='#909090', lenmode="pixels", len=300, yanchor="top", y=1, )) fig.update_yaxes(showgrid=True, gridwidth=1, tickson='boundaries', gridcolor='LightGray', fixedrange=True) fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='LightGray', fixedrange=True) return True def make_shot_dist_chart(input_df, color_continuous_scale=None, size_col='shots_count', col_col='pl_acc', range_color=None): max_bubble_size = 15 if color_continuous_scale is None: color_continuous_scale = px.colors.diverging.RdYlBu_r if range_color is None: range_color = [min(input_df[col_col]), max(input_df[col_col])] fig = px.scatter( input_df, x='min_mid', y='player', size=size_col, color=col_col, color_continuous_scale=color_continuous_scale, range_color=range_color, range_x=[0, 49], range_y=[-1, len(input_df.player.unique())], hover_name='player', hover_data=['min_start', 'min_end', 'shots_count', 'shots_made', 'shots_freq', 'shots_acc', ], render_mode='svg' ) fig.update_coloraxes(colorbar=dict(title='Points per<BR>100 shots')) fig.update_traces(marker=dict(sizeref=2. * 30 / (max_bubble_size ** 2))) fig.update_yaxes(title="Player") fig.update_xaxes(title='Minute', tickvals=list(range(0, 54, 6))) return fig app.title = 'Dash Demo - NBA' team_names = all_teams_df.group.unique() team_names.sort() app.layout = html.Div([ html.Div([ dcc.Markdown( """ #### Shot Frequencies & Efficiencies (2019-20 NBA Season) This page compares players based on shot *frequency* and *efficiency*, divided up into minutes of regulation time for each team. Use the pulldown to select a team, or select 'Leaders' to see leaders from each team. *Notes*: * **Frequency**: A team's shots a player is taking, indicated by **size**. * **Efficiency**: Points scored per 100 shots, indicated by **colour** (red == better, blue == worse). * Players with <1% of team shots are shown under 'Others' """ ), html.P([html.Small("See more data / NBA analytics content, find me on "), html.A(html.Small("twitter"), href="https://twitter.com/_jphwang", title="twitter"), html.Small("!")]), ]), html.Div([ dcc.Dropdown( id='group-select', options=[{'label': i, 'value': i} for i in team_names], value='TOR', style={'width': '140px'} ) ]), dcc.Graph( 'shot-dist-graph', config={'displayModeBar': False} ) ]) @app.callback( Output('shot-dist-graph', 'figure'), [Input('group-select', 'value')] ) def update_graph(grpname): fig = make_shot_dist_chart( all_teams_df[all_teams_df.group == grpname], col_col='pl_pps', range_color=[90, 120], size_col='shots_freq') clean_chart_format(fig) if len(grpname) > 3: fig.update_layout(height=850, width=1250) else: fig.update_layout(height=500, width=1250) return fig
Puedes leer más artículos de Data Science en español aquí
La mayoría de los cambios son cosméticos, pero notará que aquí, sólo escribo el texto del cuerpo en Markdown, y simplemente llevo mis funciones de formato de Plotly para ser usadas en el formato de los gráficos en Dash.
Esto me ahorra una tremenda cantidad de tiempo entre el análisis de datos y la visualización hasta el despliegue a las vistas de los clientes.
Con todo, desde que empecé con mi gráfico inicial, creo que probablemente llevó menos de una hora desplegarlo en Heroku. Lo cual es bastante sorprendente.
Me adentraré en las características más avanzadas de Dash, y en realidad haciendo algunas cosas geniales con él en cuanto a funcionalidad, pero estoy muy contento con este resultado en términos de facilidad y velocidad.
Pruébalo tú mismo, creo que te impresionará. La próxima vez, pienso escribir sobre algunas cosas realmente geniales que puedes hacer con Dash, y construir dashboards verdaderamente interactivos.
Si te gustó esto, di 👋 / follow en twitter, o sigueme para las actualizaciones. Este es el artículo en el que se basan los datos