From 5474591535fd54ce98fd10ab936b0ff59ce4a087 Mon Sep 17 00:00:00 2001 From: AmyNickollse Date: Wed, 5 Jun 2024 16:28:40 +0100 Subject: [PATCH] adjusted chart added to all table adjustment pages --- dm_regional_app/charts.py | 154 ++++++++++++++++++ .../dm_regional_app/views/adjusted.html | 5 +- .../dm_regional_app/views/entry_rates.html | 23 ++- .../dm_regional_app/views/exit_rates.html | 22 ++- .../views/transition_rates.html | 15 ++ dm_regional_app/views.py | 105 +++++++++++- 6 files changed, 314 insertions(+), 10 deletions(-) diff --git a/dm_regional_app/charts.py b/dm_regional_app/charts.py index 11c6276..9a15a08 100644 --- a/dm_regional_app/charts.py +++ b/dm_regional_app/charts.py @@ -203,3 +203,157 @@ def entry_rate_table(data): df.columns = ["Age Group", "Placement", "Entry rate"] return df + + +def compare_forecast( + historic_data: PopulationStats, + base_forecast: Prediction, + adjusted_forecast: Prediction, + **kwargs +): + # pop start and end dates to visualise reference period + reference_start_date = kwargs.pop("reference_start_date") + reference_end_date = kwargs.pop("reference_end_date") + + # dataframe containing total children in historic data + df_hd = historic_data.stock.unstack().reset_index() + df_hd.columns = ["from", "date", "historic"] + df_hd = df_hd[["date", "historic"]].groupby(by="date").sum().reset_index() + df_hd["date"] = pd.to_datetime(df_hd["date"]).dt.date + + # dataframe containing total children in base forecast + df = base_forecast.population.unstack().reset_index() + + df.columns = ["from", "date", "forecast"] + df = df[df["from"].apply(lambda x: "Not in care" in x) == False] + df = df[["date", "forecast"]].groupby(by="date").sum().reset_index() + df["date"] = pd.to_datetime(df["date"]).dt.date + + # dataframe containing upper and lower confidence intervals for base forecast + df_ci = base_forecast.variance.unstack().reset_index() + df_ci.columns = ["bin", "date", "variance"] + df_ci = df_ci[["date", "variance"]].groupby(by="date").sum().reset_index() + df_ci["date"] = pd.to_datetime(df_ci["date"]).dt.date + df_ci["upper"] = df["forecast"] + df_ci["variance"] + df_ci["lower"] = df["forecast"] - df_ci["variance"] + + # dataframe containing total children in adjusted forecast + df_af = adjusted_forecast.population.unstack().reset_index() + + df_af.columns = ["from", "date", "forecast"] + df_af = df_af[df_af["from"].apply(lambda x: "Not in care" in x) == False] + df_af = df_af[["date", "forecast"]].groupby(by="date").sum().reset_index() + df_af["date"] = pd.to_datetime(df_af["date"]).dt.date + + # dataframe containing upper and lower confidence intervals for adjusted forecast + df_df_ci = adjusted_forecast.variance.unstack().reset_index() + df_df_ci.columns = ["bin", "date", "variance"] + df_df_ci = df_df_ci[["date", "variance"]].groupby(by="date").sum().reset_index() + df_df_ci["date"] = pd.to_datetime(df_df_ci["date"]).dt.date + df_df_ci["upper"] = df_af["forecast"] + df_df_ci["variance"] + df_df_ci["lower"] = df_af["forecast"] - df_df_ci["variance"] + + # visualise prediction using unstacked dataframe + fig = go.Figure() + + # Display confidence interval as filled shape + fig.add_trace( + go.Scatter( + x=df_ci["date"], + y=df_ci["lower"], + line_color="rgba(255,255,255,0)", + name="Confidence interval", + showlegend=False, + ) + ) + + fig.add_trace( + go.Scatter( + x=df_ci["date"], + y=df_ci["upper"], + fill="tonexty", + fillcolor="rgba(0,176,246,0.2)", + line_color="rgba(255,255,255,0)", + name="Base confidence interval", + showlegend=True, + ) + ) + + # Display confidence interval as filled shape + fig.add_trace( + go.Scatter( + x=df_ci["date"], + y=df_ci["lower"], + line_color="rgba(255,255,255,0)", + name="Confidence interval", + showlegend=False, + ) + ) + + fig.add_trace( + go.Scatter( + x=df_ci["date"], + y=df_ci["upper"], + fill="tonexty", + fillcolor="rgba(0,176,246,0.2)", + line_color="rgba(255,255,255,0)", + name="Adjusted confidence interval", + showlegend=True, + ) + ) + + # add base forecast for total children + fig.add_trace( + go.Scatter( + x=df_af["date"], + y=df_af["forecast"], + name="Adjusted Forecast", + line=dict(color="black", width=1.5, dash="dash"), + ) + ) + + # add adjusted forecast for total children + fig.add_trace( + go.Scatter( + x=df["date"], + y=df["forecast"], + name="Base Forecast", + line=dict(color="black", width=1.5), + ) + ) + + # add historic data for total children + fig.add_trace( + go.Scatter( + x=df_hd["date"], + y=df_hd["historic"], + name="Historic data", + line=dict(color="black", width=1.5, dash="dot"), + ) + ) + + # add shaded reference period + fig.add_shape( + type="rect", + xref="x", + yref="paper", + x0=reference_start_date, + y0=0, + x1=reference_end_date, + y1=1, + line=dict( + width=0, + ), + label=dict( + text="Reference period", textposition="top center", font=dict(size=14) + ), + fillcolor="rgba(105,105,105,0.1)", + layer="above", + ) + + fig.update_layout( + title="Forecast", xaxis_title="Date", yaxis_title="Number of children" + ) + fig.update_yaxes(rangemode="tozero") + fig_html = fig.to_html(full_html=False) + return fig_html diff --git a/dm_regional_app/templates/dm_regional_app/views/adjusted.html b/dm_regional_app/templates/dm_regional_app/views/adjusted.html index 212c15b..4289b22 100644 --- a/dm_regional_app/templates/dm_regional_app/views/adjusted.html +++ b/dm_regional_app/templates/dm_regional_app/views/adjusted.html @@ -62,8 +62,9 @@
To make adjustments, you can view the table by:

