Η παρούσα ανάλυση αξιολογεί την αποτελεσματικότητα μιας νέας διαφημιστικής καμπάνιας μέσω A/B testing. Εξετάζουμε αν η νέα διαφήμιση αυξάνει τα conversion rates σε σχέση με ένα ουδέτερο μήνυμα (PSA).
Βασικά Ευρήματα:
# ============================================================
# ΕΡΓΑΣΙΑ 010 — A/B Testing & Causal Inference
# Dataset: Marketing A/B Testing (Kaggle)
# Στόχος: Αξιολόγηση αποτελεσματικότητας διαφημιστικής καμπάνιας
# ============================================================
# --- Φόρτωση βιβλιοθηκών ---
library(tidyverse)
library(pwr)
library(broom)
library(scales)
library(janitor)
set.seed(42)# --- Φόρτωση δεδομένων ---
# Σημείωση: Βεβαιωθείτε ότι το αρχείο marketing_AB.csv βρίσκεται στον working directory
ads <- read_csv("marketing_AB.csv") |>
janitor::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, …
Περιγραφή Δεδομένων:
user_id: Unique identifier για κάθε χρήστηtest_group: Ομάδα πειράματος (“psa” = control, “ad” =
treatment)converted: Binary outcome (1 = conversion, 0 = no
conversion)most_ads_day: Ημέρα εβδομάδας με τις περισσότερες
διαφημίσειςΠριν αναλύσουμε τα πραγματικά δεδομένα, θα δημιουργήσουμε ένα προσομοιωμένο πείραμα όπου γνωρίζουμε την πραγματική επίδραση. Αυτό μας επιτρέπει να επαληθεύσουμε τις μεθόδους μας.
# ============================================================
# TODO 1: Δημιουργία προσομοιωμένου πειράματος
# ============================================================
# --- Παράμετροι πειράματος ---
n_control <- 8000 # μέγεθος ομάδας ελέγχου
n_treatment <- 8000 # μέγεθος πειραματικής ομάδας
p_control <- 0.08 # baseline conversion rate (8%)
p_treatment <- 0.10 # treatment conversion rate (10%)
# Η πραγματική διαφορά είναι: +2 percentage points (25% relative lift)
# --- Δημιουργία δεδομένων ---
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), # Control group conversions
rbinom(n_treatment, 1, p_treatment) # Treatment group conversions
)
)
# Προβολή πρώτων γραμμών
head(experiment, 10)Επεξήγηση:
rbinom() για να προσομοιώσουμε τυχαία
binary outcomes# ============================================================
# TODO 2: Υπολογισμός στατιστικών ανά ομάδα
# ============================================================
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
)
summary_statsΕρμηνεία:
# ============================================================
# TODO 3: Οπτικοποίηση conversion rates με 95% CI
# ============================================================
ggplot(summary_stats, aes(x = group, y = conversion_rate, fill = group)) +
geom_col(alpha = 0.7, width = 0.6) +
geom_errorbar(
aes(ymin = ci_lower, ymax = ci_upper),
width = 0.2,
linewidth = 1
) +
geom_text(
aes(label = paste0(round(conversion_rate * 100, 2), "%")),
vjust = -0.5,
hjust = 0.5,
nudge_y = 0.005,
size = 5,
fontface = "bold"
) +
scale_y_continuous(
labels = percent_format(accuracy = 1),
limits = c(0, 0.13),
expand = c(0, 0)
) +
scale_fill_manual(values = c("control" = "#3498db", "treatment" = "#e74c3c")) +
labs(
title = "Conversion Rate Comparison: Control vs Treatment",
subtitle = "Simulated A/B Test (n = 16,000)",
x = NULL,
y = "Conversion Rate",
caption = "Error bars represent 95% confidence intervals"
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "none",
plot.title = element_text(face = "bold", size = 16),
panel.grid.major.x = element_blank()
)Παρατηρήσεις:
# ============================================================
# TODO 4: Έλεγχος υποθέσεων με prop.test()
# ============================================================
# Δημιουργία πίνακα για prop.test
conversions_table <- summary_stats |>
select(conversions, n) |>
as.matrix()
# Διεξαγωγή two-sample proportion test
test_result <- prop.test(
x = conversions_table[, "conversions"],
n = conversions_table[, "n"],
correct = FALSE # χωρίς Yates continuity correction
)
# Μορφοποιημένο output
tidy(test_result)Υποθέσεις:
Αποτελέσματα:
# ============================================================
# TODO 5: Manual verification των αποτελεσμάτων
# ============================================================
# Εξαγωγή δεδομένων ανά ομάδα
p_control_hat <- summary_stats$conversion_rate[1]
p_treatment_hat <- summary_stats$conversion_rate[2]
n_c <- summary_stats$n[1]
n_t <- summary_stats$n[2]
# (α) Pooled estimate
p_pool <- (summary_stats$conversions[1] + summary_stats$conversions[2]) /
(n_c + n_t)
# (β) Pooled standard error
se_pool <- sqrt(p_pool * (1 - p_pool) * (1/n_c + 1/n_t))
# (γ) Διαφορά conversion rates
delta <- p_treatment_hat - p_control_hat
# (δ) SE για τη διαφορά (unpooled)
se_diff <- sqrt(
(p_control_hat * (1 - p_control_hat) / n_c) +
(p_treatment_hat * (1 - p_treatment_hat) / n_t)
)
# 95% CI για τη διαφορά
ci_diff_lower <- delta - 1.96 * se_diff
ci_diff_upper <- delta + 1.96 * se_diff
# Εκτύπωση αποτελεσμάτων
cat("=== Χειρωνακτικοί Υπολογισμοί ===\n\n")## === Χειρωνακτικοί Υπολογισμοί ===
## Pooled estimate (p̂_pool): 0.0901
## Pooled SE: 0.00453
## Διαφορά (δ): 0.0109 (1.09 percentage points)
## SE της διαφοράς: 0.00453
## 95% CI για δ: [ 0.002 , 0.0197 ]
## === Σύγκριση με prop.test() ===
cat("prop.test() CI: [",
round(test_result$conf.int[1], 4), ",",
round(test_result$conf.int[2], 4), "]\n")## prop.test() CI: [ -0.0197 , -0.002 ]
## Manual CI: [ 0.002 , 0.0197 ]
# Υπολογισμός z-statistic
z_stat <- delta / se_diff
p_value_manual <- 2 * (1 - pnorm(abs(z_stat)))
cat("Z-statistic:", round(z_stat, 3), "\n")## Z-statistic: 2.403
## p-value (manual): 0.01626
## p-value (prop.test): 0.01628
Ερμηνεία:
prop.test()# ============================================================
# TODO 6: Power analysis — πόσα δεδομένα χρειαζόμασταν;
# ============================================================
# Υπολογισμός effect size (Cohen's h)
effect_size <- ES.h(p1 = p_treatment, p2 = p_control)
cat("Cohen's h (effect size):", round(effect_size, 4), "\n\n")## Cohen's h (effect size): 0.07
# Power analysis για power = 80%, α = 0.05
power_result <- pwr.2p.test(
h = effect_size,
sig.level = 0.05,
power = 0.80,
alternative = "two.sided"
)
print(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
##
## === Σύγκριση Μεγέθους Δείγματος ===
## Απαιτούμενο n ανά ομάδα (80% power): 3205
## Πραγματικό n ανά ομάδα: 8000
## Oversampling factor: 2.5 x
# Υπολογισμός της power με το δείγμα μας
achieved_power <- pwr.2p.test(
h = effect_size,
n = n_control,
sig.level = 0.05,
alternative = "two.sided"
)$power
cat("Statistical power με n =", n_control, ":",
percent(achieved_power, accuracy = 0.1), "\n")## Statistical power με n = 8000 : 99.3%
Ερμηνεία:
Τώρα εφαρμόζουμε τις ίδιες μεθόδους στα πραγματικά δεδομένα του Kaggle.
# ============================================================
# TODO 7: Έλεγχος τυχαιοποίησης (randomization check)
# ============================================================
# (α) Αναλογία ad/psa
group_balance <- ads |>
count(group) |>
mutate(
proportion = n / sum(n),
percentage = percent(proportion, accuracy = 0.01)
)
group_balance# Test για 50/50 split
binom.test(
x = group_balance$n[2], # ad group
n = sum(group_balance$n),
p = 0.5
)##
## Exact binomial test
##
## data: group_balance$n[2] and sum(group_balance$n)
## number of successes = 564577, number of trials = 588101, p-value <
## 2.2e-16
## alternative hypothesis: true probability of success is not equal to 0.5
## 95 percent confidence interval:
## 0.9594961 0.9604995
## sample estimates:
## probability of success
## 0.9600001
Παρατηρήσεις:
# (β) Κατανομή ανά ημέρα εβδομάδας
day_distribution <- ads |>
count(group, most_ads_day) |>
group_by(group) |>
mutate(proportion = n / sum(n))
ggplot(day_distribution, aes(x = most_ads_day, y = proportion, fill = group)) +
geom_col(position = "dodge", alpha = 0.8) +
scale_y_continuous(labels = percent_format(accuracy = 1)) +
scale_fill_manual(
values = c("psa" = "#3498db", "ad" = "#e74c3c"),
labels = c("Control (PSA)", "Treatment (Ad)")
) +
labs(
title = "Distribution of Users Across Days of the Week",
subtitle = "Checking for balance in temporal covariates",
x = "Day of the Week",
y = "Proportion of Users",
fill = "Group"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "top",
plot.title = element_text(face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1)
)Ερμηνεία:
# ============================================================
# TODO 8: Conversion rate analysis στα πραγματικά δεδομένα
# ============================================================
# Υπολογισμός στατιστικών ανά ομάδα
real_summary <- 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
)
real_summary# Proportion test
real_test <- prop.test(
x = c(real_summary$conversions[1], real_summary$conversions[2]),
n = c(real_summary$n[1], real_summary$n[2]),
correct = FALSE
)
# Μορφοποιημένο output
tidy(real_test) |>
mutate(
statistic = round(statistic, 3),
p.value = format.pval(p.value, digits = 4),
conf.low = round(conf.low, 5),
conf.high = round(conf.high, 5)
)# Visualization
ggplot(real_summary, aes(x = group, y = conversion_rate, fill = group)) +
geom_col(alpha = 0.7, width = 0.6) +
geom_errorbar(
aes(ymin = ci_lower, ymax = ci_upper),
width = 0.2,
linewidth = 1
) +
geom_text(
aes(label = paste0(round(conversion_rate * 100, 2), "%")),
vjust = -0.5,
hjust = 0.5,
nudge_y = 0.001,
size = 5,
fontface = "bold"
) +
scale_y_continuous(
labels = percent_format(accuracy = 0.1),
limits = c(0, max(real_summary$ci_upper) * 1.15),
expand = c(0, 0)
) +
scale_fill_manual(
values = c("psa" = "#3498db", "ad" = "#e74c3c"),
labels = c("Control (PSA)", "Treatment (Ad)")
) +
labs(
title = "Real-World A/B Test Results",
subtitle = paste0("Marketing Campaign Effectiveness (n = ",
format(sum(real_summary$n), big.mark = ","), ")"),
x = NULL,
y = "Conversion Rate",
caption = "Error bars represent 95% confidence intervals",
fill = NULL
) +
theme_minimal(base_size = 13) +
theme(
legend.position = "top",
plot.title = element_text(face = "bold", size = 16),
panel.grid.major.x = element_blank()
)Βασικά Ευρήματα:
# ============================================================
# TODO 9: Conversion rate ανά ημέρα εβδομάδας
# ============================================================
# Υπολογισμός conversion rate + CI ανά ημέρα και ομάδα
daily_conversion <- ads |>
group_by(group, most_ads_day) |>
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"
)
# Εύρεση ημέρας με μέγιστη διαφορά
daily_diff <- daily_conversion |>
select(group, most_ads_day, conversion_rate) |>
pivot_wider(names_from = group, values_from = conversion_rate) |>
mutate(
diff = ad - psa,
abs_diff = abs(diff)
) |>
arrange(desc(abs_diff))
cat("Ημέρα με τη μεγαλύτερη διαφορά:\n")## Ημέρα με τη μεγαλύτερη διαφορά:
## # A tibble: 7 × 4
## most_ads_day psa ad diff
## <chr> <dbl> <dbl> <dbl>
## 1 Tuesday 0.0144 0.0304 0.0160
## 2 Monday 0.0226 0.0332 0.0107
## 3 Wednesday 0.0158 0.0254 0.00960
## 4 Saturday 0.0140 0.0213 0.00731
## 5 Friday 0.0163 0.0225 0.00616
## 6 Sunday 0.0206 0.0246 0.00402
## 7 Thursday 0.0202 0.0216 0.00141
# Line plot με ribbon για 95% CI
ggplot(daily_conversion, aes(x = most_ads_day, y = conversion_rate,
color = group, group = group)) +
geom_ribbon(
aes(ymin = ci_lower, ymax = ci_upper, fill = group),
alpha = 0.15,
color = NA
) +
geom_line(linewidth = 1.2) +
geom_point(size = 3, shape = 21, fill = "white", stroke = 1.5) +
scale_y_continuous(
labels = percent_format(accuracy = 0.1),
limits = c(0, max(daily_conversion$ci_upper) * 1.05)
) +
scale_color_manual(
values = c("psa" = "#3498db", "ad" = "#e74c3c"),
labels = c("Control (PSA)", "Treatment (Ad)")
) +
scale_fill_manual(
values = c("psa" = "#3498db", "ad" = "#e74c3c"),
labels = c("Control (PSA)", "Treatment (Ad)")
) +
labs(
title = "Conversion Rate by Day of the Week",
subtitle = "Segmented analysis with 95% confidence intervals",
x = "Day of the Week",
y = "Conversion Rate",
color = "Group",
fill = "Group",
caption = "Shaded areas represent 95% CI"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "top",
plot.title = element_text(face = "bold", size = 15),
axis.text.x = element_text(angle = 45, hjust = 1)
)Insights:
# ============================================================
# TODO 10 (BONUS): Επιχειρηματική απόφαση
# ============================================================
# Ορισμός minimum detectable effect (practical significance threshold)
delta_min <- 0.005 # 0.5 percentage points
# Εξαγωγή αποτελεσμάτων
p_control_real <- real_summary$conversion_rate[1]
p_treatment_real <- real_summary$conversion_rate[2]
# Absolute lift
absolute_lift <- p_treatment_real - p_control_real
# Relative lift
relative_lift <- (p_treatment_real - p_control_real) / p_control_real
# 95% CI για τη διαφορά
ci_lower_real <- real_test$conf.int[1]
ci_upper_real <- real_test$conf.int[2]
# Κατηγοριοποίηση αποτελέσματος
decision_case <- case_when(
ci_lower_real > delta_min ~ "A: Strong Win - Launch",
ci_lower_real > 0 & ci_upper_real > delta_min ~ "B: Likely Win - Launch with monitoring",
ci_lower_real < 0 & ci_upper_real > delta_min ~ "C: Inconclusive - Need more data",
ci_upper_real < 0 & ci_lower_real < -delta_min ~ "D: Strong Loss - Do not launch",
TRUE ~ "E: Marginal - Cost-benefit analysis needed"
)
# Εκτύπωση αποτελεσμάτων
cat("=== ΕΠΙΧΕΙΡΗΜΑΤΙΚΗ ΑΞΙΟΛΟΓΗΣΗ ===\n\n")## === ΕΠΙΧΕΙΡΗΜΑΤΙΚΗ ΑΞΙΟΛΟΓΗΣΗ ===
## Absolute Lift: 0.77%
## Relative Lift: 43.1%
cat("95% CI για τη διαφορά: [",
percent(ci_lower_real, accuracy = 0.01), ",",
percent(ci_upper_real, accuracy = 0.01), "]\n\n")## 95% CI για τη διαφορά: [ -0.94% , -0.60% ]
## Minimum Detectable Effect (MDE): 0%
## Decision Case: D: Strong Loss - Do not launch
## === ΤΕΛΙΚΗ ΣΥΣΤΑΣΗ ===
if (ci_lower_real > delta_min) {
cat("✅ STRONG RECOMMENDATION: Launch the new ad campaign\n")
cat("Η νέα διαφήμιση έχει στατιστικά σημαντική ΚΑΙ επιχειρηματικά ουσιαστική επίδραση.\n")
cat("Το lower bound του CI υπερβαίνει το MDE → υψηλή βεβαιότητα για θετικό ROI.\n")
} else if (ci_lower_real > 0) {
cat("⚠️ CONDITIONAL RECOMMENDATION: Launch with close monitoring\n")
cat("Υπάρχει στατιστικά σημαντική διαφορά, αλλά το CI περιλαμβάνει μικρές τιμές.\n")
cat("Προτείνεται σταδιακή υλοποίηση και continuous measurement.\n")
} else {
cat("❌ DO NOT LAUNCH: Insufficient evidence of positive effect\n")
cat("Τα δεδομένα δεν υποστηρίζουν θετική επίδραση της καμπάνιας.\n")
}## ❌ DO NOT LAUNCH: Insufficient evidence of positive effect
## Τα δεδομένα δεν υποστηρίζουν θετική επίδραση της καμπάνιας.
Απάντηση:
## p-value (simulated data): 0.01628
## p-value (real data): 1.705e-13
## Για α = 0.05:
if (real_test$p.value < 0.05) {
cat("✅ ΑΠΟΡΡΙΠΤΟΥΜΕ την H₀\n")
cat("Συμπέρασμα: Υπάρχει στατιστικά σημαντική διαφορά στα conversion rates.\n")
} else {
cat("❌ ΔΕΝ απορρίπτουμε την H₀\n")
cat("Συμπέρασμα: Δεν υπάρχει επαρκές evidence για διαφορά.\n")
}## ✅ ΑΠΟΡΡΙΠΤΟΥΜΕ την H₀
## Συμπέρασμα: Υπάρχει στατιστικά σημαντική διαφορά στα conversion rates.
Απάντηση:
Τα confidence intervals είναι σχεδόν πανομοιότυπα:
Οι μικρές διαφορές οφείλονται σε:
prop.test() χρησιμοποιεί Wilson score
interval (πιο accurate για extreme proportions)Και οι δύο μέθοδοι οδηγούν στο ίδιο συμπέρασμα.
Απάντηση:
## Απαιτούμενο n ανά ομάδα (80% power): 3205
## Πραγματικό n ανά ομάδα: 8000
## Oversampling factor: 2.5 x
## Achieved power με n = 8000: 99.3%
## Τι συνεπάγεται αυτό:
## • Το δείγμα μας είναι ΥΠΕΡ-αρκετό για να ανιχνεύσουμε το effect
## • Έχουμε σχεδόν 100% power → ελάχιστη πιθανότητα Type II error
cat("• Θα μπορούσαμε να τρέξουμε το πείραμα με ~", ceiling(power_result$n),
" ανά ομάδα και να έχουμε αξιόπιστα αποτελέσματα\n")## • Θα μπορούσαμε να τρέξουμε το πείραμα με ~ 3205 ανά ομάδα και να έχουμε αξιόπιστα αποτελέσματα
Implications:
prop.test()⚠️ CONDITIONAL LAUNCH
University of Macedonia | Business Analytics
Gio — 2025