Skip to content

Commit eed0639

Browse files
committed
Added checkbox for switching charts to logarithmic scale on the y-axis
1 parent a8c21bc commit eed0639

File tree

5 files changed

+185
-40
lines changed

5 files changed

+185
-40
lines changed

src/penn_chime/model/parameters.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def validate(string):
136136
"ventilated": ValDisposition,
137137
"hospitalized": ValDisposition,
138138
"icu": ValDisposition,
139+
"use_log_scale": OptionalValue
139140
}
140141

141142

@@ -160,16 +161,17 @@ def validate(string):
160161
"relative_contact_rate": "Social distancing reduction rate: 0.0 - 1.0",
161162
"ventilated_days": "Average days on ventilator",
162163
"ventilated_rate": "Ventilated Rate: 0.0 - 1.0",
164+
"use_log_scale": "Flag to use logarithmic scale on charts instead of linear scale."
163165
}
164166

165167

166168
ARGS = (
167169
(
168170
"parameters",
169171
str,
170-
None,
171-
None,
172-
False,
172+
None, # Min value
173+
None, # Max value
174+
False, # Whether it is required or optional.
173175
),
174176
(
175177
"current_hospitalized",
@@ -297,15 +299,24 @@ def validate(string):
297299
1.0,
298300
True,
299301
),
302+
(
303+
"use_log_scale",
304+
bool,
305+
None,
306+
None,
307+
False
308+
)
300309
)
301310

302311

303312
def to_cli(name):
304313
return "--" + name.replace('_', '-')
305314

306-
307315
class Parameters:
308-
"""Parameters."""
316+
"""
317+
Object containing all of the parameters that can be adjusted by the user, either from the command line or using
318+
the side bar of the web app.
319+
"""
309320

310321
@classmethod
311322
def parser(cls):
@@ -314,11 +325,20 @@ def parser(cls):
314325

315326
for name, cast, min_value, max_value, required in ARGS:
316327
arg = to_cli(name)
317-
parser.add_argument(
318-
arg,
319-
type=validator(arg, cast, min_value, max_value, required),
320-
help=HELP.get(name),
321-
)
328+
if cast == bool:
329+
# This argument is a command-line flag and does not need validation.
330+
parser.add_argument(
331+
arg,
332+
action='store_true',
333+
help=HELP.get(name),
334+
)
335+
else:
336+
# Use a custom validator for any arguments that take in values.
337+
parser.add_argument(
338+
arg,
339+
type=validator(arg, cast, min_value, max_value, required),
340+
help=HELP.get(name),
341+
)
322342
return parser
323343

324344
@classmethod
@@ -395,6 +415,7 @@ def __init__(self, **kwargs):
395415
self.relative_contact_rate = None
396416
self.recovered = None
397417
self.ventilated = None
418+
self.use_log_scale = False
398419

399420
passed_and_default_parameters = {}
400421
for key, value in kwargs.items():

src/penn_chime/view/charts.py

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
11
from typing import Dict, Optional
22

3-
from altair import Chart
3+
from altair import Chart, Scale
44
import pandas as pd
55
import numpy as np
66

77
from ..constants import DATE_FORMAT
88

99

1010
def build_admits_chart(
11-
*, alt, admits_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None
11+
*, alt, admits_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None, use_log_scale: bool = False
1212
) -> Chart:
13-
"""Build admits chart."""
14-
y_scale = alt.Scale()
15-
if max_y_axis is not None:
16-
y_scale.domain = (0, max_y_axis)
13+
"""
14+
This builds the "New Admissions" chart, projecting daily admissions over time.
1715
18-
x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
16+
Args:
17+
alt: Reference to Altair package.
18+
admits_floor_df: Pandas data frame containing three columns: "admits_hospitalized", "admits_icu", and
19+
"admits_ventilated".
20+
max_y_axis: Optional maximum value for the Y axis of the chart.
21+
use_log_scale: Set to true to use a logarithmic scale on the Y axis. Default is linear scale.
22+
23+
Returns: The newly created chart.
24+
25+
"""
26+
27+
adjusted_admits_floor_df = __adjust_data_for_log_scale(admits_floor_df) if use_log_scale else admits_floor_df
28+
y_scale = __build_y_scale(alt, max_y_axis, use_log_scale)
29+
30+
x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=DATE_FORMAT))
1931
y = dict(shorthand="value:Q", title="Daily admissions", scale=y_scale)
2032
color = "key:N"
2133
tooltip = ["date:T", alt.Tooltip("value:Q", format=".0f", title="Admit"), "key:N"]
@@ -40,19 +52,31 @@ def build_admits_chart(
4052
.mark_rule(color="black", opacity=0.35, size=2)
4153
)
4254
return (
43-
alt.layer(points, bar, data=admits_floor_df)
55+
alt.layer(points, bar, data=adjusted_admits_floor_df)
4456
.configure_legend(orient="bottom")
4557
.interactive()
4658
)
4759

