Nothing
#' List the datasets of the Swiss Stats Explorer
#'
#' List the datasets (dataflows) of the Swiss Stats Explorer (SSE) API
#' (<https://stats.swiss/>), to find the id of a dataset for
#' [bfs_sse_dimensions()] and [bfs_sse_data()].
#'
#' @param language Language of the names: `"de"` (default), `"fr"`, `"it"`
#' or `"en"`.
#'
#' @returns A tibble with one row per version of each dataset, the latest
#' version first:
#' \describe{
#' \item{dataset}{The id of the dataset, e.g. `"DF_LWZ_1"`.}
#' \item{name}{The name of the dataset.}
#' \item{version}{The version of the dataset.}
#' \item{agency}{The agency publishing the dataset.}
#' \item{description}{The description of the dataset version, or `NA`, as
#' given by the API: it can contain HTML tags such as `<br>`.}
#' }
#' Returns `NULL` if there is no internet connection.
#'
#' @author Felix Luginbuhl
#' @seealso [bfs_sse_dimensions()], [bfs_sse_data()]
#' @export
#'
#' @importFrom rlang %||%
#'
#' @examplesIf interactive()
#' datasets <- bfs_sse_datasets(language = "en")
#' datasets
#'
#' # datasets about tourism, in their name or description
#' datasets[grepl("touris", paste(datasets$name, datasets$description), ignore.case = TRUE), ]
bfs_sse_datasets <- function(language = "de") {
language <- rlang::arg_match(language, c("de", "fr", "it", "en"))
# fail gracefully if no internet connection
if (!curl::has_internet()) {
message("No internet connection")
return(NULL)
}
flows <- sse_dataflows(language)
flows[order(flows$dataset, -xtfrm(numeric_version(flows$version, strict = FALSE))), ]
}
#' Get the dimensions of a Swiss Stats Explorer dataset
#'
#' Get the dimensions of a dataset of the Swiss Stats Explorer (SSE) API
#' (<https://stats.swiss/>) and the values allowed in each dimension. Use
#' them to build the `filters` of [bfs_sse_data()].
#'
#' @param dataset The id of a dataset, e.g. `"DF_LWZ_1"`: the column
#' `dataset` of [bfs_sse_datasets()], also shown in the URL of the dataset
#' on <https://stats.swiss/>.
#' @param ... These dots are for future extensions and must be empty: the
#' arguments after `dataset` must be named. The arguments of
#' [bfs_get_sse_metadata()], such as `number_bfs`, give an error that names
#' their new name.
#' @param language Language of the labels: `"de"` (default), `"fr"`, `"it"`
#' or `"en"`. A label missing in this language falls back to another
#' language.
#' @param version Version of the dataset, e.g. `"1.1.0"`: the column
#' `version` of [bfs_sse_datasets()]. If `NULL` (default), the API uses the
#' latest version. Set it in a data pipeline to keep the same structure
#' when a new version is published.
#'
#' @returns A tibble with one row per value allowed in each dimension (some
#' values may have no data):
#' \describe{
#' \item{dimension}{The dimension id, to use as a name in `filters`.}
#' \item{dimension_label}{The dimension label.}
#' \item{value}{The value code, to use as a value in `filters`.}
#' \item{value_label}{The value label.}
#' \item{position}{The position of the dimension in the dataset.}
#' \item{parent}{The code of the parent value in the codelist, e.g. the
#' district of a commune and the canton of a district, or `NA`. Use it to
#' select all the values under a value.}
#' }
#' Returns `NULL` if there is no internet connection.
#'
#' @author Felix Luginbuhl
#' @seealso [bfs_sse_datasets()], [bfs_sse_data()]
#' @export
#'
#' @examplesIf interactive()
#' dimensions <- bfs_sse_dimensions("DF_PASTA_552_MONTHLY", language = "en")
#' dimensions
#'
#' # the dimensions only
#' unique(dimensions[c("dimension", "dimension_label")])
#'
#' # the communes of the canton of Geneva: the values whose parent is a
#' # district of Geneva
#' regions <- bfs_sse_dimensions("DF_LWZ_1", language = "en")
#' districts <- regions$value[regions$parent %in% "GE"]
#' regions[regions$parent %in% districts, ]
bfs_sse_dimensions <- function(dataset, ..., language = "de", version = NULL) {
sse_check_dots("bfs_sse_dimensions()", ...)
sse_check_string(dataset, "dataset", null = FALSE)
sse_check_string(version, "version")
language <- rlang::arg_match(language, c("de", "fr", "it", "en"))
# fail gracefully if no internet connection
if (!curl::has_internet()) {
message("No internet connection")
return(NULL)
}
sse_structure(sse_get_structure(dataset, version, "all"), language)
}
#' Get data from the Swiss Stats Explorer
#'
#' Download the data of a dataset of the Swiss Stats Explorer (SSE) API
#' (<https://stats.swiss/>). Select the data you need with `filters`, built
#' from [bfs_sse_dimensions()].
#'
#' The data is downloaded in the SDMX-CSV format, with the labels in
#' `language` given by the API. It is smaller and faster to read than the
#' SDMX-ML format used by [bfs_get_sse_data()].
#'
#' The function sends two requests: one for the dimensions of the dataset, to
#' build the query from `filters`, and one for the data. The values in
#' `filters` are checked by the API: a value not in the dataset gives no
#' data.
#'
#' @inheritParams bfs_sse_dimensions
#' @param ... These dots are for future extensions and must be empty: the
#' arguments after `dataset` must be named. The arguments of
#' [bfs_get_sse_data()], such as `number_bfs` or `query`, give an error that
#' names their new name.
#' @param filters A named list: the names are dimensions (column `dimension`
#' of [bfs_sse_dimensions()]) and the values are value codes (column
#' `value`), e.g. `list(FREQ = "M", COUNTRY_ORIGIN = c("CH", "AUSL"))`. A
#' dimension not in `filters` keeps all its values. If `NULL` (default),
#' the whole dataset is downloaded, which can be large and slow. An unknown
#' dimension is an error, as the data wouldn't be filtered on it.
#' @param start_period,end_period Optional first and last period of the data,
#' e.g. `"2020"` or `"2020-01"`.
#' @param first_n,last_n Optional number of first or last periods of each
#' series (each combination of the dimensions), e.g. `last_n = 1` for the
#' latest value. Sent to the API as the SDMX parameters
#' `firstNObservations` and `lastNObservations`. They can be combined with
#' `start_period` and `end_period`.
#' @param labels The values of the dimensions and attributes: `"both"`
#' (default) to keep the codes and add a column `<dimension>_label` after
#' each dimension, `"code"` for the codes only, or `"label"` for the labels
#' only. Different codes can have the same label (e.g. the canton and the
#' commune of Bern), so use `"label"` only to display the data.
#' @param attributes If `TRUE`, add the attributes of the observations after
#' `value`, e.g. `OBS_STATUS` (status of the value, e.g. provisional). They
#' can change from one version of a dataset to another.
#'
#' @returns A tibble with one column per dimension, named with the dimension
#' id whatever the `language`, then `TIME_PERIOD` as text (e.g. `"2024"`,
#' `"2024-Q1"`, `"2024-01"`), as given by the API, and the observations in
#' the numeric `value` column. Returns `NULL` if there is no internet
#' connection. The errors have a class, to handle them with
#' [tryCatch()]:
#' \describe{
#' \item{`bfs_no_records`}{No data matches the query.}
#' \item{`bfs_sse_filter_error`}{`filters` wouldn't filter the data, e.g.
#' an unknown dimension.}
#' \item{`bfs_sse_argument_error`}{Another argument is not valid.}
#' \item{`bfs_sse_dataset_error`}{The dataset or the version doesn't exist.}
#' \item{`bfs_sse_query_error`}{The API rejects the query, e.g. a period in
#' a wrong format or a URL too long. The error gives the message of the
#' API.}
#' \item{`bfs_http_error`}{The API is not available.}
#' }
#'
#' @author Felix Luginbuhl
#' @seealso [bfs_sse_datasets()], [bfs_sse_dimensions()]
#' @export
#'
#' @examplesIf interactive()
#' bfs_sse_data(
#' "DF_PASTA_552_MONTHLY",
#' filters = list(
#' FREQ = "M",
#' ACCOMMODATION_TYPE = "552001",
#' COUNTRY_ORIGIN = c("CH", "AUSL")
#' ),
#' start_period = "2020",
#' end_period = "2023",
#' language = "en"
#' )
#'
#' # the latest vacancy rate of each canton
#' bfs_sse_data(
#' "DF_LWZ_1",
#' filters = list(
#' GR_KT_GDE = c("GE", "ZH", "BE"),
#' WOHN_ANZAHL = "_T",
#' LEERWOHN_TYP = "_T",
#' MEASURE_DIMENSION = "RATE"
#' ),
#' last_n = 1,
#' language = "en"
#' )
bfs_sse_data <- function(dataset, ..., filters = NULL, start_period = NULL, end_period = NULL,
first_n = NULL, last_n = NULL, language = "de",
labels = c("both", "code", "label"), attributes = FALSE, version = NULL) {
sse_check_dots("bfs_sse_data()", ...)
sse_check_string(dataset, "dataset", null = FALSE)
sse_check_string(version, "version")
sse_check_string(start_period, "start_period")
sse_check_string(end_period, "end_period")
sse_check_count(first_n, "first_n")
sse_check_count(last_n, "last_n")
sse_check_filters(filters)
language <- rlang::arg_match(language, c("de", "fr", "it", "en"))
labels <- rlang::arg_match(labels)
# fail gracefully if no internet connection
if (!curl::has_internet()) {
message("No internet connection")
return(NULL)
}
# the dimensions, to build the key, and the exact version, so the data is
# of the same version as the dimensions
structure <- sse_get_structure(dataset, version, "datastructure")
keys <- sse_dimension_ids(structure)
sse_check_filter_names(filters, keys)
params <- sse_query_params(start_period, end_period, first_n, last_n)
url <- paste0(sse_base_url, "/data/", sse_flow_ref(structure), "/", sse_key(filters, keys),
if (params != "") "?", params)
resp <- sse_request(url) %>%
httr2::req_headers(
Accept = paste0("application/vnd.sdmx.data+csv;version=1.0.0",
if (labels != "code") ";labels=both"),
`Accept-Language` = language
) %>%
sse_perform()
content_type <- httr2::resp_content_type(resp)
if (!identical(content_type, "application/vnd.sdmx.data+csv")) {
rlang::abort(
c("The SSE API returned data in an unexpected format.",
i = paste0("Content type: ", content_type, ", expected SDMX-CSV.")),
class = "bfs_sse_format_error",
call = NULL
)
}
# all columns as text: codes such as "001" or "NA" (Namibia) are kept.
# Read from a file, faster than from a string for large datasets, ending
# with a newline to avoid the warning "incomplete final line"
path <- tempfile(fileext = ".csv")
on.exit(unlink(path), add = TRUE)
con <- file(path, open = "wb")
writeBin(httr2::resp_body_raw(resp), con)
writeBin(charToRaw("\n"), con)
close(con)
csv <- utils::read.csv(path, colClasses = "character", na.strings = "",
check.names = FALSE, encoding = "UTF-8")
# SDMX-CSV columns: DATAFLOW, the dimensions, TIME_PERIOD, OBS_VALUE and
# the attributes, named "<id>: <label>" with labels. Columns are taken by
# position, as the attribute DATAFLOW has the same id as the first column
ids <- sse_csv_code(names(csv))
obs <- match("OBS_VALUE", ids)
time <- match("TIME_PERIOD", ids)
if (is.na(obs)) {
rlang::abort(
c("The SSE API returned data in an unexpected format.",
i = "The SDMX-CSV data has no column `OBS_VALUE`."),
class = "bfs_sse_format_error",
call = NULL
)
}
attribute_cols <- if (attributes) setdiff(which(seq_along(csv) > obs), which(ids == "DATAFLOW"))
dat <- list()
for (i in which(ids %in% keys)) {
dat <- sse_add_column(dat, ids[i], csv[[i]], labels)
}
if (!is.na(time)) dat$TIME_PERIOD <- sse_csv_code(csv[[time]])
dat$value <- as.numeric(csv[[obs]])
for (i in attribute_cols) {
dat <- sse_add_column(dat, ids[i], csv[[i]], labels)
}
tibble::as_tibble(dat)
}
# Add the SDMX-CSV column `x` to `dat` as codes, labels or both
sse_add_column <- function(dat, id, x, labels) {
if (labels == "code") {
dat[[id]] <- x
return(dat)
}
code <- sse_csv_code(x)
label <- sse_csv_label(x)
if (labels == "label") {
dat[[id]] <- dplyr::coalesce(label, code)
} else {
dat[[id]] <- code
dat[[paste0(id, "_label")]] <- label
}
dat
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.