# --- Παράμετροι πειράματος ---
n_control <- 8000 # μέγεθος ομάδας ελέγχου
n_treatment <- 8000 # μέγεθος πειραματικής ομάδας
p_control <- 0.08 # baseline conversion rate
p_treatment <- 0.10 # μετά την αλλαγή (true effect = +2%)
experiment <- tibble(
user_id = 1:(n_control + n_treatment),
group = c(rep("control", n_control),
rep("treatment", n_treatment)),
clicked = 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, 18,…
## $ group <chr> "control", "control", "control", "control", "control", "contro…
## $ clicked <int> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1,…
summary_stats <- experiment |>
group_by(group) |>
summarise(
n = n(),
clicks = sum(clicked),
ctp = mean(clicked),
se = sqrt(ctp * (1 - ctp) / n),
ci_lower = ctp - 1.96 * se,
ci_upper = ctp + 1.96 * se
)
summary_stats
## # A tibble: 2 × 7
## group n clicks ctp se ci_lower ci_upper
## <chr> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 control 8000 675 0.0844 0.00311 0.0783 0.0905
## 2 treatment 8000 821 0.103 0.00339 0.0960 0.109
Παρατηρείται πως δεν υπάρχει επικάλυψη μεταξύ των δύο group, συνεπώς μπορούμε να συνεχίσουμε.
ggplot(summary_stats, aes(x = group, y = ctp, 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(ctp, 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" = "#6b7280",
"treatment" = "#3b82f6")) +
labs(
title = "A/B Test: Πιθανότητα κλικ ανά ομάδα",
subtitle = "Με 95% διαστήματα εμπιστοσύνης",
x = NULL, y = "Click-through probability"
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none")
Παρατηρείται πως το treatment group εχει σχετικά μεγαλύτερο click
through rate. Αυτό σημαίνει ότι σε σχέση με το αρχικό group υπήρξε
αλλαγή αν και αυτή περιορίζεται σε αύξηση του ποσοστού κλικ κατά
1,9%.
clicks <- c(summary_stats$clicks[1], summary_stats$clicks[2])
visitors <- c(summary_stats$n[1], summary_stats$n[2])
test_result <- prop.test(
x = clicks,
n = visitors,
conf.level = 0.95,
correct = FALSE
)
test_result
##
## 2-sample test for equality of proportions without continuity correction
##
## data: clicks out of visitors
## X-squared = 15.718, df = 1, p-value = 7.351e-05
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## -0.027267671 -0.009232329
## sample estimates:
## prop 1 prop 2
## 0.084375 0.102625
To p-value είναι 0,00007315, συνεπώς το H0 απορρίπτεται και άρα ισχυεί η εναλλακτική υπόθεση. Η treated ομάδα έχει όντως μεγαλύτερο CTR.
# 1. Pooled estimate
p_pool <- sum(clicks) / sum(visitors)
# 2. Pooled standard error
se_pool <- sqrt(p_pool * (1 - p_pool) *
(1/visitors[1] + 1/visitors[2]))
# 3. Διαφορά και διάστημα εμπιστοσύνης
delta <- summary_stats$ctp[2] - summary_stats$ctp[1]
m <- 1.96 * se_pool
cat(sprintf("Pooled p̂ = %.4f\n", p_pool))
## Pooled p̂ = 0.0935
cat(sprintf("Pooled SE = %.4f\n", se_pool))
## Pooled SE = 0.0046
cat(sprintf("δ = %.4f\n", delta))
## δ = 0.0182
cat(sprintf("95%% CI for δ: [%.4f, %.4f]\n", delta - m, delta + m))
## 95% CI for δ: [0.0092, 0.0273]
# Επιχειρηματική απόφαση
delta_min <- 0.01 # κατώφλι επιχειρηματικής ουσίας
if (delta - m > delta_min) {
cat("✅ ΣΥΜΠΕΡΑΣΜΑ: Υλοποιήστε την αλλαγή!\n")
} else {
cat("⚠️ ΣΥΜΠΕΡΑΣΜΑ: Στατιστικά σημαντικό αλλά χωρίς επιχειρηματική ουσία.\n")
}
## ⚠️ ΣΥΜΠΕΡΑΣΜΑ: Στατιστικά σημαντικό αλλά χωρίς επιχειρηματική ουσία.
Με βάση το παραπάνω υπάρχει στατιστική σημαντικότητα, δηλαδή η διαφορά μεταξύ των δύο υπάρχει ωστόσο δεν συνεισφέρει στην επιχείρηση κάτι η υλοποιήση της. Πρακτικά επιτρέπεται να διατηρηθεί η παλία μέθοδος. Επίσης παρατηρείται πως το διάστημα εμπιστοσύνης μεταξύ των μεθόδων ισούται σε απόλυτη τιμή διαφέρει σε προσημασμένη. Αυτό οφείλεται στην διαφορά μεταξύ των δύο τρόπων υπολογισμού. Στην μια μέθοδο αφαιρούμε την ομάδα 1 από την 2 ενώ στην άλλη το ανάποδο.
effect_size <- ES.h(p1 = 0.12, p2 = 0.10)
cat(sprintf("Cohen's h = %.4f\n", effect_size))
## Cohen's h = 0.0640
sample_size_calc <- pwr.2p.test(
h = effect_size,
sig.level = 0.05,
power = 0.80,
alternative = "two.sided"
)
sample_size_calc
##
## Difference of proportion power calculation for binomial distribution (arcsine transformation)
##
## h = 0.0639821
## n = 3834.596
## sig.level = 0.05
## power = 0.8
## alternative = two.sided
##
## NOTE: same sample sizes
Χρειαζόμαστε συνολικά 3.835 άτομα ανά ομάδα και εδώ χρσηιμοποιήθηκαν 8.000, και άρα έχουμε power αρκετά μεγαλύτερο του 80%. Συνεπώς με μείωση του δείγματος πάλι ενδέχεται η ανίχνυεση του effect, ωστόσο αν το effect size γίνει πολύ μεγάλο υπάρχει περίπτωση να δωθεί παρπάνω βάση σε ασήμαντα χαρακτηριστικά.
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
Εδώ βλέπουμε πως έχουμε ανακατανομή των γκρουπ, καθώς το ad αποτελεί 96% των παρατηρήσεων, ενώ το psa αποτελεί το 4%. Αυτό προφανώς είναι κακό καθώς όποιο μοντέλο προσπαθήσουμε να δημιουργήσουμε θα κάνει bias υπέρ του ad λόγω υψηλότερης αντιπροσωπευτικότητας.
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()
ads_summary <- ads |>
group_by(group) |>
summarise(
n = n(),
conversions = sum(converted),
conversion_rate = mean(converted),
se = sqrt(conversion_rate * (1 - conversion_rate) / n)
) |>
mutate(
ci_lower = conversion_rate - 1.96 * se,
ci_upper = conversion_rate + 1.96 * se
)
ads_summary
## # A tibble: 2 × 7
## group n conversions conversion_rate se ci_lower ci_upper
## <fct> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 psa 23524 420 0.0179 0.000863 0.0162 0.0195
## 2 ad 564577 14423 0.0255 0.000210 0.0251 0.0260
Δεν φαίνεται να υπάρχει επικάλυψη των δύο ομάδων, καθώς επίσης το conversion rate των διαφημήσεων είναι μεγαλύτερο από αυτό των psa, 2,56% > 1,79%.
# Conversion rate ανά ημέρα και ομάδα
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)
Παρατηρείται πως ενώ υπάρχει πλήρης διαχωρισμός των δύο ομάδων για τις
περισσότερες μέρες της εβδομάδας, με τις διαφημήσεις να επιφέρουν
σημαντικά καλύτερο conversion rate, εντούτοις τις Κυριακές το κενό
μεταξύ τους μικραίνει, και τις Πέμπτες εμφανίζεται σημαντική
σύγκλιση.