Skip to content

Commit 9f5ee8c

Browse files
committed
Require >=1 of before,after; ensure time_step receives integer
- Raise error, not warning, if both `before` and `after` are missing - Substantially reword docs for `before, after` - Actually deliver an integer to `time_step` as currently advertised - Slightly loosen and rearrange validation of `before`, `after` to ease conversion to integer. Looseness is from allowing things that can be cast to appropriate integers, rather than requiring integer-like numerics.
1 parent 5cd8ea9 commit 9f5ee8c

File tree

4 files changed

+109
-115
lines changed

4 files changed

+109
-115
lines changed

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Imports:
3737
tidyr,
3838
tidyselect,
3939
tsibble,
40-
utils
40+
utils,
41+
vctrs
4142
Suggests:
4243
covidcast,
4344
epidatr,
@@ -46,7 +47,6 @@ Suggests:
4647
outbreaks,
4748
rmarkdown,
4849
testthat (>= 3.0.0),
49-
vctrs,
5050
waldo (>= 0.3.1),
5151
withr
5252
VignetteBuilder:

R/slide.R

Lines changed: 59 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -15,43 +15,35 @@
1515
#' and any number of named arguments (which will be taken from `...`). If a
1616
#' formula, `f` can operate directly on columns accessed via `.x$var`, as
1717
#' in `~ mean(.x$var)` to compute a mean of a column var over a sliding
18-
#' window of n time steps. As well, `.y` may be used in the formula to refer
18+
#' window. As well, `.y` may be used in the formula to refer
1919
#' to the groupings that would be described by `g` if `f` was a function.
2020
#' @param ... Additional arguments to pass to the function or formula specified
2121
#' via `f`. Alternatively, if `f` is missing, then the current argument is
2222
#' interpreted as an expression for tidy evaluation. See details.
23-
#' @param before A nonnegative integer specifying the number of time steps
24-
#' before the `ref_time_value` to use in the rolling window.
25-
#' This must be a vector of length 1.
26-
#' Set to 0 for a left-aligned/leading sliding window, meaning that no
27-
#' `time_value` after the slide will be used for the sliding calculation.
28-
#' It is mandatory to specify a `before` value, unless `after` is specified
29-
#' as a non-zero value. In this case, `before` will be assumed to be 0, as it
30-
#' assumes the user wants to do a left-aligned/leading sliding window.
31-
#' For example, if `before = 3`, and one time step is one day, then to produce
32-
#' a value on January 7, we apply the given function or formula to data on
33-
#' January 4 and later (with the latest date dependent on `after`).
34-
#' A warning will be produced if `before` is not specified, but `after` is
35-
#' also not specified or 0. Should this happen, `before` will be set to 0.
36-
#' @param after A nonnegative integer specifying the number of time steps
37-
#' after the `ref_time_value` to use in the rolling window. This must be a
38-
#' vector of length 1. The default value for this is 0. Set to 0 for a
39-
#' right-aligned/trailing sliding window, meaning that no
40-
#' `time_value` before the slide will be used for the sliding calculation.
41-
#' To specify this to be centrally aligned, set `before` and `after` to be
42-
#' the same.
43-
#' For example, if `after = 3`, and one time step is one day, then to produce
44-
#' a value on January 7, we apply the given function or formula to data on
45-
#' January 10 and earlier (with the earliest date dependent on `before`).
46-
#' A warning will be produced if `after` is not specified, but `before` is
47-
#' also not specified or 0. Should this happen, `after` will be set to 0.
23+
#' @param before,after How far `before` and `after` each `ref_time_value` should
24+
#' the sliding window extend? At least one of these two arguments must be
25+
#' provided; the other's default will be 0. Any value provided for either
26+
#' argument must be a single, non-`NA`, non-negative,
27+
#' [vctrs:vec_cast][integer-compatible] number of time steps. Endpoints of the
28+
#' window are inclusive. Common settings:
29+
#' * For trailing/right-aligned windows from `ref_time_value - time_step(k)`
30+
#' to `ref_time_value`: either pass `before=k` by itself, or pass `before=k,
31+
#' after=0`.
32+
#' * For center-aligned windows from `ref_time_value - time_step(k)` to
33+
#' `ref_time_value + time_step(k)`: pass `before=k, after=k`.
34+
#' * For leading/left-aligned windows from `ref_time_value` to `ref_time_value
35+
#' + time_step(k)`: either pass pass `after=k` by itself, or pass `before=0,
36+
#' after=k`.
37+
#' See "Details:" about the definition of a time step, (non)treatment of
38+
#' missing rows within the window, and avoiding warnings about
39+
#' `before`&`after` settings for a certain uncommon use case.
4840
#' @param ref_time_values Time values for sliding computations, meaning, each
4941
#' element of this vector serves as the reference time point for one sliding
5042
#' window. If missing, then this will be set to all unique time values in the
5143
#' underlying data table, by default.
5244
#' @param time_step Optional function used to define the meaning of one time
5345
#' step, which if specified, overrides the default choice based on the
54-
#' `time_value` column. This function must take a positive integer and return
46+
#' `time_value` column. This function must take a non-negative integer and return
5547
#' an object of class `lubridate::period`. For example, we can use `time_step
5648
#' = lubridate::hours` in order to set the time step to be one hour (this
5749
#' would only be meaningful if `time_value` is of class `POSIXct`).
@@ -91,9 +83,18 @@
9183
#' `before = (n-1)/2` and `after = (n-1)/2` when the number of `time_value`s
9284
#' in a sliding window is odd and `before = n/2-1` and `after = n/2` when
9385
#' `n` is even.
94-
#'
95-
#' If `f` is missing, then an expression for tidy evaluation can be specified,
96-
#' for example, as in:
86+
#'
87+
#' Sometimes, we want to experiment with various trailing or leading window
88+
#' widths and compare the slide outputs. In the (uncommon) case where
89+
#' zero-width windows are considered, manually pass both the `before` and
90+
#' `after` arguments in order to prevent potential warnings. (E.g., `before=k`
91+
#' with `k=0` and `after` missing may produce a warning. To avoid warnings,
92+
#' use `before=k, after=0` instead; otherwise, it looks too much like a
93+
#' leading window was intended, but the `after` argument was forgotten or
94+
#' misspelled.)
95+
#'
96+
#' If `f` is missing, then an expression for tidy evaluation can be specified,
97+
#' for example, as in:
9798
#' ```
9899
#' epi_slide(x, cases_7dav = mean(cases), before = 6)
99100
#' ```
@@ -121,7 +122,7 @@
121122
#' # slide a 7-day leading average
122123
#' jhu_csse_daily_subset %>%
123124
#' group_by(geo_value) %>%
124-
#' epi_slide(cases_7dav = mean(cases), before = 0, after = 6) %>%
125+
#' epi_slide(cases_7dav = mean(cases), after = 6) %>%
125126
#' # rmv a nonessential var. to ensure new col is printed
126127
#' dplyr::select(-death_rate_7d_av)
127128
#'
@@ -166,51 +167,43 @@ epi_slide = function(x, f, ..., before, after, ref_time_values,
166167
unique(x$time_value)]
167168
}
168169

