1 Σκοπός εργασίας

Σε αυτή την εργασία αναλύεται ένα A/B test για νέα διαφημιστική καμπάνια. Η ομάδα control είδε ουδέτερο μήνυμα, δηλαδή psa, ενώ η ομάδα treatment είδε τη νέα διαφήμιση, δηλαδή ad.

Το βασικό ερώτημα είναι:

Η νέα διαφήμιση αυξάνει πραγματικά τις μετατροπές ή η διαφορά που παρατηρούμε μπορεί να οφείλεται στην τύχη;

Θα εξετάσουμε το ερώτημα με δύο τρόπους:

  1. Με προσομοιωμένο A/B test, ώστε να φανεί καθαρά η λογική του ελέγχου.
  2. Με το πραγματικό dataset marketing_AB.csv από το Kaggle.

2 Φόρτωση πακέτων και δεδομένων

packages <- c("tidyverse", "pwr", "broom", "scales", "janitor")

missing_packages <- packages[!sapply(packages, requireNamespace, quietly = TRUE)]

if (length(missing_packages) > 0) {
  install.packages(missing_packages)
}

invisible(lapply(packages, library, character.only = TRUE))
set.seed(42)

data_path <- params$data_file

if (!file.exists(data_path)) {
  stop("Δεν βρέθηκε το αρχείο marketing_AB.csv. Βάλτε το CSV στον ίδιο φάκελο με το .Rmd ή αλλάξτε το params$data_file στο YAML header.")
}

ads <- read_csv(data_path, show_col_types = FALSE) |>
  janitor::clean_names() |>
  mutate(
    group = factor(test_group, levels = c("psa", "ad")),
    converted = as.integer(converted),
    most_ads_day = factor(
      most_ads_day,
      levels = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
    )
  )

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  <fct> Monday, Tuesday, Tuesday, Tuesday, Friday, Saturday, Wed…
## $ 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, …

Το dataset περιέχει παρατηρήσεις χρηστών, την ομάδα στην οποία ανήκουν (psa ή ad), καθώς και το binary outcome converted, όπου:

  • converted = 1: ο χρήστης έκανε conversion.
  • converted = 0: ο χρήστης δεν έκανε conversion.

3 Μέρος Α — Simulated A/B Test

3.1 TODO 1 — Δημιουργία προσομοιωμένου πειράματος

Αρχικά δημιουργούμε ένα απλό προσομοιωμένο A/B test. Η ομάδα control έχει baseline conversion rate 8%, ενώ η treatment ομάδα έχει πραγματικό conversion rate 10%. Άρα το πραγματικό effect που θέλουμε να εντοπίσουμε είναι +2 ποσοστιαίες μονάδες.

