From 6e0b884d923eacc5f6850f6403b04886faaaa664 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Mon, 21 Nov 2022 16:48:50 -0700 Subject: [PATCH 01/17] init --- demo/app.py | 59 +++++++ demo/assets/app.css | 417 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 476 insertions(+) create mode 100644 demo/app.py create mode 100644 demo/assets/app.css diff --git a/demo/app.py b/demo/app.py new file mode 100644 index 0000000..e792e73 --- /dev/null +++ b/demo/app.py @@ -0,0 +1,59 @@ +from dash import Dash, dcc, html, Input, Output +from pathlib import Path +import plotly.express as px +import pandas as pd + + +app = Dash(__name__) + +DATA_DIR = Path("docs/examples/electricity").resolve() + + +# assume you have a "long-form" data frame +# see https://plotly.com/python/px-arguments/ for more options +df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") + + +app.layout = html.Div([ + html.Div(children=[ + html.H1( + children='Electricity Use Forecasting', + style={ + 'textAlign': 'center', + } + ), + + html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', style={ + 'textAlign': 'center', + }), + + dcc.Graph(id="time-series-chart"), + html.P("Select client:"), + dcc.Dropdown( + id="group", + options=df['group'].unique(), + value="MT_001", + clearable=False, + ), + ]), +]) + +@app.callback( + Output("time-series-chart", "figure"), + Input("group", "value")) +def display_time_series(group): + _df = df.loc[df["group"] == group] + fig = px.line(_df, x='time', y='kW') + + fig.update_layout( + plot_bgcolor='#DDDDDD', + paper_bgcolor='#EEEEEE', + font_color='#7FDBFF', + font_family="Courier New", + title_font_family="Courier New", + ) + + return fig + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/demo/assets/app.css b/demo/assets/app.css new file mode 100644 index 0000000..f16050e --- /dev/null +++ b/demo/assets/app.css @@ -0,0 +1,417 @@ +/* Table of contents +–––––––––––––––––––––––––––––––––––––––––––––––––– +- Plotly.js +- Grid +- Base Styles +- Typography +- Links +- Buttons +- Forms +- Lists +- Code +- Tables +- Spacing +- Utilities +- Clearing +- Media Queries +*/ + +/* PLotly.js +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* plotly.js's modebar's z-index is 1001 by default + * https://github.com/plotly/plotly.js/blob/7e4d8ab164258f6bd48be56589dacd9bdd7fded2/src/css/_modebar.scss#L5 + * In case a dropdown is above the graph, the dropdown's options + * will be rendered below the modebar + * Increase the select option's z-index + */ + +/* This was actually not quite right - + dropdowns were overlapping each other (edited October 26) + +.Select { + z-index: 1002; +}*/ + + +/* Grid +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.container { + position: relative; + width: 100%; + max-width: 960px; + margin: 0 auto; + padding: 0 20px; + box-sizing: border-box; } +.column, +.columns { + width: 100%; + float: left; + box-sizing: border-box; } + +/* For devices larger than 400px */ +@media (min-width: 400px) { + .container { + width: 85%; + padding: 0; } +} + +/* For devices larger than 550px */ +@media (min-width: 550px) { + .container { + width: 80%; } + .column, + .columns { + margin-left: 4%; } + .column:first-child, + .columns:first-child { + margin-left: 0; } + + .one.column, + .one.columns { width: 4.66666666667%; } + .two.columns { width: 13.3333333333%; } + .three.columns { width: 22%; } + .four.columns { width: 30.6666666667%; } + .five.columns { width: 39.3333333333%; } + .six.columns { width: 48%; } + .seven.columns { width: 56.6666666667%; } + .eight.columns { width: 65.3333333333%; } + .nine.columns { width: 74.0%; } + .ten.columns { width: 82.6666666667%; } + .eleven.columns { width: 91.3333333333%; } + .twelve.columns { width: 100%; margin-left: 0; } + + .one-third.column { width: 30.6666666667%; } + .two-thirds.column { width: 65.3333333333%; } + + .one-half.column { width: 48%; } + + /* Offsets */ + .offset-by-one.column, + .offset-by-one.columns { margin-left: 8.66666666667%; } + .offset-by-two.column, + .offset-by-two.columns { margin-left: 17.3333333333%; } + .offset-by-three.column, + .offset-by-three.columns { margin-left: 26%; } + .offset-by-four.column, + .offset-by-four.columns { margin-left: 34.6666666667%; } + .offset-by-five.column, + .offset-by-five.columns { margin-left: 43.3333333333%; } + .offset-by-six.column, + .offset-by-six.columns { margin-left: 52%; } + .offset-by-seven.column, + .offset-by-seven.columns { margin-left: 60.6666666667%; } + .offset-by-eight.column, + .offset-by-eight.columns { margin-left: 69.3333333333%; } + .offset-by-nine.column, + .offset-by-nine.columns { margin-left: 78.0%; } + .offset-by-ten.column, + .offset-by-ten.columns { margin-left: 86.6666666667%; } + .offset-by-eleven.column, + .offset-by-eleven.columns { margin-left: 95.3333333333%; } + + .offset-by-one-third.column, + .offset-by-one-third.columns { margin-left: 34.6666666667%; } + .offset-by-two-thirds.column, + .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } + + .offset-by-one-half.column, + .offset-by-one-half.columns { margin-left: 52%; } + +} + + +/* Base Styles +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* NOTE +html is set to 62.5% so that all the REM measurements throughout Skeleton +are based on 10px sizing. So basically 1.5rem = 15px :) */ +html { + font-size: 62.5%; } +body { + font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ + line-height: 1.6; + font-weight: 400; + font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; + color: #7FDBFF; + background-color: rgb(11, 11, 11); +} + + +/* Typography +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +h1, h2, h3, h4, h5, h6 { + margin-top: 0; + margin-bottom: 0; + font-weight: 300; } +h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } +h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} +h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} +h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} +h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} +h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} + +p { + margin-top: 0; } + + +/* Blockquotes +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +blockquote { + border-left: 4px lightgrey solid; + padding-left: 1rem; + margin-top: 2rem; + margin-bottom: 2rem; + margin-left: 0rem; +} + + +/* Links +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +a { + color: #1EAEDB; + text-decoration: underline; + cursor: pointer;} +a:hover { + color: #0FA0CE; } + + +/* Buttons +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.button, +button, +input[type="submit"], +input[type="reset"], +input[type="button"] { + display: inline-block; + height: 38px; + padding: 0 30px; + color: #555; + text-align: center; + font-size: 11px; + font-weight: 600; + line-height: 38px; + letter-spacing: .1rem; + text-transform: uppercase; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border-radius: 4px; + border: 1px solid #bbb; + cursor: pointer; + box-sizing: border-box; } +.button:hover, +button:hover, +input[type="submit"]:hover, +input[type="reset"]:hover, +input[type="button"]:hover, +.button:focus, +button:focus, +input[type="submit"]:focus, +input[type="reset"]:focus, +input[type="button"]:focus { + color: #333; + border-color: #888; + outline: 0; } +.button.button-primary, +button.button-primary, +input[type="submit"].button-primary, +input[type="reset"].button-primary, +input[type="button"].button-primary { + color: #FFF; + background-color: #33C3F0; + border-color: #33C3F0; } +.button.button-primary:hover, +button.button-primary:hover, +input[type="submit"].button-primary:hover, +input[type="reset"].button-primary:hover, +input[type="button"].button-primary:hover, +.button.button-primary:focus, +button.button-primary:focus, +input[type="submit"].button-primary:focus, +input[type="reset"].button-primary:focus, +input[type="button"].button-primary:focus { + color: #FFF; + background-color: #1EAEDB; + border-color: #1EAEDB; } + + +/* Forms +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea, +select { + height: 38px; + padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ + background-color: #fff; + border: 1px solid #D1D1D1; + border-radius: 4px; + box-shadow: none; + box-sizing: border-box; + font-family: inherit; + font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/} +/* Removes awkward default styles on some inputs for iOS */ +input[type="email"], +input[type="number"], +input[type="search"], +input[type="text"], +input[type="tel"], +input[type="url"], +input[type="password"], +textarea { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; } +textarea { + min-height: 65px; + padding-top: 6px; + padding-bottom: 6px; } +input[type="email"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="text"]:focus, +input[type="tel"]:focus, +input[type="url"]:focus, +input[type="password"]:focus, +textarea:focus, +select:focus { + border: 1px solid #33C3F0; + outline: 0; } +label, +legend { + display: block; + margin-bottom: 0px; } +fieldset { + padding: 0; + border-width: 0; } +input[type="checkbox"], +input[type="radio"] { + display: inline; } +label > .label-body { + display: inline-block; + margin-left: .5rem; + font-weight: normal; } + + +/* Lists +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +ul { + list-style: circle inside; } +ol { + list-style: decimal inside; } +ol, ul { + padding-left: 0; + margin-top: 0; } +ul ul, +ul ol, +ol ol, +ol ul { + margin: 1.5rem 0 1.5rem 3rem; + font-size: 90%; } +li { + margin-bottom: 1rem; } + + +/* Tables +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +table { + border-collapse: collapse; +} +th:not(.CalendarDay), +td:not(.CalendarDay) { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #E1E1E1; } +th:first-child:not(.CalendarDay), +td:first-child:not(.CalendarDay) { + padding-left: 0; } +th:last-child:not(.CalendarDay), +td:last-child:not(.CalendarDay) { + padding-right: 0; } + + +/* Spacing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +button, +.button { + margin-bottom: 0rem; } +input, +textarea, +select, +fieldset { + margin-bottom: 0rem; } +pre, +dl, +figure, +table, +form { + margin-bottom: 0rem; } +p, +ul, +ol { + margin-bottom: 0.75rem; } + +/* Utilities +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +.u-full-width { + width: 100%; + box-sizing: border-box; } +.u-max-full-width { + max-width: 100%; + box-sizing: border-box; } +.u-pull-right { + float: right; } +.u-pull-left { + float: left; } + + +/* Misc +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +hr { + margin-top: 3rem; + margin-bottom: 3.5rem; + border-width: 0; + border-top: 1px solid #E1E1E1; } + + +/* Clearing +–––––––––––––––––––––––––––––––––––––––––––––––––– */ + +/* Self Clearing Goodness */ +.container:after, +.row:after, +.u-cf { + content: ""; + display: table; + clear: both; } + + +/* Media Queries +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +/* +Note: The best way to structure the use of media queries is to create the queries +near the relevant code. For example, if you wanted to change the styles for buttons +on small devices, paste the mobile query code up in the buttons section and style it +there. +*/ + + +/* Larger than mobile */ +@media (min-width: 400px) {} + +/* Larger than phablet (also point when grid becomes active) */ +@media (min-width: 550px) {} + +/* Larger than tablet */ +@media (min-width: 750px) {} + +/* Larger than desktop */ +@media (min-width: 1000px) {} + +/* Larger than Desktop HD */ +@media (min-width: 1200px) {} From dbac7e75980c9365dd48739ecc9a4cedb8a1ce75 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Mon, 21 Nov 2022 16:52:11 -0700 Subject: [PATCH 02/17] setup --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 14f9374..854e5be 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,10 @@ 'pytorch_lightning>=1.5', 'torch_optimizer>=0.3.0', 'matplotlib' + ], + 'demo': [ + 'dash', + 'jupyter-dash', ] } ) From 014f05850adeeef66886476fbde9826817705033 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 22 Nov 2022 19:20:57 -0700 Subject: [PATCH 03/17] more dashboarding --- demo/app.py | 205 +++++++++++++++++++++++++++++++++++++------- demo/assets/app.css | 4 +- 2 files changed, 177 insertions(+), 32 deletions(-) diff --git a/demo/app.py b/demo/app.py index e792e73..93af02f 100644 --- a/demo/app.py +++ b/demo/app.py @@ -1,7 +1,9 @@ +from typing import List from dash import Dash, dcc, html, Input, Output from pathlib import Path import plotly.express as px import pandas as pd +import numpy as np app = Dash(__name__) @@ -12,48 +14,191 @@ # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") +all_groups: np.ndarray = df['group'].unique() -app.layout = html.Div([ - html.Div(children=[ - html.H1( - children='Electricity Use Forecasting', - style={ - 'textAlign': 'center', - } - ), - - html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', style={ - 'textAlign': 'center', - }), - - dcc.Graph(id="time-series-chart"), - html.P("Select client:"), - dcc.Dropdown( - id="group", - options=df['group'].unique(), - value="MT_001", - clearable=False, - ), - ]), -]) +app.layout = html.Div( + [ + # col 1 + html.Div(children=[ + html.H1(children='Electricity Use Forecasting', + style={'textAlign': 'center',}), + + html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', + style={'textAlign': 'center',}), + + dcc.Graph(id="time-series-chart"), + + # the horizontal part with 3parts + html.Div(children=[ + # Part 1 + html.Div( + dcc.Graph(id="histogram-chart"), + style={'padding': 10, 'flex': 1}), + + # Part 2 + html.Div(id="correlation-div",), + + # Part 3 + html.Div( + children=[ + dcc.RadioItems( + id="prediction_toggle", + options=[ + {"label": "No Predictions", "value": "none"}, + {"label": "Exponential Smoothing", "value": "es"}, + {"label": "Neural-Network", "value": "nn"} + ] + ), + + + ], + style={'padding': 10, 'display': 'flex', 'flex': 1, 'align-items': 'center', 'justify-content': 'center'}), + ], style={'display': 'flex', 'flex-direction': 'row'} + ) + ], style={'padding': 10, 'flex': 2.5}), + + # col 2 + html.Div(children=[ + html.Div(children=[ + html.H6("Drop irregular clients:", style={"margin-top": "100px"}), + dcc.RadioItems(id="drop_irregular", + options=[{"label": "No", "value": False}, + {"label": "Yes", "value": True}], + value=False, inline=True), + ]), + + html.Div(children=[ + html.H6("Clients (max 2)"), + # dcc.Dropdown( + # id="group_dropdown", + # options=df['group'].unique(), + # value="MT_001", + # clearable=False, + # ), + dcc.Checklist( + id="group_checklist", + options=all_groups, + value=["MT_001"], + style={"max-height": "250px", "overflow-y": "auto"}, + ), + ]), + + ], style={'padding': 10, 'flex': 1}), + ], + style={'display': 'flex', 'flex-direction': 'row'} +) + +# --- plotting +# main plot @app.callback( Output("time-series-chart", "figure"), - Input("group", "value")) -def display_time_series(group): - _df = df.loc[df["group"] == group] - fig = px.line(_df, x='time', y='kW') + Output("histogram-chart", "figure"), + Input("group_checklist", "value") +) +def display_time_series(groups: List[str]): + _df = df.loc[df['group'].isin(groups)] + fig_ts = px.line(_df, x='time', y='kW', color='group') + fig_hist = px.histogram(_df, x='kW', color='group', opacity=0.8, histnorm='probability density') - fig.update_layout( + fig_ts.update_layout( plot_bgcolor='#DDDDDD', paper_bgcolor='#EEEEEE', - font_color='#7FDBFF', + font_color='#000000', font_family="Courier New", title_font_family="Courier New", ) + fig_hist\ + .update_layout( + plot_bgcolor='#DDDDDD', + paper_bgcolor='#EEEEEE', + font_color='#000000', + font_family="Courier New", + title_font_family="Courier New", + margin={"autoexpand": True})\ + .update_yaxes(visible=False) + + fig_hist.update_layout( + legend=dict( + yanchor="top", + y=0.99, + xanchor="right", + x=0.99 + ) + ) + return fig_ts, fig_hist + +# Correlation plot +@app.callback( + Output("correlation-div", "children"), + Output("correlation-div", "style"), + Input("group_checklist", "value") +) +def correlation_plot(group_checklist_values: List[str]): + if len(group_checklist_values) == 2: + group1: pd.DataFrame = df.loc[df['group'] == group_checklist_values[0]] + group2: pd.DataFrame = df.loc[df['group'] == group_checklist_values[1]] + groups_merged: pd.DataFrame = group1.merge( + group2, + on=["time"], + validate="1:1", + how="inner", + ) + fig_corr = px.density_heatmap( + data_frame=groups_merged, + x="kW_x", + y="kW_y", + labels={ + "kW_x": f"{group_checklist_values[0]} Power Use (kW)", + "kW_y": f"{group_checklist_values[1]} Power Use (kW)", + } + ) + fig_corr\ + .update_layout( + plot_bgcolor='#DDDDDD', + paper_bgcolor='#EEEEEE', + font_color='#000000', + font_family="Courier New", + title_font_family="Courier New", + margin={"autoexpand": True})\ + .update_coloraxes(showscale=False) - return fig + children=[ + dcc.Graph(id="correlation-chart", figure=fig_corr) + ] + style={'padding': 10, 'flex': 1 } + else: + children = [ + html.P(children="Select two clients to show correlation"), + ] + style={'padding': 10, 'display': 'flex', 'flex': 1, 'align-items': 'center', 'justify-content': 'center'} + return children, style + + +# --- Station selection +# limit the number of options +_max_selected = 2 +@app.callback( + Output("group_checklist", "options"), + Input("group_checklist", "value"), + Input("drop_irregular", "value"), +) +def update_multi_options(groups_selected: List[str], drop_irregular: bool): + options = all_groups + if drop_irregular: + options = options[:10] + + if len(groups_selected) >= _max_selected: + options = [ + { + "label": option, + "value": option, + "disabled": option not in groups_selected, + } + for option in options + ] + return options if __name__ == "__main__": app.run_server(debug=True) diff --git a/demo/assets/app.css b/demo/assets/app.css index f16050e..1c6c33f 100644 --- a/demo/assets/app.css +++ b/demo/assets/app.css @@ -132,8 +132,8 @@ body { line-height: 1.6; font-weight: 400; font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #7FDBFF; - background-color: rgb(11, 11, 11); + color: #111111; + background-color: #EEEEEE; } From 7dc63829a48eb3a10c8890307da46c3670a9f3a3 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 22 Nov 2022 19:28:24 -0700 Subject: [PATCH 04/17] saving data from example --- docs/examples/electricity.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/examples/electricity.py b/docs/examples/electricity.py index 0380a97..eac119c 100644 --- a/docs/examples/electricity.py +++ b/docs/examples/electricity.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.13.0 +# jupytext_version: 1.14.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -96,6 +96,9 @@ # %% df_elec.head() +# %% +df_elec.to_parquet("df_elec.pq") + # %% [markdown] # Electricity-demand data can be challenging because of its complexity. In traditional forecasting applications, we divide our model into siloed processes that each contribute to separate behaviors of the time-series. For example: # @@ -348,6 +351,9 @@ def plot_2x2(df: pd.DataFrame, plot_2x2(df_forecast_ex2) +# %% +df_forecast_ex2.to_parquet("df_forecast_ex2.pq") + # %% [markdown] # Viewing the forecasts in this way helps us see a lingering serious issue: the annual seasonal pattern is very different for daytimes and nighttimes, but the model isn't capturing that. # @@ -376,7 +382,7 @@ def plot_2x2(df: pd.DataFrame, # %% [markdown] -# Since we're working with more data, we'll need to use a dataloader (`torchcast` provies `TimeSeriesDataLoader`): +# Since we're working with more data, we'll need to use a dataloader (`torchcast` provides `TimeSeriesDataLoader`): # %% def make_dataloader(type_: str, @@ -728,6 +734,9 @@ def configure_optimizers(self) -> torch.optim.Optimizer: # %% plot_2x2(df_forecast_nn.query("group==@example_group")) +# %% +df_forecast_nn.to_parquet("df_forecast_nn.pq") + # %% [markdown] # Let's confirm quantitatively that the 2nd model does indeed substantially reduce forecast error, relative to the 'standard' model: @@ -760,3 +769,5 @@ def inverse_transform(df: pd.DataFrame) -> pd.DataFrame: merge(df_nn_err, on=['group', 'validation'], suffixes=('_es', '_es_nn')) + +# %% From c4e8be65b2434de6136c617f1037cee77a0cc33d Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 22 Nov 2022 20:13:33 -0700 Subject: [PATCH 05/17] added last options --- demo/app.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/demo/app.py b/demo/app.py index 93af02f..511ecec 100644 --- a/demo/app.py +++ b/demo/app.py @@ -42,6 +42,7 @@ # Part 3 html.Div( children=[ + html.H6("Select ML model:"), dcc.RadioItems( id="prediction_toggle", options=[ @@ -50,10 +51,28 @@ {"label": "Neural-Network", "value": "nn"} ] ), - - + html.H6("Time"), + dcc.Checklist( + id="day_night_toggle", + options=[ + {"label": "Day", "value": "day"}, + {"label": "Night", "value": "night"} + ], + value=["day", "night"], + inline=True, + ), + html.H6("Day"), + dcc.Checklist( + id="day_of_week_toggle", + options=[ + {"label": "Weekdays", "value": "weekdays"}, + {"label": "Weekends", "value": "weekends"}, + ], + value=["weekdays", "weekends"], + inline=True, + ) ], - style={'padding': 10, 'display': 'flex', 'flex': 1, 'align-items': 'center', 'justify-content': 'center'}), + style={'padding': 10, 'flex': 1, 'align-items': 'center', 'justify-content': 'center'}), ], style={'display': 'flex', 'flex-direction': 'row'} ) ], style={'padding': 10, 'flex': 2.5}), From c9a37ab52d78dccff3f0286f64eedf306fadbb58 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 22 Nov 2022 21:05:09 -0700 Subject: [PATCH 06/17] vline --- demo/app.py | 4 ++++ setup.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/demo/app.py b/demo/app.py index 511ecec..7d10dbd 100644 --- a/demo/app.py +++ b/demo/app.py @@ -1,3 +1,4 @@ +from datetime import datetime from typing import List from dash import Dash, dcc, html, Input, Output from pathlib import Path @@ -118,7 +119,10 @@ ) def display_time_series(groups: List[str]): _df = df.loc[df['group'].isin(groups)] + fig_ts = px.line(_df, x='time', y='kW', color='group') + fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="green") + fig_hist = px.histogram(_df, x='kW', color='group', opacity=0.8, histnorm='probability density') fig_ts.update_layout( diff --git a/setup.py b/setup.py index 854e5be..f1ddb05 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,9 @@ ], 'demo': [ 'dash', + 'dash-daq', 'jupyter-dash', + 'pyarrow', # to support parquet files ] } ) From 7fe6260badf5d2f73502d82dac3153adf374ce2d Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 08:40:46 -0700 Subject: [PATCH 07/17] regular groups --- demo/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/demo/app.py b/demo/app.py index 7d10dbd..3318f25 100644 --- a/demo/app.py +++ b/demo/app.py @@ -14,8 +14,9 @@ # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options -df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") +df: pd.DataFrame = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") all_groups: np.ndarray = df['group'].unique() +regular_groups: pd.DataFrame = pd.read_parquet(DATA_DIR / 'train_groups.pq') app.layout = html.Div( @@ -210,7 +211,7 @@ def correlation_plot(group_checklist_values: List[str]): def update_multi_options(groups_selected: List[str], drop_irregular: bool): options = all_groups if drop_irregular: - options = options[:10] + options = regular_groups["train_groups"].to_list() if len(groups_selected) >= _max_selected: options = [ From 1329e6e16a6f1d31063508653d674bbbc296d342 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 09:21:44 -0700 Subject: [PATCH 08/17] formatting --- demo/app.py | 253 +++++++++++++++++++++++++++----------------- demo/assets/app.css | 43 ++++++-- 2 files changed, 193 insertions(+), 103 deletions(-) diff --git a/demo/app.py b/demo/app.py index 3318f25..afc876d 100644 --- a/demo/app.py +++ b/demo/app.py @@ -14,100 +14,133 @@ # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options -df: pd.DataFrame = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") +df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") all_groups: np.ndarray = df['group'].unique() -regular_groups: pd.DataFrame = pd.read_parquet(DATA_DIR / 'train_groups.pq') app.layout = html.Div( [ - # col 1 html.Div(children=[ - html.H1(children='Electricity Use Forecasting', - style={'textAlign': 'center',}), - - html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', - style={'textAlign': 'center',}), + html.Div(children=[ + html.H1(children='Electricity Use Forecasting', + style={'textAlign': 'left',}), - dcc.Graph(id="time-series-chart"), + html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', + style={'textAlign': 'left', "margin-bottom": "40px"}), + ], + ), + html.Img( + src='assets/logo.png', + style={'width':'127px', 'height': '48px', 'position': 'absolute', 'right': '75px', 'align-self': 'center'} + ), + ], + style={'display':'inline-flex'}, + ), - # the horizontal part with 3parts + html.Div(children=[ + # col 1 html.Div(children=[ - # Part 1 - html.Div( - dcc.Graph(id="histogram-chart"), - style={'padding': 10, 'flex': 1}), - # Part 2 - html.Div(id="correlation-div",), + dcc.Graph( + id="time-series-chart", + className="card", + style={"margin-bottom": "30px" } + ), + + # the horizontal part with 2 parts + html.Div(children=[ + # Part 1 + html.Div( + dcc.Graph(id="histogram-chart"), + style={ 'flex': 1, "margin-right": "30px"}, + className="card", + ), + + # Part 2 + html.Div( + id="correlation-div", + style={"width": "323px"}, + className="card",), + ], + style={'display': 'flex', 'flex-direction': 'row'}, + ) + ], + style={'flex': 2.5}, + ), - # Part 3 + # col 2 + html.Div(children=[ html.Div( - children=[ - html.H6("Select ML model:"), - dcc.RadioItems( - id="prediction_toggle", - options=[ - {"label": "No Predictions", "value": "none"}, - {"label": "Exponential Smoothing", "value": "es"}, - {"label": "Neural-Network", "value": "nn"} - ] + children=[ + html.H6("Select ML model:"), + dcc.RadioItems( + id="prediction_toggle", + options=[ + {"label": "No Predictions", "value": "none"}, + {"label": "Exponential Smoothing", "value": "es"}, + {"label": "Neural-Network", "value": "nn"} + ], + className="form-item" + ), + html.H6("Time"), + dcc.Checklist( + id="day_night_toggle", + options=[ + {"label": "Day", "value": "day"}, + {"label": "Night", "value": "night"} + ], + value=["day", "night"], + inline=True, + className="form-item" + ), + html.H6("Day"), + dcc.Checklist( + id="day_of_week_toggle", + options=[ + {"label": "Weekdays", "value": "weekdays"}, + {"label": "Weekends", "value": "weekends"}, + ], + value=["weekdays", "weekends"], + inline=True, + className="form-item" + ) + ], + style={'flex': 1, 'align-items': 'left', 'justify-content': 'left'}, ), - html.H6("Time"), - dcc.Checklist( - id="day_night_toggle", - options=[ - {"label": "Day", "value": "day"}, - {"label": "Night", "value": "night"} - ], - value=["day", "night"], - inline=True, - ), - html.H6("Day"), - dcc.Checklist( - id="day_of_week_toggle", - options=[ - {"label": "Weekdays", "value": "weekdays"}, - {"label": "Weekends", "value": "weekends"}, - ], - value=["weekdays", "weekends"], - inline=True, - ) - ], - style={'padding': 10, 'flex': 1, 'align-items': 'center', 'justify-content': 'center'}), - ], style={'display': 'flex', 'flex-direction': 'row'} - ) - ], style={'padding': 10, 'flex': 2.5}), - - # col 2 - html.Div(children=[ - html.Div(children=[ - html.H6("Drop irregular clients:", style={"margin-top": "100px"}), - dcc.RadioItems(id="drop_irregular", - options=[{"label": "No", "value": False}, - {"label": "Yes", "value": True}], - value=False, inline=True), - ]), + html.Div(children=[ + html.H6("Drop irregular clients:",), + dcc.RadioItems(id="drop_irregular", + options=[{"label": "No", "value": False}, + {"label": "Yes", "value": True}], + value=False, inline=True, + className="form-item"), + ]), - html.Div(children=[ - html.H6("Clients (max 2)"), - # dcc.Dropdown( - # id="group_dropdown", - # options=df['group'].unique(), - # value="MT_001", - # clearable=False, - # ), - dcc.Checklist( - id="group_checklist", - options=all_groups, - value=["MT_001"], - style={"max-height": "250px", "overflow-y": "auto"}, - ), - ]), + html.Div(children=[ + html.H6("Clients (max 2)"), + # dcc.Dropdown( + # id="group_dropdown", + # options=df['group'].unique(), + # value="MT_001", + # clearable=False, + # ), + dcc.Checklist( + id="group_checklist", + options=all_groups, + value=["MT_001"], + className="form-item", + style={"max-height": "190px", "overflow-y": "auto"}, + ), + ]), - ], style={'padding': 10, 'flex': 1}), + ], + style={ 'flex': 1, "margin-left": "30px"}, + className="card",), + ], + className="bg-strong-dark-blue", + style={'display': 'flex', 'flex-direction': 'row'}, + ) ], - style={'display': 'flex', 'flex-direction': 'row'} ) @@ -121,26 +154,47 @@ def display_time_series(groups: List[str]): _df = df.loc[df['group'].isin(groups)] - fig_ts = px.line(_df, x='time', y='kW', color='group') - fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="green") + fig_ts = px.line(_df, x='time', y='kW', color='group', height=308,) + fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="white") - fig_hist = px.histogram(_df, x='kW', color='group', opacity=0.8, histnorm='probability density') + fig_hist = px.histogram(_df, x='kW', color='group', opacity=0.8, histnorm='probability density', width=493, height=275,) fig_ts.update_layout( - plot_bgcolor='#DDDDDD', - paper_bgcolor='#EEEEEE', - font_color='#000000', + legend=dict( + yanchor="top", + y=0.99, + xanchor="right", + x=0.99 + ), + margin=dict( + l=0, + r=0, + b=0, + t=0, + pad=0, + ), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#F4F4F4', font_family="Courier New", title_font_family="Courier New", ) + fig_ts.update_xaxes(showgrid=False) + fig_ts.update_yaxes(showgrid=False) fig_hist\ .update_layout( - plot_bgcolor='#DDDDDD', - paper_bgcolor='#EEEEEE', - font_color='#000000', + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#F4F4F4', font_family="Courier New", title_font_family="Courier New", - margin={"autoexpand": True})\ + margin=dict( + l=8, + r=8, + b=0, + t=10, + pad=0, + ))\ .update_yaxes(visible=False) fig_hist.update_layout( @@ -176,27 +230,34 @@ def correlation_plot(group_checklist_values: List[str]): labels={ "kW_x": f"{group_checklist_values[0]} Power Use (kW)", "kW_y": f"{group_checklist_values[1]} Power Use (kW)", - } + }, + width=440, height=275 ) fig_corr\ .update_layout( - plot_bgcolor='#DDDDDD', - paper_bgcolor='#EEEEEE', - font_color='#000000', + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#FFF', font_family="Courier New", title_font_family="Courier New", - margin={"autoexpand": True})\ + margin=dict( + l=0, + r=0, + b=0, + t=24, + pad=0, + ),)\ .update_coloraxes(showscale=False) children=[ dcc.Graph(id="correlation-chart", figure=fig_corr) ] - style={'padding': 10, 'flex': 1 } + style={ 'flex': 1, "width": "464px"} else: children = [ html.P(children="Select two clients to show correlation"), ] - style={'padding': 10, 'display': 'flex', 'flex': 1, 'align-items': 'center', 'justify-content': 'center'} + style={ 'flex': 1, "width": "464px", "text-align": "center", "padding-top": "150px"} return children, style @@ -211,7 +272,7 @@ def correlation_plot(group_checklist_values: List[str]): def update_multi_options(groups_selected: List[str], drop_irregular: bool): options = all_groups if drop_irregular: - options = regular_groups["train_groups"].to_list() + options = options[:10] if len(groups_selected) >= _max_selected: options = [ diff --git a/demo/assets/app.css b/demo/assets/app.css index 1c6c33f..df9c925 100644 --- a/demo/assets/app.css +++ b/demo/assets/app.css @@ -32,6 +32,9 @@ z-index: 1002; }*/ +.modebar { + display: none !important; +} /* Grid –––––––––––––––––––––––––––––––––––––––––––––––––– */ @@ -132,8 +135,8 @@ body { line-height: 1.6; font-weight: 400; font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; - color: #111111; - background-color: #EEEEEE; + color: #F4F4F4; + margin: 75px; } @@ -143,12 +146,12 @@ h1, h2, h3, h4, h5, h6 { margin-top: 0; margin-bottom: 0; font-weight: 300; } -h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 2rem; } +h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom: 0.5rem; } h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} -h6 { font-size: 2.0rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.75rem; margin-top: 0.75rem;} +h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 8px;} p { margin-top: 0; } @@ -231,8 +234,8 @@ input[type="submit"].button-primary:focus, input[type="reset"].button-primary:focus, input[type="button"].button-primary:focus { color: #FFF; - background-color: #1EAEDB; - border-color: #1EAEDB; } + background-color: #2A3D82; + border-color: #2A3D82; } /* Forms @@ -291,7 +294,8 @@ fieldset { border-width: 0; } input[type="checkbox"], input[type="radio"] { - display: inline; } + display: inline; + color: #2A3D82} label > .label-body { display: inline-block; margin-left: .5rem; @@ -356,6 +360,31 @@ ul, ol { margin-bottom: 0.75rem; } +/* Colors +–––––––––––––––––––––––––––––––––––––––––––––––––– */ +body, .bg-strong-dark-blue { + background-color: #2A3D82; +} + +.card { + background-color: #263775; + padding: 32px; + border-radius: 6px; + filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); +} + +.form-item { + margin-bottom: 16px; +} +.form-item label { + margin-right: 16px; + font-size: 16px; +} + +.form-item input { + margin-right: 8px; + margin-bottom: 16px; +} /* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */ .u-full-width { From 8276413526605227091ca9ca6853e5cace7b305a Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 09:31:47 -0700 Subject: [PATCH 09/17] added logo --- demo/app.py | 2 +- demo/assets/strong-dark-blue-background.png | Bin 0 -> 3159 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 demo/assets/strong-dark-blue-background.png diff --git a/demo/app.py b/demo/app.py index afc876d..a0e2b74 100644 --- a/demo/app.py +++ b/demo/app.py @@ -30,7 +30,7 @@ ], ), html.Img( - src='assets/logo.png', + src='assets/strong-dark-blue-background.png', style={'width':'127px', 'height': '48px', 'position': 'absolute', 'right': '75px', 'align-self': 'center'} ), ], diff --git a/demo/assets/strong-dark-blue-background.png b/demo/assets/strong-dark-blue-background.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3e26587846c4fb1bd41d9a496159f051c5e7a1 GIT binary patch literal 3159 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 literal 0 HcmV?d00001 From 169511289b1b5f57ef7004d83a0990e1ebe9e073 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 09:38:33 -0700 Subject: [PATCH 10/17] change defaults and prompts --- demo/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/app.py b/demo/app.py index a0e2b74..4812259 100644 --- a/demo/app.py +++ b/demo/app.py @@ -80,6 +80,7 @@ {"label": "Exponential Smoothing", "value": "es"}, {"label": "Neural-Network", "value": "nn"} ], + value="none", className="form-item" ), html.H6("Time"), @@ -112,7 +113,7 @@ dcc.RadioItems(id="drop_irregular", options=[{"label": "No", "value": False}, {"label": "Yes", "value": True}], - value=False, inline=True, + value=True, inline=True, className="form-item"), ]), From e5acf0ea3c04a198a1ee14d8c7f250d28638a53d Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 14:21:10 -0700 Subject: [PATCH 11/17] formatting --- demo/app.py | 348 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 220 insertions(+), 128 deletions(-) diff --git a/demo/app.py b/demo/app.py index 4812259..bb6a377 100644 --- a/demo/app.py +++ b/demo/app.py @@ -1,5 +1,7 @@ +from typing import Dict, List from datetime import datetime -from typing import List +from functools import lru_cache +from typing import List, Optional from dash import Dash, dcc, html, Input, Output from pathlib import Path import plotly.express as px @@ -15,157 +17,243 @@ # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") +nn_df: pd.DataFrame = pd.read_parquet(DATA_DIR / "df_forecast_nn.pq") all_groups: np.ndarray = df['group'].unique() +regular_groups = pd.read_parquet(DATA_DIR / "train_groups.pq").squeeze().to_list() +strong_color_cycle: Dict[str, str] = dict(color_discrete_sequence=["#F4F4F4", "#B5C3FF"]) app.layout = html.Div( [ - html.Div(children=[ - html.Div(children=[ - html.H1(children='Electricity Use Forecasting', - style={'textAlign': 'left',}), - - html.Div(children='Investigating at the electricity use of 370 clients from 2011 to 2014', - style={'textAlign': 'left', "margin-bottom": "40px"}), - ], - ), - html.Img( - src='assets/strong-dark-blue-background.png', - style={'width':'127px', 'height': '48px', 'position': 'absolute', 'right': '75px', 'align-self': 'center'} - ), + html.Div( + children=[ + html.Div( + children=[ + html.H1( + children="Electricity Use Forecasting", + style={ + "textAlign": "left", + }, + ), + html.Div( + children="Investigating the electricity use of 370 clients in the grid.", + style={"textAlign": "left", "margin-bottom": "40px"}, + ), + ], + ), + html.Img( + src="assets/strong-dark-blue-background.png", + style={ + "width": "127px", + "height": "48px", + "position": "absolute", + "right": "75px", + "align-self": "center", + }, + ), ], - style={'display':'inline-flex'}, - ), - - html.Div(children=[ - # col 1 - html.Div(children=[ - - dcc.Graph( - id="time-series-chart", - className="card", - style={"margin-bottom": "30px" } + style={"display": "inline-flex"}, + ), + html.Div( + children=[ + # col 1 + html.Div( + children=[ + dcc.Graph( + id="time-series-chart", + className="card", + style={"margin-bottom": "30px"}, + ), + # the horizontal part with 2 parts + html.Div( + children=[ + # Part 1 + html.Div( + dcc.Graph(id="histogram-chart"), + style={"flex": 1, "margin-right": "30px"}, + className="card", + ), + # Part 2 + html.Div( + id="correlation-div", + style={"width": "323px"}, + className="card", + ), + ], + style={"display": "flex", "flex-direction": "row"}, + ), + ], + style={"flex": 2.5}, ), - - # the horizontal part with 2 parts - html.Div(children=[ - # Part 1 - html.Div( - dcc.Graph(id="histogram-chart"), - style={ 'flex': 1, "margin-right": "30px"}, - className="card", - ), - - # Part 2 - html.Div( - id="correlation-div", - style={"width": "323px"}, - className="card",), - ], - style={'display': 'flex', 'flex-direction': 'row'}, - ) - ], - style={'flex': 2.5}, - ), - - # col 2 - html.Div(children=[ + # col 2 html.Div( - children=[ - html.H6("Select ML model:"), - dcc.RadioItems( - id="prediction_toggle", - options=[ - {"label": "No Predictions", "value": "none"}, - {"label": "Exponential Smoothing", "value": "es"}, - {"label": "Neural-Network", "value": "nn"} - ], - value="none", - className="form-item" - ), - html.H6("Time"), - dcc.Checklist( - id="day_night_toggle", - options=[ - {"label": "Day", "value": "day"}, - {"label": "Night", "value": "night"} - ], - value=["day", "night"], - inline=True, - className="form-item" - ), - html.H6("Day"), - dcc.Checklist( - id="day_of_week_toggle", - options=[ - {"label": "Weekdays", "value": "weekdays"}, - {"label": "Weekends", "value": "weekends"}, - ], - value=["weekdays", "weekends"], - inline=True, - className="form-item" - ) - ], - style={'flex': 1, 'align-items': 'left', 'justify-content': 'left'}, + children=[ + html.Div( + children=[ + html.H6("Select ML model:"), + dcc.RadioItems( + id="prediction_toggle", + options=[ + {"label": "No Predictions", "value": "none"}, + {"label": "Exponential Smoothing", "value": "es",}, + {"label": "Neural-Network", "value": "nn"}, + ], + value="none", + className="form-item", + ), + html.H6("Time"), + dcc.Checklist( + id="day_night_toggle", + options=[ + {"label": "Day", "value": "day"}, + {"label": "Night", "value": "night"}, + ], + value=["day", "night"], + inline=True, + className="form-item", + ), + html.H6("Day"), + dcc.Checklist( + id="day_of_week_toggle", + options=[ + {"label": "Weekdays", "value": "weekdays"}, + {"label": "Weekends", "value": "weekends"}, + ], + value=["weekdays", "weekends"], + inline=True, + className="form-item", + ), + ], + style={ + "flex": 1, + "align-items": "left", + "justify-content": "left", + }, ), - html.Div(children=[ - html.H6("Drop irregular clients:",), - dcc.RadioItems(id="drop_irregular", - options=[{"label": "No", "value": False}, - {"label": "Yes", "value": True}], - value=True, inline=True, - className="form-item"), - ]), - - html.Div(children=[ - html.H6("Clients (max 2)"), - # dcc.Dropdown( - # id="group_dropdown", - # options=df['group'].unique(), - # value="MT_001", - # clearable=False, - # ), - dcc.Checklist( - id="group_checklist", - options=all_groups, - value=["MT_001"], - className="form-item", - style={"max-height": "190px", "overflow-y": "auto"}, - ), - ]), - - ], - style={ 'flex': 1, "margin-left": "30px"}, - className="card",), - ], - className="bg-strong-dark-blue", - style={'display': 'flex', 'flex-direction': 'row'}, + html.Div( + children=[ + html.H6( + "Drop irregular clients:", + ), + dcc.RadioItems( + id="drop_irregular", + options=[ + {"label": "No", "value": False}, + {"label": "Yes", "value": True}, + ], + value=True, + inline=True, + className="form-item", + ), + ] + ), + html.Div( + children=[ + html.H6("Clients"), + # dcc.Dropdown( + # id="group_dropdown", + # options=df['group'].unique(), + # value="MT_001", + # clearable=False, + # ), + dcc.Checklist( + id="group_checklist", + options=all_groups, + value=["MT_328"], + className="form-item", + style={"max-height": "190px", "overflow-y": "auto"}, + ), + ] + ), + ], + style={"flex": 1, "margin-left": "30px"}, + className="card", + ), + ], + className="bg-strong-dark-blue", + style={"display": "flex", "flex-direction": "row"}, + ), + html.Div( + id="footer", + children=['Powered by ', + html.A(href="https://strong.io/", children="Strong Analytics", + target="_blank", rel="noopener noreferrer")], + style={"margin-top": "16px", "padding-left": "5px"}, ) ], ) +@lru_cache() +def get_es_prediction_df(group: str) -> Optional[pd.DataFrame]: + try: + return pd.read_parquet(DATA_DIR / f"es_{group}_2.pq") + except FileNotFoundError: + print(f"Couldn't find the es predictions for {group}") + return None + # --- plotting # main plot @app.callback( Output("time-series-chart", "figure"), Output("histogram-chart", "figure"), - Input("group_checklist", "value") + Input("group_checklist", "value"), + Input("prediction_toggle", "value"), ) -def display_time_series(groups: List[str]): - _df = df.loc[df['group'].isin(groups)] +def display_time_series( + groups: List[str], + prediction_toggle_value: str, +): + _df = df.loc[df['group'].isin(groups)].copy() + train_val_split_dt = pd.Timestamp("2013-06-01") + + if prediction_toggle_value == "none": + fig_ts = px.line(_df, x='time', y='kW', color='group', height=308, **strong_color_cycle) + elif prediction_toggle_value == "es": + # Load the predictions from each group + es_predictions: List[pd.DataFrame] = [get_es_prediction_df(group) for group in groups] + melted_dfs: List[pd.DataFrame] = [pd.DataFrame(columns=['group', 'time', 'actual_or_mean', 'kW'])] + for es_prediction in es_predictions: + melted = es_prediction.melt( + value_vars=["actual", "mean"], + id_vars=["group", "time"], + value_name="kW", + var_name="actual_or_mean", + ) + melted = melted.query("(actual_or_mean == 'actual') | (time > @train_val_split_dt)") + melted_dfs.append( + melted + ) + _df = pd.concat(melted_dfs) + fig_ts = px.line(_df, x='time', y='kW', color='group', line_dash='actual_or_mean', height=308, **strong_color_cycle) + else: + _df = ( + nn_df + .query("group.isin(@groups)") + .melt( + value_vars=["actual", "mean"], + id_vars=["group", "time"], + value_name="kW", + var_name="actual_or_mean", + ) + .query("(actual_or_mean == 'actual') | (time > @train_val_split_dt)") + ) + + fig_ts = px.line(_df, x='time', y='kW', color='group', line_dash='actual_or_mean', height=308, **strong_color_cycle) - fig_ts = px.line(_df, x='time', y='kW', color='group', height=308,) fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="white") - fig_hist = px.histogram(_df, x='kW', color='group', opacity=0.8, histnorm='probability density', width=493, height=275,) + fig_hist = px.histogram(_df, x='kW', nbins=80, color='group', opacity=0.8, histnorm='probability density', width=493, + height=275, **strong_color_cycle) fig_ts.update_layout( legend=dict( - yanchor="top", - y=0.99, - xanchor="right", - x=0.99 + yanchor="bottom", + y=1.01, + xanchor="center", + x=0.50, + orientation='h', + title="Group", ), margin=dict( l=0, @@ -182,6 +270,7 @@ def display_time_series(groups: List[str]): ) fig_ts.update_xaxes(showgrid=False) fig_ts.update_yaxes(showgrid=False) + fig_ts.update_traces(line=dict(width=1.0)) fig_hist\ .update_layout( plot_bgcolor='rgba(0,0,0,0)', @@ -270,10 +359,13 @@ def correlation_plot(group_checklist_values: List[str]): Input("group_checklist", "value"), Input("drop_irregular", "value"), ) -def update_multi_options(groups_selected: List[str], drop_irregular: bool): +def update_multi_options( + groups_selected: List[str], + drop_irregular: bool, +): options = all_groups if drop_irregular: - options = options[:10] + options = regular_groups if len(groups_selected) >= _max_selected: options = [ From 847b55dcfa983a0c70cc1e7baf879c04af0b68ea Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 23 Nov 2022 14:22:31 -0700 Subject: [PATCH 12/17] ignore cached model predictions --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4fb062d..cd44f03 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ docs/_html/* *.pt *.gz docs/examples/lightning_logs/* +docs/examples/electricity/*.pq From 6a0bccd5c41fe4323b20d43f418c978ae2b3b0f8 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 29 Nov 2022 18:28:16 -0700 Subject: [PATCH 13/17] updated formatting, increased functionality --- demo/app.py | 282 +++++++++++++++++++++++++++++--------------- demo/assets/app.css | 16 ++- 2 files changed, 196 insertions(+), 102 deletions(-) diff --git a/demo/app.py b/demo/app.py index bb6a377..74f4220 100644 --- a/demo/app.py +++ b/demo/app.py @@ -7,17 +7,41 @@ import plotly.express as px import pandas as pd import numpy as np +import logging + +logging.basicConfig( + format='%(asctime)s %(levelname)s %(module)s - %(funcName)s: %(message)s', + level=logging.DEBUG, + datefmt='%H:%M:%S', +) app = Dash(__name__) DATA_DIR = Path("docs/examples/electricity").resolve() +train_val_split_dt: pd.Timestamp = pd.Timestamp("2013-06-01") # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options -df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz") +df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz", parse_dates=["time"]) nn_df: pd.DataFrame = pd.read_parquet(DATA_DIR / "df_forecast_nn.pq") +# the nn_df and exponential smoothing dfs have sqrt and centered data. +df["kW_sqrt"] = np.sqrt(df["kW"]) +centerings = ( + nn_df + .sort_values("time") + .groupby("group") + .head(1) + .filter(["actual", "time", "group"]) + .merge(df) + .set_index("group") + .pipe( + lambda frame: + frame["kW_sqrt"] - frame["actual"] + ) +) + all_groups: np.ndarray = df['group'].unique() regular_groups = pd.read_parquet(DATA_DIR / "train_groups.pq").squeeze().to_list() strong_color_cycle: Dict[str, str] = dict(color_discrete_sequence=["#F4F4F4", "#B5C3FF"]) @@ -42,7 +66,7 @@ ], ), html.Img( - src="assets/strong-dark-blue-background.png", + src="assets/strong-logo-white.svg", style={ "width": "127px", "height": "48px", @@ -61,7 +85,7 @@ children=[ dcc.Graph( id="time-series-chart", - className="card", + className="card main-card", style={"margin-bottom": "30px"}, ), # the horizontal part with 2 parts @@ -76,8 +100,8 @@ # Part 2 html.Div( id="correlation-div", - style={"width": "323px"}, className="card", + style={"flex": "0 0 323px"} ), ], style={"display": "flex", "flex-direction": "row"}, @@ -90,17 +114,18 @@ children=[ html.Div( children=[ - html.H6("Select ML model:"), + html.H5("ML model"), dcc.RadioItems( id="prediction_toggle", options=[ - {"label": "No Predictions", "value": "none"}, + {"label": "None", "value": "none"}, {"label": "Exponential Smoothing", "value": "es",}, {"label": "Neural-Network", "value": "nn"}, ], value="none", className="form-item", ), + html.H5(" "), html.H6("Time"), dcc.Checklist( id="day_night_toggle", @@ -123,15 +148,7 @@ inline=True, className="form-item", ), - ], - style={ - "flex": 1, - "align-items": "left", - "justify-content": "left", - }, - ), - html.Div( - children=[ + html.H5(" "), html.H6( "Drop irregular clients:", ), @@ -145,10 +162,6 @@ inline=True, className="form-item", ), - ] - ), - html.Div( - children=[ html.H6("Clients"), # dcc.Dropdown( # id="group_dropdown", @@ -161,12 +174,12 @@ options=all_groups, value=["MT_328"], className="form-item", - style={"max-height": "190px", "overflow-y": "auto"}, + style={"max-height": "190px", "overflow-y": "scroll", "scrollbar-color": "dark"}, ), - ] + ], ), ], - style={"flex": 1, "margin-left": "30px"}, + style={"flex": "0 0 250px", "margin-left": "30px"}, className="card", ), ], @@ -184,7 +197,7 @@ ) -@lru_cache() +@lru_cache def get_es_prediction_df(group: str) -> Optional[pd.DataFrame]: try: return pd.read_parquet(DATA_DIR / f"es_{group}_2.pq") @@ -192,6 +205,42 @@ def get_es_prediction_df(group: str) -> Optional[pd.DataFrame]: print(f"Couldn't find the es predictions for {group}") return None +@lru_cache +def get_combined_df(group: str) -> pd.DataFrame: + es_prediction_df_subset: pd.DataFrame = ( + get_es_prediction_df(group) + .assign(ES=lambda df: (df["mean"] + centerings.at[group]).pow(2)) + .filter(["time", "ES"]) + ) + nn_df_subset: pd.DataFrame = ( + nn_df + .query(f"group == '{group}'") + .assign(NN=lambda df: (df["mean"] + centerings.at[group]).pow(2)) + .filter(["time", "NN"]) + ) + original_data_subset = ( + df + .query(f"group == '{group}'") + .assign(actual = lambda df: df["kW"]) + .filter(["group", "time", "actual"]) + ) + combined = ( + original_data_subset + .merge(es_prediction_df_subset, how="outer") + .merge(nn_df_subset, how="outer") + .assign( + is_train = lambda df: df["time"] < train_val_split_dt + ) + .melt( + value_vars=["actual", "ES", "NN"], + id_vars=["group", "time", "is_train"], + value_name="kW", + var_name="model", + ) + ) + logging.info(combined) + return combined + # --- plotting # main plot @app.callback( @@ -204,73 +253,110 @@ def display_time_series( groups: List[str], prediction_toggle_value: str, ): - _df = df.loc[df['group'].isin(groups)].copy() - train_val_split_dt = pd.Timestamp("2013-06-01") + + _df = pd.concat( + [pd.DataFrame(columns=["group", "time", "is_train", "model", "kW"])] + + [get_combined_df(group) for group in groups] + ) + + ts_fig_height_px = 400 if prediction_toggle_value == "none": - fig_ts = px.line(_df, x='time', y='kW', color='group', height=308, **strong_color_cycle) + fig_ts = px.line( + _df.query("model == 'actual'"), + x='time', y='kW', + color='group', **strong_color_cycle, + height=ts_fig_height_px, + ) elif prediction_toggle_value == "es": - # Load the predictions from each group - es_predictions: List[pd.DataFrame] = [get_es_prediction_df(group) for group in groups] - melted_dfs: List[pd.DataFrame] = [pd.DataFrame(columns=['group', 'time', 'actual_or_mean', 'kW'])] - for es_prediction in es_predictions: - melted = es_prediction.melt( - value_vars=["actual", "mean"], - id_vars=["group", "time"], - value_name="kW", - var_name="actual_or_mean", - ) - melted = melted.query("(actual_or_mean == 'actual') | (time > @train_val_split_dt)") - melted_dfs.append( - melted - ) - _df = pd.concat(melted_dfs) - fig_ts = px.line(_df, x='time', y='kW', color='group', line_dash='actual_or_mean', height=308, **strong_color_cycle) - else: - _df = ( - nn_df - .query("group.isin(@groups)") - .melt( - value_vars=["actual", "mean"], - id_vars=["group", "time"], - value_name="kW", - var_name="actual_or_mean", - ) - .query("(actual_or_mean == 'actual') | (time > @train_val_split_dt)") + # do the plotting + fig_ts = px.line( + _df.query("(model == 'actual') | ((model == 'ES') and (is_train == False))"), + x='time', y='kW', + color='group', **strong_color_cycle, + line_dash='model', + height=ts_fig_height_px ) + else: + fig_ts = px.line( + _df.query("(model == 'actual') | ((model == 'NN') & (is_train == False))"), + x='time', y='kW', + color='group', **strong_color_cycle, + line_dash='model', + height=ts_fig_height_px) - fig_ts = px.line(_df, x='time', y='kW', color='group', line_dash='actual_or_mean', height=308, **strong_color_cycle) - + # Add the vertical line between train-val split fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="white") - fig_hist = px.histogram(_df, x='kW', nbins=80, color='group', opacity=0.8, histnorm='probability density', width=493, - height=275, **strong_color_cycle) - - fig_ts.update_layout( - legend=dict( - yanchor="bottom", - y=1.01, - xanchor="center", - x=0.50, - orientation='h', - title="Group", - ), - margin=dict( - l=0, - r=0, - b=0, - t=0, - pad=0, - ), - plot_bgcolor='rgba(0,0,0,0)', - paper_bgcolor='rgba(0,0,0,0)', - font_color='#F4F4F4', - font_family="Courier New", - title_font_family="Courier New", + fig_hist = px.histogram( + _df.query("model == 'actual'"), + x='kW', + nbins=80, + color='group', **strong_color_cycle, + opacity=0.8, + histnorm='probability density', + height=275 ) - fig_ts.update_xaxes(showgrid=False) - fig_ts.update_yaxes(showgrid=False) - fig_ts.update_traces(line=dict(width=1.0)) + + # Styling of time series + fig_ts \ + .update_layout( + xaxis=dict( + rangeselector=dict( + buttons=list([ + dict(count=1, + label="1m", + step="month", + stepmode="backward"), + dict(count=6, + label="6m", + step="month", + stepmode="backward"), + dict(count=1, + label="1y", + step="year", + stepmode="backward"), + dict(step="all") + ]), + font=dict( + color="#111" + ) + ), + rangeslider=dict( + visible=True + ), + range=(train_val_split_dt - pd.Timedelta("7D"), + train_val_split_dt - pd.Timedelta("7D") + pd.Timedelta("30D")), + type="date" + ) + ) \ + .update_layout( + legend=dict( + yanchor="bottom", + y=1.01, + xanchor="center", + x=0.50, + orientation='h', + title="Group", + ), + margin=dict( + l=0, + r=0, + b=0, + t=0, + pad=0, + ), + plot_bgcolor='rgba(0,0,0,0)', + paper_bgcolor='rgba(0,0,0,0)', + font_color='#F4F4F4', + font_family="Courier New", + title_font_family="Courier New", + ) \ + .update_xaxes(showgrid=False) \ + .update_yaxes(showgrid=False) \ + .update_traces(line=dict(width=1.0)) + + # Update looks of histogram fig_hist\ .update_layout( plot_bgcolor='rgba(0,0,0,0)', @@ -284,17 +370,17 @@ def display_time_series( b=0, t=10, pad=0, - ))\ - .update_yaxes(visible=False) - - fig_hist.update_layout( - legend=dict( - yanchor="top", - y=0.99, - xanchor="right", - x=0.99 + )) \ + .update_yaxes(visible=False) \ + .update_layout( + legend=dict( + yanchor="top", + y=0.99, + xanchor="right", + x=0.99 + ) ) - ) + return fig_ts, fig_hist # Correlation plot @@ -321,7 +407,7 @@ def correlation_plot(group_checklist_values: List[str]): "kW_x": f"{group_checklist_values[0]} Power Use (kW)", "kW_y": f"{group_checklist_values[1]} Power Use (kW)", }, - width=440, height=275 + width=323, height=275 ) fig_corr\ .update_layout( @@ -342,12 +428,12 @@ def correlation_plot(group_checklist_values: List[str]): children=[ dcc.Graph(id="correlation-chart", figure=fig_corr) ] - style={ 'flex': 1, "width": "464px"} + style={ "flex": "0 0 323px"} else: children = [ html.P(children="Select two clients to show correlation"), ] - style={ 'flex': 1, "width": "464px", "text-align": "center", "padding-top": "150px"} + style={ "flex": "0 0 323px", "text-align": "center", "padding-top": "150px"} return children, style @@ -379,4 +465,8 @@ def update_multi_options( return options if __name__ == "__main__": - app.run_server(debug=True) + app.run_server( + debug=True, + host="andys-macbook-pro", + port=80, + ) diff --git a/demo/assets/app.css b/demo/assets/app.css index df9c925..4af03b3 100644 --- a/demo/assets/app.css +++ b/demo/assets/app.css @@ -150,8 +150,8 @@ h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} -h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 0.6rem;} -h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 8px;} +h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 2.0rem;} +h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0px;} p { margin-top: 0; } @@ -366,6 +366,10 @@ body, .bg-strong-dark-blue { background-color: #2A3D82; } +.main-card { + background-color: #1D2B5C !important; +} + .card { background-color: #263775; padding: 32px; @@ -374,16 +378,16 @@ body, .bg-strong-dark-blue { } .form-item { - margin-bottom: 16px; + margin-bottom: 8px; } .form-item label { - margin-right: 16px; - font-size: 16px; + margin-right: 16px; + font-size: 16px; } .form-item input { margin-right: 8px; - margin-bottom: 16px; + margin-bottom: 8px; } /* Utilities –––––––––––––––––––––––––––––––––––––––––––––––––– */ From 1a80e23e5e559f127bcf4d5d5b3f058a3230a1af Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Tue, 29 Nov 2022 21:08:07 -0700 Subject: [PATCH 14/17] v1 of app --- demo/app.py | 152 +++++++++++++++++++----------- demo/assets/app.css | 15 +-- demo/assets/strong-logo-white.svg | 1 + setup.py | 2 +- 4 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 demo/assets/strong-logo-white.svg diff --git a/demo/app.py b/demo/app.py index 74f4220..7e7d10d 100644 --- a/demo/app.py +++ b/demo/app.py @@ -2,8 +2,11 @@ from datetime import datetime from functools import lru_cache from typing import List, Optional + +from plotly.graph_objs import Figure from dash import Dash, dcc, html, Input, Output from pathlib import Path +import dash_daq as daq import plotly.express as px import pandas as pd import numpy as np @@ -86,7 +89,7 @@ dcc.Graph( id="time-series-chart", className="card main-card", - style={"margin-bottom": "30px"}, + style={"flex": "1 1", "margin-bottom": "30px"}, ), # the horizontal part with 2 parts html.Div( @@ -94,7 +97,7 @@ # Part 1 html.Div( dcc.Graph(id="histogram-chart"), - style={"flex": 1, "margin-right": "30px"}, + style={"flex": "1 1", "margin-right": "30px"}, className="card", ), # Part 2 @@ -107,61 +110,46 @@ style={"display": "flex", "flex-direction": "row"}, ), ], - style={"flex": 2.5}, + style={"flex": "1 1"}, ), # col 2 html.Div( children=[ html.Div( children=[ - html.H5("ML model"), + html.H6("ML model"), dcc.RadioItems( id="prediction_toggle", options=[ {"label": "None", "value": "none"}, {"label": "Exponential Smoothing", "value": "es",}, - {"label": "Neural-Network", "value": "nn"}, + {"label": "Neural Network", "value": "nn"}, ], value="none", className="form-item", ), + daq.BooleanSwitch(id="show_future_switch", on=False, label="Show future", labelPosition="bottom"), html.H5(" "), html.H6("Time"), - dcc.Checklist( - id="day_night_toggle", - options=[ - {"label": "Day", "value": "day"}, - {"label": "Night", "value": "night"}, - ], - value=["day", "night"], - inline=True, - className="form-item", + dcc.RangeSlider( + 0, 24, 1, value=[0, 24], marks={0: "12AM", 6: "6AM", 12: "12PM", 18: "6PM", 24: "12AM"}, + id="time_of_day_range", ), html.H6("Day"), dcc.Checklist( id="day_of_week_toggle", options=[ - {"label": "Weekdays", "value": "weekdays"}, - {"label": "Weekends", "value": "weekends"}, + {"label": label, "value": value} + for label, value in zip( + "MTWRFSS", + range(7) + ) ], - value=["weekdays", "weekends"], + value=list(range(7)), inline=True, className="form-item", ), html.H5(" "), - html.H6( - "Drop irregular clients:", - ), - dcc.RadioItems( - id="drop_irregular", - options=[ - {"label": "No", "value": False}, - {"label": "Yes", "value": True}, - ], - value=True, - inline=True, - className="form-item", - ), html.H6("Clients"), # dcc.Dropdown( # id="group_dropdown", @@ -174,8 +162,21 @@ options=all_groups, value=["MT_328"], className="form-item", - style={"max-height": "190px", "overflow-y": "scroll", "scrollbar-color": "dark"}, + style={"max-height": "250px", "overflow-y": "scroll", "scrollbar-color": "dark"}, ), + # html.H6( + # "Drop irregular clients", + # ), + # dcc.RadioItems( + # id="drop_irregular", + # options=[ + # {"label": "No", "value": False}, + # {"label": "Yes", "value": True}, + # ], + # value=True, + # inline=True, + # className="form-item", + # ), ], ), ], @@ -238,7 +239,6 @@ def get_combined_df(group: str) -> pd.DataFrame: var_name="model", ) ) - logging.info(combined) return combined # --- plotting @@ -248,17 +248,53 @@ def get_combined_df(group: str) -> pd.DataFrame: Output("histogram-chart", "figure"), Input("group_checklist", "value"), Input("prediction_toggle", "value"), + Input("show_future_switch", "on"), + Input("time_of_day_range", "value"), + Input("day_of_week_toggle", "value"), + Input("time-series-chart", "figure"), ) def display_time_series( groups: List[str], prediction_toggle_value: str, + show_future: bool, + time_of_day_range: List[int], + day_of_week_toggle: List[int], + existing_fig_ts: Optional[Figure], ): _df = pd.concat( - [pd.DataFrame(columns=["group", "time", "is_train", "model", "kW"])] + [pd.DataFrame({ + "group": pd.Series(dtype='str'), + "time": pd.Series(dtype='datetime64[s]'), + "is_train": pd.Series(dtype='bool'), + "model": pd.Series(dtype='str'), + "kW": pd.Series(dtype='float64') + })] + [get_combined_df(group) for group in groups] ) + # Get the y range now before it gets overwritten + + y_range = (0, _df["kW"].max() * 1.1) + x_range = existing_fig_ts and existing_fig_ts['layout']['xaxis']['range'] + x_max_range = (_df["time"].min(), _df["time"].max()) + + if not show_future: + _df = _df.query("(is_train == True) | (model != 'actual')") + + if time_of_day_range == [0, 24]: + pass + else: + valid_hours = list(range(*time_of_day_range)) + _df = _df.query("time.dt.hour.isin(@valid_hours)") + + if day_of_week_toggle == list(range(7)): + pass + else: + _df = _df.query("time.dt.dayofweek.isin(@day_of_week_toggle)") + + + ts_fig_height_px = 400 if prediction_toggle_value == "none": @@ -288,16 +324,6 @@ def display_time_series( # Add the vertical line between train-val split fig_ts.add_vline(x=datetime(2013, 6, 1), line_width=3, line_dash="dash", line_color="white") - fig_hist = px.histogram( - _df.query("model == 'actual'"), - x='kW', - nbins=80, - color='group', **strong_color_cycle, - opacity=0.8, - histnorm='probability density', - height=275 - ) - # Styling of time series fig_ts \ .update_layout( @@ -323,10 +349,13 @@ def display_time_series( ) ), rangeslider=dict( - visible=True + visible=True, + range=x_max_range, + ), + range=x_range or ( + train_val_split_dt - pd.Timedelta("7D"), + train_val_split_dt - pd.Timedelta("7D") + pd.Timedelta("30D") ), - range=(train_val_split_dt - pd.Timedelta("7D"), - train_val_split_dt - pd.Timedelta("7D") + pd.Timedelta("30D")), type="date" ) ) \ @@ -353,9 +382,19 @@ def display_time_series( title_font_family="Courier New", ) \ .update_xaxes(showgrid=False) \ - .update_yaxes(showgrid=False) \ + .update_yaxes(range=y_range, showgrid=False) \ .update_traces(line=dict(width=1.0)) + # Create the histogram + fig_hist = px.histogram( + _df.query("model == 'actual'"), + x='kW', + nbins=80, + color='group', **strong_color_cycle, + opacity=0.8, + histnorm='probability density', + height=275 + ) # Update looks of histogram fig_hist\ .update_layout( @@ -381,6 +420,7 @@ def display_time_series( ) ) + return fig_ts, fig_hist # Correlation plot @@ -391,8 +431,8 @@ def display_time_series( ) def correlation_plot(group_checklist_values: List[str]): if len(group_checklist_values) == 2: - group1: pd.DataFrame = df.loc[df['group'] == group_checklist_values[0]] - group2: pd.DataFrame = df.loc[df['group'] == group_checklist_values[1]] + group1: pd.DataFrame = get_combined_df(group_checklist_values[0]).query("model == 'actual'") + group2: pd.DataFrame = get_combined_df(group_checklist_values[1]).query("model == 'actual'") groups_merged: pd.DataFrame = group1.merge( group2, on=["time"], @@ -407,6 +447,7 @@ def correlation_plot(group_checklist_values: List[str]): "kW_x": f"{group_checklist_values[0]} Power Use (kW)", "kW_y": f"{group_checklist_values[1]} Power Use (kW)", }, + color_continuous_scale="haline", width=323, height=275 ) fig_corr\ @@ -420,7 +461,7 @@ def correlation_plot(group_checklist_values: List[str]): l=0, r=0, b=0, - t=24, + t=0, pad=0, ),)\ .update_coloraxes(showscale=False) @@ -443,16 +484,17 @@ def correlation_plot(group_checklist_values: List[str]): @app.callback( Output("group_checklist", "options"), Input("group_checklist", "value"), - Input("drop_irregular", "value"), + # Input("drop_irregular", "value"), ) def update_multi_options( groups_selected: List[str], - drop_irregular: bool, + # drop_irregular: bool, ): - options = all_groups - if drop_irregular: - options = regular_groups + # options = all_groups + # if drop_irregular: + # options = regular_groups + options = regular_groups if len(groups_selected) >= _max_selected: options = [ { diff --git a/demo/assets/app.css b/demo/assets/app.css index 4af03b3..ea8c368 100644 --- a/demo/assets/app.css +++ b/demo/assets/app.css @@ -150,11 +150,12 @@ h1 { font-size: 4.5rem; line-height: 1.2; letter-spacing: -.1rem; margin-bottom h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; margin-bottom: 1.8rem; margin-top: 1.8rem;} h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; margin-bottom: 1.5rem; margin-top: 1.5rem;} h4 { font-size: 2.6rem; line-height: 1.35; letter-spacing: -.08rem; margin-bottom: 1.2rem; margin-top: 1.2rem;} -h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 0.6rem; margin-top: 2.0rem;} -h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0px;} +h5 { font-size: 2.2rem; line-height: 1.5; letter-spacing: -.05rem; margin-bottom: 1.2rem; margin-top: 2.0rem;} +h6 { font-size: 1.8rem; line-height: 1.6; letter-spacing: 0; margin-bottom: 0.6rem; margin-top: 1.2rem;} p { - margin-top: 0; } + margin-top: 0; +} /* Blockquotes @@ -171,11 +172,11 @@ blockquote { /* Links –––––––––––––––––––––––––––––––––––––––––––––––––– */ a { - color: #1EAEDB; + color: #EEE; text-decoration: underline; cursor: pointer;} a:hover { - color: #0FA0CE; } + color: #FFF; } /* Buttons @@ -295,7 +296,9 @@ fieldset { input[type="checkbox"], input[type="radio"] { display: inline; - color: #2A3D82} + color: #2A3D82; + accent-color: #AAAAAA; +} label > .label-body { display: inline-block; margin-left: .5rem; diff --git a/demo/assets/strong-logo-white.svg b/demo/assets/strong-logo-white.svg new file mode 100644 index 0000000..eccba65 --- /dev/null +++ b/demo/assets/strong-logo-white.svg @@ -0,0 +1 @@ +Strong Logo Working_White \ No newline at end of file diff --git a/setup.py b/setup.py index f1ddb05..f4cec51 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ ], 'demo': [ 'dash', - 'dash-daq', + 'dash_daq', 'jupyter-dash', 'pyarrow', # to support parquet files ] From 79309655334e74963876ea29a401f405bbaa36e1 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 30 Nov 2022 13:07:15 -0700 Subject: [PATCH 15/17] delete logo --- demo/assets/strong-dark-blue-background.png | Bin 3159 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 demo/assets/strong-dark-blue-background.png diff --git a/demo/assets/strong-dark-blue-background.png b/demo/assets/strong-dark-blue-background.png deleted file mode 100644 index 0e3e26587846c4fb1bd41d9a496159f051c5e7a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3159 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 From 7c145600f7f5737d65808de2260f227354f92cff Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Wed, 30 Nov 2022 13:31:10 -0700 Subject: [PATCH 16/17] Revert changes to electricity.py --- docs/examples/electricity.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/docs/examples/electricity.py b/docs/examples/electricity.py index eac119c..0380a97 100644 --- a/docs/examples/electricity.py +++ b/docs/examples/electricity.py @@ -6,7 +6,7 @@ # extension: .py # format_name: percent # format_version: '1.3' -# jupytext_version: 1.14.1 +# jupytext_version: 1.13.0 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -96,9 +96,6 @@ # %% df_elec.head() -# %% -df_elec.to_parquet("df_elec.pq") - # %% [markdown] # Electricity-demand data can be challenging because of its complexity. In traditional forecasting applications, we divide our model into siloed processes that each contribute to separate behaviors of the time-series. For example: # @@ -351,9 +348,6 @@ def plot_2x2(df: pd.DataFrame, plot_2x2(df_forecast_ex2) -# %% -df_forecast_ex2.to_parquet("df_forecast_ex2.pq") - # %% [markdown] # Viewing the forecasts in this way helps us see a lingering serious issue: the annual seasonal pattern is very different for daytimes and nighttimes, but the model isn't capturing that. # @@ -382,7 +376,7 @@ def plot_2x2(df: pd.DataFrame, # %% [markdown] -# Since we're working with more data, we'll need to use a dataloader (`torchcast` provides `TimeSeriesDataLoader`): +# Since we're working with more data, we'll need to use a dataloader (`torchcast` provies `TimeSeriesDataLoader`): # %% def make_dataloader(type_: str, @@ -734,9 +728,6 @@ def configure_optimizers(self) -> torch.optim.Optimizer: # %% plot_2x2(df_forecast_nn.query("group==@example_group")) -# %% -df_forecast_nn.to_parquet("df_forecast_nn.pq") - # %% [markdown] # Let's confirm quantitatively that the 2nd model does indeed substantially reduce forecast error, relative to the 'standard' model: @@ -769,5 +760,3 @@ def inverse_transform(df: pd.DataFrame) -> pd.DataFrame: merge(df_nn_err, on=['group', 'validation'], suffixes=('_es', '_es_nn')) - -# %% From e059b8da675f793dd72160d284fe11452d8c71a7 Mon Sep 17 00:00:00 2001 From: Andy Wong Date: Mon, 20 Nov 2023 14:25:25 -0700 Subject: [PATCH 17/17] deployment --- demo/Dockerfile | 21 +++++++++++++++++++ demo/app.py | 55 +++++++++++++++++++++++-------------------------- setup.py | 1 + 3 files changed, 48 insertions(+), 29 deletions(-) create mode 100644 demo/Dockerfile diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000..21d6b1d --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.9 + +WORKDIR /app + +RUN python3 -m pip install --upgrade pip setuptools && \ + python3 -m pip install gunicorn + +COPY setup.py . + +# Install only the dependencies +RUN python setup.py egg_info && \ + pip install -r *.egg-info/requires.txt && \ + rm -rf *.egg-info/ + +COPY . . + +RUN pip install .[demo] + +EXPOSE 8097 + +CMD ["gunicorn", "--bind", "0.0.0.0:8097", "demo.app:server"] diff --git a/demo/app.py b/demo/app.py index 7e7d10d..d0f5f40 100644 --- a/demo/app.py +++ b/demo/app.py @@ -1,16 +1,15 @@ -from typing import Dict, List +import logging from datetime import datetime from functools import lru_cache -from typing import List, Optional - -from plotly.graph_objs import Figure -from dash import Dash, dcc, html, Input, Output from pathlib import Path +from typing import Dict, List, Optional + import dash_daq as daq -import plotly.express as px -import pandas as pd import numpy as np -import logging +import pandas as pd +import plotly.express as px +from dash import Dash, Input, Output, dcc, html +from plotly.graph_objs import Figure logging.basicConfig( format='%(asctime)s %(levelname)s %(module)s - %(funcName)s: %(message)s', @@ -19,12 +18,12 @@ ) -app = Dash(__name__) +app = Dash(__name__, eager_loading=True) +server = app.server DATA_DIR = Path("docs/examples/electricity").resolve() train_val_split_dt: pd.Timestamp = pd.Timestamp("2013-06-01") - # assume you have a "long-form" data frame # see https://plotly.com/python/px-arguments/ for more options df = pd.read_csv(DATA_DIR / "df_electricity.csv.gz", parse_dates=["time"]) @@ -151,18 +150,15 @@ ), html.H5(" "), html.H6("Clients"), - # dcc.Dropdown( - # id="group_dropdown", - # options=df['group'].unique(), - # value="MT_001", - # clearable=False, - # ), - dcc.Checklist( - id="group_checklist", + dcc.Dropdown( + id="group_dropdown", options=all_groups, value=["MT_328"], - className="form-item", - style={"max-height": "250px", "overflow-y": "scroll", "scrollbar-color": "dark"}, + # className="form-item", + clearable=False, + multi=True, + # style={.Select-value-label {color: white !important;}}, + style={"color": "black"}, ), # html.H6( # "Drop irregular clients", @@ -246,7 +242,7 @@ def get_combined_df(group: str) -> pd.DataFrame: @app.callback( Output("time-series-chart", "figure"), Output("histogram-chart", "figure"), - Input("group_checklist", "value"), + Input("group_dropdown", "value"), Input("prediction_toggle", "value"), Input("show_future_switch", "on"), Input("time_of_day_range", "value"), @@ -427,7 +423,7 @@ def display_time_series( @app.callback( Output("correlation-div", "children"), Output("correlation-div", "style"), - Input("group_checklist", "value") + Input("group_dropdown", "value") ) def correlation_plot(group_checklist_values: List[str]): if len(group_checklist_values) == 2: @@ -482,8 +478,8 @@ def correlation_plot(group_checklist_values: List[str]): # limit the number of options _max_selected = 2 @app.callback( - Output("group_checklist", "options"), - Input("group_checklist", "value"), + Output("group_dropdown", "options"), + Input("group_dropdown", "value"), # Input("drop_irregular", "value"), ) def update_multi_options( @@ -506,9 +502,10 @@ def update_multi_options( ] return options + if __name__ == "__main__": - app.run_server( - debug=True, - host="andys-macbook-pro", - port=80, - ) + app.run_server( + # debug=True, + host="0.0.0.0", + port=8097, + ) diff --git a/setup.py b/setup.py index f4cec51..bc68e38 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'dash', 'dash_daq', 'jupyter-dash', + 'pandas', 'pyarrow', # to support parquet files ] }