# --- Παράμετροι πειράματος ---
n_control <- 8000 # μέγεθος ομάδας ελέγχου
n_treatment <- 8000 # μέγεθος πειραματικής ομάδας
p_control <- 0.08 # baseline conversion rate
p_treatment <- 0.10 # μετά την αλλαγή (true effect = +2%)
set.seed(42)
experiment <- tibble(
user_id = 1:(n_control + n_treatment),
group = c(rep("control", n_control),
rep("treatment", n_treatment)),
converted = c(rbinom(n_control, 1, p_control), rbinom(n_treatment, 1, p_treatment))
)
glimpse(experiment)
## Rows: 16,000
## Columns: 3
## $ user_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 1…
## $ group <chr> "control", "control", "control", "control", "control", "cont…
## $ converted <int> 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, …
summary_stats <- experiment |>
group_by(group) |>
summarise(
n = n(),
conversions = sum(converted),
conversion_rate = (conversions / n) * 100,
cvr = mean(converted),
se = sqrt(cvr * (1 - cvr) / n),
ci_lower = cvr - 1.96 * se,
ci_upper = cvr + 1.96 * se
)
print(summary_stats)
## # A tibble: 2 × 8
## group n conversions conversion_rate cvr se ci_lower ci_upper
## <chr> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 control 8000 677 8.46 0.0846 0.00311 0.0785 0.0907
## 2 treatment 8000 764 9.55 0.0955 0.00329 0.0891 0.102
ggplot(summary_stats, aes(x = group, y = cvr, fill = group)) +
geom_col(width = 0.5, alpha = 0.85) +
geom_errorbar(aes(ymin = ci_lower, ymax = ci_upper),
width = 0.15, linewidth = 0.8) +
geom_text(aes(label = percent(cvr, accuracy = 0.1)),
vjust = -1.5, size = 5, fontface = "bold") +
scale_y_continuous(labels = percent, limits = c(0, 0.16)) +
scale_fill_manual(values = c("control (psa)" = "#6b7280",
"treatment (ads)" = "#3b82f6")) +
labs(
title = "A/B Test: Πιθανότητα Conversion ανά ομάδα",
subtitle = "Με 95% διαστήματα εμπιστοσύνης",
x = NULL, y = "Conversion Rate Probability "
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none")
Παρατηρούμε πως τα διαστήματα εμπιστοσύνης έχουν πολύ μικρή επικάλυψη {0.10 μονάδες}.
Control CI ->[0.0785, 0.0907] Treatment CI -> [0.0891,0.102]
Είμαστε 95% σίγουροι πως η πιθανότητα conversion οταν ένας χρήστης βλέπει τις διαφημίσεις είναι 8.91%-10.20%
conversions <- c(summary_stats$conversions[1], summary_stats$conversions[2])
visitors <- c(summary_stats$n[1], summary_stats$n[2])
test_result <- prop.test(
x = conversions,
n = visitors,
conf.level = 0.95,
correct = FALSE # χωρίς Yates correction (ταιριάζει με τον τύπο μας)
)
print(test_result)
##
## 2-sample test for equality of proportions without continuity correction
##
## data: conversions out of visitors
## X-squared = 5.7725, df = 1, p-value = 0.01628
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## -0.019744875 -0.002005125
## sample estimates:
## prop 1 prop 2
## 0.084625 0.095500
To p value < 0.05 σημαινει οτι απορρίπτουμε την μηδενική υπόθεση (H0) και συμπεραίνουμε οτι ειναι στατιστικά σημαντική συσχέτιση ( ο ρόλος των διαφημίσεων στα conversions). Το prop 2 δείχνει οτι η ομάδα treatment (που είδε τις διαφημίσεις) έφερε περισσότερα leads.
Το p value μας δείχνει πως το να βλέπαμε αυτα τα αποτελέσματα αν οι δύο ομάδες ήταν ίδιες ειναι μόλις 0.01628%.
# 1. Pooled estimate
p_pool <- sum(conversions) / sum(visitors)
# 2. Pooled standard error
se_pool <- sqrt(p_pool * (1 - p_pool) *
(1/visitors[1] + 1/visitors[2]))
# 3. Διαφορά και διάστημα εμπιστοσύνης
delta <- summary_stats$cvr[2] - summary_stats$cvr[1]
m <- 1.96 * se_pool
cat(sprintf("Pooled p̂ = %.4f\n", p_pool))
## Pooled p̂ = 0.0901
cat(sprintf("Pooled SE = %.4f\n", se_pool))
## Pooled SE = 0.0045
cat(sprintf("δ = %.4f\n", delta))
## δ = 0.0109
cat(sprintf("95%% CI for δ: [%.4f, %.4f]\n", delta - m, delta + m))
## 95% CI for δ: [0.0020, 0.0197]
# Επιχειρηματική απόφαση
delta_min <- 0.01 # κατώφλι επιχειρηματικής ουσίας
if (delta - m > delta_min) {
cat("✅ ΣΥΜΠΕΡΑΣΜΑ: Υλοποιήστε την αλλαγή!\n")
} else {
cat("⚠️ ΣΥΜΠΕΡΑΣΜΑ: Στατιστικά σημαντικό αλλά χωρίς επιχειρηματική ουσία.\n")
}
## ⚠️ ΣΥΜΠΕΡΑΣΜΑ: Στατιστικά σημαντικό αλλά χωρίς επιχειρηματική ουσία.
Τα διαστήματα εμπιστοσύνης μεταξύ των 2 τρόπων είναι ακριβώς τα ίδια, ωστόσο στο prop.test η R κάνει prop [1] - prop [2] για αυτό βγήκε αρνητικό το διάστημα εμπιστοσύνης (η Ομάδα 2 έχει μεγαλύερο σκορ).
Η διαφορά μας αφού τοποθετήθηκαν οι διαφημίσεις είναι αύξηση 1.73% στα conversions. Απο επιχειρηματικής πλευράς μειώνεται το user experience και δεν είναι μεγάλη η αύξηση για να βάζουμε τόσες διαφημίσεις. Επίσης θα φύγουν κάποιοι χρήστες εάν βλέπουν τόσες πολλες διαφημίσεις την ημέρα (μερικοί είδαν παραπάνω απο 20 σε μια μέρα).
Πόσο δείγμα χρειαζόταν πραγματικά για power = 80% και α = 0.05;
# h = Cohen's effect size για δύο αναλογίες
effect_size <- ES.h(p1 = 0.08, p2 = 0.10)
cat(sprintf("Cohen's h = %.4f\n", effect_size))
## Cohen's h = -0.0700
# Υπολογισμός απαιτούμενου μεγέθους δείγματος ανά ομάδα
sample_size_calc <- pwr.2p.test(
h = effect_size,
sig.level = 0.05,
power = 0.80,
alternative = "two.sided"
)
print(sample_size_calc)
##
## Difference of proportion power calculation for binomial distribution (arcsine transformation)
##
## h = 0.069988
## n = 3204.715
## sig.level = 0.05
## power = 0.8
## alternative = two.sided
##
## NOTE: same sample sizes
Το πλήθος n που χρειαζόμαστε είναι 3204 ανα ομάδα ενω εμεις εχουμε 16.000 σύνολο (control + experiment).
ads <- read_csv("data.csv") |>
janitor::clean_names() |>
rename(user_id = user_id) |>
mutate(
group = factor(test_group, levels = c("psa", "ad")),
converted = as.integer(converted)
)
# 1. Αναλογία στις ομάδες
ads |>
count(group) |>
mutate(pct = n / sum(n))
## # A tibble: 2 × 3
## group n pct
## <fct> <int> <dbl>
## 1 psa 23524 0.0400
## 2 ad 564577 0.960
# 2. Κατανομή ανά ημέρα εβδομάδας — πρέπει να είναι παρόμοια
ads |>
count(group, most_ads_day) |>
group_by(group) |>
mutate(pct = n / sum(n)) |>
ggplot(aes(x = most_ads_day, y = pct, fill = group)) +
geom_col(position = "dodge") +
scale_y_continuous(labels = percent) +
labs(title = "Invariant check: κατανομή ανά ημέρα",
x = NULL, y = "% της ομάδας") +
theme_minimal()
Παρατηρούμε πως η ομάδα psa αποτελεί μόνο το 4% του συνολικού δείγματος. Είναι ανισόρροπες ομάδες. Υπάρχουν ανισσοροπίες κάθε Τετάρτή, Πέμπτη, Σάββατο και Κυριακή,
ads_summary <- ads |>
group_by(group) |>
summarise(
n = n(),
conversions = sum(converted),
conversion_rate = mean(converted),
cvr = mean(converted) * 100,
se = sqrt(conversion_rate * (1 - conversion_rate) / n)
) |>
mutate(
ci_lower = conversion_rate - 1.96 * se,
ci_upper = conversion_rate + 1.96 * se
)
print(ads_summary)
## # A tibble: 2 × 8
## group n conversions conversion_rate cvr se ci_lower ci_upper
## <fct> <int> <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 psa 23524 420 0.0179 1.79 0.000863 0.0162 0.0195
## 2 ad 564577 14423 0.0255 2.55 0.000210 0.0251 0.0260
test <- prop.test(
x = ads_summary$conversions,
n = ads_summary$n,
correct = FALSE
)
# tidy output — πολύ καλύτερο από το σκέτο print()
broom::tidy(test) |>
select(estimate1, estimate2, statistic, p.value,
conf.low, conf.high)
## # A tibble: 1 × 6
## estimate1 estimate2 statistic p.value conf.low conf.high
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0.0179 0.0255 54.3 1.71e-13 -0.00943 -0.00595
ads_by_day <- ads |>
group_by(most_ads_day, group) |>
summarise(
n = n(),
conversion_rate = mean(converted),
se = sqrt(conversion_rate * (1 - conversion_rate) / n),
.groups = "drop"
)
ggplot(ads_by_day, aes(x = most_ads_day,
y = conversion_rate,
color = group, group = group)) +
geom_line(linewidth = 1.2) +
geom_point(size = 3) +
geom_ribbon(aes(ymin = conversion_rate - 1.96 * se,
ymax = conversion_rate + 1.96 * se,
fill = group),
alpha = 0.15, color = NA) +
scale_y_continuous(labels = percent_format(accuracy = 0.1)) +
scale_color_manual(values = c("psa" = "#6b7280", "ad" = "#3b82f6")) +
scale_fill_manual(values = c("psa" = "#6b7280", "ad" = "#3b82f6")) +
labs(title = "Conversion rate ανά ημέρα εβδομάδας",
subtitle = "Με 95% διαστήματα εμπιστοσύνης",
x = NULL, y = "Conversion rate") +
theme_minimal(base_size = 13)
Παρατηρούμε αισθητά οτι το group που βλέπει διαφημίσεις αποφέρει περισσότερα conversions. H Δευτέρα είναι η καλύτερη μέρα για να τρέξουμε διαφημίσεις.
# CI για τη διαφορά (lift)
conf_int <- broom::tidy(test)
# Σωστοί υπολογισμοί βασισμένοι στο Experiment (estimate2) έναντι του Control (estimate1)
abs_lift <- (conf_int$estimate2 - conf_int$estimate1) * 100
lift_pct <- (conf_int$estimate2 - conf_int$estimate1) / conf_int$estimate1 * 100
delta_min <- 0.005 # κατώφλι επιχειρηματικής ουσίας
delta <- (conf_int$estimate2 - conf_int$estimate1)
# Ορισμός των σωστών ορίων μετά την αντιστροφή των προσήμων
ci_lower_positive <- abs(conf_int$conf.high)
ci_upper_positive <- abs(conf_int$conf.low)
cat(sprintf("Absolute lift: %.2f ποσοστ. μονάδες\n", abs_lift))
## Absolute lift: 0.77 ποσοστ. μονάδες
cat(sprintf("Relative lift: %+.1f%%\n", lift_pct))
## Relative lift: +43.1%
# Μετατρέπουμε τα όρια σε θετικά (απόλυτες τιμές) για να συμφωνούν με το Experiment - Control
cat(sprintf("95%% CI for difference: [%.4f, %.4f]\n",
abs(conf_int$conf.high), abs(conf_int$conf.low)))
## 95% CI for difference: [0.0060, 0.0094]
if (ci_lower_positive > delta_min) {
cat("✅ ΣΥΜΠΕΡΑΣΜΑ: Υλοποιήστε την αλλαγή!\n")
} else {
cat("⚠️ ΣΥΜΠΕΡΑΣΜΑ: Στατιστικά σημαντικό αλλά χωρίς επιχειρηματική ουσία.\n")
}
## ✅ ΣΥΜΠΕΡΑΣΜΑ: Υλοποιήστε την αλλαγή!
Ερμηνεία relative lift/absolute lift:
Βελτιώθηκε κατά \(+43\%\) το conversion rate, απο το experiment group. Αξίζει επιχειρηματικά η αλλάγη επειδή το κατώφλι της επιχείρησης ήθελε ελάχιστη αύξηση 0.5% και εμείς έχουμε 0.77%.
Δηλαδή αν είχαμε 100 conversions με 1000 χρήστες , με το experiment group θα είχαμε 177 conversions απο 1000 χρήστες.
** Κάναμε estimate 2 - estimate 1 και βγήκαν τα νούμερα θετικά **