点击按钮触发仪表盘



我正在研究一个dash应用程序,在那里我尝试集成ExplainerDashboard。

如果我这样做:

app.config.external_stylesheets = [dbc.themes.BOOTSTRAP]
app.layout = html.Div([
html.Button('Submit', id='submit', n_clicks=0),
html.Div(id='container-button-basic', children='')
])
X_train, y_train, X_test, y_test = titanic_survive()
model = LogisticRegression().fit(X_train, y_train)
explainer = ClassifierExplainer(model, X_test, y_test)
db = ExplainerDashboard(explainer, shap_interaction=False)
db.explainer_layout.register_callbacks(app)     
@app.callback(
Output('container-button-basic', 'children'),
Input('submit', 'n_clicks'),
)
def update_output(n_clicks):
if n_clicks == 1:
return db.explainer_layout.layout()

仪表盘在点击按钮时被触发,然而,它是在我点击按钮之前和仪表盘开始时计算的。如果我改变它,像这样把计算放到回调中,我得到了仪表板但看起来寄存器回调不起作用所有的图都是空的

app.config.external_stylesheets = [dbc.themes.BOOTSTRAP]
app.layout = html.Div([
html.Button('Submit', id='submit', n_clicks=0),
html.Div(id='container-button-basic', children='')
])
X_train, y_train, X_test, y_test = titanic_survive()
model = LogisticRegression().fit(X_train, y_train)
explainer = ClassifierExplainer(model, X_test, y_test)
@app.callback(
Output('container-button-basic', 'children'),
Input('submit', 'n_clicks'),
)
def update_output(n_clicks):
if n_clicks == 1:
db = ExplainerDashboard(explainer, shap_interaction=False) 
db.explainer_layout.register_callbacks(app)     
return db.explainer_layout.layout()

原因为什么你的例子不起作用的是,在Dash中,回调必须在服务器启动之前注册。因此,您不能在回调中注册新的回调。

数据预处理管道

我认为最干净的解决方案是将数据处理移动到预处理管道。它可以是像在Dataiku节点上运行笔记本电脑一样简单的东西。代码应该是这样的

from explainerdashboard import ClassifierExplainer
from explainerdashboard.datasets import titanic_survive
from sklearn.linear_model import LogisticRegression
X_train, y_train, X_test, y_test = titanic_survive()
model = LogisticRegression().fit(X_train, y_train)
explainer = ClassifierExplainer(model, X_test, y_test)
explainer.dump("/data/dataiku/titanic.joblib") # save to some writeable location

相应的webapp代码应该是这样的,

import dash_bootstrap_components as dbc
from dash import Dash
from explainerdashboard import ClassifierExplainer, ExplainerDashboard
explainer = ClassifierExplainer.from_file("/data/dataiku/titanic.joblib")  # load pre-processed data
db = ExplainerDashboard(explainer, shap_interaction=False)
app.config.external_stylesheets = [dbc.themes.BOOTSTRAP]
app.layout = db.explainer_layout.layout()
db.explainer_layout.register_callbacks(app)

部署过程将是(1)运行笔记本,(2)(重新)启动web应用程序后端。注意,应用程序必须重复这个过程才能获取新数据。

使用模拟数据的回调注册

另一种方法可能是使用一个小的模拟数据集,但具有与正常(大)数据集相同的结构,用于在应用程序初始化期间构造ExplainerDashboard。这种方法可以实现快速的初始加载,并在应用启动前进行回调注册。然后你可以使用回调来加载完整的数据集,也就是类似于你最初的想法。下面是一些示例代码,

import dash_bootstrap_components as dbc
from dash import html, Dash, Output, Input, dcc
from dash.exceptions import PreventUpdate
from explainerdashboard import ClassifierExplainer, ExplainerDashboard
from explainerdashboard.datasets import titanic_survive
from sklearn.linear_model import LogisticRegression

def get_explainer(X_train, y_train, X_test, y_test, limit=-1):
model = LogisticRegression().fit(X_train[:limit], y_train[:limit])
return ClassifierExplainer(model, X_test[:limit], y_test[:limit])

def inject_inplace(src, dst):
for attr in dir(dst):
try:
setattr(dst, attr, getattr(src, attr))
except AttributeError:
pass
except NotImplementedError:
pass

X_train, y_train, X_test, y_test = titanic_survive()
# Create explainer with minimal data to ensure fast initial load.
explainer = get_explainer(X_train, y_train, X_test, y_test, limit=5)
dashboard = ExplainerDashboard(explainer, shap_interaction=False)
# Setup app with (hidden) dummy classifier layout.
dummy_layout = html.Div(dashboard.explainer_layout.layout(), style=dict(display="none"))
app = Dash()  # not needed in Dataiku
app.config.external_stylesheets = [dbc.themes.BOOTSTRAP]
app.layout = html.Div([
html.Button('Submit', id='submit', n_clicks=0),
dcc.Loading(html.Div(id='container', children=dummy_layout), fullscreen=True)
])
# Register the callback before the app starts.
dashboard.explainer_layout.register_callbacks(app)