# Παράμετροι πειράματος
n_control   <- 8000
n_treatment <- 8000
p_control   <- 0.08
p_treatment <- 0.10

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)
  )
) |>
  mutate(group = factor(group, levels = c("control", "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     <fct> control, control, control, control, control, control, contro…
## $ converted <int> 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, …

3.2 TODO 2 — Περιγραφικά στατιστικά ανά ομάδα

Υπολογίζουμε για κάθε ομάδα:

  • το πλήθος των χρηστών,
  • τον αριθμό των conversions,
  • το conversion rate,
  • το standard error,
  • το 95% confidence interval.
summary_stats <- experiment |>
  group_by(group) |>
  summarise(
    n = n(),
    conversions = sum(converted),
    conversion_rate = mean(converted),
    se = sqrt(conversion_rate * (1 - conversion_rate) / n),
    .groups = "drop"
  ) |>
  mutate(
    ci_lower = conversion_rate - 1.96 * se,
    ci_upper = conversion_rate + 1.96 * se
  )

summary_stats

Η μεταβλητή conversion_rate είναι η βασική μετρική του A/B test. Όσο υψηλότερη είναι, τόσο μεγαλύτερο ποσοστό χρηστών έκανε conversion.

3.3 TODO 3 — Οπτικοποίηση του simulated A/B test

ggplot(summary_stats, aes(x = group, y = conversion_rate, fill = group)) +
  geom_col(width = 0.55, 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.3,
    size = 5,
    fontface = "bold"
  ) +
  scale_y_continuous(labels = percent, limits = c(0, 0.13)) +
  scale_fill_manual(values = c("control" = "#6b7280", "treatment" = "#3b82f6")) +
  labs(
    title = "Simulated A/B Test: Conversion rate ανά ομάδα",
    subtitle = "Με 95% διαστήματα εμπιστοσύνης",
    x = NULL,
    y = "Conversion rate"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Από το γράφημα βλέπουμε αν η treatment ομάδα έχει υψηλότερο conversion rate από την control ομάδα και πόση αβεβαιότητα υπάρχει γύρω από κάθε εκτίμηση.

3.4 TODO 4 — Έλεγχος υποθέσεων με prop.test()

Ο στατιστικός έλεγχος που χρησιμοποιούμε είναι έλεγχος δύο αναλογιών.

Οι υποθέσεις είναι:

  • H₀: Δεν υπάρχει διαφορά στα conversion rates των δύο ομάδων.
  • H₁: Υπάρχει διαφορά στα conversion rates των δύο ομάδων.
sim_clicks <- summary_stats |>
  arrange(group) |>
  pull(conversions)

sim_visitors <- summary_stats |>
  arrange(group) |>
  pull(n)

test_result <- prop.test(
  x = sim_clicks,
  n = sim_visitors,
  conf.level = 0.95,
  correct = FALSE
)

test_result
## 
##  2-sample test for equality of proportions without continuity correction
## 
## data:  sim_clicks out of sim_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
sim_test_tidy <- broom::tidy(test_result) |>
  select(estimate1, estimate2, statistic, p.value, conf.low, conf.high)

sim_test_tidy

Προσοχή: επειδή η σειρά είναι control, treatment, το prop.test() δίνει confidence interval για τη διαφορά:

\[ control - treatment \]

Για να το ερμηνεύσουμε ως effect της treatment ομάδας, κοιτάμε αντίστροφα:

\[ treatment - control \]

3.5 TODO 5 — Χειρωνακτική επαλήθευση του confidence interval

Υπολογίζουμε χειρωνακτικά το pooled estimate, το pooled standard error, τη διαφορά και το 95% confidence interval.

p_control_hat <- summary_stats |>
  filter(group == "control") |>
  pull(conversion_rate)

p_treatment_hat <- summary_stats |>
  filter(group == "treatment") |>
  pull(conversion_rate)

n_control_hat <- summary_stats |>
  filter(group == "control") |>
  pull(n)

n_treatment_hat <- summary_stats |>
  filter(group == "treatment") |>
  pull(n)

# 1. Pooled estimate
p_pool <- sum(sim_clicks) / sum(sim_visitors)

# 2. Pooled standard error
se_pool <- sqrt(
  p_pool * (1 - p_pool) *
    (1 / n_control_hat + 1 / n_treatment_hat)
)

# 3. Difference: treatment - control
delta_sim <- p_treatment_hat - p_control_hat

# 4. 95% CI με pooled SE
margin_pool <- 1.96 * se_pool
manual_ci_pooled <- c(delta_sim - margin_pool, delta_sim + margin_pool)

# 5. Unpooled Wald CI, χρήσιμο για σύγκριση
se_unpooled <- sqrt(
  p_control_hat * (1 - p_control_hat) / n_control_hat +
    p_treatment_hat * (1 - p_treatment_hat) / n_treatment_hat
)

manual_ci_unpooled <- c(
  delta_sim - 1.96 * se_unpooled,
  delta_sim + 1.96 * se_unpooled
)

# Το prop.test CI είναι για control - treatment, άρα το αντιστρέφουμε
prop_ci_treatment_minus_control <- c(
  -sim_test_tidy$conf.high,
  -sim_test_tidy$conf.low
)

ci_comparison_sim <- tibble(
  method = c("Manual pooled", "Manual unpooled Wald", "prop.test inverted"),
  estimate_delta = c(delta_sim, delta_sim, delta_sim),
  ci_low = c(manual_ci_pooled[1], manual_ci_unpooled[1], prop_ci_treatment_minus_control[1]),
  ci_high = c(manual_ci_pooled[2], manual_ci_unpooled[2], prop_ci_treatment_minus_control[2])
)

ci_comparison_sim

Το χειρωνακτικό CI με pooled SE μπορεί να μη συμπίπτει απόλυτα με το CI του prop.test(), επειδή το prop.test() χρησιμοποιεί διαφορετική προσέγγιση για το confidence interval. Το pooled SE χρησιμοποιείται κυρίως για τον έλεγχο της μηδενικής υπόθεσης, όπου υποθέτουμε ότι οι δύο αναλογίες είναι ίσες.

3.6 TODO 6 — Power analysis

Το power analysis απαντά στο ερώτημα:

Πόσους χρήστες χρειαζόμασταν ανά ομάδα για να έχουμε 80% πιθανότητα να εντοπίσουμε διαφορά από 8% σε 10%, αν αυτή η διαφορά όντως υπάρχει;

effect_size <- ES.h(p1 = p_treatment, p2 = p_control)

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
required_n_per_group <- ceiling(sample_size_calc$n)
actual_n_per_group <- n_control

tibble(
  required_n_per_group = required_n_per_group,
  actual_n_per_group = actual_n_per_group,
  required_total_n = 2 * required_n_per_group,
  actual_total_n = n_control + n_treatment
)

Στο προσομοιωμένο παράδειγμα τρέξαμε 8000 χρήστες ανά ομάδα. Το απαιτούμενο δείγμα για power 80% είναι περίπου 3205 χρήστες ανά ομάδα. Άρα το simulated experiment έχει αρκετά μεγαλύτερο δείγμα από το ελάχιστο απαιτούμενο.

4 Μέρος Β — Real-World A/B Analysis

4.1 TODO 7 — Invariants check: έλεγχος τυχαιοποίησης

Πριν κοιτάξουμε το conversion rate, ελέγχουμε αν οι ομάδες φαίνονται ισορροπημένες ως προς μεταβλητές που δεν πρέπει να επηρεάζονται από το treatment. Αυτό λέγεται invariant check.

4.1.1 Αναλογία ad και psa

group_balance <- ads |>
  count(group) |>
  mutate(pct = n / sum(n))

group_balance
ggplot(group_balance, aes(x = group, y = pct, fill = group)) +
  geom_col(width = 0.55, alpha = 0.85) +
  geom_text(
    aes(label = percent(pct, accuracy = 0.1)),
    vjust = -0.5,
    size = 5,
    fontface = "bold"
  ) +
  scale_y_continuous(labels = percent, limits = c(0, 1)) +
  scale_fill_manual(values = c("psa" = "#6b7280", "ad" = "#3b82f6")) +
  labs(
    title = "Αναλογία χρηστών ανά ομάδα",
    subtitle = "Έλεγχος αν το split είναι κοντά στο 50/50",
    x = NULL,
    y = "% χρηστών"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Αν το split δεν είναι 50/50, αυτό δεν σημαίνει απαραίτητα ότι το πείραμα είναι άκυρο. Μπορεί η καμπάνια να σχεδιάστηκε με άνιση κατανομή. Ωστόσο, το μικρότερο control group δίνει μικρότερη ακρίβεια στην εκτίμηση του baseline conversion rate.

4.1.2 Κατανομή ανά ημέρα εβδομάδας

day_balance <- ads |>
  count(group, most_ads_day) |>
  group_by(group) |>
  mutate(pct = n / sum(n)) |>
  ungroup()

day_balance
ggplot(day_balance, aes(x = most_ads_day, y = pct, fill = group)) +
  geom_col(position = "dodge", alpha = 0.85) +
  scale_y_continuous(labels = percent) +
  scale_fill_manual(values = c("psa" = "#6b7280", "ad" = "#3b82f6")) +
  labs(
    title = "Invariant check: κατανομή ανά ημέρα εβδομάδας",
    subtitle = "Οι αναλογίες ανά ημέρα πρέπει να είναι σχετικά παρόμοιες μεταξύ των ομάδων",
    x = NULL,
    y = "% της κάθε ομάδας",
    fill = "Group"
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 35, hjust = 1))

day_imbalance <- day_balance |>
  select(group, most_ads_day, pct) |>
  pivot_wider(names_from = group, values_from = pct) |>
  mutate(
    diff_ad_minus_psa = ad - psa,
    abs_diff = abs(diff_ad_minus_psa)
  ) |>
  arrange(desc(abs_diff))

day_imbalance

Ο παραπάνω πίνακας δείχνει σε ποιες ημέρες υπάρχει η μεγαλύτερη διαφορά στην κατανομή των χρηστών μεταξύ ad και psa. Μεγάλη πρακτική ανισορροπία θα μπορούσε να δημιουργήσει bias, ειδικά αν η ημέρα εβδομάδας σχετίζεται με την πιθανότητα conversion.

4.2 TODO 8 — Conversion rate, confidence interval και prop.test()

ads_summary <- ads |>
  group_by(group) |>
  summarise(
    n = n(),
    conversions = sum(converted),
    conversion_rate = mean(converted),
    se = sqrt(conversion_rate * (1 - conversion_rate) / n),
    .groups = "drop"
  ) |>
  mutate(
    ci_lower = conversion_rate - 1.96 * se,
    ci_upper = conversion_rate + 1.96 * se
  )

ads_summary
ggplot(ads_summary, aes(x = group, y = conversion_rate, fill = group)) +
  geom_col(width = 0.55, 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.01)),
    vjust = -1.1,
    size = 5,
    fontface = "bold"
  ) +
  scale_y_continuous(labels = percent, limits = c(0, max(ads_summary$ci_upper) * 1.25)) +
  scale_fill_manual(values = c("psa" = "#6b7280", "ad" = "#3b82f6")) +
  labs(
    title = "Real A/B Test: Conversion rate ανά ομάδα",
    subtitle = "Με 95% διαστήματα εμπιστοσύνης",
    x = NULL,
    y = "Conversion rate"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

real_clicks <- ads_summary |>
  arrange(group) |>
  pull(conversions)

real_visitors <- ads_summary |>
  arrange(group) |>
  pull(n)

real_test <- prop.test(
  x = real_clicks,
  n = real_visitors,
  correct = FALSE
)

real_test_tidy <- broom::tidy(real_test) |>
  select(estimate1, estimate2, statistic, p.value, conf.low, conf.high)

real_test_tidy

Επειδή η σειρά των ομάδων είναι psa, ad, το estimate1 είναι το conversion rate του psa και το estimate2 είναι το conversion rate του ad. Το confidence interval του prop.test() είναι για τη διαφορά:

\[ psa - ad \]

Για επιχειρηματική ερμηνεία, όμως, μας ενδιαφέρει:

\[ ad - psa \]

real_effect <- tibble(
  psa_rate = real_test_tidy$estimate1,
  ad_rate = real_test_tidy$estimate2,
  absolute_lift = ad_rate - psa_rate,
  relative_lift = absolute_lift / psa_rate,
  ci_low_ad_minus_psa = -real_test_tidy$conf.high,
  ci_high_ad_minus_psa = -real_test_tidy$conf.low,
  p_value = real_test_tidy$p.value
)

real_effect |>
  mutate(
    psa_rate = percent(psa_rate, accuracy = 0.01),
    ad_rate = percent(ad_rate, accuracy = 0.01),
    absolute_lift = percent(absolute_lift, accuracy = 0.01),
    relative_lift = percent(relative_lift, accuracy = 0.1),
    ci_low_ad_minus_psa = percent(ci_low_ad_minus_psa, accuracy = 0.01),
    ci_high_ad_minus_psa = percent(ci_high_ad_minus_psa, accuracy = 0.01)
  )

Με βάση το πραγματικό dataset, το conversion rate της ομάδας ad είναι μεγαλύτερο από το conversion rate της ομάδας psa. Το p-value δείχνει αν αυτή η διαφορά είναι στατιστικά σημαντική.

4.3 TODO 9 — Segmentation: conversion rate ανά ημέρα εβδομάδας

ads_by_day <- ads |>
  group_by(most_ads_day, group) |>
  summarise(
    n = n(),
    conversions = sum(converted),
    conversion_rate = mean(converted),
    se = sqrt(conversion_rate * (1 - conversion_rate) / n),
    .groups = "drop"
  ) |>
  mutate(
    ci_lower = conversion_rate - 1.96 * se,
    ci_upper = conversion_rate + 1.96 * se
  )

ads_by_day
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 = ci_lower, ymax = ci_upper, 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",
    color = "Group",
    fill = "Group"
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 35, hjust = 1))

day_lift <- ads_by_day |>
  select(most_ads_day, group, conversion_rate) |>
  pivot_wider(names_from = group, values_from = conversion_rate) |>
  mutate(
    absolute_lift = ad - psa,
    relative_lift = absolute_lift / psa
  ) |>
  arrange(desc(absolute_lift))

day_lift |>
  mutate(
    psa = percent(psa, accuracy = 0.01),
    ad = percent(ad, accuracy = 0.01),
    absolute_lift = percent(absolute_lift, accuracy = 0.01),
    relative_lift = percent(relative_lift, accuracy = 0.1)
  )
best_day <- day_lift |>
  slice_max(absolute_lift, n = 1, with_ties = FALSE)

Η ημέρα με τη μεγαλύτερη απόλυτη διαφορά μεταξύ ad και psa είναι η Tuesday. Αυτό σημαίνει ότι εκεί η καμπάνια φαίνεται να έχει τη μεγαλύτερη διαφορά conversion rate σε σχέση με το control.

5 TODO 10 — Επιχειρηματική απόφαση

Ορίζουμε ως ελάχιστο επιχειρηματικά ουσιαστικό effect:

\[ \delta_{min} = 0.005 \]

Δηλαδή, για να προτείνουμε υλοποίηση/κλιμάκωση της καμπάνιας, δεν αρκεί απλώς να υπάρχει στατιστικά σημαντική διαφορά. Θέλουμε το lift να είναι τουλάχιστον 0.5 ποσοστιαίες μονάδες.

delta_min <- 0.005

absolute_lift <- real_effect$absolute_lift
relative_lift <- real_effect$relative_lift
ci_low <- real_effect$ci_low_ad_minus_psa
ci_high <- real_effect$ci_high_ad_minus_psa
p_value <- real_effect$p_value

classify_ci <- function(ci_low, ci_high, delta_min) {
  if (ci_low > delta_min) {
    return("A")
  } else if (ci_low > 0 && ci_high > delta_min) {
    return("B")
  } else if (ci_low > 0 && ci_high <= delta_min) {
    return("C")
  } else if (ci_low <= 0 && ci_high >= delta_min) {
    return("D")
  } else if (ci_low <= 0 && ci_high > 0 && ci_high < delta_min) {
    return("E")
  } else if (ci_high <= 0) {
    return("F")
  } else {
    return("Unclassified")
  }
}

ci_case <- classify_ci(ci_low, ci_high, delta_min)

case_explanation <- case_when(
  ci_case == "A" ~ "Το 95% CI είναι ολόκληρο πάνω από το επιχειρηματικό κατώφλι. Η καμπάνια είναι στατιστικά και επιχειρηματικά ισχυρή.",
  ci_case == "B" ~ "Το 95% CI είναι πάνω από το 0, αλλά ακουμπά/διασχίζει το επιχειρηματικό κατώφλι. Υπάρχει στατιστική σημαντικότητα, αλλά η επιχειρηματική ουσία έχει αβεβαιότητα.",
  ci_case == "C" ~ "Το 95% CI είναι πάνω από το 0 αλλά κάτω από το επιχειρηματικό κατώφλι. Υπάρχει στατιστική σημαντικότητα, αλλά όχι αρκετή επιχειρηματική ουσία.",
  ci_case == "D" ~ "Το 95% CI περιλαμβάνει και το 0 και το επιχειρηματικό κατώφλι. Το αποτέλεσμα είναι αβέβαιο/inconclusive.",
  ci_case == "E" ~ "Το 95% CI περιλαμβάνει το 0 και βρίσκεται κάτω από το επιχειρηματικό κατώφλι. Δεν υπάρχει καθαρή ένδειξη για ουσιαστικό θετικό effect.",
  ci_case == "F" ~ "Το 95% CI είναι κάτω από το 0. Η καμπάνια φαίνεται πιθανώς επιβλαβής.",
  TRUE ~ "Δεν ταξινομήθηκε."
)

recommendation <- case_when(
  ci_case == "A" ~ "Σύσταση: Υλοποίηση/κλιμάκωση της διαφημιστικής καμπάνιας.",
  ci_case == "B" ~ "Σύσταση: Προσεκτική υλοποίηση ή επιπλέον test, επειδή υπάρχει στατιστικό lift αλλά όχι πλήρης βεβαιότητα για το επιχειρηματικό κατώφλι.",
  ci_case == "C" ~ "Σύσταση: Όχι άμεση υλοποίηση, εκτός αν το κόστος είναι πολύ χαμηλό.",
  ci_case == "D" ~ "Σύσταση: Συνέχιση/επανάληψη του test για περισσότερα δεδομένα.",
  ci_case == "E" ~ "Σύσταση: Μη υλοποίηση με τα τρέχοντα δεδομένα.",
  ci_case == "F" ~ "Σύσταση: Διακοπή της καμπάνιας ή επανασχεδιασμός.",
  TRUE ~ "Σύσταση: Χρειάζεται περαιτέρω έλεγχος."
)

business_summary <- tibble(
  delta_min = delta_min,
  absolute_lift = absolute_lift,
  relative_lift = relative_lift,
  ci_low = ci_low,
  ci_high = ci_high,
  p_value = p_value,
  ci_case = ci_case,
  explanation = case_explanation,
  recommendation = recommendation
)

business_summary |>
  mutate(
    delta_min = percent(delta_min, accuracy = 0.01),
    absolute_lift = percent(absolute_lift, accuracy = 0.01),
    relative_lift = percent(relative_lift, accuracy = 0.1),
    ci_low = percent(ci_low, accuracy = 0.01),
    ci_high = percent(ci_high, accuracy = 0.01)
  )

Η τελική κατηγορία CI είναι A.
Το 95% CI είναι ολόκληρο πάνω από το επιχειρηματικό κατώφλι. Η καμπάνια είναι στατιστικά και επιχειρηματικά ισχυρή.
Σύσταση: Υλοποίηση/κλιμάκωση της διαφημιστικής καμπάνιας.

6 Απαντήσεις στα ζητούμενα

6.1 1. Ποιο είναι το p-value; Απορρίπτουμε την H₀ σε α = 0.05;

Το p-value του πραγματικού A/B test είναι:

tibble(
  p_value = p_value,
  alpha = 0.05,
  reject_H0 = p_value < 0.05
)

Εφόσον p-value < 0.05, απορρίπτουμε τη μηδενική υπόθεση H₀. Αυτό σημαίνει ότι υπάρχει στατιστικά σημαντική διαφορά στο conversion rate μεταξύ psa και ad.

Πιο συγκεκριμένα, το conversion rate του ad είναι 2.55%, ενώ το conversion rate του psa είναι 1.79%.

6.2 2. Συμπίπτει το χειρωνακτικό CI με αυτό του prop.test(); Αν όχι, γιατί;

Στο simulated μέρος, το χειρωνακτικό CI με pooled SE δεν είναι απαραίτητο να συμπίπτει απόλυτα με το CI του prop.test().

Ο λόγος είναι ότι το pooled SE βασίζεται στην υπόθεση της H₀, δηλαδή ότι οι δύο αναλογίες είναι ίσες. Είναι κατάλληλο για τον υπολογισμό του test statistic. Το confidence interval, όμως, μπορεί να υπολογιστεί με διαφορετική προσέγγιση, όπως κάνει το prop.test().

Άρα περιμένουμε τα αποτελέσματα να είναι κοντά, αλλά όχι ακριβώς ίδια.

ci_comparison_sim

6.3 3. Πόσα άτομα χρειαζόντουσαν για power 80%; Πόσα τρέξαμε; Τι συνεπάγεται αυτό;

Στο simulated A/B test, το power analysis έδειξε ότι χρειαζόμασταν περίπου 3205 χρήστες ανά ομάδα για power 80% και α = 0.05.

Εμείς τρέξαμε 8000 χρήστες ανά ομάδα, δηλαδή 16000 συνολικά.

Αυτό σημαίνει ότι το πείραμα έχει περισσότερο δείγμα από το ελάχιστο απαιτούμενο. Άρα έχει αρκετή στατιστική ισχύ για να εντοπίσει μια διαφορά της τάξης των 2 ποσοστιαίων μονάδων.

7 Τελικό συμπέρασμα

Η πραγματική ανάλυση δείχνει ότι η ομάδα που είδε τη νέα διαφήμιση (ad) έχει υψηλότερο conversion rate από την ομάδα control (psa). Το αποτέλεσμα είναι στατιστικά σημαντικό, επειδή το p-value είναι μικρότερο από 0.05.

Επιπλέον, το absolute lift είναι 0.77% και το 95% CI για το lift είναι [0.60%, 0.94%]. Εφόσον το confidence interval είναι πάνω από το επιχειρηματικό κατώφλι 0.50%, η σύσταση είναι θετική.

Τελική σύσταση: η fintech startup μπορεί να υλοποιήσει ή να κλιμακώσει τη νέα διαφημιστική καμπάνια, υπό την προϋπόθεση ότι το κόστος απόκτησης/conversion παραμένει αποδεκτό και ότι δεν υπάρχουν άλλοι επιχειρησιακοί περιορισμοί.