Skip to content

Commit 9cbdd04

Browse files
authored
Merge pull request #1515 from exploratory-io/fix/issue-36055
Add probability distribution option to One-Sample Proportion Test and Two-Sample t-Test
2 parents e3d5d4a + 6386934 commit 9cbdd04

5 files changed

Lines changed: 140 additions & 2 deletions

File tree

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: exploratory
22
Type: Package
33
Title: R package for Exploratory
4-
Version: 15.1.11
4+
Version: 15.1.12
55
Date: 2026-05-30
66
Authors@R: c(person("Hideaki", "Hayashi", email = "hideaki@exploratory.io", role = c("aut", "cre")), person("Hide", "Kojima", email = "hide@exploratory.io", role = c("aut")), person("Kan", "Nishida", email = "kan@exploratory.io", role = c("aut")), person("Kei", "Saito", email = "kei@exploratory.io", role = c("aut")), person("Yosuke", "Yasuda", email = "double.y.919.quick@gmail.com", role = c("aut")))
77
URL: https://github.com/exploratory-io/exploratory_func

R/test_wrapper.R

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,33 @@ tidy.ttest_exploratory <- function(x, type="model", conf_level=0.95) {
12941294
ret <- generate_ttest_density_data(x$statistic, p.value=x$p.value, x$parameter, sig_level=x$test_sig_level, alternative=x$alternative)
12951295
ret
12961296
}
1297+
else if (type == "prob_dist_diff") {
1298+
if ("error" %in% class(x)) {
1299+
ret <- tibble::tibble()
1300+
return(ret)
1301+
}
1302+
# Data for the probability distribution (line) chart on the DIFFERENCE scale. This is
1303+
# the sampling distribution of the difference of means under H0 "no difference", i.e.
1304+
# the t-value distribution relocated/rescaled onto the difference axis via
1305+
# difference = 0 + t * stderr. We reuse the t-value generator and remap its x column
1306+
# so the shading semantics (critical region, sig.level based) stay identical to the
1307+
# t-value tab. The marked statistic point lands at t * stderr = the observed
1308+
# difference, and the curve peaks at 0 (the hypothesized difference).
1309+
# estimate is c(mean of group1, mean of group2) for an unpaired test, or a single
1310+
# value (mean of the differences) for a paired test; stderr = difference / t.
1311+
est <- unname(x$estimate)
1312+
difference <- if (length(est) >= 2) est[1] - est[2] else est[1]
1313+
t_stat <- unname(x$statistic)
1314+
se_val <- if (!is.na(t_stat) && t_stat != 0) abs(difference / t_stat) else NA_real_
1315+
if (is.na(se_val) || se_val == 0) {
1316+
ret <- tibble::tibble(x = numeric(0), y = numeric(0)) # degenerate input -> chart no-ops
1317+
} else {
1318+
ret <- generate_ttest_density_data(t = t_stat, p.value = x$p.value, df = unname(x$parameter),
1319+
sig_level = x$test_sig_level, alternative = x$alternative) %>%
1320+
dplyr::mutate(x = x * se_val)
1321+
}
1322+
ret
1323+
}
12971324
else { # type == "data"
12981325
if ("error" %in% class(x)) {
12991326
ret <- tibble::tibble()
@@ -3429,7 +3456,7 @@ exp_one_sample_prop_test <- function(df, var, p = 0.5, alternative = "two.sided"
34293456

34303457
model <- list(htest = res, x = x, n = n, p = p, observed_prop = observed_prop,
34313458
success_value = "TRUE", alternative = alternative,
3432-
method_used = method_used, conf_level = conf_level, z = z,
3459+
method_used = method_used, conf_level = conf_level, z = z, se = se,
34333460
cohens_h = cohens_h, power = power, var_col = var_col,
34343461
sig.level = sig.level)
34353462
class(model) <- c("one_sample_prop_test_exploratory", class(model))
@@ -3488,6 +3515,22 @@ tidy.one_sample_prop_test_exploratory <- function(x, type = "model") {
34883515
# statistic (x$z, the same value shown in the summary table) on it.
34893516
generate_norm_density_data(x$z, p.value = x$htest$p.value, mu = 0, sigma = 1,
34903517
sig_level = x$sig.level, alternative = x$alternative)
3518+
} else if (type == "prob_dist_prop") {
3519+
# Data for the probability distribution (line) chart on the PROPORTION scale.
3520+
# This is the sampling distribution of the sample proportion under H0
3521+
# "the proportion equals the benchmark": a normal distribution centered at the
3522+
# benchmark proportion (x$p) with standard deviation equal to the standard error
3523+
# sqrt(p0 * (1 - p0) / n). We mark the observed proportion (x$observed_prop, the
3524+
# same value shown in the summary table) on it. The shaded critical region matches
3525+
# the Z value chart (sig.level based), so both probability distribution charts read
3526+
# the same way.
3527+
if (is.na(x$se) || x$se <= 0) {
3528+
tibble::tibble(x = numeric(0), y = numeric(0)) # degenerate input -> chart no-ops
3529+
} else {
3530+
generate_norm_density_data(x$observed_prop, p.value = x$htest$p.value, mu = x$p, sigma = x$se,
3531+
sig_level = x$sig.level, alternative = x$alternative) %>%
3532+
dplyr::filter(x >= 0 & x <= 1)
3533+
}
34913534
} else { # type == "data"
34923535
# Return the original data so that the data-level charts (Error Bar Plot,
34933536
# Data Distribution) can map to the target column. The chart templates

tests/testthat/test_one_sample_prop_test.R

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,60 @@ test_that("tidy prob_dist critical region follows the alternative direction", {
360360
ts <- tidy(exp_one_sample_prop_test(df, outcome, p = 0.4, alternative = "two.sided", method = "approximate")$model[[1]], type = "prob_dist")
361361
expect_true(any(ts$x[which(ts$critical)] > 0) && any(ts$x[which(ts$critical)] < 0))
362362
})
363+
364+
# --- tidy(type = "prob_dist_prop") ---
365+
366+
test_that("tidy prob_dist_prop marks the observed proportion on the proportion scale", {
367+
df <- data.frame(outcome = c(rep(TRUE, 50), rep(FALSE, 50)))
368+
model <- exp_one_sample_prop_test(df, outcome, p = 0.4, method = "approximate")$model[[1]]
369+
pd <- tidy(model, type = "prob_dist_prop")
370+
expect_true(all(c("x", "y", "statistic", "critical", "p.value") %in% names(pd)))
371+
# SE = sqrt(p0*(1-p0)/n); the marked point sits at the observed proportion (NOT z).
372+
se <- sqrt(0.4 * 0.6 / 100)
373+
marked <- pd[which(pd$statistic), ]
374+
expect_equal(nrow(marked), 1)
375+
expect_equal(marked$x, 0.5)
376+
expect_equal(marked$p.value, model$htest$p.value)
377+
# Normal centered at the benchmark proportion p0 with sd = SE.
378+
expect_equal(unique(pd$mean), 0.4)
379+
expect_equal(unique(pd$sd), se)
380+
expect_true(max(pd$y, na.rm = TRUE) <= dnorm(0.4, mean = 0.4, sd = se) + 1e-9)
381+
})
382+
383+
test_that("tidy prob_dist_prop always uses the proportion-scale normal even for the exact method", {
384+
df <- data.frame(outcome = c(rep(TRUE, 12), rep(FALSE, 88)))
385+
model <- exp_one_sample_prop_test(df, outcome, p = 0.1, method = "exact")$model[[1]]
386+
expect_equal(model$method_used, "Exact Binomial Test")
387+
pd <- tidy(model, type = "prob_dist_prop")
388+
se <- sqrt(0.1 * 0.9 / 100)
389+
marked <- pd[which(pd$statistic), ]
390+
expect_equal(nrow(marked), 1)
391+
expect_equal(marked$x, 0.12)
392+
expect_equal(unique(pd$mean), 0.1)
393+
expect_equal(unique(pd$sd), se)
394+
})
395+
396+
test_that("tidy prob_dist_prop critical region follows the alternative direction", {
397+
df <- data.frame(outcome = c(rep(TRUE, 50), rep(FALSE, 50)))
398+
p0 <- 0.4
399+
# greater -> rejection region is the upper tail only (above the benchmark proportion).
400+
gt <- tidy(exp_one_sample_prop_test(df, outcome, p = p0, alternative = "greater", method = "approximate")$model[[1]], type = "prob_dist_prop")
401+
expect_true(all(gt$x[which(gt$critical)] > p0))
402+
# less -> rejection region is the lower tail only.
403+
lt <- tidy(exp_one_sample_prop_test(df, outcome, p = p0, alternative = "less", method = "approximate")$model[[1]], type = "prob_dist_prop")
404+
expect_true(all(lt$x[which(lt$critical)] < p0))
405+
# two.sided -> both tails relative to the benchmark proportion.
406+
ts <- tidy(exp_one_sample_prop_test(df, outcome, p = p0, alternative = "two.sided", method = "approximate")$model[[1]], type = "prob_dist_prop")
407+
expect_true(any(ts$x[which(ts$critical)] > p0) && any(ts$x[which(ts$critical)] < p0))
408+
})
409+
410+
test_that("tidy prob_dist_prop returns an empty tibble when the SE is 0/NA", {
411+
# n*p0 produces a valid SE normally; build a degenerate model object directly to
412+
# exercise the guard (the benchmark SE is 0 only for a degenerate p0).
413+
model <- structure(
414+
list(htest = list(p.value = NA_real_), p = 0.4, observed_prop = 0.5,
415+
se = 0, sig.level = 0.05, alternative = "two.sided"),
416+
class = c("one_sample_prop_test_exploratory", "list"))
417+
pd <- tidy(model, type = "prob_dist_prop")
418+
expect_equal(nrow(pd), 0)
419+
})

tests/testthat/test_test_wrapper_1.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ test_that("test exp_ttest_aggregated", {
141141
ret <- model_df %>% tidy_rowwise(model, type="data_summary")
142142
ret <- model_df %>% tidy_rowwise(model, type="prob_dist")
143143
expect_true("p.value" %in% colnames(ret))
144+
# Difference-scale probability distribution (shares the ttest_exploratory tidy branch).
145+
ret_diff <- model_df %>% tidy_rowwise(model, type="prob_dist_diff")
146+
expect_true(all(c("x", "y", "statistic", "critical", "p.value") %in% colnames(ret_diff)))
144147
})
145148

146149
test_that("test two sample t-test with column name", {

tests/testthat/test_test_wrapper_2.R

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,38 @@ test_that("test exp_wilcox with group-level error", {
231231
ret <- model_df %>% tidy_rowwise(model, type='prob_dist')
232232
expect_equal(nrow(ret), 0)
233233
})
234+
235+
# --- tidy(type = "prob_dist_diff") for two-sample t-test ---
236+
237+
test_that("exp_ttest tidy prob_dist_diff marks the observed difference on the difference scale", {
238+
# Two clearly separated groups so the mean difference is non-trivial.
239+
test_df <- data.frame(
240+
cat = c(rep("cat1", 20), rep("cat2", 20)),
241+
val = c(seq(1, 20), seq(4, 23))
242+
)
243+
model_df <- test_df %>% exp_ttest(val, cat)
244+
pd <- model_df %>% tidy_rowwise(model, type = "prob_dist_diff")
245+
expect_true(all(c("x", "y", "statistic", "critical", "p.value") %in% names(pd)))
246+
diff <- (model_df %>% tidy_rowwise(model, type = "model"))$Difference
247+
marked <- pd[which(pd$statistic), ]
248+
expect_equal(nrow(marked), 1)
249+
# The marked point sits at the observed mean difference, NOT the standardized t.
250+
expect_equal(marked$x, diff, tolerance = 1e-6)
251+
# The difference-scale distribution is centered at 0 (H0: no difference): the density
252+
# peak (the curve rows, excluding the marked statistic point) is at x = 0.
253+
curve <- pd[!is.na(pd$y) & is.na(pd$statistic), ]
254+
expect_equal(curve$x[which.max(curve$y)], 0, tolerance = 1e-6)
255+
})
256+
257+
test_that("exp_ttest tidy prob_dist_diff critical region follows the alternative direction", {
258+
test_df <- data.frame(
259+
cat = c(rep("cat1", 20), rep("cat2", 20)),
260+
val = c(seq(1, 20), seq(4, 23))
261+
)
262+
gt <- test_df %>% exp_ttest(val, cat, alternative = "greater") %>% tidy_rowwise(model, type = "prob_dist_diff")
263+
expect_true(all(gt$x[which(gt$critical)] > 0))
264+
lt <- test_df %>% exp_ttest(val, cat, alternative = "less") %>% tidy_rowwise(model, type = "prob_dist_diff")
265+
expect_true(all(lt$x[which(lt$critical)] < 0))
266+
ts <- test_df %>% exp_ttest(val, cat, alternative = "two.sided") %>% tidy_rowwise(model, type = "prob_dist_diff")
267+
expect_true(any(ts$x[which(ts$critical)] > 0) && any(ts$x[which(ts$critical)] < 0))
268+
})

0 commit comments

Comments
 (0)