-
Notifications
You must be signed in to change notification settings - Fork 418
Expand file tree
/
Copy pathexpand.R
More file actions
346 lines (299 loc) · 10.1 KB
/
Copy pathexpand.R
File metadata and controls
346 lines (299 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#' Expand data frame to include all possible combinations of values
#'
#' @description
#' `expand()` generates all combination of variables found in a dataset.
#' It is paired with `nesting()` and `crossing()` helpers. `crossing()`
#' is a wrapper around [expand_grid()] that de-duplicates and sorts its inputs;
#' `nesting()` is a helper that only finds combinations already present in the
#' data.
#'
#' `expand()` is often useful in conjunction with joins:
#'
#' * use it with `right_join()` to convert implicit missing values to
#' explicit missing values (e.g., fill in gaps in your data frame).
#' * use it with `anti_join()` to figure out which combinations are missing
#' (e.g., identify gaps in your data frame).
#'
#' @section Grouped data frames:
#' With grouped data frames created by [dplyr::group_by()], `expand()` operates
#' _within_ each group. Because of this, you cannot expand on a grouping column.
#'
#' @inheritParams expand_grid
#' @param data A data frame.
#' @param ... <[`data-masking`][tidyr_data_masking]> Specification of columns
#' to expand or complete. Columns can be atomic vectors or lists.
#'
#' * To find all unique combinations of `x`, `y` and `z`, including those not
#' present in the data, supply each variable as a separate argument:
#' `expand(df, x, y, z)` or `complete(df, x, y, z)`.
#' * To find only the combinations that occur in the
#' data, use `nesting`: `expand(df, nesting(x, y, z))`.
#' * You can combine the two forms. For example,
#' `expand(df, nesting(school_id, student_id), date)` would produce
#' a row for each present school-student combination for all possible
#' dates.
#'
#' When used with factors, [expand()] and [complete()] use the full set of
#' levels, not just those that appear in the data. If you want to use only the
#' values seen in the data, use `forcats::fct_drop()`.
#'
#' When used with continuous variables, you may need to fill in values
#' that do not appear in the data: to do so use expressions like
#' `year = 2010:2020` or `year = full_seq(year,1)`.
#' @seealso [complete()] to expand list objects. [expand_grid()]
#' to input vectors rather than a data frame.
#' @export
#' @examples
#' # Finding combinations ------------------------------------------------------
#' fruits <- tibble(
#' type = c("apple", "orange", "apple", "orange", "orange", "orange"),
#' year = c(2010, 2010, 2012, 2010, 2011, 2012),
#' size = factor(
#' c("XS", "S", "M", "S", "S", "M"),
#' levels = c("XS", "S", "M", "L")
#' ),
#' weights = rnorm(6, as.numeric(size) + 2)
#' )
#'
#' # All combinations, including factor levels that are not used
#' fruits |> expand(type)
#' fruits |> expand(size)
#' fruits |> expand(type, size)
#' fruits |> expand(type, size, year)
#'
#' # Only combinations that already appear in the data
#' fruits |> expand(nesting(type))
#' fruits |> expand(nesting(size))
#' fruits |> expand(nesting(type, size))
#' fruits |> expand(nesting(type, size, year))
#'
#' # Other uses ----------------------------------------------------------------
#' # Use with `full_seq()` to fill in values of continuous variables
#' fruits |> expand(type, size, full_seq(year, 1))
#' fruits |> expand(type, size, 2010:2013)
#'
#' # Use `anti_join()` to determine which observations are missing
#' all <- fruits |> expand(type, size, year)
#' all
#' all |> dplyr::anti_join(fruits)
#'
#' # Use with `right_join()` to fill in missing rows (like `complete()`)
#' fruits |> dplyr::right_join(all)
#'
#' # Use with `group_by()` to expand within each group
#' fruits |>
#' dplyr::group_by(type) |>
#' expand(year, size)
expand <- function(data, ..., .name_repair = "check_unique") {
UseMethod("expand")
}
#' @export
expand.data.frame <- function(data, ..., .name_repair = "check_unique") {
out <- grid_dots(..., `_data` = data)
out <- map(out, sorted_unique)
# Flattens unnamed data frames returned from `grid_dots()`
out <- expand_grid(!!!out, .name_repair = .name_repair)
reconstruct_tibble(data, out)
}
#' @export
expand.grouped_df <- function(data, ..., .name_repair = "check_unique") {
out <- dplyr::reframe(
data,
expand(
data = dplyr::pick(everything()),
...,
.name_repair = .name_repair
)
)
drop <- dplyr::group_by_drop_default(data)
dplyr::group_by(
out,
!!!dplyr::groups(data),
.drop = drop
)
}
# Nesting & crossing ------------------------------------------------------
#' @rdname expand
#' @export
crossing <- function(..., .name_repair = "check_unique") {
out <- grid_dots(...)
out <- map(out, sorted_unique)
# Flattens unnamed data frames returned from `grid_dots()`
expand_grid(!!!out, .name_repair = .name_repair)
}
#' @rdname expand
#' @export
nesting <- function(..., .name_repair = "check_unique") {
out <- grid_dots(...)
if (length(out) == 0L) {
# This matches `crossing()`, `expand_grid()`, and `expand()`, which return
# a 1 row / 0 col tibble. Computations involving the number of combinations
# of an empty set should return 1.
size <- 1L
} else {
size <- NULL
}
# Flattens unnamed data frames
out <- data_frame(!!!out, .size = size, .name_repair = .name_repair)
out <- tibble::new_tibble(out, nrow = vec_size(out))
out <- sorted_unique(out)
out
}
# expand_grid -------------------------------------------------------------
#' Create a tibble from all combinations of inputs
#'
#' @description
#' `expand_grid()` is heavily motivated by [expand.grid()].
#' Compared to `expand.grid()`, it:
#'
#' * Produces sorted output by varying the first column the slowest by default.
#' * Returns a tibble, not a data frame.
#' * Never converts strings to factors.
#' * Does not add any additional attributes.
#' * Can expand any generalised vector, including data frames.
#'
#' @inheritParams vctrs::vec_expand_grid
#'
#' @param ... Name-value pairs. The name will become the column name in the
#' output.
#'
#' @return A tibble with one column for each input in `...`. The output will
#' have one row for each combination of the inputs, i.e. the size will be
#' equal to the product of the sizes of the inputs. This implies that if any
#' input has length 0, the output will have zero rows. The ordering of the
#' output depends on the `.vary` argument.
#'
#' @export
#' @examples
#' # Default behavior varies the first column "slowest"
#' expand_grid(x = 1:3, y = 1:2)
#'
#' # Vary the first column "fastest", like `expand.grid()`
#' expand_grid(x = 1:3, y = 1:2, .vary = "fastest")
#'
#' # Can also expand data frames
#' expand_grid(df = tibble(x = 1:2, y = c(2, 1)), z = 1:3)
#'
#' # And matrices
#' expand_grid(x1 = matrix(1:4, nrow = 2), x2 = matrix(5:8, nrow = 2))
#'
#' # And named lists using the splice operator
#' my_params <- list(a = 1:3, b = c("a", "b", "c", "d"))
#' expand_grid(!!!my_params)
expand_grid <- function(..., .name_repair = "check_unique", .vary = "slowest") {
out <- grid_dots(...)
names <- names2(out)
unnamed <- which(names == "")
any_unnamed <- any(unnamed)
if (any_unnamed) {
# `vec_expand_grid()` requires all inputs to be named.
# Most are auto named by `grid_dots()`, but unnamed data frames are not.
# So we temporarily name unnamed data frames that eventually get spliced.
names[unnamed] <- vec_paste0("...", unnamed)
names(out) <- names
}
out <- vec_expand_grid(
!!!out,
.vary = .vary,
.name_repair = "minimal",
.error_call = current_env()
)
if (any_unnamed) {
names[unnamed] <- ""
names(out) <- names
}
size <- vec_size(out)
# Flattens unnamed data frames after grid expansion
out <- tidyr_new_list(out)
out <- df_list(
!!!out,
.name_repair = .name_repair,
.error_call = current_env()
)
out <- tibble::new_tibble(out, nrow = size)
out
}
# Helpers -----------------------------------------------------------------
sorted_unique <- function(x) {
if (is.factor(x)) {
fct_unique(x)
} else if (is_bare_list(x)) {
vec_unique(x)
} else {
vec_sort(vec_unique(x))
}
}
# forcats::fct_unique
fct_unique <- function(x) {
levels <- levels(x)
out <- levels
if (!anyNA(levels) && anyNA(x)) {
out <- c(out, NA_character_)
}
factor(out, levels = levels, exclude = NULL, ordered = is.ordered(x))
}
grid_dots <- function(..., `_data` = NULL, .error_call = caller_env()) {
dots <- enquos(...)
n_dots <- length(dots)
names <- names(dots)
needs_auto_name <- names == ""
# Silently uniquely repair "auto-names" to avoid collisions
# from truncated long names. Probably not a perfect system, but solves
# most of the reported issues.
auto_names <- names(exprs_auto_name(
exprs = dots[needs_auto_name],
repair_auto = "unique",
repair_quiet = TRUE
))
names[needs_auto_name] <- auto_names
# Set up a mask for repeated `eval_tidy()` calls that support iterative
# expressions
env <- new_environment()
mask <- new_data_mask(env)
mask$.data <- as_data_pronoun(env)
if (!is.null(`_data`)) {
# Pre-load the data mask with `_data`
cols <- tidyr_new_list(`_data`)
col_names <- names(cols)
for (i in seq_along(cols)) {
col <- cols[[i]]
col_name <- col_names[[i]]
env[[col_name]] <- col
}
}
out <- vector("list", length = n_dots)
null <- vector("logical", length = n_dots)
for (i in seq_len(n_dots)) {
dot <- dots[[i]]
dot <- eval_tidy(dot, data = mask)
if (is.null(dot)) {
null[[i]] <- TRUE
next
}
arg <- paste0("..", i)
vec_assert(dot, arg = arg, call = .error_call)
out[[i]] <- dot
is_unnamed_data_frame <- is.data.frame(dot) && needs_auto_name[[i]]
if (is_unnamed_data_frame) {
# Signal that unnamed data frame should be spliced by setting its name
# to `""`. Then add its individual columns into the mask.
names[[i]] <- ""
dot_names <- names(dot)
for (i in seq_along(dot)) {
dot_col <- dot[[i]]
dot_name <- dot_names[[i]]
env[[dot_name]] <- dot_col
}
} else {
# Install `dot` in the mask for iterative evaluations
name <- names[[i]]
env[[name]] <- dot
}
}
if (any(null)) {
out <- out[!null]
names <- names[!null]
}
names(out) <- names
out
}