| Title: | Automated Selection and Visualisation of Statistical Hypothesis Tests |
|---|---|
| Description: | Automated test selection, visualised. 'visStatistics' automatically selects and visualises statistical hypothesis tests comparing two vectors, based on their class, distribution, and sample size. Visual outputs, including box plots, bar charts, regression lines with confidence bands, mosaic plots, residual plots, and Q-Q plots, are annotated with relevant test statistics, assumption checks, and post-hoc analyses where applicable. The algorithmic workflow shifts attention from ad-hoc test selection to visual diagnostic assessment and statistical interpretation. It is particularly suited for server-side R applications, where end users interact solely through a web interface to select data groups and receive a complete visual statistical analysis automatically. The same automation makes it useful in time-constrained contexts such as statistical consulting, where it reduces effort spent on test selection and leaves more room for interpretation. The implemented tests cover the most frequently applied inferential methods in biomedical research (Hayat et al. (2017) <doi:10.1371/journal.pone.0179032>). The test selection algorithm proceeds as follows: Input vectors of class numeric or integer are considered numerical; those of class factor are considered categorical; those of class ordered are considered ordinal. Assumptions of residual normality and homogeneity of variances are considered met if the corresponding test yields a p-value greater than the significance level alpha = 1 - conf.level. (1) When the response is numerical and the predictor is categorical, a test comparing central tendencies is selected. If every group contains more than 50 observations, the sampling distribution of the group means is assumed approximately normal by the central limit theorem (Lumley et al. (2002) <doi:10.1146/annurev.publhealth.23.100901.140546>); otherwise, residual normality is assessed using shapiro.test() applied to the standardised residuals of lm(). If normality is not met, wilcox.test() is used when the predictor has two levels and kruskal.test() followed by pairwise.wilcox.test() otherwise. If normality is met, levene.test() assesses variance homogeneity. For two-level predictors, Student's t.test(var.equal = TRUE) is applied if variances are homogeneous and Welch's t.test() otherwise. For predictors with more than two levels, aov() followed by TukeyHSD() is applied if variances are homogeneous, and oneway.test() followed by games.howell() otherwise. (2) When both vectors are numerical, lm() is fitted by default (correlation = FALSE). If correlation = TRUE, Spearman rank correlation is performed. (3) When the response is ordinal, it is converted to numeric ranks and the non-parametric path from (1) is followed (Wilcoxon or Kruskal-Wallis). When both variables are ordinal and correlation = TRUE, Kendall's tau_b is used instead. (4) When both vectors are categorical, Cochran's rule (Cochran (1954) <doi:10.2307/3001666>) is applied to test independence either by chisq.test() or fisher.test(). |
| Authors: | Sabine Schilling [cre, aut, cph] (ORCID: <https://orcid.org/0000-0002-8318-9421>, year: 2026), Peter Kauf [ctb] |
| Maintainer: | Sabine Schilling <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.2.1 |
| Built: | 2026-06-02 15:06:39 UTC |
| Source: | https://github.com/shhschilling/visstatistics |
Convert data frame of counts to dataframe of cases. The data frame must contain a column with frequencies (counts) as generated by as.data.frame from a contingency table
counts_to_cases(x, countcol = "Freq")counts_to_cases(x, countcol = "Freq")
x |
a |
countcol |
character string, name of the column of x containing the counts. Default name of the column is 'Freq'. |
data frame of cases of dimension (total number of counts as sum of 'Freq' in x) times 2.
counts_to_cases(as.data.frame(HairEyeColor[, , 1]), countcol = "Freq")counts_to_cases(as.data.frame(HairEyeColor[, , 1]), countcol = "Freq")
effect_size() returns the effect-size estimate associated with a
visstat() result. If result$effect_size is already present, it
is returned unchanged. Otherwise, the estimate is computed from the test
object stored in result; for some base R stats results, it is
extracted directly from the returned object.
effect_size(result, x = NULL, y = NULL)effect_size(result, x = NULL, y = NULL)
result |
A list returned by |
x |
First input vector, matching the first argument of
|
y |
Second input vector, matching the second argument of
|
Notation used below: and are the two variables entering
the selected analysis, is the total number of non-missing
observations, is the sample size in group , is
the number of groups, is the mean of numeric vector
in group , and is the variance of numeric
vector in group .
The following estimates are computed internally:
Student's two-sample t.test(..., var.equal = TRUE):
Hedges' , where
and
.
Welch's two-sample t.test(..., var.equal = FALSE):
Hedges' , where
and
.
Wilcoxon rank-sum test: signed rank-biserial correlation
, where is the statistic returned by
wilcox.test() for the first group.
Fisher's one-way ANOVA: omega-squared
, where is the
ordinary one-way ANOVA statistic, , and
. Negative estimates are truncated to zero.
Welch's one-way test: approximate omega-squared-type estimate
, where is the
Welch ANOVA statistic, , and is the
usually fractional denominator degree of freedom returned by
oneway.test(). Negative estimates are truncated to zero.
Kruskal-Wallis test: Kelley-adjusted eta-squared based on
, , where is the
Kruskal-Wallis statistic. Negative estimates are truncated to zero.
Pearson's chi-squared test: Cramer's for general
tables,
, where and
are the numbers of rows and columns. For tables this
is phi, . The chi-squared
statistic is used as supplied by chisq.test().
The following estimates are extracted from existing result objects:
from summary(lm())$r.squared.
Spearman's from
cor.test(method = "spearman")$estimate.
Kendall's from
cor.test(method = "kendall")$estimate.
The conditional maximum-likelihood odds ratio from
fisher.test()$estimate and its confidence interval from
fisher.test()$conf.int for tables.
A list with components name, estimate,
effect_size_method, and optionally conf.int.
Hedges, L. V. (1981). Distribution theory for Glass's estimator of effect size and related estimators. Journal of Educational Statistics, 6(2), 107–128. doi:10.3102/10769986006002107.
Delacre, M., Lakens, D., Ley, C., Liu, L., & Leys, C. (2021). Why Hedges' g*s based on the non-pooled standard deviation should be reported with Welch's t-test. PsyArXiv. doi:10.31234/osf.io/tu6mp.
Glass, G. V. (1965). A ranking variable analogue of biserial correlation: Implications for short-cut item analysis. Journal of Educational Measurement, 2(1), 91–95. doi:10.1111/j.1745-3984.1965.tb00396.x.
Albers, C., & Lakens, D. (2018). When power analyses based on pilot data are biased: Inaccurate effect size estimators and follow-up bias. Journal of Experimental Social Psychology, 74, 187–195. doi:10.1016/j.jesp.2017.09.004.
Kelley, T. L. (1935). An unbiased correlation ratio measure. Proceedings of the National Academy of Sciences, 21(9), 554–559. doi:10.1073/pnas.21.9.554.
Cohen, J. (2013). Statistical power analysis for the behavioral sciences. Routledge. doi:10.4324/9780203771587.
x <- ToothGrowth$supp y <- ToothGrowth$len tt <- list("t-test-statistics" = t.test(y ~ x, var.equal = TRUE)) effect_size(tt, x = x, y = y) kw <- list( "Kruskal Wallis rank sum test" = kruskal.test(Petal.Width ~ Species, data = iris) ) effect_size(kw, x = iris$Species, y = iris$Petal.Width) tab <- matrix(c(10, 5, 4, 12), nrow = 2) effect_size(chisq.test(tab)) ## Not run: ## Large-sample example with a statistically significant Student's ## t-test p-value but a small effect size, measured by Hedges' g ## using the pooled standard deviation. A small mean shift is added ## to noisy normal data. Because N is large, the t-test p-value ## becomes small, while Hedges' g remains close to zero. ## The residual Shapiro-Wilk p-value in the diagnostic panel is NA ## because shapiro.test() is limited to n <= 5000. set.seed(20260525) n <- 2501 mean_shift <- 0.1 group <- factor(rep(c("control", "treatment"), each = n)) response <- rnorm(2 * n) + rep(c(0, mean_shift), each = n) res <- visstat(group, response) res[["t-test-statistics"]]$method res[["t-test-statistics"]]$p.value res$effect_size$effect_size_method res$effect_size$estimate ## End(Not run)x <- ToothGrowth$supp y <- ToothGrowth$len tt <- list("t-test-statistics" = t.test(y ~ x, var.equal = TRUE)) effect_size(tt, x = x, y = y) kw <- list( "Kruskal Wallis rank sum test" = kruskal.test(Petal.Width ~ Species, data = iris) ) effect_size(kw, x = iris$Species, y = iris$Petal.Width) tab <- matrix(c(10, 5, 4, 12), nrow = 2) effect_size(chisq.test(tab)) ## Not run: ## Large-sample example with a statistically significant Student's ## t-test p-value but a small effect size, measured by Hedges' g ## using the pooled standard deviation. A small mean shift is added ## to noisy normal data. Because N is large, the t-test p-value ## becomes small, while Hedges' g remains close to zero. ## The residual Shapiro-Wilk p-value in the diagnostic panel is NA ## because shapiro.test() is limited to n <= 5000. set.seed(20260525) n <- 2501 mean_shift <- 0.1 group <- factor(rep(c("control", "treatment"), each = n)) response <- rnorm(2 * n) + rep(c(0, mean_shift), each = n) res <- visstat(group, response) res[["t-test-statistics"]]$method res[["t-test-statistics"]]$p.value res$effect_size$effect_size_method res$effect_size$estimate ## End(Not run)
Cairo wrapper function returning NULL if not type is specified.
Enhanced version that can capture plots for later replay.
openGraphCairo( width = 640, height = 480, fileName = NULL, type = NULL, fileDirectory = getwd(), pointsize = 12, bg = "transparent", canvas = "white", units = "px", dpi = 150 )openGraphCairo( width = 640, height = 480, fileName = NULL, type = NULL, fileDirectory = getwd(), pointsize = 12, bg = "transparent", canvas = "white", units = "px", dpi = 150 )
width |
see |
height |
see |
fileName |
name of file to be created. Does not include both file
extension '. |
type |
Supported output types are 'png', 'jpeg', 'pdf', 'svg', 'ps' and
'tiff'. See |
fileDirectory |
path of directory, where plot is stored. Default current working directory. |
pointsize |
see |
bg |
see |
canvas |
see |
units |
see |
dpi |
DPI used for the conversion of units to pixels. Default value 150. |
openGraphCairo() Cairo() wrapper function. Differences to
Cairo: a) prematurely ends the function call to Cairo()
returning NULL, if no output type of types 'png', 'jpeg', 'pdf',
'svg', 'ps' or 'tiff' is provided. b) The file argument of the
underlying Cairo function is generated by
file.path(fileDirectory,paste(fileName,'.', type, sep = '')).
c) Can set up plot capture when capture_env is provided.
NULL, if no type is specified. Otherwise see Cairo()
## adapted from example in \code{Cairo()} openGraphCairo(fileName = "normal_dist", type = "pdf", fileDirectory = tempdir()) plot(rnorm(4000), rnorm(4000), col = "#ff000018", pch = 19, cex = 2) dev.off() # creates a file 'normal_dist.pdf' in the directory specified in fileDirectory # ## remove the plot from fileDirectory file.remove(file.path(tempdir(), "normal_dist.pdf"))## adapted from example in \code{Cairo()} openGraphCairo(fileName = "normal_dist", type = "pdf", fileDirectory = tempdir()) plot(rnorm(4000), rnorm(4000), col = "#ff000018", pch = 19, cex = 2) dev.off() # creates a file 'normal_dist.pdf' in the directory specified in fileDirectory # ## remove the plot from fileDirectory file.remove(file.path(tempdir(), "normal_dist.pdf"))
Closes all graphical devices with dev.off() and saves the output only
if both fileName and type are provided. Enhanced version that
can capture plots before closing devices.
saveGraphVisstat( fileName = NULL, type = NULL, fileDirectory = getwd(), oldfile = NULL, capture_env = NULL )saveGraphVisstat( fileName = NULL, type = NULL, fileDirectory = getwd(), oldfile = NULL, capture_env = NULL )
fileName |
name of file to be created in directory |
type |
see |
fileDirectory |
path of directory, where graphic is stored. Default setting current working directory. |
oldfile |
old file of same name to be overwritten |
capture_env |
Environment to store captured plots. If NULL, no capture occurs. |
NULL, if no type or fileName is provided, file path if graph
is created
# very simple KDE (adapted from example in Cairo()) openGraphCairo(type = "png", fileDirectory = tempdir()) plot(rnorm(4000), rnorm(4000), col = "#ff000018", pch = 19, cex = 2) # save file 'norm.png' in directory specified in fileDirectory saveGraphVisstat("norm", type = "png", fileDirectory = tempdir()) file.remove(file.path(tempdir(), "norm.png")) # remove file 'norm.png'# very simple KDE (adapted from example in Cairo()) openGraphCairo(type = "png", fileDirectory = tempdir()) plot(rnorm(4000), rnorm(4000), col = "#ff000018", pch = 19, cex = 2) # save file 'norm.png' in directory specified in fileDirectory saveGraphVisstat("norm", type = "png", fileDirectory = tempdir()) file.remove(file.path(tempdir(), "norm.png")) # remove file 'norm.png'
visstat() is a wrapper around visstat_core
that provides three alternative input styles: a formula interface, a
standardised vector interface, and a backward-compatible data frame interface.
visstat_core defines the decision logic for statistical
hypothesis testing and visualisation between two variables of class
"numeric", "integer", or "factor".
visstat( x, y, ..., data = NULL, conf.level = 0.95, correlation = FALSE, numbers = TRUE, minpercent = 0.05, graphicsoutput = NULL, plotName = NULL, plotDirectory = getwd() )visstat( x, y, ..., data = NULL, conf.level = 0.95, correlation = FALSE, numbers = TRUE, minpercent = 0.05, graphicsoutput = NULL, plotName = NULL, plotDirectory = getwd() )
x |
For the formula interface: a formula of the form |
y |
For the formula interface: not used (variables are extracted from
the formula). For the standardised form: a vector of class |
... |
For the backward-compatible form only: a |
data |
A |
conf.level |
Confidence level for statistical inference; default is
|
correlation |
Logical. If |
numbers |
Logical. Whether to annotate plots with numeric values. |
minpercent |
Number between 0 and 1 indicating minimal fraction of total count data of a category to be displayed in mosaic count plots. |
graphicsoutput |
Saves plot(s) of type |
plotName |
Graphical output is stored following the naming convention
|
plotDirectory |
Specifies directory where generated plots are stored. Default is current working directory. |
This wrapper supports three input formats:
(1) Formula interface: visstat(y ~ x, data = df), where the formula
specifies the response (y) and predictor (x) variables, and
data is a data frame containing these variables.
(2) Standardised form: visstat(x, y), where both x and y
are vectors of class "numeric", "integer", or "factor".
Here x is the predictor or grouping variable and y is the
response variable.
(3) Backward-compatible form: visstat(dataframe, "name_of_y", "name_of_x"),
where the first character string refers to the response variable and the
second to the predictor or grouping variable. Both must be column names in
dataframe. This form gives a warning and may be removed in a future
version.
The interpretation of x and y depends on the variable classes.
Throughout, data of class numeric or integer are referred to
as numeric, while data of class factor are referred to as categorical:
If one variable is numeric and the other a factor, the numeric vector is the
response (y) and the factor is the grouping variable (x). This
supports tests of central tendencies (e.g., t-test, Welch's ANOVA, Wilcoxon,
Kruskal-Wallis).
If both variables are numeric, a linear model is fitted with y as the
response and x as the predictor.
If both variables are factors, an association test (Chi-squared or Fisher's
exact) is used. The test result is invariant to variable order, but
visualisations (e.g., axis layout, bar orientation) depend on the roles of
x and y.
This wrapper standardises the input and calls visstat_core,
which selects and executes the appropriate test with visual output and
assumption diagnostics.
An object of class "visstat" containing the results of
the automatically selected statistical test. The specific contents depend on
which test was performed. Additionally, the returned object includes two
attributes:
plot_paths: Character vector of file paths where plots were
saved (if graphicsoutput was specified)
captured_plots: List of captured plot objects for programmatic
access
In case of insufficient data, returns a list with an error element and
basic input summary information.
For best visualization, ensure that the RStudio Plots pane is adequately
sized. If you get "figure margins too large" errors, try expanding the Plots
pane in RStudio, using dev.new(width=10, height=6) for a larger plot
window, or reducing the cex parameter.
visstat_core defining the decision logic, the
package's vignette vignette("visStatistics") explaining the decision
logic accompanied by illustrative examples, and the accompanying webpage
https://shhschilling.github.io/visStatistics/.
# Formula interface mtcars$am <- as.factor(mtcars$am) visstat(mpg ~ am, data = mtcars) # Standardised usage visstat(mtcars$am, mtcars$mpg) ## Student's t-test (equal variances, two groups) # When residuals are normally distributed and Levene's test indicates # homoscedasticity, the classic Student's t-test with pooled variance is used df <- droplevels(subset(PlantGrowth, group %in% c("ctrl", "trt1"))) visstat(df$group,df$weight) ## Welch's t-test (unequal variances, two groups) # When residuals are normally distributed but Levene's test indicates # heteroscedasticity, Welch's t-test is used visstat(mtcars$am, mtcars$mpg) ## Wilcoxon rank sum test (non-normal, two groups) # When residuals are not normally distributed grades_gender <- data.frame( Sex = as.factor(c(rep("Girl", 20), rep("Boy", 20))), Grade = c( 19.3, 18.1, 15.2, 18.3, 7.9, 6.2, 19.4, 20.3, 9.3, 11.3, 18.2, 17.5, 10.2, 20.1, 13.3, 17.2, 15.1, 16.2, 17.3, 16.5, 5.1, 15.3, 17.1, 14.8, 15.4, 14.4, 7.5, 15.5, 6.0, 17.4, 7.3, 14.3, 13.5, 8.0, 19.5, 13.4, 17.9, 17.7, 16.4, 15.6 ) ) visstat(grades_gender$Sex, grades_gender$Grade) ## Fisher's ANOVA (equal variances, >2 groups) # When residuals are normally distributed and Levene's test indicates # homoscedasticity, classic Fisher's ANOVA with TukeyHSD post-hoc is used. # Different green letters indicate significant differences between groups. visstat(PlantGrowth$group, PlantGrowth$weight) ## Welch's one-way ANOVA (unequal variances, >2 groups) set.seed(123) values <- c(rnorm(20, 10, 1),rnorm(20, 15, 5),rnorm(20, 12, 2)) groups <- factor(rep(c("A", "B", "C"), each = 20)) visstat(groups, values) ## Kruskal-Wallis (non-normal, >2 groups) # When residuals are not normally distributed, kruskal.test() is followed by # pairwise.wilcox.test. visstat(iris$Species, iris$Petal.Width) ## Simple linear regression (both numeric) visstat(trees$Height, trees$Girth, conf.level = 0.99) ## Pearson's Chi-squared test (both factors, large expected counts) HairEyeColorDataFrame <- counts_to_cases(as.data.frame(HairEyeColor)) visstat(HairEyeColorDataFrame$Eye, HairEyeColorDataFrame$Hair) ## Fisher's exact test (both factors, small expected counts) HairEyeColorMaleFisher <- HairEyeColor[, , 1] blackBrownHazelGreen <- HairEyeColorMaleFisher[1:2, 3:4] blackBrownHazelGreen <- counts_to_cases(as.data.frame(blackBrownHazelGreen)) visstat(blackBrownHazelGreen$Eye, blackBrownHazelGreen$Hair) ## Save PNG visstat(blackBrownHazelGreen$Hair, blackBrownHazelGreen$Eye, graphicsoutput = "png", plotDirectory = tempdir()) ## Custom plot name visstat(iris$Species, iris$Petal.Width, graphicsoutput = "pdf", plotName = "kruskal_iris", plotDirectory = tempdir())# Formula interface mtcars$am <- as.factor(mtcars$am) visstat(mpg ~ am, data = mtcars) # Standardised usage visstat(mtcars$am, mtcars$mpg) ## Student's t-test (equal variances, two groups) # When residuals are normally distributed and Levene's test indicates # homoscedasticity, the classic Student's t-test with pooled variance is used df <- droplevels(subset(PlantGrowth, group %in% c("ctrl", "trt1"))) visstat(df$group,df$weight) ## Welch's t-test (unequal variances, two groups) # When residuals are normally distributed but Levene's test indicates # heteroscedasticity, Welch's t-test is used visstat(mtcars$am, mtcars$mpg) ## Wilcoxon rank sum test (non-normal, two groups) # When residuals are not normally distributed grades_gender <- data.frame( Sex = as.factor(c(rep("Girl", 20), rep("Boy", 20))), Grade = c( 19.3, 18.1, 15.2, 18.3, 7.9, 6.2, 19.4, 20.3, 9.3, 11.3, 18.2, 17.5, 10.2, 20.1, 13.3, 17.2, 15.1, 16.2, 17.3, 16.5, 5.1, 15.3, 17.1, 14.8, 15.4, 14.4, 7.5, 15.5, 6.0, 17.4, 7.3, 14.3, 13.5, 8.0, 19.5, 13.4, 17.9, 17.7, 16.4, 15.6 ) ) visstat(grades_gender$Sex, grades_gender$Grade) ## Fisher's ANOVA (equal variances, >2 groups) # When residuals are normally distributed and Levene's test indicates # homoscedasticity, classic Fisher's ANOVA with TukeyHSD post-hoc is used. # Different green letters indicate significant differences between groups. visstat(PlantGrowth$group, PlantGrowth$weight) ## Welch's one-way ANOVA (unequal variances, >2 groups) set.seed(123) values <- c(rnorm(20, 10, 1),rnorm(20, 15, 5),rnorm(20, 12, 2)) groups <- factor(rep(c("A", "B", "C"), each = 20)) visstat(groups, values) ## Kruskal-Wallis (non-normal, >2 groups) # When residuals are not normally distributed, kruskal.test() is followed by # pairwise.wilcox.test. visstat(iris$Species, iris$Petal.Width) ## Simple linear regression (both numeric) visstat(trees$Height, trees$Girth, conf.level = 0.99) ## Pearson's Chi-squared test (both factors, large expected counts) HairEyeColorDataFrame <- counts_to_cases(as.data.frame(HairEyeColor)) visstat(HairEyeColorDataFrame$Eye, HairEyeColorDataFrame$Hair) ## Fisher's exact test (both factors, small expected counts) HairEyeColorMaleFisher <- HairEyeColor[, , 1] blackBrownHazelGreen <- HairEyeColorMaleFisher[1:2, 3:4] blackBrownHazelGreen <- counts_to_cases(as.data.frame(blackBrownHazelGreen)) visstat(blackBrownHazelGreen$Eye, blackBrownHazelGreen$Hair) ## Save PNG visstat(blackBrownHazelGreen$Hair, blackBrownHazelGreen$Eye, graphicsoutput = "png", plotDirectory = tempdir()) ## Custom plot name visstat(iris$Species, iris$Petal.Width, graphicsoutput = "pdf", plotName = "kruskal_iris", plotDirectory = tempdir())