Skip to content

Commit 26dd9c6

Browse files
authored
Merge branch 'develop' into PhilMiller/model-performance
2 parents 256b517 + fe8f41a commit 26dd9c6

File tree

4 files changed

+122
-118
lines changed

4 files changed

+122
-118
lines changed

src/penn_chime/cli.py

Lines changed: 40 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,11 @@
44
Action,
55
ArgumentParser,
66
)
7-
from datetime import datetime
87

98
from pandas import DataFrame
109

1110
from .constants import CHANGE_DATE
12-
from .parameters import Parameters, Disposition
11+
from .parameters import Parameters, Disposition, ACCEPTED_PARAMETERS
1312
from .models import SimSirModel as Model
1413

1514

@@ -21,8 +20,16 @@ def __call__(self, parser, namespace, values, option_string=None):
2120
parser.parse_args(f.read().split(), namespace)
2221

2322

24-
def cast_date(string):
25-
return datetime.strptime(string, '%Y-%m-%d').date()
23+
def declarative_validator(cast):
24+
"""Validator."""
25+
26+
def validate(string):
27+
"""Validate."""
28+
if string == '' and cast != str:
29+
return None
30+
return cast(string)
31+
32+
return validate
2633

2734

2835
def validator(arg, cast, min_value, max_value, required=True):
@@ -49,31 +56,18 @@ def parse_args():
4956
parser = ArgumentParser(description=f"penn_chime: {CHANGE_DATE}")
5057
parser.add_argument("--file", type=open, action=FromFile)
5158

59+
for name, (params_validator, default, cast, help) in ACCEPTED_PARAMETERS.items():
60+
if cast is None:
61+
continue
62+
63+
parser.add_argument(
64+
"--" + name.replace('_', '-'),
65+
type=declarative_validator(cast),
66+
default=default,
67+
help=help
68+
)
69+
5270
for arg, cast, min_value, max_value, help, required in (
53-
(
54-
"--current-hospitalized",
55-
int,
56-
0,
57-
None,
58-
"Currently hospitalized COVID-19 patients (>= 0)",
59-
True,
60-
),
61-
(
62-
"--date-first-hospitalized",
63-
cast_date,
64-
None,
65-
None,
66-
"Current date",
67-
False,
68-
),
69-
(
70-
"--doubling-time",
71-
float,
72-
0.0,
73-
None,
74-
"Doubling time before social distancing (days)",
75-
True,
76-
),
7771
("--hospitalized-days", int, 0, None, "Average hospital length of stay (in days)", True),
7872
(
7973
"--hospitalized-rate",
@@ -85,25 +79,7 @@ def parse_args():
8579
),
8680
("--icu-days", int, 0, None, "Average days in ICU", True),
8781
("--icu-rate", float, 0.0, 1.0, "ICU rate: 0.0 - 1.0", True),
88-
(
89-
"--market_share",
90-
float,
91-
0.00001,
92-
1.0,
93-
"Hospital market share (0.00001 - 1.0)",
94-
True,
95-
),
96-
("--infectious-days", float, 0.0, None, "Infectious days", True),
97-
("--n-days", int, 0, None, "Number of days to project >= 0", True),
98-
(
99-
"--relative-contact-rate",
100-
float,
101-
0.0,
102-
1.0,
103-
"Social distancing reduction rate: 0.0 - 1.0",
104-
True,
105-
),
106-
("--population", int, 1, None, "Regional population >= 1", True),
82+
10783
("--ventilated-days", int, 0, None, "Average days on ventilator", True),
10884
("--ventilated-rate", float, 0.0, 1.0, "Ventilated Rate: 0.0 - 1.0", True),
10985
):
@@ -119,19 +95,24 @@ def main():
11995
"""Main."""
12096
a = parse_args()
12197

98+
del a.file
99+
100+
hospitalized = Disposition(a.hospitalized_rate, a.hospitalized_days)
101+
icu = Disposition(a.icu_rate, a.icu_days)
102+
ventilated = Disposition(a.ventilated_rate, a.ventilated_days)
103+
104+
del a.hospitalized_days
105+
del a.hospitalized_rate
106+
del a.icu_days
107+
del a.icu_rate
108+
del a.ventilated_days
109+
del a.ventilated_rate
110+
122111
p = Parameters(
123-
current_hospitalized=a.current_hospitalized,
124-
date_first_hospitalized=a.date_first_hospitalized,
125-
doubling_time=a.doubling_time,
126-
infectious_days=a.infectious_days,
127-
market_share=a.market_share,
128-
n_days=a.n_days,
129-
relative_contact_rate=a.relative_contact_rate,
130-
population=a.population,
131-
132-
hospitalized=Disposition(a.hospitalized_rate, a.hospitalized_days),
133-
icu=Disposition(a.icu_rate, a.icu_days),
134-
ventilated=Disposition(a.ventilated_rate, a.ventilated_days),
112+
hospitalized=hospitalized,
113+
icu=icu,
114+
ventilated=ventilated,
115+
**vars(a)
135116
)
136117

