Skip to content

Commit d275f21

Browse files
authored
Add weekly_to_daily functionality, move time handling functions to their own module (#483)
1 parent 6627740 commit d275f21

File tree

4 files changed

+517
-175
lines changed

4 files changed

+517
-175
lines changed

pyrenew/convolve.py

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -217,77 +217,3 @@ def compute_delay_ascertained_incidence(
217217
mode="valid",
218218
)
219219
return delay_obs_incidence
220-
221-
222-
def daily_to_weekly(
223-
daily_values: ArrayLike,
224-
input_data_first_dow: int = 0,
225-
week_start_dow: int = 0,
226-
) -> ArrayLike:
227-
"""
228-
Aggregate daily values (e.g.
229-
incident hospital admissions) into weekly total values.
230-
231-
Parameters
232-
----------
233-
daily_values : ArrayLike
234-
Daily timeseries values (e.g. incident infections or incident ed visits).
235-
input_data_first_dow : int
236-
First day of the week in the input timeseries `daily_values`.
237-
An integer between 0 and 6, inclusive (0 for Monday, 6 for Sunday).
238-
If `input_data_first_dow` does not match `week_start_dow`, the incomplete first
239-
week is ignored and weekly values starting
240-
from the second week are returned. Defaults to 0.
241-
week_start_dow : int
242-
The desired starting day of the week for the output weekly aggregation.
243-
An integer between 0 and 6, inclusive. Defaults to 0 (Monday).
244-
245-
Returns
246-
-------
247-
ArrayLike
248-
Data converted to weekly values starting
249-
with the first full week available.
250-
"""
251-
if input_data_first_dow < 0 or input_data_first_dow > 6:
252-
raise ValueError(
253-
"First day of the week for input timeseries must be between 0 and 6."
254-
)
255-
256-
if week_start_dow < 0 or week_start_dow > 6:
257-
raise ValueError(
258-
"Week start date for output aggregated values must be between 0 and 6."
259-
)
260-
261-
offset = (week_start_dow - input_data_first_dow) % 7
262-
daily_values = daily_values[offset:]
263-
264-
if len(daily_values) < 7:
265-
raise ValueError("No complete weekly values available")
266-
267-
weekly_values = jnp.convolve(daily_values, jnp.ones(7), mode="valid")[::7]
268-
269-
return weekly_values
270-
271-
272-
def daily_to_mmwr_epiweekly(
273-
daily_values: ArrayLike, input_data_first_dow: int = 0
274-
) -> ArrayLike:
275-
"""
276-
Convert daily values to MMWR epidemiological weeks.
277-
278-
Parameters
279-
----------
280-
daily_values : ArrayLike
281-
Daily timeseries values.
282-
input_data_first_dow : int
283-
First day of the week in the input timeseries `daily_values`.
284-
Defaults to 0 (Monday).
285-
286-
Returns
287-
-------
288-
ArrayLike
289-
Data converted to epiweekly values.
290-
"""
291-
return daily_to_weekly(
292-
daily_values, input_data_first_dow, week_start_dow=6
293-
)

pyrenew/time.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
"""
2+
Helper functions for handling timeseries in Pyrenew
3+
4+
Days of the week in pyrenew are 0-indexed and follow
5+
ISO standards, so 0 is Monday at 6 is Sunday.
6+
"""
7+
8+
import jax.numpy as jnp
9+
from jax.typing import ArrayLike
10+
11+
12+
def validate_dow(day_of_week: int, variable_name: str) -> None:
13+
"""
14+
Confirm that an integer is a valid Pyrenew day of the week
15+
index, with informative error messages on failure.
16+
17+
Parameters
18+
----------
19+
day_of_week: int
20+
Integer to validate.
21+
22+
variable_name: str
23+
Name of the variable being validated, to increase
24+
the informativeness of the error message.
25+
26+
Returns
27+
-------
28+
None
29+
If validation passes.
30+
31+
Raises
32+
------
33+
ValueError
34+
If validation fails.
35+
"""
36+
if not isinstance(day_of_week, int):
37+
raise ValueError(
38+
"Day-of-week indices must be integers "
39+
"between 0 and 6, inclusive. "
40+
f"Got {day_of_week} for {variable_name}, "
41+
"which is a "
42+
f"{type(day_of_week)}"
43+
)
44+
if day_of_week < 0 or day_of_week > 6:
45+
raise ValueError(
46+
"Day-of-week indices must be a integers "
47+
"between 0 and 6, inclusive. "
48+
f"Got {day_of_week} for {variable_name}."
49+
)
50+
return None
51+
52+
53+
def daily_to_weekly(
54+
daily_values: ArrayLike,
55+
input_data_first_dow: int = 0,
56+
week_start_dow: int = 0,
57+
) -> ArrayLike:
58+
"""
59+
Aggregate daily values (e.g.
60+
incident hospital admissions)
61+
to weekly total values.
62+
63+
Parameters
64+
----------
65+
daily_values : ArrayLike
66+
Daily timeseries values (e.g. incident infections or
67+
incident ed visits).
68+
input_data_first_dow : int
69+
First day of the week in the input timeseries `daily_values`.
70+
An integer between 0 and 6, inclusive (0 for Monday, 1 for Tuesday,
71+
..., 6 for Sunday).
72+
If `input_data_first_dow` does not match `week_start_dow`, the
73+
incomplete first week is ignored and weekly values starting
74+
from the second week are returned. Defaults to 0.
75+
week_start_dow : int
76+
Day of the week on which weeks are considered to
77+
start in the output timeseries of weekly values
78+
(e.g. ISO weeks start on Mondays and end on Sundays;
79+
MMWR epiweeks start on Sundays and end on Saturdays).
80+
An integer between 0 and 6, inclusive (0 for Monday,
81+
1 for Tuesday, ..., 6 for Sunday).
82+
Default 0 (i.e. ISO weeks, starting on Mondays).
83+
84+
Returns
85+
-------
86+
ArrayLike
87+
Data converted to weekly values starting
88+
with the first full week available.
89+
90+
Raises
91+
------
92+
ValueError
93+
If the specified days of the week fail validation.
94+
95+
Notes
96+
-----
97+
This is _not_ a simple inverse of :func:`weekly_to_daily`.
98+
This function aggregates (by summing) daily values to
99+
create a timeseries of weekly total values.
100+
:func:`weekly_to_daily` broadcasts a _single shared value_
101+
for a given week as the (repeated) daily value for each day
102+
of that week.
103+
"""
104+
105+
validate_dow(input_data_first_dow, "input_data_first_dow")
106+
validate_dow(week_start_dow, "week_start_dow")
107+
108+
offset = (week_start_dow - input_data_first_dow) % 7
109+
daily_values = daily_values[offset:]
110+
111+
if daily_values.shape[0] < 7:
112+
raise ValueError("No complete weekly values available")
113+
114+
n_weeks = daily_values.shape[0] // 7
115+
trimmed = daily_values[: n_weeks * 7]
116+
weekly_values = trimmed.reshape(n_weeks, 7, *daily_values.shape[1:]).sum(
117+
axis=1
118+
)
119+
120+
return weekly_values
121+
122+
123+
def daily_to_mmwr_epiweekly(
124+
daily_values: ArrayLike,
125+
input_data_first_dow: int = 6,
126+
) -> ArrayLike:
127+
"""
128+
Aggregate daily values to weekly values
129+
using :func:`daily_to_weekly` with
130+
MMWR epidemiological weeks (begin on Sundays,
131+
end on Saturdays).
132+
133+
Parameters
134+
----------
135+
daily_values : ArrayLike
136+
Daily timeseries values.
137+
input_data_first_dow : int
138+
First day of the week in the input timeseries `daily_values`.
139+
An integer between 0 and 6, inclusive (0 for Monday, 1 for
140+
Tuesday, ..., 6 for Sunday).
141+
If `input_data_first_dow` is _not_ the MMWR epiweek start day
142+
(6, Sunday), the incomplete first week is ignored and
143+
weekly values starting from the second week are returned.
144+
Defaults to 6 (Sunday).
145+
146+
Returns
147+
-------
148+
ArrayLike
149+
Data converted to epiweekly values.
150+
"""
151+
return daily_to_weekly(
152+
daily_values, input_data_first_dow, week_start_dow=6
153+
)
154+
155+
156+
def weekly_to_daily(
157+
weekly_values: ArrayLike,
158+
week_start_dow: int = 0,
159+
output_data_first_dow: int = None,
160+
) -> ArrayLike:
161+
"""
162+
Broadcast a weekly timeseries to a daily
163+
timeseries. The value for the week will be used
164+
as the value each day in that week, via
165+
:func:`jnp.repeat`.
166+
167+
Parameters
168+
----------
169+
weekly_values: ArrayLike
170+
Timeseries of weekly values, where
171+
(discrete) time is the first dimension of
172+
the array (following Pyrenew convention).
173+
174+
week_start_dow: int
175+
Day of the week on which weeks are considered to
176+
start in the input ``weekly_values`` timeseries
177+
(e.g. ISO weeks start on Mondays and end on Sundays;
178+
MMWR epiweeks start on Sundays and end on Saturdays).
179+
An integer between 0 and 6, inclusive (0 for Monday,
180+
1 for Tuesday, ..., 6 for Sunday).
181+
Default 0 (i.e. ISO weeks, starting on Mondays).
182+
183+
output_data_first_dow: int
184+
Day of the week on which to start the output timeseries.
185+
An integer between 0 and 6, inclusive (0 for Monday,
186+
1 for Tuesday, ..., 6 for Sunday). Defaults to the week
187+
start date as specified by ``week_start_dow``.
188+
If ``output_data_first_dow`` is _not_ equal to ``week_start_dow``,
189+
the first weekly value will be partial (i.e. represented by
190+
between 1 and 6 entries in the output timeseries) and
191+
all subsequent weeks will be complete (represented by 7
192+
values each).
193+
194+
Returns
195+
-------
196+
ArrayLike
197+
The daily timeseries.
198+
199+
Raises
200+
------
201+
ValueError
202+
If the specified days of the week fail validation.
203+
204+
Notes
205+
-----
206+
This is _not_ a simple inverse of :func:`daily_to_weekly`.
207+
:func:`daily_to_weekly` aggregates (by summing) daily values to
208+
create a timeseries of weekly total values.
209+
This function broadcasts a _single shared value_
210+
for a given week as the (repeated) daily value for each day
211+
of that week.
212+
"""
213+
214+
validate_dow(week_start_dow, "week_start_dow")
215+
if output_data_first_dow is None:
216+
output_data_first_dow = week_start_dow
217+
validate_dow(output_data_first_dow, "output_data_first_dow")
218+
219+
offset = (output_data_first_dow - week_start_dow) % 7
220+
return jnp.repeat(
221+
weekly_values,
222+
repeats=7,
223+
axis=0,
224+
)[offset:]
225+
226+
227+
def mmwr_epiweekly_to_daily(
228+
weekly_values: ArrayLike,
229+
output_data_first_dow: int = 6,
230+
) -> ArrayLike:
231+
"""
232+
Convert an MMWR epiweekly timeseries to a daily
233+
timeseries using :func:`weekly_to_daily`.
234+
235+
Parameters
236+
----------
237+
weekly_values: ArrayLike
238+
Timeseries of weekly values, where
239+
(discrete) time is the first dimension of
240+
the array (following Pyrenew convention).
241+
242+
output_data_first_dow: int
243+
Day of the week on which to start the output timeseries.
244+
An integer between 0 and 6, inclusive (0 for Monday,
245+
1 for Tuesday, ..., 6 for Sunday). Defaults to the MMWR
246+
epiweek start day (6, Sunday).
247+
If ``output_data_first_dow`` is _not_ equal to 6 (Sunday,
248+
the start of an MMWR epiweek), the first weekly value will
249+
be partial (i.e. represented by between 1 and 6 entries
250+
in the output timeseries) and all subsequent weeks will be
251+
complete (represented by 7 values each).
252+
253+
Returns
254+
-------
255+
ArrayLike
256+
The daily timeseries.
257+
"""
258+
return weekly_to_daily(
259+
weekly_values,
260+
output_data_first_dow=output_data_first_dow,
261+
week_start_dow=6,
262+
)

0 commit comments

Comments
 (0)