Skip to content

Commit

Permalink
adds dump_explainer param, makes waitress default
Browse files Browse the repository at this point in the history
  • Loading branch information
oegedijk committed Dec 20, 2020
1 parent 9ce027d commit 21354f0
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 51 deletions.
40 changes: 40 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Release Notes

## 0.2.17:
### Breaking Changes
-
-

### New Features
- Introducing `ExplainerHub`: combine multiple dashboards together behind a single frontend with convenient url paths.
- example:
```python
db1 = ExplainerDashboard(explainer, title="Dashboard One", name='dashboard1')
db2 = ExplainerDashboard(explainer, title="Dashboard Two", name='dashboard2')

hub = ExplainerHub([db1, db2])
hub.run()

# store an recover from config:
hub.to_yaml("hub.yaml")
hub2 = ExplainerHub.from_config("hub.yaml")
```
- adds option `dump_explainer` to `ExplainerDashboard.to_yaml` to automatically
dump the explainerfile along with the yaml.
- adds option `use_waitress` to `ExplainerDashboard.run()` and `ExplainerHub.run()`, to use the `waitress` python webserver instead of the `Flask` development server
- adds parameters to `ExplainerDashboard`:
- `name`: this will be used to assign a url for `ExplainerHub`
- `description`: this will be used for the title tooltip in the dashboard
and in the `ExplainerHub` frontend.


### Bug Fixes
-
-

### Improvements
- the `cli` now uses the `waitress` server by default.
-

### Other Changes
-
-

## Version 0.2.16.2:

### Bug fix/Improvement
Expand Down
11 changes: 6 additions & 5 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,16 @@
- http://manifold.mlvis.io/
- generate groups programmatically!

## Hub:
- automatic reloads with watchdog
- add dashboard specific logins

### Components
- autodetect when uuid name get rendered and issue warning
- automatically call register_components()
- add show points to categorical shap dependence
- Add side-by-side option to cutoff selector component


- add querystring method to ExplainerComponents
- add pos_label_name property to PosLabelConnector search
- add "number of indexes" indicator to RandomIndexComponents for current restrictions
Expand All @@ -88,6 +91,7 @@
- Add this method? : https://arxiv.org/abs/2006.04750?

## Tests:
- write tests for ExplainerHub
- test model_output='probability' and 'raw' or 'logodds' seperately
- write tests for explainer_methods
- write tests for explainer_plots
Expand All @@ -108,11 +112,8 @@

## Library level:
- Make example heroku deployment repo
- add waitress to CLI

- hide (prefix '_') to non-public API class methods
- submit pull request to shap with broken test for
https://github.com/slundberg/shap/issues/723
- build explainerhub for hosting multiple explainerdashboard models
- use waitress as wsgi
- use watchdog to rebuild and reload.

22 changes: 11 additions & 11 deletions explainerdashboard/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@
import joblib
import click

from explainerdashboard import ClassifierExplainer, RegressionExplainer
import waitress

from explainerdashboard import *
from explainerdashboard.explainers import BaseExplainer
from explainerdashboard.dashboards import ExplainerDashboard


explainer_ascii = """
_ _ _ _ _ _
| | (_) | | | | | | | |
_____ ___ __ | | __ _ _ _ __ ___ _ __ __| | __ _ ___| |__ | |__ ___ __ _ _ __ __| |
/ _ \ \/ | '_ \| |/ _` | | '_ \ / _ | '__/ _` |/ _` / __| '_ \| '_ \ / _ \ / _` | '__/ _` |
| __/> <| |_) | | (_| | | | | | __| | | (_| | (_| \__ | | | | |_) | (_) | (_| | | | (_| |
\___/_/\_| .__/|_|\__,_|_|_| |_|\___|_| \__,_|\__,_|___|_| |_|_.__/ \___/ \__,_|_| \__,_|
| |
|_|
_____ ___ __| |__ _(_)_ _ ___ _ _ __| |__ _ __| |_ | |__ ___ __ _ _ _ __| |
/ -_) \ / '_ \ / _` | | ' \/ -_) '_/ _` / _` (_-< ' \| '_ \/ _ \/ _` | '_/ _` |
\___/_\_\ .__/_\__,_|_|_||_\___|_| \__,_\__,_/__/_||_|_.__/\___/\__,_|_| \__,_|
|_|
"""


