Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ License: file LICENSE
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Depends:
R (>= 3.5.0)
Imports:
httr,
readr
httr2 (>= 1.0.0),
readr (>= 2.0.0)
Suggests:
haven,
haven (>= 2.0.0),
testthat (>= 3.0.0)
Config/testthat/edition: 3
URL: https://github.com/KMueller-Lab/Global-Macro-Database-R
Expand Down
107 changes: 86 additions & 21 deletions R/gmd.R
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
#' all specified variables are missing.
#'
#' @param variables A character vector of variable names to include (e.g., \code{"rGDP"}
#' or \code{c("rGDP", "unemp")}). Can also be used with \code{sources} to load
#' specific variables from a given source.
#' or \code{c("rGDP", "unemp")}). Case-insensitive (e.g. \code{"rgdp"} matches
#' \code{"rGDP"}). Can also be used with \code{sources} to load specific variables
#' from a given source.
#' @param country A character vector of ISO3 country codes (e.g., \code{"USA"} or
#' \code{c("USA", "CHN")}). Case-insensitive.
#' @param version A string specifying which version of the dataset to load (e.g.,
Expand All @@ -30,6 +31,10 @@
#' with \code{variables} to load only specific variables from that source.
#' @param cite A string. \code{"load"} to load the full citation list as a dataframe,
#' or a specific source key (e.g., \code{"GMD"}) to display its BibTeX citation.
#' @param start_year A single number. If supplied, only observations with
#' \code{year >= start_year} are returned.
#' @param end_year A single number. If supplied, only observations with
#' \code{year <= end_year} are returned.
#' @return A dataframe containing the requested macroeconomic data.
#'
#' @examples
Expand All @@ -43,6 +48,9 @@
#' # Load data for a specific country
#' df <- gmd(country = "USA")
#'
#' # Restrict to a range of years
#' df <- gmd(country = "USA", variables = "rGDP", start_year = 2000, end_year = 2010)
#'
#' # Load a specific version for reproducibility
#' df <- gmd(version = "2025_01")
#'
Expand Down Expand Up @@ -94,7 +102,8 @@
#' @export
gmd <- function(variables = NULL, country = NULL, version = NULL,
raw = FALSE, iso = FALSE, vars = FALSE,
sources = NULL, cite = NULL) {
sources = NULL, cite = NULL,
start_year = NULL, end_year = NULL) {

base_url <- "https://gmd-releases.s3.ap-southeast-2.amazonaws.com/data"
ID_COLS <- c("ISO3", "year", "countryname", "id")
Expand All @@ -103,6 +112,31 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
message("Website: https://www.globalmacrodata.com")
message("")

# --- Input validation (runs before any download) ---

if (!is.null(version) &&
(!is.character(version) || length(version) != 1 || is.na(version))) {
stop("`version` must be a single non-NA character string, or NULL.")
}
if (!is.null(country) && length(country) == 0) {
stop("`country` must contain at least one ISO3 code, or be NULL.")
}
if (!is.null(variables) && length(variables) == 0) {
stop("`variables` must contain at least one variable name, or be NULL.")
}

validate_year <- function(value, name) {
if (is.null(value)) return(invisible(NULL))
if (length(value) != 1 || is.na(value) || !is.numeric(value)) {
stop(sprintf("`%s` must be a single non-NA number, or NULL.", name))
}
}
validate_year(start_year, "start_year")
validate_year(end_year, "end_year")
if (!is.null(start_year) && !is.null(end_year) && start_year > end_year) {
stop("`start_year` must not be greater than `end_year`.")
}

# --- Internal helpers ---

require_haven <- function() {
Expand All @@ -115,27 +149,35 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
resp <- .gmd_safe_get(paste0(base_url, "/helpers/countrylist.dta"))
if (!is.null(resp)) {
require_haven()
return(haven::read_dta(httr::content(resp, as = "raw")))
return(haven::read_dta(httr2::resp_body_raw(resp)))
}
# Fallback to bundled CSV
path <- system.file("isomapping.csv", package = "globalmacrodata")
if (nzchar(path) && file.exists(path)) {
message("Loading country list from local fallback.")
return(readr::read_csv(path, show_col_types = FALSE))
return(readr::read_csv(path,
col_types = readr::cols(countryname = readr::col_character(),
ISO3 = readr::col_character())))
}
stop("Unable to load country list. Check internet connection or reinstall the package.")
}

load_varlist <- function() {
resp <- .gmd_safe_get(paste0(base_url, "/helpers/varlist.csv"))
if (!is.null(resp)) {
return(readr::read_csv(httr::content(resp, as = "text", encoding = "UTF-8"), show_col_types = FALSE))
return(readr::read_csv(httr2::resp_body_string(resp, encoding = "UTF-8"),
col_types = readr::cols(variables = readr::col_character(),
units = readr::col_character(),
definition = readr::col_character())))
}
# Fallback to bundled CSV
path <- system.file("varlist.csv", package = "globalmacrodata")
if (nzchar(path) && file.exists(path)) {
message("Loading variable list from local fallback.")
return(readr::read_csv(path, show_col_types = FALSE))
return(readr::read_csv(path,
col_types = readr::cols(variables = readr::col_character(),
units = readr::col_character(),
definition = readr::col_character())))
}
stop("Unable to load variable list. Check internet connection or reinstall the package.")
}
Expand All @@ -144,7 +186,7 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
country <- toupper(country)
invalid <- country[!country %in% country_mapping$ISO3]
if (length(invalid) > 0) {
stop(sprintf("Error: Invalid country code(s): %s\n\nTo see the list of valid country codes, use: gmd(iso = TRUE)",
stop(sprintf("Invalid country code(s): %s\n\nTo see the list of valid country codes, use: gmd(iso = TRUE)",
paste(invalid, collapse = ", ")))
}
country
Expand All @@ -162,6 +204,12 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
df[, c(first, rest), drop = FALSE]
}

apply_year_filter <- function(df) {
if (!is.null(start_year)) df <- df[df$year >= start_year, , drop = FALSE]
if (!is.null(end_year)) df <- df[df$year <= end_year, , drop = FALSE]
df
}

print_citation <- function(version) {
message(sprintf("Version: %s", version))
message("")
Expand Down Expand Up @@ -196,8 +244,9 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
}

if ((iso || vars) &&
(!is.null(variables) || !is.null(country) || !is.null(version) || raw != FALSE)) {
warning("When iso = TRUE or vars = TRUE, should not enter other inputs (variables, country, version, raw).")
(!is.null(variables) || !is.null(country) || !is.null(version) ||
raw != FALSE || !is.null(sources) || !is.null(cite))) {
stop("When iso = TRUE or vars = TRUE, do not supply other inputs (variables, country, version, raw, sources, cite).")
}

# ============================================================================
Expand Down Expand Up @@ -238,7 +287,9 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
if (is.null(cite_resp)) {
stop("Unable to import the list of sources to cite. Check internet connection.")
}
cite_df <- readr::read_csv(httr::content(cite_resp, as = "text", encoding = "UTF-8"), show_col_types = FALSE)
cite_df <- readr::read_csv(httr2::resp_body_string(cite_resp, encoding = "UTF-8"),
col_types = readr::cols(source_name = readr::col_character(),
citation = readr::col_character()))

if (tolower(cite) == "load") {
message("Imported the list of sources to cite.")
Expand Down Expand Up @@ -272,7 +323,7 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
data_url <- paste0(base_url, "/distribute/GMD_", current_version, ".dta")
} else {
if (!version %in% available_versions) {
stop(sprintf("Error: %s is not valid\nAvailable versions are: %s\nThe current version is: %s",
stop(sprintf("%s is not valid\nAvailable versions are: %s\nThe current version is: %s",
version, paste(sort(available_versions), collapse = ", "), current_version))
}
data_url <- paste0(base_url, "/distribute/GMD_", version, ".dta")
Expand All @@ -293,7 +344,8 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
if (is.null(source_resp)) {
stop("Unable to load source list. Check internet connection.")
}
source_df <- readr::read_csv(httr::content(source_resp, as = "text", encoding = "UTF-8"), show_col_types = FALSE)
source_df <- readr::read_csv(httr2::resp_body_string(source_resp, encoding = "UTF-8"),
col_types = readr::cols(source_name = readr::col_character()))

if (tolower(sources) == "load") {
message("Imported the list of sources.")
Expand All @@ -305,27 +357,28 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
}

sources <- trimws(sources)
source_resp <- .gmd_safe_get(paste0(base_url, "/clean/combined/", sources, ".dta"))
source_resp <- .gmd_safe_get(paste0(base_url, "/clean/combined/", sources, ".dta"), quiet = TRUE)

# Case-insensitive fallback
if (is.null(source_resp)) {
sl_resp <- .gmd_safe_get(paste0(base_url, "/helpers/source_list.csv"))
if (is.null(sl_resp)) {
stop("Unable to access source list. Check internet connection.")
}
sl_df <- readr::read_csv(httr::content(sl_resp, as = "text", encoding = "UTF-8"), show_col_types = FALSE)
sl_df <- readr::read_csv(httr2::resp_body_string(sl_resp, encoding = "UTF-8"),
col_types = readr::cols(source_name = readr::col_character()))
matched_source <- sl_df$source_name[tolower(sl_df$source_name) == tolower(sources)]
if (length(matched_source) == 1) {
sources <- matched_source
source_resp <- .gmd_safe_get(paste0(base_url, "/clean/combined/", sources, ".dta"))
source_resp <- .gmd_safe_get(paste0(base_url, "/clean/combined/", sources, ".dta"), quiet = TRUE)
}
if (is.null(source_resp)) {
stop(sprintf("Invalid source name: %s\nTo see the list of sources, use: gmd(sources = 'list')", sources))
}
}

require_haven()
df <- haven::read_dta(httr::content(source_resp, as = "raw"))
df <- haven::read_dta(httr2::resp_body_raw(source_resp))

if (!is.null(variables)) {
source_vars <- paste0(sources, "_", variables)
Expand All @@ -345,9 +398,12 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
df <- df[df$ISO3 %in% country, , drop = FALSE]
}

df <- apply_year_filter(df)
df <- df[order(df$ISO3, df$year), ]
df <- drop_na_cols(df)

if (nrow(df) == 0) stop("No data available for the specified parameters")

n_vars <- ncol(df) - length(intersect(ID_COLS, colnames(df)))
message(sprintf("Final dataset: %d observations of %d variables", nrow(df), n_vars))
print_citation(current_version)
Expand All @@ -366,11 +422,14 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
}

valid_vars <- get_varlist()$variables
invalid_vars <- setdiff(variables, valid_vars)
# Match case-insensitively, then normalize to the canonical casing (e.g. "rgdp" -> "rGDP")
canonical <- valid_vars[match(tolower(variables), tolower(valid_vars))]
invalid_vars <- variables[is.na(canonical)]
if (length(invalid_vars) > 0) {
stop(sprintf("Invalid variable code(s): %s\n\nTo see the list of valid variable codes, use: gmd(vars = TRUE)",
paste(invalid_vars, collapse = ", ")))
}
variables <- canonical
}

# ============================================================================
Expand All @@ -392,13 +451,14 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
}
}

df <- readr::read_csv(httr::content(raw_resp, as = "text", encoding = "UTF-8"), show_col_types = FALSE)
df <- readr::read_csv(httr2::resp_body_string(raw_resp, encoding = "UTF-8"), show_col_types = FALSE)

if (!is.null(country)) {
country <- validate_country(country, get_country_mapping())
df <- df[df$ISO3 %in% country, , drop = FALSE]
}

df <- apply_year_filter(df)
df <- reorder_cols(df)
df <- df[order(df$countryname, df$year), ]

Expand All @@ -416,9 +476,12 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
require_haven()
main_resp <- .gmd_safe_get(data_url)
if (is.null(main_resp)) {
stop(sprintf("Error: Data file not found at %s\nCheck internet connection.", data_url))
stop(sprintf(paste0("Could not retrieve the data file for version '%s' (%s).\n",
"The version is listed as available but the file may be temporarily ",
"unavailable on the server; check your internet connection or try another version."),
current_version, data_url))
}
df <- haven::read_dta(httr::content(main_resp, as = "raw"))
df <- haven::read_dta(httr2::resp_body_raw(main_resp))

if (!is.null(country)) {
country <- validate_country(country, get_country_mapping())
Expand All @@ -442,6 +505,8 @@ gmd <- function(variables = NULL, country = NULL, version = NULL,
}
}

df <- apply_year_filter(df)

df <- drop_na_cols(df, protect = ID_COLS)
df <- reorder_cols(df)
df <- df[order(df$countryname, df$year), ]
Expand Down
34 changes: 24 additions & 10 deletions R/helpers.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
.gmd_safe_get <- function(url) {
.gmd_safe_get <- function(url, quiet = FALSE) {
tryCatch(
{
response <- httr::GET(url)
if (httr::status_code(response) == 200) response else NULL
# Disable httr2's default "HTTP >= 400 is an error" so we can handle
# non-200 the same way as a network failure (return NULL), while still
# being able to report the status code.
req <- httr2::req_error(httr2::request(url), is_error = function(resp) FALSE)
response <- httr2::req_perform(req)
status <- httr2::resp_status(response)
if (status == 200) {
response
} else {
if (!quiet) message(sprintf("Request to %s returned HTTP status %d.", url, status))
NULL
}
},
error = function(e) NULL
error = function(e) {
if (!quiet) message(sprintf("Request to %s failed: %s", url, conditionMessage(e)))
NULL
}
)
}

Expand All @@ -14,8 +27,8 @@
response <- .gmd_safe_get(versions_url)
if (!is.null(response)) {
versions_df <- readr::read_csv(
httr::content(response, as = "text", encoding = "UTF-8"),
show_col_types = FALSE
httr2::resp_body_string(response, encoding = "UTF-8"),
col_types = readr::cols(versions = readr::col_character())
)
if ("versions" %in% names(versions_df)) {
return(versions_df)
Expand All @@ -25,10 +38,11 @@
fallback_path <- system.file("versions.csv", package = "globalmacrodata")
if (nzchar(fallback_path) && file.exists(fallback_path)) {
message("Loading version list from local fallback.")
return(readr::read_csv(fallback_path, show_col_types = FALSE))
return(readr::read_csv(fallback_path,
col_types = readr::cols(versions = readr::col_character())))
}

stop("Error: Unable to access version information. Check internet connection or reinstall the package.")
stop("Unable to access version information. Check internet connection or reinstall the package.")
}

#' Get available versions of the Global Macro Database
Expand All @@ -38,12 +52,12 @@
get_available_versions <- function() {
versions_df <- .gmd_load_versions_df()
if (!"versions" %in% names(versions_df)) {
stop("Error: Version information is malformed.")
stop("Version information is malformed.")
}

versions <- sort(unique(versions_df$versions), decreasing = TRUE)
if (length(versions) == 0) {
stop("Error: Version information is empty.")
stop("Version information is empty.")
}

versions
Expand Down
4 changes: 3 additions & 1 deletion inst/versions.csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ versions
2026_01
2025_12
2025_09
2025_08
2025_06
2025_05
2025_03
2025_01
2025_01
Loading