Executive Summary
Did Post Score change from Pre Score?
Yes, Post Score increased genuinely from Pre Score (Paired t-test, p < 0.001). The mean change is 5.05 units (95% CI 4.44 to 5.66), a large effect with Cohen's dz = 1.848. Of the 80 pairs, 98.75% increased and 1.25% decreased. The differences are close enough to normal that the paired t-test is the safe headline. The answer to "did it change?" is unambiguous: a real, large, and consistent increase.
Analysis Overview
Paired comparison of Pre Score and Post Score across 80 matched pairs.
Post Score increased substantially from Pre Score across 80 matched pairs. The paired design isolates within-subject change by canceling stable person-to-person variation—a key advantage here because the two measurements correlate strongly at r = 0.96, removing much of the noise that would cloud an independent-groups comparison. The mean change of 5.05 units is large relative to typical variation, and the standardized effect size (Cohen's dz = 1.848) confirms a substantial shift. By working on paired differences rather than raw scores, the test gains power to detect real change without being confounded by baseline differences between subjects.
Data Quality
Pair completeness and the normality check on the differences.
All 80 rows loaded formed complete pairs, with 0 rows removed. Data quality was high: both measurements were coerced to numeric form before pairing. The Shapiro-Wilk normality test on the paired differences yielded p = 0.168, indicating no strong departure from normality. This result supports the use of the parametric paired t-test as the primary headline, since the differences are approximately normally distributed and the paired t-test is robust under this condition.
Before vs After
Every pair's Pre Score plotted against its Post Score.
98.75% of the 80 pairs sit above the no-change diagonal, showing that the typical movement is upward. The high correlation (r = 0.96) between Pre Score and Post Score demonstrates tight tracking within each subject—individuals who started low tended to stay low, and those who started high stayed high, but nearly all moved up. This strong alignment along the diagonal is why pairing is effective: it removes the large stable differences between subjects and focuses the test on the common upward shift.
Distribution of Changes
How much each pair changed between Pre Score and Post Score.
Per-pair changes are centered at 5.05 with a standard deviation of 2.73. The distribution sits predominantly to the right of zero, with only one pair showing a decrease (difference = −0.05). The bulk of changes range from roughly 1 to 8 units, with a few larger jumps (e.g., 12.01, 11.96, 11.06). This concentration of positive changes away from zero, combined with the tight standard deviation relative to the mean, confirms the shift is both real and reasonably consistent across pairs rather than driven by a few outliers.
Statistical Tests
Paired t-test, Wilcoxon signed-rank, effect size, and the normality check.
| Test | Statistic | P Value | Effect Or CI | Interpretation |
|---|---|---|---|---|
| Paired t-test | 16.53 | < 0.001 | mean difference 5.05 (95% CI 4.44 to 5.66) | Tests whether the mean change in Post Score differs from zero: highly significant (p < 0.001). |
| Wilcoxon signed-rank | 3239 | < 0.001 | median difference 5.17 | Rank-based, no normality assumption: highly significant (p < 0.001). |
| Cohen's dz | 1.848 | n/a | large effect | Standardized paired effect size: dz = 1.85. |
| Shapiro-Wilk (differences) | — | 0.168 | normality plausible | No strong evidence against normal differences — the paired t-test is reliable. |
The paired t-test (statistic = 16.53, p < 0.001) and Wilcoxon signed-rank test (statistic = 3239, p < 0.001) agree, strengthening confidence in the verdict. The paired t-test estimates mean difference of 5.05 (95% CI 4.44 to 5.66); the Wilcoxon median difference is 5.17. Cohen's dz = 1.848 marks a large standardized effect. The Shapiro-Wilk test (p = 0.168) shows no strong evidence against normality of the differences, making the paired t-test the recommended headline. Parametric and rank-based tests converge, a robust confirmation that Post Score genuinely increased.
Methodology
Statistical methodology and diagnostics for Paired Comparison — Before vs After
Statistical Method
Standard-library analysis: did a paired or repeated measurement actually change? Map a before column and an after column measured on the same subject — before vs after, treatment vs control on the same unit, two raters on the same items — and get the paired t-test with a confidence interval on the mean change, a Wilcoxon signed-rank cross-check, Cohen's dz effect size, a normality check that picks the safer headline, and the share of pairs that moved up or down.
- Each row is one subject measured twice: a before value and an after value on the same unit
- Both measurements are numeric or cleanly convertible
- The paired differences are approximately normal for the paired t-test — the Wilcoxon signed-rank test is reported as a rank-based cross-check
- Rows missing either the before or the after value are dropped as incomplete pairs
- At least 6 complete pairs are required to run the comparison
- A significant change says the measurement moved, not why — it does not rule out other causes acting between the two measurements
- Very few pairs have little power; a non-significant result is not proof of no change
Analysis Code
Complete R source code for this analysis
Paired Comparison — Before vs After
Did a paired or repeated measurement change? Each row is one subject measured twice — before vs after, treatment vs control on the same unit, two raters on the same items. The analysis works on the within-pair change (after minus before) and runs the whole standard paired toolkit at once: a paired t-test with a confidence interval on the mean change, a Wilcoxon signed-rank cross-check, Cohen's dz effect size, and a normality check on the differences that decides which result is the safer headline.
Why This Method?
When the same unit is measured twice, a two-sample test wastes power on the stable person-to-person differences. Working on each subject's own change cancels that variation out, so a paired test detects a real shift with far fewer observations. Running the parametric and rank-based versions together, with a normality check on the differences, lets the report itself say which result to trust.
What This Analysis Covers
- Before against after per pair (scatter with a no-change diagonal)
- The distribution of per-pair changes (histogram)
- The paired t-test, Wilcoxon signed-rank, effect size, and normality check
Standard Library
Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {before, after}. 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 $ row accounting
# n / n_dropped $ complete pairs used + incomplete rows dropped
# before_h / after_h $ humanized user names for the two mapped columns
# mean_d / sd_d / median_d / ci_low / ci_high $ change statistics
# paired_r $ within-subject correlation of before vs after
# t_stat / t_p $ paired t-test statistic + p
# w_V / w_p $ Wilcoxon signed-rank statistic + p
# dz $ Cohen's dz (mean(d)/sd(d))
# shapiro_p / nonparam_preferred $ normality of differences + headline choice
# pct_inc/pct_dec/pct_unch $ share of pairs up/down/flat
# dir_word $ direction language consistent with sign of mean_d
# primary_test/primary_p/primary_sig $ the headline result
# test_results_df $ test, statistic, p_value, effect_or_ci, interpretation
# paired_points_df $ before_value, after_value (<= 1000 sampled)
# difference_dist_df $ difference (<= 2000 sampled)
# metrics / json_output
# === /SHARED EXPORTS ===Step 1: Resolve mapped columns (humanized for all prose)
initial_rows <- nrow(df)
before_h <- humanize_semantic("before", col_map)
after_h <- humanize_semantic("after", col_map)
if (!("before" %in% names(df)) || !("after" %in% names(df))) {
stop(sprintf("Paired comparison needs both '%s' (the before measurement) and '%s' (the after measurement) mapped.",
before_h, after_h))
}Step 2: Coerce both measurements to numeric (95% rule)
coerce_num <- function(v, label_h) {
if (is.numeric(v)) return(v)
ch <- as.character(v)
non_blank <- !is.na(ch) & trimws(ch) != ""
conv <- suppressWarnings(as.numeric(ch))
if (sum(non_blank) == 0 ||
sum(!is.na(conv[non_blank])) < 0.95 * sum(non_blank)) {
stop(sprintf("The column '%s' does not look numeric — fewer than 95%% of its values parse as numbers. Pick a numeric column.",
label_h))
}
conv
}
b <- coerce_num(df$before, before_h)
a <- coerce_num(df$after, after_h)Step 3: Keep row-wise complete pairs; drop incomplete rows (reported)
keep <- !is.na(b) & !is.na(a)
n_dropped <- sum(!keep)
b <- b[keep]; a <- a[keep]
n <- length(b)
final_rows <- n
rows_removed <- initial_rows - final_rows
if (n < 6) {
stop(sprintf("Only %d complete %s / %s pairs remained after dropping incomplete rows — at least 6 are needed for a paired comparison.",
n, before_h, after_h))
}Step 4: The within-pair change and its summary statistics
d <- a - b
mean_d <- mean(d)
sd_d <- stats::sd(d)
median_d <- stats::median(d)
paired_r <- suppressWarnings(tryCatch(stats::cor(b, a), error = function(e) NA_real_))
if (is.na(sd_d)) sd_d <- 0Step 5: Paired t-test (mean change + 95% CI)
tt <- tryCatch(stats::t.test(a, b, paired = TRUE), error = function(e) NULL)
if (!is.null(tt)) {
t_stat <- unname(tt$statistic)
t_p <- tt$p.value
ci <- as.numeric(tt$conf.int)
ci_low <- ci[1]; ci_high <- ci[2]
} else {
t_stat <- NA_real_; t_p <- NA_real_; ci_low <- mean_d; ci_high <- mean_d
}Step 6: Wilcoxon signed-rank (rank-based, no normality assumption)
ww <- tryCatch(suppressWarnings(stats::wilcox.test(a, b, paired = TRUE)),
error = function(e) NULL)
w_V <- if (!is.null(ww)) unname(ww$statistic) else NA_real_
w_p <- if (!is.null(ww)) ww$p.value else NA_real_Step 7: Cohen's dz + normality of the differences
dz <- if (!is.na(sd_d) && sd_d > 0) mean_d / sd_d else NA_real_
shapiro_p <- NA_real_
if (n >= 3 && n <= 5000) {
shapiro_p <- tryCatch(stats::shapiro.test(d)$p.value, error = function(e) NA_real_)
}
nonparam_preferred <- !is.na(shapiro_p) && shapiro_p < 0.05Step 8: Direction language + share of pairs moving each way
n_inc <- sum(d > 0); n_dec <- sum(d < 0); n_unch <- sum(d == 0)
pct_inc <- 100 * n_inc / n
pct_dec <- 100 * n_dec / n
pct_unch <- 100 * n_unch / n
dir_word <- if (mean_d > 0) "increased" else if (mean_d < 0) "decreased" else "stayed flat"
primary_test <- if (nonparam_preferred) "Wilcoxon signed-rank" else "Paired t-test"
primary_p <- if (nonparam_preferred) w_p else t_p
primary_sig <- !is.na(primary_p) && primary_p < 0.05Step 9: Test-results table (paired t, Wilcoxon, Cohen's dz, normality)
tr_rows <- list()
add_test <- function(test, statistic, p_value, effect_or_ci, interpretation) {
tr_rows[[length(tr_rows) + 1]] <<- data.frame(
test = test,
statistic = if (is.na(statistic)) NA_real_ else round(statistic, 3),
p_value = fmt_p(p_value),
effect_or_ci = effect_or_ci,
interpretation = interpretation,
stringsAsFactors = FALSE
)
}
add_test("Paired t-test", t_stat, t_p,
sprintf("mean difference %s(95%% CI %s to %s)", r2(mean_d), r2(ci_low), r2(ci_high)),
sprintf("Tests whether the mean change in %s differs from zero: %s.", after_h, sig_phrase(t_p)))
add_test("Wilcoxon signed-rank", w_V, w_p,
sprintf("median difference %s", r2(median_d)),
sprintf("Rank-based, no normality assumption: %s.", sig_phrase(w_p)))
add_test("Cohen's dz", dz, NA_real_,
sprintf("%s effect", dz_word(dz)),
sprintf("Standardized paired effect size: dz = %s.", r2(dz)))
if (!is.na(shapiro_p)) {
add_test("Shapiro-Wilk(differences)", NA_real_, shapiro_p,
if (shapiro_p < 0.05) "normality rejected" else "normality plausible",
if (shapiro_p < 0.05)
"The paired differences are not normally distributed — favor the Wilcoxon result."
else
"No strong evidence against normal differences — the paired t-test is reliable.")
}
test_results_df <- do.call(rbind, tr_rows)
rownames(test_results_df) <- NULLStep 10: Chart datasets — sampled with a fixed seed
set.seed(42)
sidx <- if (n > 1000) sample(n, 1000) else seq_len(n)
paired_points_df <- data.frame(before_value = b[sidx], after_value = a[sidx],
stringsAsFactors = FALSE)
paired_points_df <- paired_points_df[order(paired_points_df$before_value), , drop = FALSE]
rownames(paired_points_df) <- NULL
set.seed(42)
didx <- if (n > 2000) sample(n, 2000) else seq_len(n)
difference_dist_df <- data.frame(difference = round(d[didx], 4), stringsAsFactors = FALSE)
metrics <- list(
`Pairs Analysed` = n,
`Mean Difference` = round(mean_d, 3),
`Cohen's dz` = if (is.na(dz)) NA_real_ else round(dz, 3),
`Headline Test` = primary_test,
`Headline p-value` = fmt_p(primary_p),
`Pairs Increased` = paste0(r2(pct_inc), "%")
)
json_output <- list(
answer = paste0(
"Paired comparison of ", after_h, " versus ", before_h, " across ",
format(n, big.mark = ","), " complete pairs: the mean change(", after_h,
" minus ", before_h, ") is ", r2(mean_d), " (95% CI ", r2(ci_low), " to ",
r2(ci_high), "). The ", primary_test, " finds this change ", sig_phrase(primary_p),
" (Cohen's dz = ", r2(dz), ", ", dz_word(dz), " effect); ",
r2(pct_inc), "% of pairs increased and ", r2(pct_dec), "% decreased."
),
cards = lapply(
c("tldr", "overview", "preprocessing", "paired_change",
"difference_distribution", "test_results"),
function(cid) list(id = cid, metrics = metrics)
)
)
list(
initial_rows = initial_rows, final_rows = final_rows,
rows_removed = rows_removed, n = n, n_dropped = n_dropped,
before_h = before_h, after_h = after_h,
mean_d = mean_d, sd_d = sd_d, median_d = median_d,
ci_low = ci_low, ci_high = ci_high, paired_r = paired_r,
t_stat = t_stat, t_p = t_p, w_V = w_V, w_p = w_p, dz = dz,
shapiro_p = shapiro_p, nonparam_preferred = nonparam_preferred,
pct_inc = pct_inc, pct_dec = pct_dec, pct_unch = pct_unch,
dir_word = dir_word,
primary_test = primary_test, primary_p = primary_p, primary_sig = primary_sig,
test_results_df = test_results_df,
paired_points_df = paired_points_df, difference_dist_df = difference_dist_df,
metrics = metrics, json_output = json_output
)
}