Skip to content

Commit cd4bb05

Browse files
authored
Merge pull request #1225 from stan-dev/fix-817-int-container-data
Fix list-to-array conversion for integer arrays
2 parents 6508f55 + 4156dc9 commit cd4bb05

6 files changed

Lines changed: 404 additions & 44 deletions

File tree

NEWS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# cmdstanr (development version)
22

3+
* Lists of matrices/vectors and data frames can now be supplied for variables
4+
declared as `int` in the Stan program. Previously these worked only for `real`
5+
variables and errored for `int` ones. (#817)
6+
* Data frame columns that are not numeric, integer, logical, or
7+
factor are now an error. Previously `data.matrix()` silently coerced them, so a
8+
character column reached Stan as alphabetically ordered integer codes. Convert
9+
the column explicitly, e.g. with `as.integer()`, if integer codes are what you
10+
want. (#1225)
11+
* Lists of logical vectors/matrices are now converted to integers like logical
12+
variables are, instead of erroring. (#1225)
13+
* Supplying a factor for a variable not declared as `int` is now an error. (#1225)
14+
* Factors are now accepted for length-1 `int` arrays (e.g. `array[1] int x`),
15+
which previously errored. (#1225)
316
* The `CMDSTANR_NO_VER_CHECK` R option and environment variable are deprecated
417
as of CmdStanR 1.0.0; use the lowercase `cmdstanr_no_ver_check` forms instead.
518
* `$cpp_options()` no longer includes a `STAN_VERSION` entry read from the model

R/data.R

Lines changed: 101 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,43 @@
1515
#' file:
1616
#'
1717
#' * `logical` -> `integer` (`TRUE` -> `1`, `FALSE` -> `0`)
18-
#' * `data.frame` -> `matrix` (via [data.matrix()])
18+
#' * `factor` -> `integer` (the index of each value's level)
19+
#' * `data.frame` -> `matrix` (via [data.matrix()]); every column must be
20+
#' numeric, integer, logical, or factor
1921
#' * `list` -> `array`
2022
#' * `table` -> `vector`, `matrix`, or `array` (depending on dimensions of table)
2123
#'
24+
#' ### Factor conversion
25+
#' Factors are written as their level indices: the position of each value in
26+
#' `levels(x)` rather than the value itself. The default levels are the sorted
27+
#' unique values, so `factor(c(10, 9, 8))` has levels `8`, `9`, `10` and is
28+
#' written as `[3, 2, 1]`, and an unused level shifts the indices of the levels
29+
#' after it. If the original values are what you want, convert them first, e.g.
30+
#' with `as.numeric(as.character(x))`. The fitting methods of a model compiled
31+
#' from a Stan file error if a factor is supplied for a variable that is not
32+
#' declared as `int`, but `write_stan_json()` has no declarations to check
33+
#' against and so always converts.
34+
#'
35+
#'
36+
#' ### List to array conversion
2237
#' The `list` to `array` conversion is intended to make it easier to prepare
2338
#' the data for certain Stan declarations involving arrays:
2439
#'
2540
#' * `array[K] vector[J] v ` can be constructed in \R as a list with `K`
2641
#' elements where each element is a vector of length `J`
2742
#' * `array[K] matrix[I,J] m ` can be constructed in \R as a list with `K`
2843
#' elements where each element is an `IxJ` matrix
44+
#' * `array[K,I,J] int n ` can be constructed in \R as a list with `K`
45+
#' elements where each element is an `IxJ` matrix of integers
2946
#'
3047
#' These can also be passed in from \R as arrays instead of lists but the list
31-
#' option is provided for convenience. Unfortunately for arrays with more than
32-
#' one dimension (e.g. `array[K,L] vector[J] v `) it is not possible to use an
33-
#' \R list and an array must be used instead. For this example the array in \R
34-
#' should have dimensions `KxLxJ`.
48+
#' option is provided for convenience. A list always contributes exactly one
49+
#' leading dimension, so `array[K,L] vector[J] v ` can be supplied either as a
50+
#' list of `K` matrices each with dimensions `LxJ` or as a single \R array with
51+
#' dimensions `KxLxJ`. Nested lists are not supported: every element of the list
52+
#' must be a vector, matrix, or array.
3553
#'
54+
#' ### Scalar vs. length-1 vector
3655
#' Because \R does not distinguish between a scalar and a vector of length 1, a
3756
#' length-1 vector like `c(42)` is written to JSON as a scalar (`42`) rather
3857
#' than an array (`[42]`). If a Stan variable is declared as a vector or array
@@ -97,23 +116,12 @@ write_stan_json <- function(data, file, always_decimal = FALSE) {
97116
if (is.null(var)) {
98117
stop("Variable '", var_name, "' is NULL.", call. = FALSE)
99118
}
100-
if (!(is.numeric(var) || is.factor(var) || is.logical(var) ||
101-
is.data.frame(var) || is.list(var))) {
102-
stop("Variable '", var_name, "' is of invalid type.", call. = FALSE)
103-
}
119+
validate_data_type(var, var_name)
120+
var <- convert_to_array(var, var_name)
121+
# after the conversion, so that NAs nested inside a list are also found
104122
if (anyNA(var)) {
105123
stop("Variable '", var_name, "' has NA values.", call. = FALSE)
106124
}
107-
108-
if (is.table(var)) {
109-
var <- unclass(var)
110-
} else if (is.logical(var)) {
111-
mode(var) <- "integer"
112-
} else if (is.data.frame(var)) {
113-
var <- data.matrix(var)
114-
} else if (is.list(var)) {
115-
var <- list_to_array(var, var_name)
116-
}
117125
data[[var_name]] <- var
118126
}
119127

@@ -130,6 +138,55 @@ write_stan_json <- function(data, file, always_decimal = FALSE) {
130138
}
131139

132140

141+
# Types accepted for a data variable and for each column of a data frame
142+
is_valid_data_type <- function(x) {
143+
is.numeric(x) || is.factor(x) || is.logical(x)
144+
}
145+
146+
147+
# TRUE for a factor, or a data frame with any factor column
148+
has_factor <- function(x) {
149+
is.factor(x) || (is.data.frame(x) && any(vapply(x, is.factor, logical(1))))
150+
}
151+
152+
153+
# Error if a variable is not one of the types accepted in a data list. Data
154+
# frames and lists are accepted here and converted by convert_to_array().
155+
validate_data_type <- function(var, var_name) {
156+
if (!is_valid_data_type(var) && !is.data.frame(var) && !is.list(var)) {
157+
stop("Variable '", var_name, "' is of invalid type.", call. = FALSE)
158+
}
159+
invisible(NULL)
160+
}
161+
162+
163+
# Convert the R container types accepted in a data list to the atomic arrays
164+
# CmdStan's JSON reader expects. Used by both write_stan_json() and
165+
# process_data() so that the two paths agree.
166+
convert_to_array <- function(var, var_name = NULL) {
167+
if (is.table(var)) {
168+
var <- unclass(var)
169+
} else if (is.data.frame(var)) {
170+
# data.matrix() silently coerces character columns to factor codes and
171+
# date/time columns to their numeric representation, so apply the same
172+
# type check used for the variables themselves (#817)
173+
invalid <- !vapply(var, is_valid_data_type, logical(1))
174+
if (any(invalid)) {
175+
stop("Variable '", var_name, "' has columns of invalid type: ",
176+
paste(names(var)[invalid], collapse = ", "), ".", call. = FALSE)
177+
}
178+
var <- data.matrix(var)
179+
} else if (is.list(var)) {
180+
var <- list_to_array(var, var_name)
181+
}
182+
# after the conversions above so that lists of logicals are also converted
183+
if (is.logical(var)) {
184+
mode(var) <- "integer"
185+
}
186+
var
187+
}
188+
189+
133190
list_to_array <- function(x, name = NULL) {
134191
list_length <- length(x)
135192
if (list_length == 0) {
@@ -142,9 +199,9 @@ list_to_array <- function(x, name = NULL) {
142199
if (!all_equal_dim) {
143200
stop("All matrices/vectors in list '", name, "' must be the same size!", call. = FALSE)
144201
}
145-
all_numeric <- all(sapply(x, function(a) is.numeric(a)))
202+
all_numeric <- all(sapply(x, function(a) is.numeric(a) || is.logical(a)))
146203
if (!all_numeric) {
147-
stop("All elements in list '", name, "' must be numeric!", call. = FALSE)
204+
stop("All elements in list '", name, "' must be numeric or logical!", call. = FALSE)
148205
}
149206
element_num_of_dim <- length(all_dims[[1]])
150207
x <- unlist(x)
@@ -158,12 +215,6 @@ list_to_array <- function(x, name = NULL) {
158215
#' @noRd
159216
#' @param data If not `NULL`, then either a path to a data file compatible with
160217
#' CmdStan, or a named list of \R objects to pass to [write_stan_json()].
161-
#' @param stan_file If not `NULL`, the path to the Stan model for which to
162-
#' process the named list suppiled to the `data` argument. The Stan model
163-
#' is used for checking whether the supplied named list has all the
164-
#' required elements/Stan variables and to help differentiate between a
165-
#' vector of length 1 and a scalar when genereting the JSON file. This
166-
#' argument is ignored when a path to a data file is supplied for `data`.
167218
#' @param model_variables A list of all parameters with their types and
168219
#' number of dimensions. Typically the output of model$variables().
169220
#' @return Path to data file.
@@ -192,6 +243,21 @@ process_data <- function(data, model_variables = NULL) {
192243
if (is.null(data[[var_name]])) {
193244
stop("Variable '", var_name, "' is NULL.", call. = FALSE)
194245
}
246+
validate_data_type(data[[var_name]], var_name)
247+
# Factors are written as level indices, which are only meaningful for
248+
# variables declared as int. Handle them before the conversions below,
249+
# which replace factors with their codes and drop the factor class.
250+
if (data_variables[[var_name]]$type == "int") {
251+
if (is.factor(data[[var_name]])) {
252+
data[[var_name]] <- as.integer(data[[var_name]])
253+
}
254+
} else if (has_factor(data[[var_name]])) {
255+
stop("A factor was supplied for '", var_name, "', which is declared as '",
256+
data_variables[[var_name]]$type, "'.", call. = FALSE)
257+
}
258+
# Convert lists and data frames to arrays before the checks below,
259+
# which require an atomic object (#817)
260+
data[[var_name]] <- convert_to_array(data[[var_name]], var_name)
195261
# distinguish between scalars and arrays/vectors of length 1
196262
if (length(data[[var_name]]) == 1
197263
&& data_variables[[var_name]]$dimensions == 1) {
@@ -201,17 +267,15 @@ process_data <- function(data, model_variables = NULL) {
201267
# generating a decimal point in write_stan_json
202268
if (data_variables[[var_name]]$type == "int"
203269
&& !is.integer(data[[var_name]])) {
204-
if (!is.factor(data[[var_name]])) {
205-
if (!isTRUE(all(is_wholenumber(data[[var_name]])))) {
206-
# Don't warn for NULL/NA, as different warnings are used for those
207-
if (!isTRUE(anyNA(data[[var_name]]))) {
208-
warning("A non-integer value was supplied for '", var_name, "'!",
209-
" It will be truncated to an integer.", call. = FALSE)
210-
}
211-
} else {
212-
# Round before setting mode to integer to avoid floating point errors
213-
data[[var_name]] <- round(data[[var_name]])
270+
if (!isTRUE(all(is_wholenumber(data[[var_name]])))) {
271+
# Don't warn for NULL/NA, as different warnings are used for those
272+
if (!isTRUE(anyNA(data[[var_name]]))) {
273+
warning("A non-integer value was supplied for '", var_name, "'!",
274+
" It will be truncated to an integer.", call. = FALSE)
214275
}
276+
} else {
277+
# Round before setting mode to integer to avoid floating point errors
278+
data[[var_name]] <- round(data[[var_name]])
215279
}
216280
mode(data[[var_name]]) <- "integer"
217281
}

man/write_stan_json.Rd

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

0 commit comments

Comments
 (0)