169-
# We must ensure that both before and after are of length 1
170-
if ((!missing(after) && length(after) != 1L) ||
171-
(!missing(before) && length(before) != 1L)) {
172-
Abort("`before` and `after` must be vectors of length 1.")
170+
# Validate and pre-process `before`, `after`:
171+
if (!missing(before)) {
172+
before <- vctrs::vec_cast(before, integer())
173+
if (length(before) != 1L || is.na(before) || before < 0L) {
174+
Abort("`before` must be length-1, non-NA, non-negative")
175+
}
173176
}
174-
175-
# Features that are warned due to likely being mistakes:
176-
# `before` missing and `after` set to 0, and vice versa
177-
# Both `before` and `after` missing
178-
warn_before_after <- function(main_msg,set_msg) {
179-
Warn(paste(main_msg,"Did you mean to set `before` or `after`?",set_msg))
177+
if (!missing(after)) {
178+
after <- vctrs::vec_cast(after, integer())
179+
if (length(after) != 1L || is.na(after) || after < 0L) {
180+
Abort("`after` must be length-1, non-NA, non-negative")
181+
}
180182
}
181-
182183
if (missing(before)) {
183184
if (missing(after)) {
184-
warn_before_after("`before` and `after` missing.",
185-
"`before` and `after` set to 0.")
186-
after = 0
187-
} else if (after == 0) {
188-
warn_before_after("`before` missing and `after` set to 0.",
189-
"`before` set to 0.")
185+
Abort("Either or both of `before`, `after` must be provided.")
186+
} else if (after == 0L) {
187+
Warn("`before` missing, `after==0`; maybe this was intended to be some
188+
non-zero-width trailing window, but since `before` appears to be
189+
missing, it's interpreted as a zero-width window (`before=0,
190+
after=0`).")
190191
}
191-
before = 0
192+
before <- 0L
192193
} else if (missing(after)) {
193-
if (before == 0) {
194-
warn_before_after("`before` set to 0 and `after` missing.",
195-
"`after` set to 0.")
194+
if (before == 0L) {
195+
Warn("`before==0`, `after` missing; maybe this was intended to be some
196+
non-zero-width leading window, but since `after` appears to be
197+
missing, it's interpreted as a zero-width window (`before=0,
198+
after=0`).")
196199
}
197-
after = 0
198-
}
199-
if (!(is.numeric(before) && is.numeric(after))||
200-
floor(before) < ceiling(before) ||
201-
floor(after) < ceiling(after)) {
202-
Abort("`before` and `after` must be integers.")
203-
}
204-
205-
# Otherwise set up alignment based on passed before value
206-
if (before < 0 || after < 0) {
207-
Abort("`before` and `after` must be at least 0.")
200+
after <- 0L
208201
}
209202

210-
# If a custom time step is specified, then redefine units
203+
# If a custom time step is specified, then redefine units
211204
if (!missing(time_step)) {
212-
before = time_step(before)
213-
after = time_step(after)
205+
before <- time_step(before)
206+
after <- time_step(after)
214207
}
215208

216209
# Now set up starts and stops for sliding/hopping

man/epi_slide.Rd

Lines changed: 29 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-epi_slide.R

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,41 +14,42 @@ f = function(x, ...) dplyr::tibble(value=mean(x$value), count=length(x$value))
1414
## --- These cases generate errors (or not): ---
1515
test_that("`before` and `after` are both vectors of length 1", {
1616
expect_error(epi_slide(grouped, f, before = c(0,1), after = 0, ref_time_values = d+3),
17-
"`before` and `after` must be vectors of length 1.")
17+
"`before`.*length-1")
1818
expect_error(epi_slide(grouped, f, before = 1, after = c(0,1), ref_time_values = d+3),
19-
"`before` and `after` must be vectors of length 1.")
19+
"`after`.*length-1")
2020
})
2121

22-
test_that("Test warnings for discouraged features", {
23-
expect_warning(epi_slide(grouped, f, ref_time_values = d+1),
24-
"`before` and `after` missing. Did you mean to set `before` or\n`after`? `before` and `after` set to 0.")
22+
test_that("Test errors/warnings for discouraged features", {
23+
expect_error(epi_slide(grouped, f, ref_time_values = d+1),
24+
"Either or both of `before`, `after` must be provided.")
2525
expect_warning(epi_slide(grouped, f, before = 0L, ref_time_values = d+1),
26-
"`before` set to 0 and `after` missing. Did you mean to set `before`\nor `after`? `before` set to 0.")
26+
"`before==0`, `after` missing")
2727
expect_warning(epi_slide(grouped, f, after = 0L, ref_time_values = d+1),
28-
"`before` missing and `after` set to 0. Did you mean to set `before`\nor `after`? `after` set to 0.")
28+
"`before` missing, `after==0`")
29+
# Below cases should raise no errors/warnings:
2930
expect_warning(epi_slide(grouped, f, before = 1L, ref_time_values = d+2),NA)
3031
expect_warning(epi_slide(grouped, f, after = 1L, ref_time_values = d+2),NA)
3132
expect_warning(epi_slide(grouped, f, before = 0L, after = 0L, ref_time_values = d+2),NA)
3233
})
3334

34-
test_that("Both `before` and `after` must be nonnegative integers",{
35+
test_that("Both `before` and `after` must be non-NA, non-negative, integer-compatible",{
3536
expect_error(epi_slide(grouped, f, before = -1L, ref_time_values = d+2L),
36-
"`before` and `after` must be at least 0.")
37+
"`before`.*non-negative")
3738
expect_error(epi_slide(grouped, f, before = 2L, after = -1L, ref_time_values = d+2L),
38-
"`before` and `after` must be at least 0.")
39+
"`after`.*non-negative")
3940
expect_error(epi_slide(grouped, f, before = "a", ref_time_values = d+2L),
40-
"`before` and `after` must be integers.")
41+
regexp="before", class="vctrs_error_incompatible_type")
4142
expect_error(epi_slide(grouped, f, before = 1L, after = "a", ref_time_values = d+2L),
42-
"`before` and `after` must be integers.")
43+
regexp="after", class="vctrs_error_incompatible_type")
4344
expect_error(epi_slide(grouped, f, before = 0.5, ref_time_values = d+2L),
44-
"`before` and `after` must be integers.")
45+
regexp="before", class="vctrs_error_incompatible_type")
4546
expect_error(epi_slide(grouped, f, before = 1L, after = 0.5, ref_time_values = d+2L),
46-
"`before` and `after` must be integers.")
47+
regexp="after", class="vctrs_error_incompatible_type")
4748
expect_error(epi_slide(grouped, f, before = NA, after = 1L, ref_time_values = d+2L),
48-
"`before` and `after` must be integers.")
49+
"`before`.*non-NA")
4950
expect_error(epi_slide(grouped, f, before = 1L, after = NA, ref_time_values = d+2L),
50-
"`before` and `after` must be integers.")
51-
# The before and after values can be numerics that are integerish
51+
"`after`.*non-NA")
52+
# Non-integer-class but integer-compatible values are allowed:
5253
expect_error(epi_slide(grouped, f, before = 1, after = 1, ref_time_values = d+2L),NA)
5354
})
5455

@@ -82,4 +83,4 @@ test_that("these doesn't produce an error; the error appears only if the ref tim
8283
expect_identical(epi_slide(grouped, f, before=2L, ref_time_values=d+3) %>%
8384
dplyr::select("geo_value","slide_value_value"),
8485
dplyr::tibble(geo_value = c("ak", "al"), slide_value_value = c(2, -2))) # not out of range for either group
85-
})
86+
})

0 commit comments

Comments
 (0)