Once you have adjusted the table you will be able to see the base -forecast alongside your adjusted forecast to compare. You can -continue to edit the table across each of the views.

+ forecast alongside your adjusted forecast to compare. You can + continue to edit the table across each of the views.

+

Rates in these tables include any previous adjustments you might have made.

diff --git a/dm_regional_app/templates/dm_regional_app/views/entry_rates.html b/dm_regional_app/templates/dm_regional_app/views/entry_rates.html index b707453..3a18ccc 100644 --- a/dm_regional_app/templates/dm_regional_app/views/entry_rates.html +++ b/dm_regional_app/templates/dm_regional_app/views/entry_rates.html @@ -8,9 +8,11 @@

Adjust entry rates


You may want to edit the number of children per year entering care to give a more accurate - picture of future forecasting or simply to explore what changes you may expect to see.

- Rates are applied to the daily population of children. For example, a rate of 0.5 to 10-16 Fostering - would mean that every two days a child would enter this placement.
+ picture of future forecasting or simply to explore what changes you may expect to see.

+

Rates are applied to the daily population of children. For example, a rate of 0.5 to 10-16 Fostering + would mean that every two days a child would enter this placement. +

+

Rate multiplication multiplies the initial base rate produced by the model parameters you selected.

@@ -34,6 +36,21 @@

Adjust entry rates

+ +{% if is_post is True %} + + + + +{% endif %} +
{% endblock %} diff --git a/dm_regional_app/templates/dm_regional_app/views/exit_rates.html b/dm_regional_app/templates/dm_regional_app/views/exit_rates.html index be43257..d177a75 100644 --- a/dm_regional_app/templates/dm_regional_app/views/exit_rates.html +++ b/dm_regional_app/templates/dm_regional_app/views/exit_rates.html @@ -8,9 +8,12 @@

Adjust exit rates


You may want to edit the number of children per year exiting care to give a more accurate - picture of future forecasting or simply to explore what changes you may expect to see.

+ picture of future forecasting or simply to explore what changes you may expect to see.

+

Rates are applied to the daily population of children. For example, a rate of 0.5 from 10-16 Fostering - would mean that each day, 50% of children in Fostering will leave care.
+ would mean that each day, 50% of children in Fostering will leave care. +

+

Rate multiplication multiplies the initial base rate produced by the model parameters you selected.

@@ -34,6 +37,21 @@

Adjust exit rates

