From 8beebe6c1c97e4142f34c882f009a96bd326a81a Mon Sep 17 00:00:00 2001 From: Malcolm Barrett Date: Fri, 8 Aug 2025 17:11:04 -0400 Subject: [PATCH 1/2] add air support --- .Rbuildignore | 2 + .github/workflows/format-check.yaml | 23 +++++++++ .vscode/extensions.json | 5 ++ .vscode/settings.json | 10 ++++ R/plot_ess.R | 10 ++-- air.toml | 0 tests/testthat/test-check_ess.R | 61 ++++++++++++++--------- tests/testthat/test-ess.R | 2 +- tests/testthat/test-plot_ess.R | 75 +++++++++++++++++------------ 9 files changed, 129 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/format-check.yaml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 air.toml diff --git a/.Rbuildignore b/.Rbuildignore index 23c85c6..2dca8b9 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -14,3 +14,5 @@ ^scratch$ ^\.claude$ CLAUDE.md +^[.]?air[.]toml$ +^\.vscode$ diff --git a/.github/workflows/format-check.yaml b/.github/workflows/format-check.yaml new file mode 100644 index 0000000..e29e3c4 --- /dev/null +++ b/.github/workflows/format-check.yaml @@ -0,0 +1,23 @@ +# Workflow derived from https://github.com/posit-dev/setup-air/tree/main/examples + +on: + push: + branches: [main, master] + pull_request: + +name: format-check.yaml + +permissions: read-all + +jobs: + format-check: + name: format-check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install + uses: posit-dev/setup-air@v1 + + - name: Check + run: air format . --check diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..344f76e --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "Posit.air-vscode" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a9f69fe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "[r]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "Posit.air-vscode" + }, + "[quarto]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "quarto.quarto" + } +} diff --git a/R/plot_ess.R b/R/plot_ess.R index 4c21d3a..ca9f4c5 100644 --- a/R/plot_ess.R +++ b/R/plot_ess.R @@ -71,7 +71,7 @@ #' # Using pre-computed ESS data #' ess_data <- check_ess(nhefs_weights, .wts = c(w_ate, w_att)) #' plot_ess(ess_data) -#' +#' #' # Show ESS on original scale instead of percentage #' plot_ess(nhefs_weights, .wts = c(w_ate, w_att), percent_scale = FALSE) #' @@ -108,7 +108,7 @@ plot_ess <- function( # Determine if we have groups has_groups <- "group" %in% names(.data) - + # Choose which variable to plot y_var <- if (percent_scale) "ess_pct" else "ess" @@ -191,12 +191,14 @@ plot_ess <- function( p <- p + ggplot2::labs( x = "method", - y = if (percent_scale) "effective sample size (%)" else "effective sample size" + y = if (percent_scale) "effective sample size (%)" else + "effective sample size" ) + ggplot2::scale_y_continuous( limits = c(0, y_upper), expand = c(0, 0), - labels = if (percent_scale) \(x) scales::percent(x / 100) else scales::number + labels = if (percent_scale) \(x) scales::percent(x / 100) else + scales::number ) p diff --git a/air.toml b/air.toml new file mode 100644 index 0000000..e69de29 diff --git a/tests/testthat/test-check_ess.R b/tests/testthat/test-check_ess.R index 050babb..51257a1 100644 --- a/tests/testthat/test-check_ess.R +++ b/tests/testthat/test-check_ess.R @@ -1,6 +1,6 @@ test_that("check_ess works with no weights", { result <- check_ess(nhefs_weights) - + expect_s3_class(result, "tbl_df") expect_equal(nrow(result), 1) expect_equal(result$method, "observed") @@ -10,7 +10,7 @@ test_that("check_ess works with no weights", { test_that("check_ess works with single weight", { result <- check_ess(nhefs_weights, .wts = w_ate) - + expect_s3_class(result, "tbl_df") expect_equal(nrow(result), 2) expect_equal(result$method, c("observed", "w_ate")) @@ -20,7 +20,7 @@ test_that("check_ess works with single weight", { test_that("check_ess works with multiple weights", { result <- check_ess(nhefs_weights, .wts = c(w_ate, w_att, w_atm)) - + expect_s3_class(result, "tbl_df") expect_equal(nrow(result), 4) # Methods may be returned in different order due to tidyselect @@ -29,14 +29,14 @@ test_that("check_ess works with multiple weights", { test_that("check_ess works without observed", { result <- check_ess(nhefs_weights, .wts = w_ate, include_observed = FALSE) - + expect_equal(nrow(result), 1) expect_equal(result$method, "w_ate") }) test_that("check_ess works with binary groups", { result <- check_ess(nhefs_weights, .wts = w_ate, .group = qsmk) - + expect_s3_class(result, "tbl_df") expect_true("group" %in% names(result)) expect_equal(length(unique(result$group)), 2) @@ -45,7 +45,7 @@ test_that("check_ess works with binary groups", { test_that("check_ess works with categorical groups", { result <- check_ess(nhefs_weights, .wts = w_cat_ate, .group = alcoholfreq_cat) - + expect_s3_class(result, "tbl_df") expect_true("group" %in% names(result)) expect_true(length(unique(result$group)) > 2) @@ -53,7 +53,7 @@ test_that("check_ess works with categorical groups", { test_that("check_ess works with continuous groups", { result <- check_ess(nhefs_weights, .wts = w_ate, .group = age, n_tiles = 4) - + expect_s3_class(result, "tbl_df") expect_true("group" %in% names(result)) expect_equal(length(unique(result$group)), 4) @@ -62,16 +62,24 @@ test_that("check_ess works with continuous groups", { test_that("check_ess works with custom tile labels", { labels <- c("Young", "Middle", "Older") - result <- check_ess(nhefs_weights, .wts = w_ate, .group = age, - n_tiles = 3, tile_labels = labels) - + result <- check_ess( + nhefs_weights, + .wts = w_ate, + .group = age, + n_tiles = 3, + tile_labels = labels + ) + expect_setequal(as.character(unique(result$group)), labels) }) test_that("check_ess handles tidyselect syntax", { result1 <- check_ess(nhefs_weights, .wts = starts_with("w_a")) - result2 <- check_ess(nhefs_weights, .wts = c(w_ate, w_att, w_atc, w_atm, w_ato)) - + result2 <- check_ess( + nhefs_weights, + .wts = c(w_ate, w_att, w_atc, w_atm, w_ato) + ) + # Should have same number of methods (plus observed) expect_equal(nrow(result1), nrow(result2)) }) @@ -80,7 +88,7 @@ test_that("check_ess handles psw weight objects", { # Assuming psw weights are numeric vectors with special class # The extract_weight_data function should handle conversion result <- check_ess(nhefs_weights, .wts = w_ate) - + expect_true(is.numeric(result$ess)) expect_true(all(result$ess > 0)) }) @@ -90,15 +98,20 @@ test_that("check_ess validates inputs", { check_ess("not a data frame"), "halfmoon_type_error" ) - + expect_halfmoon_error( check_ess(nhefs_weights, .group = "not_a_column"), "halfmoon_column_error" ) - + expect_halfmoon_error( - check_ess(nhefs_weights, .wts = w_ate, .group = age, - n_tiles = 3, tile_labels = c("Too", "Few")), + check_ess( + nhefs_weights, + .wts = w_ate, + .group = age, + n_tiles = 3, + tile_labels = c("Too", "Few") + ), "halfmoon_length_error" ) }) @@ -106,12 +119,12 @@ test_that("check_ess validates inputs", { test_that("ESS calculation is correct", { # Create simple test case with known ESS test_df <- data.frame( - wts = c(1, 1, 1, 1), # Equal weights -> ESS = n - wts2 = c(4, 0, 0, 0) # All weight on one obs -> ESS = 1 + wts = c(1, 1, 1, 1), # Equal weights -> ESS = n + wts2 = c(4, 0, 0, 0) # All weight on one obs -> ESS = 1 ) - + result <- check_ess(test_df, .wts = c(wts, wts2), include_observed = FALSE) - + expect_equal(result$ess[result$method == "wts"], 4) expect_equal(result$ess[result$method == "wts2"], 1) expect_equal(result$ess_pct[result$method == "wts"], 100) @@ -122,10 +135,10 @@ test_that("check_ess handles NA values", { # Create data with NAs test_df <- nhefs_weights test_df$w_ate[1:10] <- NA - + result <- check_ess(test_df, .wts = w_ate) - + # Should still compute ESS on non-NA values expect_true(all(!is.na(result$ess))) expect_true(all(result$ess > 0)) -}) \ No newline at end of file +}) diff --git a/tests/testthat/test-ess.R b/tests/testthat/test-ess.R index 8e8b1e7..0b9a617 100644 --- a/tests/testthat/test-ess.R +++ b/tests/testthat/test-ess.R @@ -29,7 +29,7 @@ test_that("ess gives `NaN` if all weights are 0", { test_that("ess handles NA values", { # With na.rm = FALSE (default), should return NA expect_true(is.na(ess(c(1, 2, NA, 3)))) - + # With na.rm = TRUE, should calculate ESS on non-NA values expect_equal(ess(c(1, 1, NA, 1), na.rm = TRUE), 3) expect_true(ess(c(0.5, 2, NA, 0.1), na.rm = TRUE) < 3) diff --git a/tests/testthat/test-plot_ess.R b/tests/testthat/test-plot_ess.R index 609a4d6..dc19473 100644 --- a/tests/testthat/test-plot_ess.R +++ b/tests/testthat/test-plot_ess.R @@ -1,13 +1,13 @@ test_that("plot_ess creates a ggplot object", { p <- plot_ess(nhefs_weights, .wts = w_ate) - + expect_s3_class(p, "ggplot") expect_s3_class(p, "gg") }) test_that("plot_ess works with raw data", { p <- plot_ess(nhefs_weights, .wts = c(w_ate, w_att)) - + expect_s3_class(p, "ggplot") # Check that data was computed expect_true("ess_pct" %in% names(p$data)) @@ -16,39 +16,42 @@ test_that("plot_ess works with raw data", { test_that("plot_ess works with pre-computed ESS data", { ess_data <- check_ess(nhefs_weights, .wts = c(w_ate, w_att)) p <- plot_ess(ess_data) - + expect_s3_class(p, "ggplot") expect_equal(nrow(p$data), nrow(ess_data)) }) test_that("plot_ess works without groups", { p <- plot_ess(nhefs_weights, .wts = c(w_ate, w_att)) - + # Should have single fill color (no group aesthetic) expect_true(!"fill" %in% names(p$mapping)) - + # Check layers expect_true(any(sapply(p$layers, function(l) inherits(l$geom, "GeomCol")))) }) test_that("plot_ess works with groups", { p <- plot_ess(nhefs_weights, .wts = c(w_ate, w_att), .group = qsmk) - + # Should have fill aesthetic for groups expect_true("fill" %in% names(p$mapping)) - + # Check for dodged position - col_layer <- p$layers[[which(sapply(p$layers, function(l) inherits(l$geom, "GeomCol")))]] + col_layer <- p$layers[[which(sapply( + p$layers, + function(l) inherits(l$geom, "GeomCol") + ))]] expect_s3_class(col_layer$position, "PositionDodge") }) test_that("plot_ess includes reference line", { p <- plot_ess(nhefs_weights, .wts = w_ate) - + # Check for horizontal line at 100 hline_layer <- sapply(p$layers, function(l) inherits(l$geom, "GeomHline")) expect_true(any(hline_layer)) - + # Check that line is at 100 hline_idx <- which(hline_layer)[1] expect_equal(p$layers[[hline_idx]]$data$yintercept, 100) @@ -57,18 +60,24 @@ test_that("plot_ess includes reference line", { test_that("plot_ess labels work correctly", { p_with_labels <- plot_ess(nhefs_weights, .wts = w_ate, show_labels = TRUE) p_without_labels <- plot_ess(nhefs_weights, .wts = w_ate, show_labels = FALSE) - + # Check for text layer - text_layer_with <- sapply(p_with_labels$layers, function(l) inherits(l$geom, "GeomText")) - text_layer_without <- sapply(p_without_labels$layers, function(l) inherits(l$geom, "GeomText")) - + text_layer_with <- sapply( + p_with_labels$layers, + function(l) inherits(l$geom, "GeomText") + ) + text_layer_without <- sapply( + p_without_labels$layers, + function(l) inherits(l$geom, "GeomText") + ) + expect_true(any(text_layer_with)) expect_false(any(text_layer_without)) }) test_that("plot_ess y-axis uses percent scale", { p <- plot_ess(nhefs_weights, .wts = w_ate) - + # Check that y-axis has percent labels y_scale <- p$scales$get_scales("y") expect_true(is.function(y_scale$labels)) @@ -76,37 +85,43 @@ test_that("plot_ess y-axis uses percent scale", { test_that("plot_ess handles continuous groups", { p <- plot_ess(nhefs_weights, .wts = w_ate, .group = age, n_tiles = 4) - + expect_s3_class(p, "ggplot") expect_true("fill" %in% names(p$mapping)) }) test_that("plot_ess customization works", { p <- plot_ess( - nhefs_weights, + nhefs_weights, .wts = w_ate, fill_color = "red", alpha = 0.5, reference_line_color = "blue", reference_line_type = "solid" ) - + expect_s3_class(p, "ggplot") - + # Check fill color for non-grouped plot - col_layer <- p$layers[[which(sapply(p$layers, function(l) inherits(l$geom, "GeomCol")))]] + col_layer <- p$layers[[which(sapply( + p$layers, + function(l) inherits(l$geom, "GeomCol") + ))]] expect_equal(col_layer$aes_params$fill, "red") expect_equal(col_layer$aes_params$alpha, 0.5) - + # Check reference line - hline_layer <- p$layers[[which(sapply(p$layers, function(l) inherits(l$geom, "GeomHline")))]] + hline_layer <- p$layers[[which(sapply( + p$layers, + function(l) inherits(l$geom, "GeomHline") + ))]] expect_equal(hline_layer$aes_params$colour, "blue") expect_equal(hline_layer$aes_params$linetype, "solid") }) test_that("plot_ess has correct labels", { p <- plot_ess(nhefs_weights, .wts = w_ate) - + expect_equal(p$labels$x, "method") expect_equal(p$labels$y, "effective sample size (%)") }) @@ -114,11 +129,11 @@ test_that("plot_ess has correct labels", { test_that("plot_ess y-limits are appropriate", { # Create data with low ESS test_df <- data.frame( - wts = c(10, 0.1, 0.1, 0.1) # Very unequal weights + wts = c(10, 0.1, 0.1, 0.1) # Very unequal weights ) - + p <- plot_ess(test_df, .wts = wts, include_observed = FALSE) - + # Y-axis should start at 0 and go above the max value y_scale <- p$scales$get_scales("y") expect_equal(y_scale$limits[1], 0) @@ -131,22 +146,22 @@ test_that("plot_ess snapshot tests", { "plot_ess_basic", plot_ess(nhefs_weights, .wts = c(w_ate, w_att)) ) - + # Plot with groups expect_doppelganger( "plot_ess_groups", plot_ess(nhefs_weights, .wts = c(w_ate, w_att), .group = qsmk) ) - + # Plot without labels expect_doppelganger( "plot_ess_no_labels", plot_ess(nhefs_weights, .wts = c(w_ate, w_att), show_labels = FALSE) ) - + # Plot with continuous groups expect_doppelganger( "plot_ess_continuous", plot_ess(nhefs_weights, .wts = w_ate, .group = age, n_tiles = 3) ) -}) \ No newline at end of file +}) From 6660a0a645c5edf07a8157d8fda5b16a44f82182 Mon Sep 17 00:00:00 2001 From: Malcolm Barrett Date: Fri, 8 Aug 2025 17:14:13 -0400 Subject: [PATCH 2/2] update name --- .github/workflows/format-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/format-check.yaml b/.github/workflows/format-check.yaml index e29e3c4..bbb1384 100644 --- a/.github/workflows/format-check.yaml +++ b/.github/workflows/format-check.yaml @@ -5,7 +5,7 @@ on: branches: [main, master] pull_request: -name: format-check.yaml +name: air permissions: read-all