La página de actualización de Python Dash no actualiza los datos de origen

Aug 20 2020

He escrito una aplicación básica de plotly dash que extrae datos de un csv y los muestra en un gráfico. Luego puede alternar valores en la aplicación y las actualizaciones del gráfico.

Sin embargo, cuando agrego nuevos datos al csv (hecho una vez al día), la aplicación no actualiza los datos al actualizar la página.

La solución normalmente es que usted define su app.layoutcomo una función, como se describe aquí (desplácese hacia abajo para ver las actualizaciones al cargar la página). Verá en mi código a continuación que lo he hecho.

Aquí está mi código:

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
import numpy as np

import pandas as pd

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets)

path = 'https://raw.githubusercontent.com/tbuckworth/Public/master/CSVTest.csv'

df = pd.read_csv(path)
df2 = df[(df.Map==df.Map)]


def layout_function():

    df = pd.read_csv(path)
    df2 = df[(df.Map==df.Map)]
    
    available_strats = np.append('ALL',pd.unique(df2.Map.sort_values()))
    classes1 = pd.unique(df2["class"].sort_values())
    metrics1 = pd.unique(df2.metric.sort_values())
    
    return html.Div([
            html.Div([
                dcc.Dropdown(
                    id="Strategy",
                    options=[{"label":i,"value":i} for i in available_strats],
                    value=list(available_strats[0:1]),
                    multi=True
                ),
                dcc.Dropdown(
                    id="Class1",
                    options=[{"label":i,"value":i} for i in classes1],
                    value=classes1[0]
                ),
                dcc.Dropdown(
                    id="Metric",
                    options=[{"label":i,"value":i} for i in metrics1],
                    value=metrics1[0]
                )],
            style={"width":"20%","display":"block"}),
                
        html.Hr(),
    
        dcc.Graph(id='Risk-Report')          
    ])
            
app.layout = layout_function


@app.callback(
        Output("Risk-Report","figure"),
        [Input("Strategy","value"),
         Input("Class1","value"),
         Input("Metric","value"),
         ])

def update_graph(selected_strat,selected_class,selected_metric):
    if 'ALL' in selected_strat:
        df3 = df2[(df2["class"]==selected_class)&(df2.metric==selected_metric)]
    else:
        df3 = df2[(df2.Map.isin(selected_strat))&(df2["class"]==selected_class)&(df2.metric==selected_metric)]
    df4 = df3.pivot_table(index=["Fund","Date","metric","class"],values="value",aggfunc="sum").reset_index()
    traces = []
    for i in df4.Fund.unique():
        df_by_fund = df4[df4["Fund"] == i]
        traces.append(dict(
                x=df_by_fund["Date"],
                y=df_by_fund["value"],
                mode="lines",
                name=i
                ))
    
    if selected_class=='USD':
        tick_format=None
    else:
        tick_format='.2%'
    
    return {
            'data': traces,
            'layout': dict(
                xaxis={'type': 'date', 'title': 'Date'},
                yaxis={'title': 'Values','tickformat':tick_format},
                margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
                legend={'x': 0, 'y': 1},
                hovermode='closest'
            )
        }
    

if __name__ == '__main__':
    app.run_server(debug=True)

Cosas que he probado

  1. Quitando la inicial df = pd.read_csv(path)antes del def layout_function():. Esto resulta en un error.
  2. Creando un botón de devolución de llamada para actualizar los datos usando este código:
@app.callback(
        Output('Output-1','children'),
        [Input('reload_button','n_clicks')]        
        )

def update_data(nclicks):
    if nclicks == 0:
        raise PreventUpdate
    else:
        df = pd.read_csv(path)
        df2 = df[(df.Map==df.Map)]
        return('Data refreshed. Click to refresh again')

Esto no produce un error, pero el botón tampoco actualiza los datos.

  1. Definición dfdentro de la update_graphdevolución de llamada. Esto actualiza los datos cada vez que cambia algo, lo cual no es factible (mis datos reales son> 10 ^ 6 filas, por lo que no quiero leerlos cada vez que el usuario cambia un valor de alternancia)

En resumen, creo que definir app.layout = layout_functiondebería hacer que esto funcione, pero no es así. ¿Qué me estoy perdiendo / no viendo?

Agradezco cualquier ayuda.

Respuestas

3 emher Aug 21 2020 at 06:03

TLDR; Sugeriría que simplemente cargue los datos desde dentro de la devolución de llamada. Si el tiempo de carga es demasiado largo, puede cambiar el formato (por ejemplo, a difuminar ) y / o reducir el tamaño de los datos mediante el procesamiento previo. Si esto aún no es lo suficientemente rápido, el siguiente paso sería almacenar los datos en un caché en memoria del lado del servidor, como Redis .


Dado que está reasignando dfy df2en layout_function, estas variables se consideran locales en Python y, por lo tanto, no está modificando las variables dfy df2del alcance global. Si bien puede lograr este comportamiento utilizando la palabra clave global , se desaconseja el uso de variables globales en Dash .

El enfoque estándar en Dash sería cargar los datos en una devolución de llamada (o en el layout_function) y almacenarlos en un Storeobjeto (o equivalentemente, un oculto Div). La estructura sería algo así como

import pandas as pd
import dash_core_components as dcc
from dash.dependencies import Output, Input

app.layout = html.Div([
    ...
    dcc.Store(id="store"), html.Div(id="trigger")
])

@app.callback(Output('store','data'), [Input('trigger','children')], prevent_initial_call=False)
def update_data(children):
    df = pd.read_csv(path)
    return df.to_json()

@app.callback(Output("Risk-Report","figure"), [Input(...)], [State('store', 'data')])
def update_graph(..., data):
    if data is None:
        raise PreventUpdate
    df = pd.read_json(data)
    ...

Sin embargo, este enfoque generalmente será mucho más lento que simplemente leer los datos del disco dentro de la devolución de llamada (que parece ser lo que está tratando de evitar), ya que da como resultado que los datos se transfieran entre el servidor y el cliente.