Executive Summary
Did the yes rate really differ across Checkout Version?
The short answer
No. The new checkout's 19.1% conversion rate is 6.25 percentage points higher than the old checkout's 12.8%, but the p-value of 0.207 indicates this difference could easily arise from random sampling variation. The data do not establish a genuine difference.
The detail
New Checkout: 19.1%; Old Checkout: 12.8%; difference: 6.25 percentage points (p = 0.207, two-sample proportion z-test). The Newcombe 95% confidence interval on the difference spans -3.57 to 15.99 percentage points—wide enough to include zero, consistent with no true difference. The continuity-corrected chi-square cross-check yielded p = 0.281, in agreement. With 219 observations across two groups, the data lack sufficient power to distinguish this observed gap from chance.
What this can't tell you
Non-significance is not proof of equality. The true difference could plausibly range from -3.57 to 15.99 percentage points. A larger sample might detect a genuine effect of this magnitude, or the true effect might be smaller than the observed gap.
Analysis Overview
Comparison of yes rates in Converted between the Checkout Version groups New Checkout and Old Checkout (219 observations).
The short answer
The analysis compares conversion rates between two checkout versions using confidence intervals and a formal statistical test. The New Checkout shows a 6.25 percentage-point higher rate than the Old Checkout, but this difference falls within the range that sampling variation alone could produce.
The detail
This comparison uses Wilson 95% confidence intervals for each group's rate, a Newcombe hybrid interval for the difference, and a two-sample proportion z-test to determine whether the gap is statistically significant. The smallest cell count in the 2×2 contingency table is 14, well above the minimum of 5 required for the z-test to be valid. 'Yes' in the Converted field was treated as success. The observed difference of 6.25 percentage points carries a Newcombe 95% interval of -3.57 to 15.99 percentage points, spanning both negative and positive values.
What this can't tell you
Statistical non-significance does not prove the rates are equal—it means the data cannot rule out sampling noise as the explanation for the observed gap. The Newcombe interval shows which true differences remain plausible given this sample size and observed rates.
Data Quality
Row cleaning, group filtering, and how the binary outcome was read.
The short answer
All 219 rows were usable; none were dropped for missing values. Both Converted and Checkout Version columns were complete. The outcome was read as binary: 'yes' counted as success, 'no' as non-success. The data contained exactly two Checkout Version groups meeting the minimum 3-row threshold.
The detail
Initial and final row counts were both 219; rows_removed = 0; groups_dropped = 0. New Checkout comprised n=110 observations; Old Checkout comprised n=109. The binary outcome 'Converted' was standardized: 'yes' is a recognized affirmative value (like 1/TRUE/yes), so it was treated as the success level. No rows were filtered for missing values in either column.
What this can't tell you
This report does not address missing data patterns or whether missingness correlates with group assignment—the data supplied contained no missing values.
Rates with Confidence Intervals
Per-group yes rates with Wilson 95% confidence intervals.
The short answer
The New Checkout rate of 19.1% sits visibly above the Old Checkout rate of 12.8%, but their confidence intervals overlap substantially. This overlap is consistent with the formal test result: no statistically significant difference.
The detail
New Checkout: 19.1% yes (Wilson 95% CI: 12.8% to 27.4%). Old Checkout: 12.8% yes (Wilson 95% CI: 7.8% to 20.4%). The intervals overlap from 12.8% to 20.4%, meaning both rates are compatible with a common underlying true rate within that range. However, overlapping intervals do not themselves determine statistical significance—the formal verdict comes from the two-sample proportion z-test (p = 0.207), which accounts for both the gap and the within-group variability.
What this can't tell you
Visual overlap of confidence intervals is a rough guide only. The formal test is required to distinguish a true difference from sampling noise, and that test shows no significant difference here.
Statistical Tests
z-test and chi-square cross-check on the yes rate difference.
| Test | Statistic | P Value | Interpretation |
|---|---|---|---|
| Two-sample proportion z-test | 1.261 | 0.207 | Tests whether the yes rate differs between New Checkout and Old Checkout. The observed gap is within the range chance alone could produce. |
| Chi-square test (continuity-corrected) | 1.16 | 0.281 | Cross-check on the same 2x2 table with the Yates continuity correction, which is slightly more conservative. The continuity-corrected chi-square cross-check agrees with the z-test verdict (p = 0.281 vs p = 0.207). |
The short answer
Both the z-test (p = 0.207) and its continuity-corrected chi-square cross-check (p = 0.281) agree: no statistically significant difference between the two checkout versions. The z-test was valid because all cells of the 2×2 table were well above 5 (smallest: 14 conversions in Old Checkout).
The detail
Two-sample proportion z-test: z = 1.261, p = 0.207. Chi-square test (continuity-corrected): χ² = 1.16, p = 0.281. Both tests assess the same null hypothesis (yes rate is equal across groups) and both yield p > 0.05. The z-test requires every cell ≥ 5; the smallest cell here was 14, so the normal approximation holds and Fisher's exact test was not invoked. The z-test carries the verdict.
What this can't tell you
These tests confirm that the observed 6.25 percentage point gap is not statistically significant, but they do not prove the true rates are identical.
Group Counts & Rates
n, successes, failures, and Wilson 95% CI per Checkout Version group.
| Group | N | Successes | Failures | Rate PCT | CI Low PCT | CI High PCT |
|---|---|---|---|---|---|---|
| New Checkout | 110 | 21 | 89 | 19.09 | 12.84 | 27.43 |
| Old Checkout | 109 | 14 | 95 | 12.84 | 7.81 | 20.41 |
The short answer
New Checkout recorded 21 conversions out of 110 attempts (19.1%); Old Checkout recorded 14 out of 109 (12.8%). The absolute difference is 6.25 percentage points. The two groups are nearly equal in size, so any imbalance is not a confound.
The detail
New Checkout: n = 110, successes = 21, failures = 89, rate = 19.09%, Wilson 95% CI [12.84%, 27.43%]. Old Checkout: n = 109, successes = 14, failures = 95, rate = 12.84%, Wilson 95% CI [7.81%, 20.41%]. Absolute difference = 6.25 percentage points. In relative terms, Old Checkout's rate is 32.7% lower than New Checkout's. Both groups exceed the minimum 3-row threshold by a wide margin.
What this can't tell you
These counts describe what was observed but do not establish whether the difference is real or due to chance—that verdict comes from the hypothesis test.
Methods & Disclosure
How the rates, intervals, and verdict were computed.
| Item | Detail |
|---|---|
| Method | Two-sample comparison of proportions: z-test on the pooled standard error, continuity-corrected chi-square cross-check, Fisher's exact fallback for small cells |
| Success level | 'yes' in Converted counts as a success; 'no' as a non-success. 'yes' is a recognized affirmative value (like 1/TRUE/yes), so it was treated as the success level. |
| Comparison | Difference in yes rate: New Checkout minus Old Checkout = 6.25 percentage points |
| Per-group intervals | Wilson score 95% confidence intervals (better small-sample and near-0/1 behavior than the naive Wald interval) |
| Difference interval | Newcombe hybrid 95% CI on the difference, built from the two Wilson intervals: -3.57 to 15.99 percentage points |
| Cell-count guard | The z-test requires every cell of the 2x2 table (successes and failures in each group) to be at least 5; the smallest cell here is 14, so the z-test is valid and used. |
| Test used for the verdict | two-sample proportion z-test |
| Significance level | alpha = 0.05, two-sided |
| Assumptions | Independent observations; each row is one trial; the two groups are independent samples |
The short answer
Each group's rate is reported with a Wilson 95% confidence interval, chosen for stable behavior in small samples. The difference between rates carries a Newcombe hybrid interval. The verdict comes from a two-sample proportion z-test, with a continuity-corrected chi-square as a cross-check; the smallest cell count is 14, so both tests are valid.
The detail
Success was defined as 'yes' in Converted. Rates use Wilson score intervals rather than the naive Wald interval because Wilson intervals behave sensibly near 0% and 100% and in small samples. The difference interval (Newcombe hybrid) is -3.57 to 15.99 percentage points. The z-test uses a pooled standard error; the continuity-corrected chi-square applies the Yates correction. Alpha = 0.05, two-sided. Assumptions: independent observations; each row one trial; two independent samples. The observed gap of 6.2 percentage points falls within the range sampling noise alone could produce.
What this can't tell you
Non-significance does not prove equality. The Newcombe interval shows the range of true differences compatible with the data; a larger sample would narrow this range and might reveal a genuine effect if one exists.
Methodology
Statistical methodology and diagnostics for Proportion Tests & Confidence Intervals
Statistical Method
Standard-library analysis: did the conversion rate really differ between two groups? Map a binary outcome (0/1, yes/no, TRUE/FALSE, converted/not) and a two-level group column and get each group's rate with a Wilson 95% confidence interval, the two-sample proportion z-test with a continuity-corrected chi-square cross-check, the absolute difference with a Newcombe hybrid confidence interval, the relative change as computed, and an automatic Fisher's exact fallback — clearly disclosed — whenever any cell count is too small for the z-test.
- The outcome column is binary — exactly two distinct values (0/1, yes/no, TRUE/FALSE, converted/not)
- The group column has exactly two usable levels; each row is one independent trial
- The two groups are independent samples (not paired or repeated measurements of the same units)
- Exactly two groups are compared; use a many-group categorical test for 3+ groups
- When the outcome levels are not standard yes/no tokens, the rarer level is assumed to be the event of interest and the report says so — check that assumption
- Statistical significance is not practical importance: a tiny rate gap becomes significant with enough rows, and a real gap can be missed with too few
- Paired or clustered designs (same users in both groups, repeated visits) violate the independence assumption and need different methods
Analysis Code
Complete R source code for this analysis
Proportion Tests & Confidence Intervals — Did the Rate Really Differ?
Compares a binary outcome (converted / clicked / churned / passed) between exactly two groups: per-group proportions with Wilson 95% confidence intervals, a two-sample proportion z-test with a continuity-corrected chi-square cross-check, the absolute difference with a Newcombe hybrid confidence interval, and Fisher's exact test as an automatic fallback when any cell count is too small for the z-test.
Why This Method?
Rates from finite samples wobble. The z-test answers whether the observed gap between two proportions is bigger than chance alone would produce, and the Wilson/Newcombe intervals show how large or small the true rates and their difference could plausibly be — which is the decision-relevant part. Small cell counts silently break the normal approximation, so the tool checks the cells and switches to Fisher's exact test when needed.
What This Analysis Covers
- Per-group conversion rates with Wilson 95% CIs (bar chart with error bars)
- Two-sample proportion z-test + continuity-corrected chi-square cross-check
- Absolute difference with a Newcombe hybrid 95% CI, relative change as computed
- Fisher's exact fallback when any cell count is below 5, clearly disclosed
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {outcome, group}. All narrative is derived from the user's own column names and computed values.
suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))Core Analysis Pipeline
compute_shared <- function(df, params, col_map = list()) {
# === SHARED EXPORTS ===
# initial_rows/final_rows/rows_removed/n_na_rows $ row accounting
# outcome_h / group_h $ humanized user names for the mapped columns
# l1 / l2 $ the two group levels (factor order)
# success_label / failure_label $ outcome levels (original casing)
# success_flagged / success_note $ how the success level was chosen
# n1/n2, x1/x2, p1/p2 $ per-group n, successes, proportions
# w1 / w2 $ Wilson 95% CIs per group (low/high)
# d / d_low / d_high $ absolute difference (l1 - l2) + Newcombe 95% CI
# rel_change / rel_ok $ relative change of l2 vs l1 (only when p1 > 0)
# min_cell / z_valid $ smallest cell count; z-test validity guard
# z_stat/p_z, chi_stat/chi_df/p_chi, p_fisher/fisher_or $ the tests
# p_primary / test_used / significant / verdict_label $ the verdict
# dropped_groups_df $ groups dropped for n < 3
# rates_df / test_df / group_summary_df / methods_df $ card datasets
# metrics / json_output
# === /SHARED EXPORTS ===Step 1: Resolve mapped columns (humanized for all prose)
initial_rows <- nrow(df)
outcome_h <- humanize_semantic("outcome", col_map)
group_h <- humanize_semantic("group", col_map)
if (!("outcome" %in% names(df)) || !("group" %in% names(df))) {
stop(sprintf("Proportion testing needs both '%s' (the binary outcome) and '%s' (the two groups) mapped.",
outcome_h, group_h))
}Step 2: Drop rows with a missing outcome or group; keep accounting
o_raw <- as.character(df$outcome)
g_raw <- as.character(df$group)
o_norm <- tolower(trimws(o_raw))
g_clean <- trimws(g_raw)
keep <- !is.na(o_raw) & o_norm != "" & o_norm != "na" &
!is.na(g_raw) & g_clean != ""
n_na_rows <- sum(!keep)
o_raw <- o_raw[keep]; o_norm <- o_norm[keep]
g_clean <- g_clean[keep]
if (length(o_norm) == 0) {
stop(sprintf("No rows with usable values in both '%s' and '%s' remained after cleaning.",
outcome_h, group_h))
}Step 3: The outcome must be binary — detect the success level robustly
lev_norm <- sort(unique(o_norm))
if (length(lev_norm) != 2) {
stop(sprintf("The outcome column '%s' must be binary (exactly 2 distinct values such as 0/1, yes/no, TRUE/FALSE); it has %d distinct value(s): %s. Pick a two-valued column.",
outcome_h, length(lev_norm),
paste(head(lev_norm, 6), collapse = ", ")))
}Original-casing display labels (first occurrence per normalized level)
disp <- sapply(lev_norm, function(l) trimws(o_raw[match(l, o_norm)]))
positive_tokens <- c("1", "true", "t", "yes", "y", "converted", "success",
"succeeded", "won", "pass", "passed", "clicked",
"purchased", "retained")
negative_tokens <- c("0", "false", "f", "no", "n", "not converted",
"not_converted", "non-converted", "failure", "failed",
"lost", "fail", "none")
in_pos <- lev_norm %in% positive_tokens
in_neg <- lev_norm %in% negative_tokens
success_flagged <- FALSE
if (sum(in_pos) == 1) {
success_norm <- lev_norm[in_pos]
success_note <- sprintf("'%s' is a recognized affirmative value (like 1/TRUE/yes), so it was treated as the success level.",
disp[in_pos])
} else if (sum(in_neg) == 1) {
success_norm <- lev_norm[!in_neg]
success_note <- sprintf("'%s' is a recognized negative value (like 0/FALSE/no), so the other level, '%s', was treated as the success level.",
disp[in_neg], disp[!in_neg])
} else {Ambiguous levels: prefer the RARER level as the event of interest, and flag it
cnt <- table(o_norm)[lev_norm]
success_norm <- lev_norm[which.min(as.integer(cnt))]
success_flagged <- TRUE
success_note <- sprintf("Neither '%s' nor '%s' is a standard yes/no token, so the RARER level, '%s', was assumed to be the event of interest (the success level). If the other level is the one you care about, note that every rate below is simply 100%% minus the reported one.",
disp[1], disp[2], disp[lev_norm == success_norm])
}
success_label <- unname(disp[lev_norm == success_norm])
failure_label <- unname(disp[lev_norm != success_norm])
y <- as.integer(o_norm == success_norm)Step 4: Clean the groups — drop n<3 (reported), require exactly 2 levels
tab <- table(g_clean)
small <- names(tab)[tab < 3]
dropped_groups_df <- data.frame(group = character(0), n = integer(0),
stringsAsFactors = FALSE)
if (length(small) > 0) {
dropped_groups_df <- data.frame(group = small, n = as.integer(tab[small]),
stringsAsFactors = FALSE)
sel <- !(g_clean %in% small)
g_clean <- g_clean[sel]
y <- y[sel]
}
gf <- factor(g_clean)
k <- nlevels(gf)
if (k < 2) {
stop(sprintf("Comparing proportions needs exactly 2 groups in '%s' with 3 or more rows each; only %d usable group(s) remained after cleaning. Check that '%s' really splits the data into two groups.",
group_h, k, group_h))
}
if (k > 2) {
stop(sprintf("This tool compares exactly two groups, but '%s' has %d usable levels (%s). Filter the data to two groups, or use a many-group categorical test instead.",
group_h, k, paste(levels(gf), collapse = ", ")))
}
final_rows <- length(y)
rows_removed <- initial_rows - final_rows
if (final_rows < 10) {
stop(sprintf("Only %d usable rows remained — at least 10 are needed to compare '%s' rates across '%s'.",
final_rows, outcome_h, group_h))
}
if (sum(y) == 0 || sum(y) == final_rows) {
stop(sprintf("Every usable row has the same '%s' outcome ('%s') — with no variation, no rate difference can be tested.",
outcome_h, if (sum(y) == 0) failure_label else success_label))
}Step 5: Per-group proportions + Wilson 95% CIs
l1 <- levels(gf)[1]; l2 <- levels(gf)[2]
y1 <- y[gf == l1]; y2 <- y[gf == l2]
n1 <- length(y1); n2 <- length(y2)
x1 <- sum(y1); x2 <- sum(y2)
p1 <- x1 / n1; p2 <- x2 / n2
w1 <- wilson_ci(x1, n1)
w2 <- wilson_ci(x2, n2)Step 6: Absolute difference (l1 - l2) with the Newcombe hybrid 95% CI
d <- p1 - p2
d_low <- d - sqrt((p1 - w1["low"])^2 + (w2["high"] - p2)^2)
d_high <- d + sqrt((w1["high"] - p1)^2 + (p2 - w2["low"])^2)
d_low <- unname(max(-1, d_low)); d_high <- unname(min(1, d_high))Relative change of l2 vs the l1 baseline — stated only when computable
rel_ok <- p1 > 0
rel_change <- if (rel_ok) (p2 - p1) / p1 else NA_real_Step 7: Cell-count guard, z-test, chi-square cross-check, Fisher fallback
cells <- c(x1, n1 - x1, x2, n2 - x2)
min_cell <- min(cells)
z_valid <- min_cell >= 5
p_pool <- (x1 + x2) / (n1 + n2)
se0 <- sqrt(p_pool * (1 - p_pool) * (1 / n1 + 1 / n2))
z_stat <- if (se0 > 0) d / se0 else NA_real_
p_z <- if (!is.na(z_stat)) 2 * stats::pnorm(-abs(z_stat)) else NA_real_
ct <- tryCatch(
suppressWarnings(stats::prop.test(c(x1, x2), c(n1, n2), correct = TRUE)),
error = function(e) NULL)
chi_stat <- if (!is.null(ct)) unname(ct$statistic) else NA_real_
chi_df <- if (!is.null(ct)) unname(ct$parameter) else NA_real_
p_chi <- if (!is.null(ct)) ct$p.value else NA_real_
p_fisher <- NA_real_; fisher_or <- NA_real_
if (!z_valid) {
ft <- stats::fisher.test(matrix(c(x1, n1 - x1, x2, n2 - x2),
nrow = 2, byrow = TRUE))
p_fisher <- ft$p.value
fisher_or <- unname(ft$estimate)
}
if (z_valid) {
p_primary <- p_z
test_used <- "two-sample proportion z-test"
} else {
p_primary <- p_fisher
test_used <- "Fisher's exact test"
}
significant <- !is.na(p_primary) && p_primary < 0.05
verdict_label <- if (significant) "Significant difference" else "No significant difference"Step 8: Computed honesty notes (magnitude vs detectability)
abs_pp <- abs(d) * 100
magnitude_word <- if (abs_pp < 1) "small" else if (abs_pp < 5) "modest" else "sizeable"
practical_note <- if (significant) {
paste0(
"Statistical significance measures detectability, not importance: the observed gap is ",
fmt_num(abs_pp, 1), " percentage points",
if (rel_ok) paste0(" (a relative change of ", fmt_pct(abs(rel_change)),
if (rel_change >= 0) " higher" else " lower",
" for ", l2, " vs ", l1, ")") else "",
" — a ", magnitude_word, " gap in absolute terms. Whether a ",
fmt_num(abs_pp, 1), "-point shift in ", outcome_h,
" matters is a business judgment about stakes and volume, not a statistical one."
)
} else {
paste0(
"No significant difference is NOT proof the rates are equal — it means the data ",
"cannot distinguish the observed gap of ", fmt_num(abs_pp, 1),
" percentage points from sampling noise. The Newcombe 95% interval on the difference(",
fmt_num(d_low * 100, 2), " to ", fmt_num(d_high * 100, 2),
" percentage points) shows the range of true differences still compatible with this data."
)
}
cross_check_note <- if (!is.na(p_z) && !is.na(p_chi)) {
agree <- (p_z < 0.05) == (p_chi < 0.05)
if (agree) {
paste0("The continuity-corrected chi-square cross-check agrees with the z-test verdict(p = ",
fmt_p(p_chi), " vs p = ", fmt_p(p_z), ").")
} else {
paste0("Caution: the continuity-corrected chi-square(p = ", fmt_p(p_chi),
") and the z-test(p = ", fmt_p(p_z),
") land on opposite sides of the 0.05 line — the evidence is borderline and should not be over-read.")
}
} else ""Step 10: Metrics + JSON answer
metrics <- list(
`Observations` = final_rows,
`Rate` = paste0(l1, " ", fmt_pct(p1), " vs ", l2, " ", fmt_pct(p2)),
`Difference(pp)` = round(d * 100, 2),
`P-value` = fmt_p(p_primary),
`Verdict` = verdict_label,
`Test Used` = test_used
)
json_output <- list(
answer = paste0(
"Comparison of ", success_label, " rates in ", outcome_h, " between the two ",
group_h, " groups(", format(final_rows, big.mark = ","), " rows): ",
l1, " ", fmt_pct(p1), " (Wilson 95% CI ", fmt_pct(w1["low"]), " to ",
fmt_pct(w1["high"]), ", n=", format(n1, big.mark = ","), ") vs ",
l2, " ", fmt_pct(p2), " (", fmt_pct(w2["low"]), " to ", fmt_pct(w2["high"]),
", n=", format(n2, big.mark = ","), "). Absolute difference ",
fmt_num(d * 100, 2), " percentage points(Newcombe 95% CI ",
fmt_num(d_low * 100, 2), " to ", fmt_num(d_high * 100, 2),
" percentage points). Verdict by the ", test_used, ": ",
if (significant) "the rates differ beyond chance" else
"no statistically significant difference was detected",
" (p = ", fmt_p(p_primary), ").",
if (success_flagged) paste0(" Note: ", success_note) else ""
),
cards = lapply(
c("tldr", "overview", "preprocessing", "rates_chart",
"test_table", "group_summary", "methods"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows,
rows_removed = rows_removed, n_na_rows = n_na_rows,
outcome_h = outcome_h, group_h = group_h,
l1 = l1, l2 = l2,
success_label = success_label, failure_label = failure_label,
success_flagged = success_flagged, success_note = success_note,
n1 = n1, n2 = n2, x1 = x1, x2 = x2, p1 = p1, p2 = p2,
w1 = w1, w2 = w2,
d = d, d_low = d_low, d_high = d_high,
rel_change = rel_change, rel_ok = rel_ok,
min_cell = min_cell, z_valid = z_valid,
z_stat = z_stat, p_z = p_z,
chi_stat = chi_stat, chi_df = chi_df, p_chi = p_chi,
p_fisher = p_fisher, fisher_or = fisher_or,
p_primary = p_primary, test_used = test_used,
significant = significant, verdict_label = verdict_label,
practical_note = practical_note, cross_check_note = cross_check_note,
dropped_groups_df = dropped_groups_df,
rates_df = rates_df, test_df = test_df,
group_summary_df = group_summary_df, methods_df = methods_df,
metrics = metrics, json_output = json_output
)
}