4860

4961
def build_census_chart(
50-
*, alt, census_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None
62+
*, alt, census_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None, use_log_scale: bool = False
5163
) -> Chart:
52-
"""Build census chart."""
53-
y_scale = alt.Scale()
54-
if max_y_axis:
55-
y_scale.domain = (0, max_y_axis)
64+
"""
65+
This builds the "Admitted Patients" census chart, projecting total number of patients in the hospital over time.
66+
67+
Args:
68+
alt: Reference to Altair package.
69+
census_floor_df: Pandas data frame containing three columns: "census_hospitalized", "census_icu", and
70+
"census_ventilated".
71+
max_y_axis: Optional maximum value for the Y axis of the chart.
72+
use_log_scale: Set to true to use a logarithmic scale on the Y axis. Default is linear scale.
73+
74+
Returns: The newly created chart.
75+
76+
"""
77+
78+
adjusted_census_floor_df = __adjust_data_for_log_scale(census_floor_df) if use_log_scale else census_floor_df
79+
y_scale = __build_y_scale(alt, max_y_axis, use_log_scale)
5680

5781
x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
5882
y = dict(shorthand="value:Q", title="Census", scale=y_scale)
@@ -79,19 +103,31 @@ def build_census_chart(
79103
.mark_rule(color="black", opacity=0.35, size=2)
80104
)
81105
return (
82-
alt.layer(points, bar, data=census_floor_df)
106+
alt.layer(points, bar, data=adjusted_census_floor_df)
83107
.configure_legend(orient="bottom")
84108
.interactive()
85109
)
86110

87111

88112
def build_sim_sir_w_date_chart(
89-
*, alt, sim_sir_w_date_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None
113+
*, alt, sim_sir_w_date_floor_df: pd.DataFrame, max_y_axis: Optional[int] = None, use_log_scale: bool = False
90114
) -> Chart:
91-
"""Build sim sir w date chart."""
92-
y_scale = alt.Scale()
93-
if max_y_axis is not None:
94-
y_scale.domain = (0, max_y_axis)
115+
"""
116+
This builds the "Susceptible, Infected, and Recovered" chart, projecting the number of those individuals in the
117+
hospital's region over time.
118+
119+
Args:
120+
alt: Reference to the Altair package.
121+
sim_sir_w_date_floor_df: A Pandas data frame with columns named "susceptible", "infected", and "recovered".
122+
max_y_axis: Optional maximum value for the Y axis of the chart.
123+
use_log_scale: Set to true to use a logarithmic scale on the Y axis. Default is linear scale.
124+
125+
Returns: The newly created chart.
126+
127+
"""
128+
129+
adjusted_sim_sir_w_date_floor_df = __adjust_data_for_log_scale(sim_sir_w_date_floor_df) if use_log_scale else sim_sir_w_date_floor_df
130+
y_scale = __build_y_scale(alt, max_y_axis, use_log_scale)
95131

