Skip to content

Commit ac1b787

Browse files
raviselkerjonathon-love
authored andcommitted
Reject analyses when factors have empty levels
Factor levels without observations (e.g. levels retained while all their cases are filtered out) caused one-way ANOVA to fail with an unclear error. ANOVA and ANCOVA already rejected such factors, but without naming the offending levels. Both analyses now reject with a shared message that lists the empty levels and suggests how to resolve the issue.
1 parent cbdaad0 commit ac1b787

5 files changed

Lines changed: 58 additions & 7 deletions

File tree

R/ancova.b.R

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -998,14 +998,9 @@ ancovaClass <- R6::R6Class(
998998
)
999999
}
10001000

1001-
if (any(table(self$finalData[[factorName]]) == 0)) {
1002-
jmvcore::reject(
1003-
.("Factor '{factorName}' contains unused levels (after removing rows with missing values)"),
1004-
code=exceptions$dataError,
1005-
factorName=factorName
1006-
)
1007-
}
10081001
}
1002+
1003+
rejectEmptyLevels(self, self$finalData, factors)
10091004
}),
10101005
#### Active bindings ----
10111006
active=list(

R/anovaonew.b.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ anovaOneWClass <- if (requireNamespace('jmvcore')) R6::R6Class(
2222
if (ready) {
2323

2424
data <- private$.cleanData()
25+
private$.dataCheck(data)
2526
results <- private$.compute(data)
2627

2728
private$.populateAnovaTable(results)
@@ -424,6 +425,9 @@ anovaOneWClass <- if (requireNamespace('jmvcore')) R6::R6Class(
424425

425426
return(data)
426427
},
428+
.dataCheck = function(data) {
429+
rejectEmptyLevels(self, data, self$options$group)
430+
},
427431
.descPlotSize = function() {
428432

429433
group <- self$options$group

R/utilsanova.R

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,28 @@ setSingularityWarning = function(self) {
1313
)
1414
}
1515

16+
#' Reject the analysis if factors contain levels without observations
17+
#'
18+
#' @param self The analysis object (for translation)
19+
#' @param data The data the analysis is run on
20+
#' @param factorNames The names of the factors to check
21+
#' @keywords internal
22+
rejectEmptyLevels = function(self, data, factorNames) {
23+
for (factorName in factorNames) {
24+
counts <- table(data[[factorName]])
25+
emptyLevels <- names(counts)[counts == 0]
26+
27+
if (length(emptyLevels) > 0) {
28+
jmvcore::reject(
29+
.("Factor '{factorName}' contains levels with no observations: {levels}. This can happen when all observations of a level are excluded by filters or removed due to missing values. To run the analysis, remove these unused levels from the factor."),
30+
code=exceptions$dataError,
31+
factorName=factorName,
32+
levels=paste0("'", emptyLevels, "'", collapse=', ')
33+
)
34+
}
35+
}
36+
}
37+
1638
#' Create contrast labels (for AN(C)OVA and rmANOVA)
1739
#'
1840
#' @param levels character vector with the (names of the) factor levels

tests/testthat/testanova.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,3 +407,18 @@ testthat::test_that("Contrasts work with special characters in variable name", {
407407
testthat::expect_equal(contr$t, -1.091, tolerance=1e-3)
408408
testthat::expect_equal(contr$p, 0.307, tolerance=1e-3)
409409
})
410+
411+
testthat::test_that("ANOVA rejects factors containing levels with no observations", {
412+
413+
# GIVEN ToothGrowth data with dose 1 filtered out but its level retained
414+
dat <- datasets::ToothGrowth
415+
dat$dose <- factor(dat$dose)
416+
dat <- dat[dat$dose != "1", ]
417+
418+
# WHEN we run a Standard ANOVA on the filtered data
419+
# THEN the analysis should be rejected with an informative error message
420+
testthat::expect_error(
421+
jmv::ANOVA(data = dat, dep = "len", factors = "dose"),
422+
"Factor 'dose' contains levels with no observations: '1'"
423+
)
424+
})

tests/testthat/testanovaonew.R

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,18 @@ testthat::test_that('All options in the anovaOneW work (sunny)', {
7070
testthat::expect_equal(37.101, postHoc$getCell(rowKey="1", "2[df]")$value, tolerance = 1e-5)
7171
testthat::expect_equal(5.5686e-05, postHoc$getCell(rowKey="1", "2[p]")$value, tolerance = 1e-9)
7272
})
73+
74+
testthat::test_that("anovaOneW rejects group variables containing levels with no observations", {
75+
76+
# GIVEN ToothGrowth data with dose 1 filtered out but its level retained
77+
dat <- ToothGrowth
78+
dat$dose <- factor(dat$dose)
79+
dat <- dat[dat$dose != 1, ]
80+
81+
# WHEN we run a One-Way ANOVA on the filtered data
82+
# THEN the analysis should be rejected with an informative error message
83+
testthat::expect_error(
84+
jmv::anovaOneW(dat, deps = "len", group = "dose"),
85+
"Factor 'dose' contains levels with no observations: '1'"
86+
)
87+
})

0 commit comments

Comments
 (0)