Construye Un Dashboard De Datos En La Web En Sólo Minutos Con Python

Admond Lee
May 15, 2020

Construye Un Dashboard De Datos En La Web En Sólo Minutos Con Python

May 15, 2020 12 minutes read

Aumenta exponencialmente el poder y la accesibilidad convirtiendo tus visualizaciones de datos en un dashboard basado en la web con Plotly Dash.

"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


Los datos de cada jugador han sido compilados para cada minuto del juego (excluyendo las horas extras), siendo las estadísticas pl_acc y pl_pps la única excepción, ya que han sido compiladas por cuarto del juego (para cada período de 12 minutos).

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']
Entonces, nuestros datos pueden ser visualizados en Plotly, como sigue:

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

Puedes leer más sobre Plotly Dash aquí, pero por ahora todo lo que necesitas saber es que es un paquete de software de código abierto desarrollado para abstraer las dificultades de poner tus visualizaciones en la 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
Join our private community in Discord

Keep up to date by participating in our global community of data scientists and AI enthusiasts. We discuss the latest developments in data science competitions, new techniques for solving complex challenges, AI and machine learning models, and much more!