def build_explainer(explainer_config):
if isinstance(explainer_config, (Path, str)) and str(explainer_config).endswith(".yaml"):
config = yaml.safe_load(open(str(explainer_config), "r"))
Expand Down Expand Up @@ -109,7 +109,7 @@ def launch_dashboard_from_pkl(explainer_filepath, no_browser, port, no_dashboard
webbrowser.open_new(f"http://127.0.0.1:{port}/")

if not no_dashboard:
db.run(port)
waitress.serve(db.flask_server(), host='0.0.0.0', port=port)
return


Expand Down Expand Up @@ -142,7 +142,7 @@ def launch_dashboard_from_yaml(dashboard_config, no_browser, port, no_dashboard=

click.echo(f"explainerdashboard ===> Starting dashboard:")
if not no_dashboard:
db.run(port)
waitress.serve(db.flask_server(), host='0.0.0.0', port=port)
return


Expand Down
88 changes: 55 additions & 33 deletions explainerdashboard/dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ def from_config(cls, arg1, arg2=None, **update_params):
return cls(explainer, **dashboard_params, **kwargs)

def to_yaml(self, filepath=None, return_dict=False,
explainerfile="explainer.joblib"):
explainerfile=None, dump_explainer=False):
"""Returns a yaml configuration of the current ExplainerDashboard
that can be used by the explainerdashboard CLI. Recommended filename
is `dashboard.yaml`.
Expand All @@ -588,14 +588,23 @@ def to_yaml(self, filepath=None, return_dict=False,
config.
explainerfile (str, optional): filename of explainer dump. Defaults
to `explainer.joblib`.
dump_explainer (bool, optional): dump the explainer along with the yaml.
You must pass explainerfile parameter for the filename. Defaults to False.
"""
import oyaml as yaml

dashboard_config = dict(
dashboard=dict(
explainerfile=explainerfile,
explainerfile=str(explainerfile),
params=self._stored_params))

if dump_explainer:
if explainerfile is None:
raise ValueError("When you pass dump_explainer=True, then you "
"must pass an explainerfile filename parameter!")
print(f"Dumping explainer to {explainerfile}...", flush=True)
self.explainer.dump(explainerfile)
if return_dict:
return dashboard_config

Expand Down Expand Up @@ -776,11 +785,14 @@ def flask_server(self):
print("Warning: in production you should probably use mode='dash'...")
return self.app.server

def run(self, port=None, **kwargs):
def run(self, port=None, use_waitress=False, **kwargs):
"""Start ExplainerDashboard on port
Args:
port (int, optional): port to run on. If None, then use self.port.
use_waitress (bool, optional): use the waitress python web server
instead of the flask development server. Only works with mode='dash'.
Defaults to False.
Defaults to None.self.port defaults to 8050.
Raises:
Expand All @@ -791,10 +803,16 @@ def run(self, port=None, **kwargs):
pio.templates.default = "none"
if port is None:
port = self.port

if use_waitress and self.mode != 'dash':
print(f"Warning: waitress does not work with mode={self.mode}, "
"using JupyterDash server instead!", flush=True)
if self.mode == 'dash':
print(f"Starting ExplainerDashboard on http://localhost:{port}", flush=True)
self.app.run_server(port=port, **kwargs)
if use_waitress:
from waitress import serve
serve(self.app.server, host='0.0.0.0', port=port)
else:
self.app.run_server(port=port, **kwargs)
elif self.mode == 'external':
if not self.is_colab:
print(f"Starting ExplainerDashboard on http://localhost:{port}\n"
Expand Down Expand Up @@ -843,56 +861,66 @@ def terminate(cls, port, token=None):
class ExplainerHub:
"""ExplainerHub is an access point to multiple ExplainerDashboards.
Each ExplainerDashboard is hosted on its own url path,
e.g. 127.0.0.1/dashboard1 and 127.0.0.1/dashboard2.
e.g. 127.0.0.1:8050/dashboard1 and 127.0.0.1:8050/dashboard2.
The ExplainerHub then provides a nice frontend to reach these dashboards.
"""
def __init__(self, dashboards:List[ExplainerDashboard], title:str="ExplainerHub",
description:str=None, masonry:bool=False, n_dashboard_cols:int=3,
port:int=8050, **kwargs):
db_logins:dict=None, port:int=8050, **kwargs):
"""initialize ExplainerHub
Args:
dashboards (List[ExplainerDashboard]): list of ExplainerDashboard to include in ExplainerHub
title (str, optional): title to display. Defaults to "ExplainerHub".
description (str, optional): Short description of ExplainerHub. Defaults to default text.
masonry (bool, optional): Lay out dashboard cards in bootstrap masonry
responsive style. Defaults to False.
masonry (bool, optional): Lay out dashboard cards in fluid bootstrap
masonry responsive style. Defaults to False.
n_dashboard_cols (int, optional): If masonry is False, organize cards
in rows and columns. Defaults to 3.
db_logins (dict, optional): dictionary of logins for each dashboard
identified by name, e.g.
db_logins=dict(
dashboard1=['login1', 'password1'],
dashboard2=['login2', 'password2'])
)
You should avoid hardcoding these and store them somewhere safe!
port (int, optional): Port to run hub on. Defaults to 8050.
"""
self._store_params(no_store=['dashboards'])

if self.description is None:
self.description = """This ExplainerHub shows an overview of different
explainerdashboards generated for a number of different machine learning models.
ExplainerDashboards generated for a number of different machine learning models.
These dashboards make the inner workings and predictions of the trained models
transparent and explainable."""

self.app = Flask(__name__)
if db_logins is None: db_logins = {}

self.dashboards = []
for i, dashboard in enumerate(dashboards):
if dashboard.name is None:
self.dashboards.append(
ExplainerDashboard.from_config(
dashboard.explainer, deepcopy(dashboard.to_yaml(return_dict=True)),
**update_kwargs(kwargs, server=self.app, name=f"dashboard{i+1}",
url_base_pathname=f"/dashboard{i+1}/")))
print("Reminder, you can set ExplainerDashboard .name and .description "
"in order to control the url path of the dashboard. Now "
f"defaulting to name=dashboard{i+1} and default description", flush=True)
else:
self.dashboards.append(
ExplainerDashboard.from_config(
dashboard.explainer, deepcopy(dashboard.to_yaml(return_dict=True)),
**update_kwargs(kwargs, server=self.app, name=dashboard.name,
url_base_pathname=f"/{dashboard.name}/")))
dashboard.name = f"dashboard{i+1}"
update_params = dict(
server=self.app,
name=dashboard.name,
url_base_pathname = f"/{dashboard.name}/")
if dashboard.name in db_logins:
update_params['logins'] = db_logins[dashboard.name]

self.dashboards.append(
ExplainerDashboard.from_config(
dashboard.explainer, deepcopy(dashboard.to_yaml(return_dict=True)),
**update_kwargs(kwargs, **update_params)))

dashboard_names = [db.name for db in self.dashboards]
assert len(set(dashboard_names)) == len(self.dashboards), \
assert len(set(dashboard_names)) == len(dashboard_names), \
f"All dashboard .name properties should be unique, but received the folowing: {dashboard_names}"

self.index_page = self._get_index_page()
Expand Down Expand Up @@ -950,25 +978,19 @@ def to_yaml(self, filepath:Path=None, dump_explainers=True, return_dict=False):
**self._stored_params,
dashboards=[dashboard.to_yaml(
return_dict=True,
explainerfile=dashboard.name+"_explainer.joblib")
explainerfile=dashboard.name+"_explainer.joblib",
dump_explainer=dump_explainers)
for dashboard in self.dashboards]))

if return_dict:
return hub_config

if filepath is None:
return yaml.dump(hub_config)
else:
filepath = Path(filepath)

if dump_explainers:
for dashboard in self.dashboards:
print(f"dumping explainer: {dashboard.name+'_explainer.joblib'}", flush=True)
dashboard.explainer.dump(filepath.parent / (dashboard.name+"_explainer.joblib"))

if filepath is not None:
yaml.dump(hub_config, open(filepath, "w"))
return
filepath = Path(filepath)
yaml.dump(hub_config, open(filepath, "w"))
return

def _store_params(self, no_store=None, no_attr=None, no_param=None):
"""Stores the parameter of the class to instance attributes and
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

setup(
name='explainerdashboard',
version='0.2.16.2',
version='0.2.17',
description='explainerdashboard allows you quickly build an interactive dashboard to explain the inner workings of your machine learning model.',
long_description="""
Expand Down Expand Up @@ -70,7 +70,7 @@
"Topic :: Scientific/Engineering :: Artificial Intelligence"],
install_requires=['dash', 'dash-bootstrap-components', 'jupyter_dash', 'dash-auth',
'dtreeviz>=1.0', 'numpy', 'pandas', 'PDPbox', 'scikit-learn',
'shap>=0.36', 'shortuuid', 'joblib', 'oyaml', 'click'],
'shap>=0.36', 'shortuuid', 'joblib', 'oyaml', 'click', 'waitress'],
entry_points={
'console_scripts': [
'explainerdashboard = explainerdashboard.cli:explainerdashboard_cli',
Expand Down

0 comments on commit 21354f0

Please sign in to comment.