@app.callback(Output('container', 'children'), Input('submit', 'n_clicks'))
def load_complete_dataset(n_clicks):
if n_clicks != 1:
raise PreventUpdate
# Replace in-memory references to the full dataset to sure callbacks target the full dataset.
full_explainer = get_explainer(X_train, y_train, X_test, y_test)
inject_inplace(full_explainer, explainer)
return ExplainerDashboard(explainer, shap_interaction=False).explainer_layout.layout()

if __name__ == "__main__":
app.run_server(port=9024, debug=False)

TL;DR

我对Dash(一个React应用程序)的实验的理解是回调在Dash应用程序的初始化后注册(就像你在第二个代码中的回调中所做的那样),但它们不会传递到网页,因为网页上的React应用程序已经加载,所以如果你刷新页面,React应用程序获得所有回调(甚至是Dash应用程序初始化后注册的新回调)。

长版

在回调中实例化ExplainerDashboard的第二个代码的问题是,现在由ExplainerDashboard注册的回调没有传递到已经加载的网页。

原因是破折号加载页面加载的回调,所以任何回调添加页面加载后不工作。在检查了ExplainerDashboard的代码和阅读Dash的文档后,我做了各种实验。幸运的是,我遇到了一个经典的技巧(来自web开发人员)来解决这个问题。

诀窍是在第一个页面加载时启动ExplainerDashboard,显示正在进行的消息,然后在启动后激活按钮,然后加载启动的ExplainerDashboard按钮单击。下面是经过测试和验证的代码。当然,这是一个POC,可以改进后投入生产。

from dash import Dash, dcc, html, Input, Output
from sklearn.linear_model import LogisticRegression
from explainerdashboard import ClassifierExplainer, ExplainerDashboard
from explainerdashboard.datasets import titanic_survive

app = Dash(__name__)
app.index_string = '''
<!DOCTYPE html>
<html>
<head>
{%metas%}
<title>{%title%}</title>
{%favicon%}
{%css%}
<script>
window.onload = function() {
setTimeout(function() {
if (document.getElementById("explainer_is_loaded")) {
console.log("Y");
document.getElementById("submit").textContent = "Submit";
} else {
console.log("N");
window.location = "/";  /* refresh the page */
}
}, 5000);
};
</script>
</head>
<body>
{%app_entry%}
<footer>
{%config%}
{%scripts%}
{%renderer%}
</footer>
</body>
</html>
'''
# app.config.external_stylesheets = [dbc.themes.BOOTSTRAP]
app.layout = html.Div([
html.Button('Loading... Please wait...', id='submit', n_clicks=0),
html.Div(id='container-button-basic', children=''),
html.Button('', id='hidden-btn1', n_clicks=0, hidden=True),
html.Div(id='hidden-div1', children='', hidden=True),
])
X_train, y_train, X_test, y_test = titanic_survive()
model = LogisticRegression().fit(X_train, y_train)
explainer = ClassifierExplainer(model, X_test, y_test)
db = None

@app.callback(
Output('hidden-div1', 'children'),
Input('hidden-btn1', 'n_clicks'),
)
def init_explainer(n_clicks):
global db
if not db:
db = ExplainerDashboard(explainer, shap_interaction=False)
db.explainer_layout.register_callbacks(app)
else:
return html.Div(id='explainer_is_loaded', hidden=True),

@app.callback(
Output('container-button-basic', 'children'),
Input('submit', 'n_clicks'),
)
def load_explainer(n_clicks):
global db
if n_clicks >= 1 and db:
return db.explainer_layout.layout()

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

让我详细解释程序的新部分:

  1. app.index_string

这部分自定义Dash的HTML索引模板,以包含我们的页面刷新代码。它每5秒刷新一次页面,直到找到#explainer_is_loaded元素。

  1. init_explainer(n_clicks)

这部分添加了一个回调,在页面加载时运行,因为我在这个回调中没有检查n_checks。因此,我们在这个回调下启动了ExplainerDashboard。

  1. load_explainer(n_clicks)

这部分是你自己修改的回调,当点击网页上的按钮时,简单地返回db.explainer_layout.layout()

  1. global db

由于ExplainerDashboard被实例化并在不同的函数(以及不同的页面加载)中使用,我们需要一个全局变量来存储它的对象。

引用

  • https://dash.plotly.com/external-resources
  • https://dash.plotly.com/basic-callbacks
  • https://dash.plotly.com/advanced-callbacks

最新更新