137118
m = Model(p)

src/penn_chime/parameters.py

Lines changed: 54 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
"""
66

77
from collections import namedtuple
8-
from datetime import date
8+
from datetime import date, datetime
99
from typing import Optional
1010

1111
from .validators import (
12-
Positive, OptionalStrictlyPositive, StrictlyPositive, Rate, Date, OptionalDate
12+
OptionalValue, Positive, OptionalStrictlyPositive, StrictlyPositive, Rate, Date, OptionalDate, ValDisposition
1313
)
1414

1515
# Parameters for each disposition (hospitalized, icu, ventilated)
@@ -45,62 +45,60 @@ def __init__(self, **kwargs):
4545
self.population = population
4646

4747

48-
class Parameters:
49-
"""Parameters."""
48+
def cast_date(string):
49+
return datetime.strptime(string, '%Y-%m-%d').date()
50+
51+
52+
# Dictionary from parameter names to Tuples containing (validator, default value, cast function, help text)
53+
ACCEPTED_PARAMETERS = {
54+
"current_hospitalized": (Positive, None, int, "Currently hospitalized COVID-19 patients (>= 0)"),
55+
"current_date": (OptionalDate, None, cast_date, "Date on which the forecast should be based"),
56+
"date_first_hospitalized": (OptionalDate, None, cast_date, "Date the first patient was hospitalized"),
57+
"doubling_time": (OptionalStrictlyPositive, None, float, "Doubling time before social distancing (days)"),
58+
"relative_contact_rate": (Rate, None, float, "Social distancing reduction rate: 0.0 - 1.0"),
59+
"mitigation_date": (OptionalDate, None, cast_date, "Date on which social distancing measures too effect"),
60+
"infectious_days": (StrictlyPositive, 14, int, "Infectious days"),
61+
"market_share": (Rate, 1.0, float, "Hospital market share (0.00001 - 1.0)"),
62+
"max_y_axis": (OptionalStrictlyPositive, None, int, None),
63+
"n_days": (StrictlyPositive, 100, int, "Number of days to project >= 0"),
64+
"recovered": (Positive, 0, int, "Number of patients already recovered (not yet implemented)"),
65+
"population": (OptionalStrictlyPositive, None, int, "Regional population >= 1"),
66+
"region": (OptionalValue, None, None, "No help available"),
67+
68+
"hospitalized": (ValDisposition, None, None, None),
69+
"icu": (ValDisposition, None, None, None),
70+
"ventilated": (ValDisposition, None, None, None),
71+
}
5072

51-
def __init__(
52-
self,
53-
*,
54-
current_hospitalized: int,
55-
hospitalized: Disposition,
56-
icu: Disposition,
57-
relative_contact_rate: float,
58-
mitigation_date: Optional[date] = None,
59-
ventilated: Disposition,
60-
current_date: Optional[date] = None,
61-
date_first_hospitalized: Optional[date] = None,
62-
doubling_time: Optional[float] = None,
63-
infectious_days: int = 14,
64-
market_share: float = 1.0,
65-
max_y_axis: Optional[int] = None,
66-
n_days: int = 100,
67-
population: Optional[int] = None,
68-
recovered: int = 0,
69-
region: Optional[Regions] = None,
70-
):
71-
self.current_hospitalized = Positive(value=current_hospitalized)
72-
Rate(value=hospitalized.rate), Rate(value=icu.rate), Rate(value=ventilated.rate)
73-
StrictlyPositive(value=hospitalized.days), StrictlyPositive(value=icu.days),
74-
StrictlyPositive(value=ventilated.days)
75-
76-
self.hospitalized = hospitalized
77-
self.icu = icu
78-
self.ventilated = ventilated
79-
80-
if region is not None and population is None:
81-
self.region = region
82-
self.population = StrictlyPositive(value=region.population)
83-
elif population is not None:
84-
self.region = None
85-
self.population = StrictlyPositive(value=population)
86-
else:
87-
raise AssertionError('population or regions must be provided.')
8873

89-
if current_date is None:
90-
current_date = date.today()
91-
self.current_date = Date(value=current_date)
74+
class Parameters:
75+
"""Parameters."""
9276

93-
self.date_first_hospitalized = OptionalDate(value=date_first_hospitalized)
94-
self.doubling_time = OptionalStrictlyPositive(value=doubling_time)
77+
def __init__(self, **kwargs):
78+
passed_and_default_parameters = {}
79+
for key, value in kwargs.items():
80+
if key not in ACCEPTED_PARAMETERS:
81+
raise ValueError(f"Unexpected parameter {key}")
82+
passed_and_default_parameters[key] = value
83+
84+
for key, (validator, default_value, cast, help) in ACCEPTED_PARAMETERS.items():
85+
if key not in passed_and_default_parameters:
86+
passed_and_default_parameters[key] = default_value
87+
88+
for key, value in passed_and_default_parameters.items():
89+
validator = ACCEPTED_PARAMETERS[key][0]
90+
try:
91+
validator(value=value)
92+
except (TypeError, ValueError) as ve:
93+
raise ValueError(f"For parameter '{key}', with value '{value}', validation returned error \"{ve}\"")
94+
setattr(self, key, value)
9595

96-
self.relative_contact_rate = Rate(value=relative_contact_rate)
97-
self.mitigation_date = OptionalDate(value=mitigation_date)
96+
if self.region is None and self.population is None:
97+
raise AssertionError('population or regions must be provided.')
9898

99-
self.infectious_days = StrictlyPositive(value=infectious_days)
100-
self.market_share = Rate(value=market_share)
101-
self.max_y_axis = OptionalStrictlyPositive(value=max_y_axis)
102-
self.n_days = StrictlyPositive(value=n_days)
103-
self.recovered = Positive(value=recovered)
99+
if self.current_date is None:
100+
self.current_date = date.today()
101+
Date(value=self.current_date)
104102

105103
self.labels = {
106104
"hospitalized": "Hospitalized",
@@ -114,7 +112,7 @@ def __init__(
114112
}
115113

116114
self.dispositions = {
117-
"hospitalized": hospitalized,
118-
"icu": icu,
119-
"ventilated": ventilated,
115+
"hospitalized": self.hospitalized,
116+
"icu": self.icu,
117+
"ventilated": self.ventilated,
120118
}

src/penn_chime/validators/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""the callable validator design pattern"""
22

