Setup

  • Dataset: Marketing A/B Testing (Kaggle)
  • Στόχος: Αξιολόγηση αποτελεσματικότητας διαφημιστικής καμπάνιας
# Φόρτωση δεδομένων
set.seed(42)

ads <- read_csv("marketing_AB.csv") |>
  janitor::clean_names() |>
  mutate(
    group     = factor(test_group, levels = c("psa", "ad")),
    converted = as.integer(converted)
  )
## New names:
## Rows: 588101 Columns: 7
## ── Column specification
## ──────────────────────────────────────────────────────── Delimiter: "," chr
## (2): test group, most ads day dbl (4): ...1, user id, total ads, most ads hour
## lgl (1): converted
## ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
## Specify the column types or set `show_col_types = FALSE` to quiet this message.
## • `` -> `...1`
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, …

Μέρος Α — Βασικός A/B Έλεγχος (Simulated Experiment)

# Παράμετροι πειράματος
n_control   <- 8000      # μέγεθος ομάδας ελέγχου
n_treatment <- 8000      # μέγεθος πειραματικής ομάδας
p_control   <- 0.08      # baseline conversion rate
p_treatment <- 0.10      # μετά την αλλαγή (true effect = +2%)

# Δημιουργία ενός tibble «experiment» με στήλες:
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, …
# Υπολογισμός ανά ομάδα: n, conversions, conversion_rate, standard error (se) και 95% CI (ci_lower, ci_upper)

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
## # 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
# Οπτικοποίηση με ggplot2 (geom_col + geom_errorbar)

ggplot(summary_stats, aes(x = group, y = conversion_rate, 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(conversion_rate, 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" = "#00cc00",
                                "treatment" = "#3b82f6")) +
  labs(
    title    = "A/B Test: Πιθανότητα conversion ανά ομάδα",
    subtitle = "Με 95% διαστήματα εμπιστοσύνης",
    x = NULL, y = "Conversion probability"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

# Διεξαγωγή ελέγχου υποθέσεων με prop.test()

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
)

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
# Χειρωνακτική επαλήθευση

## 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$conversion_rate[2] - summary_stats$conversion_rate[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]

Παρατηρείται ότι:

  • το CI του prop.test() είναι [-0.019744875 -0.002005125]
  • το CI από την χειρωνακτική επαλήθευση είναι [[0.0020, 0.0197]]

Δηλαδή, οι τιμές είναι αντίθετες. Αύτο σημβαίνει επειδη η CI του prop.test() προκυπτει από p1-p2 (όπου p1:p_control και p2:p_treatment), ενώ το το CI από την χειρωνακτική επαλήθευση προκύπτει απο delta <- summary_stats\(conversion_rate[2] - summary_stats\)conversion_rate[1] = p2 - p1

# Power Analysis — υπολόγισε με pwr.2p.test()

## h = Cohen's effect size για δύο αναλογίες
effect_size <- ES.h(p1 = 0.10, p2 = 0.08)
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"
)

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

Θα χρειαζόμασταν >3205 δείγματα για power = 80% και α = 0.05. Με το δείγμα μας των 8000 υπερππληρόυμε τις προυποθέσεις

Μέρος Β — Πραγματικά Δεδομένα (Kaggle Marketing AB)

# Invariants check
## 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()

Note: Η αναλογία ad/psa δεν είναι 50/50 ,είναι 0.04/0.96

# Οπτικοποίηση της κατανομής ανά ημέρα εβδομάδας (most_ads_day)
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

Την μεγιστη ανισορροπία έχει κατα φθίνουσα σειρά:

  • Πέμπτη (Thursday)
  • Σαββατο (Saturday)
  • Κυριακή (Sunday)
  • Τετάρτη (Wednesday)
  • Τρίτη (Tuesday)
  • Παρασκευή (Friday)
  • Δευτέρα (monday)
# Υπολογισμός conversion rate, SE, 95% CI ανά ομάδα

test <- prop.test(
  x = ads_summary$conversions,
  n = ads_summary$n,
  correct = FALSE
)

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
# Segmentation — conversion rate ανά ημέρα εβδομάδας

# 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" = "#00cc00", "ad" = "#3b82f6")) +
  scale_fill_manual(values = c("psa" = "#00cc00", "ad" = "#3b82f6")) +
  labs(title = "Conversion rate ανά ημέρα εβδομάδας",
       subtitle = "Με 95% διαστήματα εμπιστοσύνης",
       x = NULL, y = "Conversion rate") +
  theme_minimal(base_size = 13)

Η ημέρα με τη μεγαλύτερη οπτική διαφορά μεταξύ ομάδων είναι η Τρίτη (Tuesday) λόγο της μέγιστης απόστασης μεταξύ καμπύλων

# Επιχειρηματική απόφαση

delta_min <- 0.005   # κατώφλι επιχειρηματικής ουσίας

conf_int <- broom::tidy(test)
lift_pct <- (conf_int$estimate2 - conf_int$estimate1) /
             conf_int$estimate1 * 100

cat(sprintf("Absolute lift: %.2f ποσοστ. μονάδες\n",
            (conf_int$estimate2 - conf_int$estimate1) * 100))
## Absolute lift: 0.77 ποσοστ. μονάδες
cat(sprintf("Relative lift: %+.1f%%\n", lift_pct))
## Relative lift: +43.1%
cat(sprintf("95%% CI for difference: [%.4f, %.4f]\n",
            -conf_int$conf.low, -conf_int$conf.high))
## 95% CI for difference: [0.0094, 0.0060]
#   Όρισε δ_min = 0.005 (κατώφλι επιχειρηματικής ουσίας)
#   Υπολόγισε το absolute lift και το relative lift
#   Ποια από τις 6 περιπτώσεις CI ισχύει; (A/B/C/D/E/F)
#   Ποια η τελική σύστασή σου;