96132
x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
97133
y = dict(shorthand="value:Q", title="Count", scale=y_scale)
@@ -118,7 +154,7 @@ def build_sim_sir_w_date_chart(
118154
.mark_rule(color="black", opacity=0.35, size=2)
119155
)
120156
return (
121-
alt.layer(points, bar, data=sim_sir_w_date_floor_df)
157+
alt.layer(points, bar, data=adjusted_sim_sir_w_date_floor_df)
122158
.configure_legend(orient="bottom")
123159
.interactive()
124160
)
@@ -131,3 +167,37 @@ def build_table(
131167
table_df.date = table_df.date.dt.strftime(DATE_FORMAT)
132168
table_df.rename(labels)
133169
return table_df
170+
171+
172+
def __adjust_data_for_log_scale(dataframe: pd.DataFrame) -> pd.DataFrame:
173+
"""
174+
This will clean and adjust some of the data so that Altair can plot it using a logarithmic scale. Altair does not
175+
allow zero values on the Y axis when plotting with a logarithmic scale, as log(0) is undefined.
176+
177+
Args:
178+
dataframe: The data to plot on the chart.
179+
180+
Returns: A new data frame with the appropriate adjustments for plotting on a log scale.
181+
182+
"""
183+
return dataframe.replace(0, float('nan')) # We use NaN so that the values will not appear at all on the chart.
184+
185+
186+
def __build_y_scale(alt, max_y_axis: Optional[int] = None, use_log_scale: bool = False) -> Scale:
187+
"""
188+
Creates the Y axis of the chart, taking into account some of the configuration parameters set by the user.
189+
190+
Args:
191+
alt: Reference to Altair package.
192+
max_y_axis: The maximum value of the Y axis. This is optional.
193+
use_log_scale: Whether to use a logarithmic scale instead of a linear scale.
194+
195+
Returns: A newly created Scale instance.
196+
197+
"""
198+
scale_type = 'log' if use_log_scale else 'linear'
199+
y_scale = alt.Scale(type=scale_type)
200+
if max_y_axis is not None:
201+
y_scale.domain = (0, max_y_axis)
202+
203+
return y_scale

src/penn_chime/view/st_app.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def main():
3636

3737
st.subheader("New Admissions")
3838
st.markdown("Projected number of **daily** COVID-19 admissions.")
39-
admits_chart = build_admits_chart(alt=alt, admits_floor_df=m.admits_floor_df, max_y_axis=p.max_y_axis)
39+
admits_chart = build_admits_chart(alt=alt, admits_floor_df=m.admits_floor_df, max_y_axis=p.max_y_axis, use_log_scale=p.use_log_scale)
4040
st.altair_chart(admits_chart, use_container_width=True)
4141
display_download_link(
4242
st,
@@ -46,7 +46,7 @@ def main():
4646

4747
st.subheader("Admitted Patients (Census)")
4848
st.markdown("Projected **census** of COVID-19 patients, accounting for arrivals and discharges.")
49-
census_chart = build_census_chart(alt=alt, census_floor_df=m.census_floor_df, max_y_axis=p.max_y_axis)
49+
census_chart = build_census_chart(alt=alt, census_floor_df=m.census_floor_df, max_y_axis=p.max_y_axis, use_log_scale=p.use_log_scale)
5050
st.altair_chart(census_chart, use_container_width=True)
5151
display_download_link(
5252
st,
@@ -56,7 +56,7 @@ def main():
5656

5757
st.subheader("Susceptible, Infected, and Recovered")
5858
st.markdown("The number of susceptible, infected, and recovered individuals in the hospital catchment region at any given moment")
59-
sim_sir_w_date_chart = build_sim_sir_w_date_chart(alt=alt, sim_sir_w_date_floor_df=m.sim_sir_w_date_floor_df)
59+
sim_sir_w_date_chart = build_sim_sir_w_date_chart(alt=alt, sim_sir_w_date_floor_df=m.sim_sir_w_date_floor_df, use_log_scale=p.use_log_scale)
6060
st.altair_chart(sim_sir_w_date_chart, use_container_width=True)
6161
display_download_link(
6262
st,

src/penn_chime/view/st_display.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,10 +174,10 @@ def __init__(self, st_obj, label, value=None, key=None):
174174

175175

176176
def display_sidebar(st, d: Parameters) -> Parameters:
177-
# Initialize variables
178-
# these functions create input elements and bind the values they are set to
179-
# to the variables they are set equal to
180-
# it's kindof like ember or angular if you are familiar with those
177+
"""
178+
Initializes the UI in the sidebar. These function calls create input elements, and bind the values they are set to
179+
to the appropriate variables. It's similar to Ember or Angular, if you are familiar with those frameworks.
180+
"""
181181

182182
st_obj = st.sidebar
183183
# used_widget_key = st.get_last_used_widget_key ( )
@@ -364,7 +364,9 @@ def display_sidebar(st, d: Parameters) -> Parameters:
364364
max_y_axis = max_y_axis_input()
365365

366366
current_date = current_date_input()
367-
#Subscribe implementation
367+
use_log_scale = st.sidebar.checkbox(label="Use logarithmic scale on charts instead of linear scale.", value=d.use_log_scale)
368+
369+
# Subscribe implementation
368370
subscribe(st_obj)
369371

370372
return Parameters(
@@ -389,9 +391,10 @@ def display_sidebar(st, d: Parameters) -> Parameters:
389391
ventilated=Disposition.create(
390392
rate=ventilated_rate,
391393
days=ventilated_days),
394+
use_log_scale=use_log_scale
392395
)
393396

394-
#Read the environment variables and cteate json key object to use with ServiceAccountCredentials
397+
# Read the environment variables and create json key object to use with ServiceAccountCredentials
395398
def readGoogleApiSecrets():
396399
client_secret = {}
397400
os.getenv

tests/penn_chime/view/test_charts.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import altair as alt
22
import pytest
3+
import math
34

45
from penn_chime.view.charts import (
56
build_admits_chart,
@@ -8,7 +9,6 @@
89

910
DISPOSITION_KEYS = ("hospitalized", "icu", "ventilated")
1011

11-
1212
def test_admits_chart(admits_floor_df):
1313
chart = build_admits_chart(alt=alt, admits_floor_df=admits_floor_df)
1414
assert isinstance(chart, (alt.Chart, alt.LayerChart))
@@ -28,3 +28,54 @@ def test_census_chart(census_floor_df):
2828
# test fx call with no params
2929
with pytest.raises(TypeError):
3030
build_census_chart()
31+
32+
def test_admits_chart_log_scale(admits_floor_df):
33+
"""
34+
Verifies that if the log scale is used, then the values on the chart are adjusted appropriately.
35+
36+
Args:
37+
admits_floor_df: Sample admission data.
38+
39+
"""
40+
chart = build_admits_chart(alt=alt, admits_floor_df=admits_floor_df, use_log_scale=True)
41+
42+
# We check a few values to verify that zero was replaced with NaN.
43+
assert chart.data.iloc[1].admits_hospitalized == 2
44+
assert math.isnan(chart.data.iloc[1].admits_icu)
45+
assert math.isnan(chart.data.iloc[1].admits_ventilated)
46+
47+
assert chart.data.iloc[2].admits_hospitalized == 2
48+
assert math.isnan(chart.data.iloc[2].admits_icu)
49+
assert math.isnan(chart.data.iloc[2].admits_ventilated)
50+
51+
assert chart.data.iloc[3].admits_hospitalized == 3
52+
assert math.isnan(chart.data.iloc[3].admits_icu)
53+
assert math.isnan(chart.data.iloc[3].admits_ventilated)
54+
55+
assert chart.data.iloc[4].admits_hospitalized == 3
56+
assert chart.data.iloc[4].admits_icu == 1
57+
assert math.isnan(chart.data.iloc[4].admits_ventilated)
58+
59+
def test_census_chart_log_scale(census_floor_df):
60+
"""
61+
Verifies that if the log scale is used, then the values on the chart are adjusted appropriately.
62+
63+
Args:
64+
census_floor_df: Sample census data.
65+
66+
"""
67+
chart = build_census_chart(alt=alt, census_floor_df=census_floor_df, use_log_scale=True)
68+
69+
# We check a few values to verify that zero was replaced with NaN.
70+
assert math.isnan(chart.data.iloc[0].census_hospitalized)
71+
assert math.isnan(chart.data.iloc[0].census_icu)
72+
assert math.isnan(chart.data.iloc[0].census_ventilated)
73+
74+
assert chart.data.iloc[1].census_hospitalized == 3
75+
assert chart.data.iloc[1].census_icu == 1
76+
assert chart.data.iloc[1].census_ventilated == 1
77+
78+
assert chart.data.iloc[2].census_hospitalized == 6
79+
assert chart.data.iloc[2].census_icu == 2
80+
assert chart.data.iloc[2].census_ventilated == 2
81+

0 commit comments

Comments
 (0)