3-
from .validators import Bounded, OptionalBounded, Rate, Date, OptionalDate
4-
5-
EPSILON = 1.e-7
3+
from .validators import EPSILON, OptionalValue, Bounded, OptionalBounded, Rate, Date, OptionalDate, ValDisposition
64

5+
OptionalValue = OptionalValue()
76
OptionalStrictlyPositive = OptionalBounded(lower_bound=EPSILON)
87
StrictlyPositive = Bounded(lower_bound=EPSILON)
98
Positive = Bounded(lower_bound=-EPSILON)
109
Rate = Rate() # type: ignore
1110
Date = Date() # type: ignore
1211
OptionalDate = OptionalDate() # type: ignore
12+
ValDisposition = ValDisposition()
1313
# # rolling a custom validator for doubling time in case DS wants to add upper bound
1414
# DoublingTime = OptionalBounded(lower_bound=0-EPSILON, upper_bound=None)

src/penn_chime/validators/validators.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55

66
from .base import Validator
77

8+
EPSILON = 1.e-7
9+
10+
class OptionalValue(Validator):
11+
"""Any value at all"""
12+
def __init__(self) -> None:
13+
pass
14+
15+
def validate(self, value):
16+
pass
817

918
class Bounded(Validator):
1019
"""A bounded number."""
@@ -23,6 +32,8 @@ def __init__(
2332

2433
def validate(self, value):
2534
"""This method implicitly validates isinstance(value, (float, int)) because it will throw a TypeError on comparison"""
35+
if value is None:
36+
raise ValueError(f"This parameter must be set")
2637
if (self.upper_bound is not None and value > self.upper_bound) \
2738
or (self.lower_bound is not None and value < self.lower_bound):
2839
raise ValueError(f"{value} needs to be {self.message[(self.lower_bound, self.upper_bound)]}.")
@@ -47,6 +58,8 @@ def __init__(self) -> None:
4758
pass
4859

4960
def validate(self, value):
61+
if value is None:
62+
raise ValueError(f"This parameter must be set")
5063
if 0 > value or value > 1:
5164
raise ValueError(f"{value} needs to be a rate (i.e. in [0,1]).")
5265

@@ -56,6 +69,8 @@ def __init__(self) -> None:
5669
pass
5770

5871
def validate(self, value):
72+
if value is None:
73+
raise ValueError(f"This parameter must be set")
5974
if not isinstance(value, (date, datetime)):
6075
raise (ValueError(f"{value} must be a date or datetime object."))
6176

@@ -67,3 +82,13 @@ def validate(self, value):
6782
if value is None:
6883
return None
6984
super().validate(value)
85+
86+
class ValDisposition(Validator):
87+
def __init__(self) -> None:
88+
pass
89+
90+
def validate(self, value):
91+
if value is None:
92+
raise ValueError(f"This parameter must be set")
93+
Bounded(lower_bound=EPSILON)(value=value.days)
94+
Rate()(value=value.rate)

0 commit comments

Comments
 (0)