# Εγκατάσταση
# install.packages(c("tidyverse", "pwr", "broom", "scales", "janitor"))
library(tidyverse)
library(pwr)
library(broom)
library(scales)
library(janitor)
set.seed(42)
ads <- read_csv("marketing_AB.csv") |>
clean_names() |>
mutate(
group = factor(test_group, levels = c("psa", "ad")),
converted = as.integer(converted)
)
glimpse(ads)
## Rows: 588,101
## Columns: 8
## $ x1 <dbl> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16…
## $ user_id <dbl> 1069124, 1119715, 1144181, 1435133, 1015700, 1137664, 11…
## $ test_group <chr> "ad", "ad", "ad", "ad", "ad", "ad", "ad", "ad", "ad", "a…
## $ converted <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,…
## $ total_ads <dbl> 130, 93, 21, 355, 276, 734, 264, 17, 21, 142, 209, 47, 6…
## $ most_ads_day <chr> "Monday", "Tuesday", "Tuesday", "Tuesday", "Friday", "Sa…
## $ most_ads_hour <dbl> 20, 22, 18, 10, 14, 10, 13, 18, 19, 14, 11, 13, 20, 13, …
## $ group <fct> ad, ad, ad, ad, ad, ad, ad, ad, ad, ad, ad, ad, ad, ad, …
n_control <- 8000
n_treatment <- 8000
p_control <- 0.08
p_treatment <- 0.10
experiment <- tibble(
user_id = 1:(n_control + n_treatment),
group = rep(c("control", "treatment"), times = c(n_control, n_treatment)),
converted = c(
rbinom(n_control, 1, p_control),
rbinom(n_treatment, 1, p_treatment)
)
)
head(experiment)
## # A tibble: 6 × 3
## user_id group converted
## <int> <chr> <int>
## 1 1 control 0
## 2 2 control 1
## 3 3 control 0
## 4 4 control 0
## 5 5 control 0
## 6 6 control 0
summary_stats <- experiment |>
group_by(group) |>
summarise(
n = n(),
conversions = sum(converted),
conversion_rate = mean(converted),
se = sqrt(conversion_rate * (1 - conversion_rate) / n),
ci_lower = conversion_rate - 1.96 * se,
ci_upper = conversion_rate + 1.96 * se,
.groups = "drop"
)
summary_stats
## # A tibble: 2 × 7
## group n conversions conversion_rate se ci_lower ci_upper
## <chr> <int> <int> <dbl> <dbl> <dbl> <dbl>
## 1 control 8000 677 0.0846 0.00311 0.0785 0.0907
## 2 treatment 8000 764 0.0955 0.00329 0.0891 0.102
ggplot(summary_stats, aes(x = group, y = conversion_rate, fill = group)) +
geom_col(width = 0.5) +
geom_errorbar(aes(ymin = ci_lower, ymax = ci_upper), width = 0.2) +
scale_y_continuous(labels = percent) +
labs(
title = "Conversion Rate ανά ομάδα (Simulated Data)",
x = "Ομάδα", y = "Conversion Rate"
) +
theme_minimal() +
theme(legend.position = "none")
# Πίνακας conversions & n ανά ομάδα
tab <- summary_stats |> arrange(desc(group)) # treatment πρώτο
test_result <- prop.test(
x = tab$conversions,
n = tab$n,
correct = FALSE
)
test_result
##
## 2-sample test for equality of proportions without continuity correction
##
## data: tab$conversions out of tab$n
## X-squared = 5.7725, df = 1, p-value = 0.01628
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## 0.002005125 0.019744875
## sample estimates:
## prop 1 prop 2
## 0.095500 0.084625
Ερμηνεία: Το p-value είναι 0.0163. Αν p < 0.05, απορρίπτουμε την H₀ ότι τα conversion rates είναι ίσα.
# Τιμές από τα simulated data
p1 <- summary_stats$conversion_rate[summary_stats$group == "treatment"]
p2 <- summary_stats$conversion_rate[summary_stats$group == "control"]
n1 <- summary_stats$n[summary_stats$group == "treatment"]
n2 <- summary_stats$n[summary_stats$group == "control"]
# (α) Pooled estimate
p_pool <- (p1 * n1 + p2 * n2) / (n1 + n2)
# (β) Pooled SE
se_pool <- sqrt(p_pool * (1 - p_pool) * (1/n1 + 1/n2))
# (γ) Διαφορά
delta <- p1 - p2
# (δ) 95% CI για τη διαφορά
ci_lower_manual <- delta - 1.96 * se_pool
ci_upper_manual <- delta + 1.96 * se_pool
cat("Pooled p̂:", round(p_pool, 4), "\n")
## Pooled p̂: 0.0901
cat("Pooled SE:", round(se_pool, 4), "\n")
## Pooled SE: 0.0045
cat("Διαφορά δ:", round(delta, 4), "\n")
## Διαφορά δ: 0.0109
cat("95% CI (χειρωνακτικό): [", round(ci_lower_manual, 4), ",", round(ci_upper_manual, 4), "]\n")
## 95% CI (χειρωνακτικό): [ 0.002 , 0.0197 ]
cat("95% CI (prop.test): [", round(test_result$conf.int[1], 4), ",", round(test_result$conf.int[2], 4), "]\n")
## 95% CI (prop.test): [ 0.002 , 0.0197 ]
Τα δύο CI είναι πολύ κοντά. Μικρές αποκλίσεις οφείλονται στο ότι το
prop.test() χρησιμοποιεί ελαφρώς διαφορετικό τρόπο
υπολογισμού SE (Wald vs Wilson), αλλά η ουσία παραμένει η ίδια.
# Cohen's h
h <- ES.h(p1 = 0.10, p2 = 0.08)
cat("Cohen's h:", round(h, 4), "\n")
## Cohen's h: 0.07
# Ελάχιστο n ανά ομάδα για power = 80%
power_result <- pwr.2p.test(h = h, sig.level = 0.05, power = 0.80)
power_result
##
## 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
cat("\nΑπαιτούμενο n ανά ομάδα:", ceiling(power_result$n), "\n")
##
## Απαιτούμενο n ανά ομάδα: 3205
cat("Τρέξαμε n ανά ομάδα:", n_control, "\n")
## Τρέξαμε n ανά ομάδα: 8000
Σχολιασμός: Αν τρέξαμε περισσότερα άτομα από το ελάχιστο, σημαίνει ότι είχαμε υπερ-αρκετή ισχύ (overpowered). Αν λιγότερα, θα κινδυνεύαμε να μη βρούμε τη διαφορά ακόμα κι αν υπάρχει.
# (α) Αναλογία ad/psa
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
# (β) Κατανομή ανά ημέρα εβδομάδας και ομάδα
ads |>
count(group, most_ads_day) |>
ggplot(aes(x = most_ads_day, y = n, fill = group)) +
geom_col(position = "dodge") +
labs(
title = "Κατανομή χρηστών ανά ημέρα εβδομάδας",
x = "Ημέρα", y = "Πλήθος"
) +
theme_minimal()
Σχολιασμός: Ελέγχουμε αν η κατανομή χρηστών ανά ημέρα είναι παρόμοια μεταξύ των δύο ομάδων, ώστε να επιβεβαιώσουμε ότι η τυχαιοποίηση δούλεψε σωστά.
# Summary stats
real_stats <- ads |>
group_by(group) |>
summarise(
n = n(),
conversions = sum(converted),
conversion_rate = mean(converted),
se = sqrt(conversion_rate * (1 - conversion_rate) / n),
ci_lower = conversion_rate - 1.96 * se,
ci_upper = conversion_rate + 1.96 * se,
.groups = "drop"
)
real_stats
## # 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
# Prop test (ad vs psa)
real_test <- prop.test(
x = real_stats$conversions,
n = real_stats$n,
correct = FALSE
)
tidy(real_test)
## # A tibble: 1 × 9
## estimate1 estimate2 statistic p.value parameter conf.low conf.high method
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
## 1 0.0179 0.0255 54.3 1.71e-13 1 -0.00943 -0.00595 2-sample …
## # ℹ 1 more variable: alternative <chr>
Ερμηνεία: Το p-value είναι 1.705281e-13. Αν p < 0.05, υπάρχει στατιστικά σημαντική διαφορά μεταξύ ad και psa.
day_stats <- ads |>
group_by(group, most_ads_day) |>
summarise(
n = n(),
conversion_rate = mean(converted),
se = sqrt(conversion_rate * (1 - conversion_rate) / n),
ci_lower = conversion_rate - 1.96 * se,
ci_upper = conversion_rate + 1.96 * se,
.groups = "drop"
)
# Σωστή σειρά ημερών
day_order <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
day_stats$most_ads_day <- factor(day_stats$most_ads_day, levels = day_order)
ggplot(day_stats, aes(x = most_ads_day, y = conversion_rate, color = group, group = group)) +
geom_line(linewidth = 1) +
geom_point(size = 2) +
geom_ribbon(aes(ymin = ci_lower, ymax = ci_upper, fill = group), alpha = 0.15, color = NA) +
scale_y_continuous(labels = percent) +
labs(
title = "Conversion Rate ανά ημέρα εβδομάδας",
x = "Ημέρα", y = "Conversion Rate"
) +
theme_minimal()
Σχολιασμός: Παρατηρούμε αν υπάρχει κάποια ημέρα με αισθητά μεγαλύτερη διαφορά μεταξύ ad και psa. Αυτό θα μπορούσε να αξιοποιηθεί στρατηγικά (π.χ. εστίαση διαφημίσεων τη συγκεκριμένη ημέρα).
# Κατώφλι επιχειρηματικής ουσίας
d_min <- 0.005
# Conversion rates
p_ad <- real_stats$conversion_rate[real_stats$group == "ad"]
p_psa <- real_stats$conversion_rate[real_stats$group == "psa"]
# Lifts
absolute_lift <- p_ad - p_psa
relative_lift <- absolute_lift / p_psa
cat("Conversion rate (ad):", round(p_ad, 4), "\n")
## Conversion rate (ad): 0.0255
cat("Conversion rate (psa):", round(p_psa, 4), "\n")
## Conversion rate (psa): 0.0179
cat("Absolute lift:", round(absolute_lift, 4), "\n")
## Absolute lift: 0.0077
cat("Relative lift:", percent(relative_lift, accuracy = 0.1), "\n")
## Relative lift: 43.1%
cat("Κατώφλι δ_min:", d_min, "\n")
## Κατώφλι δ_min: 0.005
# CI της διαφοράς
ci_diff <- real_test$conf.int
cat("95% CI της διαφοράς: [", round(ci_diff[1], 5), ",", round(ci_diff[2], 5), "]\n")
## 95% CI της διαφοράς: [ -0.00943 , -0.00595 ]
Ανάλυση 6 περιπτώσεων CI:
# Ποια περίπτωση ισχύει;
if (ci_diff[1] > d_min) {
cat("Περίπτωση A: Ξεκάθαρα θετικό & πρακτικά σημαντικό. Σύσταση: ΕΦΑΡΜΟΓΗ της καμπάνιας.\n")
} else if (ci_diff[1] > 0 & ci_diff[2] > d_min) {
cat("Περίπτωση B: Στατιστικά σημαντικό, πρακτική σημασία αβέβαιη. Σύσταση: Πιθανή εφαρμογή, αλλά χρειάζεται περαιτέρω αξιολόγηση.\n")
} else if (ci_diff[1] > 0 & ci_diff[2] < d_min) {
cat("Περίπτωση C: Στατιστικά σημαντικό αλλά πρακτικά ασήμαντο. Σύσταση: ΜΗ εφαρμογή — το κόστος πιθανώς ξεπερνά το όφελος.\n")
} else if (ci_diff[1] < 0 & ci_diff[2] > 0) {
cat("Περίπτωση D: Δεν απορρίπτουμε H₀. Σύσταση: ΜΗ εφαρμογή — δεν υπάρχει αρκετή απόδειξη.\n")
} else if (ci_diff[1] > -d_min & ci_diff[2] < 0) {
cat("Περίπτωση E: Αρνητικό αλλά μικρό αποτέλεσμα. Σύσταση: ΜΗ εφαρμογή.\n")
} else {
cat("Περίπτωση F: Η διαφήμιση βλάπτει σημαντικά. Σύσταση: ΑΠΟΦΥΓΗ.\n")
}
## Περίπτωση F: Η διαφήμιση βλάπτει σημαντικά. Σύσταση: ΑΠΟΦΥΓΗ.
Στα simulated data, το p-value του prop.test() είναι
0.0163. Αν είναι μικρότερο του 0.05, απορρίπτουμε την H₀ ότι τα
conversion rates είναι ίσα, δηλαδή η νέα διαφήμιση έχει στατιστικά
σημαντικό αποτέλεσμα.
Τα δύο CI είναι πολύ κοντά αλλά όχι ταυτόσημα. Αυτό συμβαίνει γιατί
το prop.test() χρησιμοποιεί τη μέθοδο Wilson score
interval, ενώ ο χειρωνακτικός υπολογισμός χρησιμοποιεί Wald interval
(normal approximation). Η διαφορά είναι αμελητέα σε μεγάλα δείγματα.
Σύμφωνα με την power analysis, χρειαζόμασταν 3205 άτομα ανά ομάδα για power 80%. Τρέξαμε 8000 ανά ομάδα. Αν 8000 > απαιτούμενο n, το πείραμα ήταν overpowered (καλό — αυξάνει την πιθανότητα ανίχνευσης του πραγματικού effect). Αν 8000 < απαιτούμενο n, θα ήταν underpowered (κίνδυνος να χάσουμε πραγματικό effect).