Skip to content

Commit bbaf1b9

Browse files
authored
add daily_to_weekly function (#478)
1 parent a63c4ea commit bbaf1b9

2 files changed

Lines changed: 175 additions & 0 deletions

File tree

pyrenew/convolve.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,3 +217,77 @@ 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+
)

test/test_daily_to_weekly.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# numpydoc ignore=GL08
2+
3+
import jax.numpy as jnp
4+
import pytest
5+
6+
from pyrenew.convolve import daily_to_mmwr_epiweekly, daily_to_weekly
7+
8+
9+
def test_daily_to_weekly_no_offset():
10+
"""
11+
Tests that the function correctly aggregates
12+
daily values into weekly totals when there
13+
is no offset both input and output start dow on Monday.
14+
"""
15+
daily_values = jnp.arange(1, 15)
16+
result = daily_to_weekly(daily_values)
17+
expected = jnp.array([28, 77])
18+
assert jnp.array_equal(result, expected)
19+
20+
21+
def test_daily_to_weekly_with_input_data_offset():
22+
"""
23+
Tests that the function correctly aggregates
24+
daily values into weekly totals with dow
25+
offset in the input data.
26+
"""
27+
daily_values = jnp.arange(1, 15)
28+
result = daily_to_weekly(daily_values, input_data_first_dow=2)
29+
expected = jnp.array([63])
30+
assert jnp.array_equal(result, expected)
31+
32+
33+
def test_daily_to_weekly_with_different_week_start():
34+
"""
35+
Tests aggregation when the desired week start
36+
differs from the input data start.
37+
"""
38+
daily_values = jnp.arange(1, 15)
39+
result = daily_to_weekly(
40+
daily_values, input_data_first_dow=2, week_start_dow=5
41+
)
42+
expected = jnp.array([49])
43+
assert jnp.array_equal(result, expected)
44+
45+
46+
def test_daily_to_weekly_incomplete_week():
47+
"""
48+
Tests that the function raises a
49+
ValueError when there are
50+
insufficient daily values to
51+
form a complete week.
52+
"""
53+
daily_values = jnp.arange(1, 5)
54+
with pytest.raises(
55+
ValueError, match="No complete weekly values available"
56+
):
57+
daily_to_weekly(daily_values, input_data_first_dow=0)
58+
59+
60+
def test_daily_to_weekly_missing_daily_values():
61+
"""
62+
Tests that the function correctly
63+
aggregates the available daily values
64+
into weekly values when there are
65+
fewer daily values than required for
66+
complete weekly totals in the final week.
67+
"""
68+
daily_values = jnp.arange(1, 10)
69+
result = daily_to_weekly(daily_values, input_data_first_dow=0)
70+
expected = jnp.array([28])
71+
assert jnp.array_equal(result, expected)
72+
73+
74+
def test_daily_to_weekly_invalid_offset():
75+
"""
76+
Tests that the function raises a
77+
ValueError when the offset is
78+
outside the valid range (0-6).
79+
"""
80+
daily_values = jnp.arange(1, 15)
81+
with pytest.raises(
82+
ValueError,
83+
match="First day of the week for input timeseries must be between 0 and 6.",
84+
):
85+
daily_to_weekly(daily_values, input_data_first_dow=-1)
86+
87+
with pytest.raises(
88+
ValueError,
89+
match="Week start date for output aggregated values must be between 0 and 6.",
90+
):
91+
daily_to_weekly(daily_values, week_start_dow=7)
92+
93+
94+
def test_daily_to_mmwr_epiweekly():
95+
"""
96+
Tests aggregation for MMWR epidemiological week.
97+
"""
98+
daily_values = jnp.arange(1, 15)
99+
result = daily_to_mmwr_epiweekly(daily_values)
100+
expected = jnp.array([70])
101+
assert jnp.array_equal(result, expected)

0 commit comments

Comments
 (0)