+ + +{% if is_post is True %} + + + +{% endif %} + +
{% endblock %} diff --git a/dm_regional_app/templates/dm_regional_app/views/transition_rates.html b/dm_regional_app/templates/dm_regional_app/views/transition_rates.html index 5153eec..2d6f207 100644 --- a/dm_regional_app/templates/dm_regional_app/views/transition_rates.html +++ b/dm_regional_app/templates/dm_regional_app/views/transition_rates.html @@ -34,6 +34,21 @@

Adjust transition rates

+ +{% if is_post is True %} + + + + +{% endif %} +
{% endblock %} diff --git a/dm_regional_app/views.py b/dm_regional_app/views.py index ef29f27..8dda016 100644 --- a/dm_regional_app/views.py +++ b/dm_regional_app/views.py @@ -7,6 +7,7 @@ from django.urls import reverse from dm_regional_app.charts import ( + compare_forecast, entry_rate_table, exit_rate_table, historic_chart, @@ -116,7 +117,36 @@ def entry_rates(request): else: session_scenario.adjusted_numbers = data session_scenario.save() - return redirect("adjusted") + + config = Config() + stats = PopulationStats(historic_data, config) + + adjusted_prediction = predict( + data=historic_data, + **session_scenario.prediction_parameters, + rate_adjustment=session_scenario.adjusted_rates + ) + + # build chart + chart = compare_forecast( + stats, + prediction, + adjusted_prediction, + **session_scenario.prediction_parameters + ) + + is_post = True + + return render( + request, + "dm_regional_app/views/entry_rates.html", + { + "entry_rate_table": entry_rates, + "form": form, + "chart": chart, + "is_post": is_post, + }, + ) else: form = DynamicForm( @@ -124,12 +154,15 @@ def entry_rates(request): dataframe=prediction.entry_rates, ) + is_post = False + return render( request, "dm_regional_app/views/entry_rates.html", { "entry_rate_table": entry_rates, "form": form, + "is_post": is_post, }, ) else: @@ -178,7 +211,36 @@ def exit_rates(request): else: session_scenario.adjusted_rates = data session_scenario.save() - return redirect("adjusted") + + config = Config() + stats = PopulationStats(historic_data, config) + + adjusted_prediction = predict( + data=historic_data, + **session_scenario.prediction_parameters, + rate_adjustment=session_scenario.adjusted_rates + ) + + # build chart + chart = compare_forecast( + stats, + prediction, + adjusted_prediction, + **session_scenario.prediction_parameters + ) + + is_post = True + + return render( + request, + "dm_regional_app/views/exit_rates.html", + { + "exit_rate_table": exit_rates, + "form": form, + "chart": chart, + "is_post": is_post, + }, + ) else: form = DynamicForm( @@ -186,12 +248,15 @@ def exit_rates(request): dataframe=prediction.transition_rates, ) + is_post = False + return render( request, "dm_regional_app/views/exit_rates.html", { "exit_rate_table": exit_rates, "form": form, + "is_post": is_post, }, ) else: @@ -240,7 +305,36 @@ def transition_rates(request): else: session_scenario.adjusted_rates = data session_scenario.save() - return redirect("adjusted") + + config = Config() + stats = PopulationStats(historic_data, config) + + adjusted_prediction = predict( + data=historic_data, + **session_scenario.prediction_parameters, + rate_adjustment=session_scenario.adjusted_rates + ) + + # build chart + chart = compare_forecast( + stats, + prediction, + adjusted_prediction, + **session_scenario.prediction_parameters + ) + + is_post = True + + return render( + request, + "dm_regional_app/views/transition_rates.html", + { + "transition_rate_table": transition_rates, + "form": form, + "chart": chart, + "is_post": is_post, + }, + ) else: form = DynamicForm( @@ -248,12 +342,15 @@ def transition_rates(request): dataframe=prediction.transition_rates, ) + is_post = False + return render( request, "dm_regional_app/views/transition_rates.html", { "transition_rate_table": transition_rates, "form": form, + "is_post": is_post, }, ) else: @@ -272,6 +369,7 @@ def adjusted(request): datacontainer = read_data(source=settings.DATA_SOURCE) if request.method == "POST": + # check if it was historic data filter form that was submitted if "uasc" in request.POST: historic_form = HistoricDataFilter( request.POST, @@ -292,6 +390,7 @@ def adjusted(request): datacontainer.enriched_view, historic_form.cleaned_data ) + # check if it was predict filter form that was submitted if "reference_start_date" in request.POST: predict_form = PredictFilter( request.POST,