ΕΡΓΑΣΙΑ 010-A/B Testing & Causal Inference

Dataset: Marketing A/B Testing (Kaggle)

Στόχος: Αξιολόγηση αποτελεσματικότητας διαφημιστικής καμπάνιας

Εγκατάσταση & Φόρτωση πακέτων

#install.packages(c("tidyverse", "pwr", "broom", "scales", "janitor"))

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.0     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(pwr)
## Warning: package 'pwr' was built under R version 4.5.3
library(broom)
## Warning: package 'broom' was built under R version 4.5.3
library(scales)
## Warning: package 'scales' was built under R version 4.5.3
## 
## Attaching package: 'scales'
## 
## The following object is masked from 'package:purrr':
## 
##     discard
## 
## The following object is masked from 'package:readr':
## 
##     col_factor
library(janitor)
## Warning: package 'janitor' was built under R version 4.5.3
## 
## Attaching package: 'janitor'
## 
## The following objects are masked from 'package:stats':
## 
##     chisq.test, fisher.test
set.seed(93)

Φόρτωση δεδομένων

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, …

ΜΕΡΟΣ Α — Simulated A/B Test

Παράμετροι πειράματος

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

Σχολιασμός:

  • Το dataset περιέχει 588.101 παρατηρήσεις και 7 μεταβλητές.

  • Μετά το clean_names() οι στήλες αποκτούν ονόματα σε καθαρή μορφή. Δημιουργούμε τα “psa”(control group) και “ad” (treatment group). Τέλος, μετατρέπουμε την στήλη converted σε 0/1 αντί για boolean.

  • Πριν αναλύσουμε τα πραγματικά δεδομένα, φτιάχνουμε ένα τεχνητό πείραμα όπου γνωρίζουμε την αλήθεια (ground truth). Έτσι ώστε να επαληθεύσουμε ότι οι στατιστικές μέθοδοί λειτουργούν σωστά.

  • Ορίζουμε ένα ρεαλιστικό σενάριο fintech όπου το baseline conversion είναι 8% και η νέα διαφήμιση αναμένεται να το ανεβάσει στο 10%. Η διαφορά (+2 ποσοστιαίες μονάδες) είναι μικρή αλλά επιχειρηματικά σημαντική.

TODO 1

  • Δημιούργησε ένα tibble «experiment» με τις στήλες:

    • user_id (αύξων αριθμός 1 … n_control+n_treatment)

    • group (“control” ή “treatment”)

    • converted (0/1 — χρησιμοποίησε rbinom())

  • Hint: rbinom(n, 1, p) επιστρέφει vector από 0/1

experiment <- tibble(
  user_id   = 1:(n_control + n_treatment),
  group     = c(rep("control", n_control), rep("treatment", n_treatment)),
  converted = c(
    rbinom(n_control,   size = 1, prob = p_control),
    rbinom(n_treatment, size = 1, prob = 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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, …

Σχολιασμός:

  • Κάθε γραμμή αντιστοιχεί σε έναν χρήστη. Οι χρήστες χωρίζονται σε δύο ομάδες, control και treatment, και για κάθε χρήστη παράγεται τυχαία μια μεταβλητή conversion (0/1) με διαφορετική πιθανότητα επιτυχίας ανά ομάδα. Στη συνέχεια, ελέγχουμε συνοπτικά τη δομή και τους τύπους των δεδομένων ώστε να επιβεβαιώσουμε ότι το dataset δημιουργήθηκε σωστά.

TODO 2

  • Υπολόγισε ανά ομάδα:

    • n, conversions, conversion_rate, standard error (se) και 95% CI (ci_lower, ci_upper)
  • Αποθήκευσε σε tibble «summary_stats»

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         643          0.0804 0.00304   0.0744   0.0863
## 2 treatment  8000         848          0.106  0.00344   0.0993   0.113

Σχολιασμός:

  • Υπολογίζουμε συνοπτικά στατιστικά για κάθε ομάδα (control και treatment). Συγκεκριμένα, βρίσκουμε το πλήθος χρηστών, τα συνολικά conversions, το conversion rate, το standard error (SE) και το 95% confidence interval. Το conversion rate υπολογίζεται ως ο μέσος όρος της μεταβλητής converted, ενώ το SE δίνεται από τον τύπο SE=p(1−p)nSE = SE=np(1−p)​​. Στη συνέχεια, χρησιμοποιούμε το 1.96 για να κατασκευάσουμε το 95% confidence interval γύρω από το εκτιμώμενο conversion rate και να συγκρίνουμε τις δύο ομάδες.

TODO 3

  • Οπτικοποίησε με ggplot2 (geom_col + geom_errorbar)

    • Να φαίνεται καθαρά ποια ομάδα έχει υψηλότερο conversion rate

    • Χρησιμοποίησε scale_y_continuous(labels = percent)

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 = -0.8, fontface = "bold", size = 4
  ) +
  scale_y_continuous(labels = percent, limits = c(0, 0.14)) +
  scale_fill_manual(values = c("control" = "#7f8c8d", "treatment" = "#2ecc71")) +
  labs(
    title    = "Conversion Rates ανά Ομάδα",
    subtitle = "Οι μπάρες σφάλματος δείχνουν 95% Confidence Intervals",
    x        = "Ομάδα",
    y        = "Conversion Rate",
    fill     = "Ομάδα"
  ) +
  theme_minimal(base_size = 13) +
  theme(legend.position = "none")

Σχολιασμός:

  • Το γράφημα δείχνει καθαρά ότι η treatment ομάδα έχει υψηλότερο conversion rate. Τα 95% CI δεν επικαλύπτονται, ισχυρή ένδειξη (αλλά όχι επίσημος έλεγχος) ότι η διαφορά είναι στατιστικά σημαντική.

TODO 4

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

    • Χρησιμοποίησε correct = FALSE (χωρίς Yates correction)

    • Αποθήκευσε το αποτέλεσμα σε «test_result»

test_result <- prop.test(
  x = c(summary_stats$conversions[summary_stats$group == "treatment"],
      summary_stats$conversions[summary_stats$group == "control"]),
  n = c(summary_stats$n[summary_stats$group == "treatment"],
      summary_stats$n[summary_stats$group == "control"]),
      correct = FALSE   # χωρίς Yates continuity correction
)

test_result
## 
##  2-sample test for equality of proportions without continuity correction
## 
## data:  c(summary_stats$conversions[summary_stats$group == "treatment"], summary_stats$conversions[summary_stats$group == "control"]) out of c(summary_stats$n[summary_stats$group == "treatment"], summary_stats$n[summary_stats$group == "control"])
## X-squared = 31.082, df = 1, p-value = 2.473e-08
## alternative hypothesis: two.sided
## 95 percent confidence interval:
##  0.01662519 0.03462481
## sample estimates:
##   prop 1   prop 2 
## 0.106000 0.080375

Σχολιασμός:

  • Η prop.test() συγκρίνει τα conversion rates των ομάδων control και treatment μέσω χ² test (ισοδύναμο με z-test για proportions). Με correct = FALSE απενεργοποιούμε τη Yates correction, καθώς το δείγμα είναι μεγάλο. Εφόσον το p-value < 0.05, απορριπτούμε τη H₀ και καταλήγουμε ότι υπάρχει στατιστικά σημαντική διαφορά μεταξύ των ομάδων.

TODO 5

  • Χειρωνακτική επαλήθευση — υπολόγισε:

    (α) pooled estimate p̂_pool

    (β) pooled SE

    (γ) δ = p_treatment - p_control

    (δ) 95% CI για τη διαφορά δ

  • Συγκρίνετε με το CI του prop.test()

# Εξάγουμε τα στατιστικά ανά ομάδα
p_ctrl <- summary_stats$conversion_rate[summary_stats$group == "control"]
p_trt  <- summary_stats$conversion_rate[summary_stats$group == "treatment"]
n_ctrl <- summary_stats$n[summary_stats$group == "control"]
n_trt  <- summary_stats$n[summary_stats$group == "treatment"]

# (α) Pooled estimate: συνδυάζουμε τις δύο ομάδες για τον έλεγχο
p_pool <- (p_ctrl * n_ctrl + p_trt * n_trt) / (n_ctrl + n_trt)

# (β) Pooled SE: χρησιμοποιείται για τον z-test
se_pool <- sqrt(p_pool * (1 - p_pool) * (1/n_ctrl + 1/n_trt))

# (γ) Διαφορά δ = p_treatment - p_control
delta <- p_trt - p_ctrl

# (δ) 95% CI για τη διαφορά (unpooled SE για CI)
se_diff  <- sqrt(p_trt*(1-p_trt)/n_trt + p_ctrl*(1-p_ctrl)/n_ctrl)
ci_lower_diff <- delta - 1.96 * se_diff
ci_upper_diff <- delta + 1.96 * se_diff

# z-statistic και p-value
z_stat  <- delta / se_pool
p_value <- 2 * pnorm(-abs(z_stat))
## Χειρωνακτικοί Υπολογισμοί
## p_control   = 0.0804
## p_treatment = 0.1060
## p_pool      = 0.0932
## SE (pooled) = 0.004596
## δ (lift)    = 0.0256 (+2.6%)
## 95% CI (δ)  = [0.0166, 0.0346]
## z-statistic = 5.5751
## p-value     = 0.000000
## 
## CI από prop.test()
## 95% CI      = [0.0166, 0.0346]

Σχολιασμός:

  • Υπάρχουν δύο διαφορετικά SE -> Pooled SE: χρησιμοποιείται στον z-test (υποθέτει κοινό p κάτω από H₀) & Unpooled SE: χρησιμοποιείται για το CI της διαφοράς (δεν υποθέτει H₀). Αυτός είναι ο λόγος που το χειρωνακτικό CI μπορεί να διαφέρει ελαφρώς από αυτό του prop.test(). Αν το 0 δεν ανήκει στο CI, απορρίπτουμε H₀.

TODO 6

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

    • Πόσο δείγμα χρειαζόταν πραγματικά για power = 80% και α = 0.05;

    • Hint: ES.h(p1 = 0.10, p2 = 0.08) δίνει το Cohen’s h

# Cohen's h: το effect size για διαφορές σε proportions
h <- ES.h(p1 = p_treatment, p2 = p_control)

# Υπολογισμός n, για power = 80%, α = 0.05
power_result <- pwr.2p.test(
  h    = h,          # Cohen's h effect size
  sig.level = 0.05,  # α
  power     = 0.80,  # 1 - β (επιθυμητή ισχύς)
  alternative = "two.sided"
)

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
## 
## Cohen's h = 0.0700
## Απαιτούμενο n ανά ομάδα = 3205
## Τρέξαμε: 8000 ανά ομάδα
## Overpowered κατά: 149.6%

Σχολιασμός:

  • Ο Cohen’s h είναι το κατάλληλο effect size για proportions. Αν το απαιτούμενο n είναι μικρότερο από αυτό που τρέξαμε (8.000), τότε το πείραμά ήταν overpowered, δηλαδή είχε ακόμα μεγαλύτερη πιθανότητα να ανιχνεύσει το αποτέλεσμα. Αν ήταν μεγαλύτερο, θα ήταν underpowered και ο έλεγχός αναξιόπιστος.

Απαντήσεις Ερωτήσεων

1.📊 Ποιο είναι το p-value του ελέγχου; Απορρίπτουμε την H₀ σε επίπεδο α = 0.05;

Το p-value είναι πολύ μικρότερο του 0.05, άρα απορρίπτουμε την H₀. Η διαφορά στα conversion rates δεν είναι τυχαία καθώς η treatment ομάδα μετατρέπει στατιστικά σημαντικά περισσότερο από την control.

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

Τα CI είναι παρόμοια αλλά δεν ταυτίζονται ακριβώς. Το prop.test() χρησιμοποιεί Wald-type CI βασισμένο στο χ² test statistic, ενώ ο χειρωνακτικός υπολογισμός χρησιμοποιεί το unpooled SE με z=1.96. Και οι δυο είναι έγκυρες προσεγγίσεις και οδηγούν στο ίδιο συμπέρασμα.

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

Αν το απαιτούμενο είναι n < 8.000 τρέξαμε περισσότερους χρήστες από ό,τι χρειαζόταν, το πείραμα ήταν overpowered, άρα ακόμα πιο αξιόπιστο. Αν το απαιτούμενο είναι n > 8.000 το πείραμα ήταν underpowered (< 80% πιθανότητα να ανιχνεύσει την αληθινή διαφορά), και ακόμα κι αν βρούμε p < 0.05, ο τύπος Ι σφάλματος αυξάνεται σχετικά.

ΜΕΡΟΣ Β — Real-World A/B Analysis

Εφαρμόζουμε τις ίδιες μεθόδους στα πραγματικά δεδομένα, όπου δεν γνωρίζουμε την αλήθεια. Επιπλέον, ελέγχουμε την ποιότητα της τυχαιοποίησης και εξετάζουμε αν το αποτέλεσμα διαφέρει ανά ημέρα εβδομάδας.

TODO 7

  • Invariants check — έλεγξε αν η τυχαιοποίηση δούλεψε

    (α) Ποια η αναλογία ad/psa; Είναι 50/50;

    (β) Οπτικοποίησε την κατανομή ανά ημέρα εβδομάδας (most_ads_day) για κάθε ομάδα — ψάξε ανισορροπίες

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

group_balance
## # A tibble: 2 × 3
##   group      n    pct
##   <fct>  <int>  <dbl>
## 1 psa    23524 0.0400
## 2 ad    564577 0.960

Σχολιασμός:

  • Ελέγχουμε αν η κατανομή είναι ισορροπημένη (50/50). Σε αυτό το dataset η κατανομή δεν είναι 50/50, η ομάδα “ad” είναι πολύ μεγαλύτερη από την “psa”. Αυτό δεν είναι απαραίτητα πρόβλημα (τα proportion tests αντέχουν ανισορροπία), αλλά πρέπει να το γνωρίζουμε και να το αναφέρουμε.
day_order <- c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")

day_dist <- ads |>
  mutate(most_ads_day = factor(most_ads_day, levels = day_order)) |>
  count(group, most_ads_day) |>
  group_by(group) |>
  mutate(pct = n / sum(n))

ggplot(day_dist, 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" = "#7f8c8d", "ad" = "#3498db")) +
  labs(
    title    = "Κατανομή Χρηστών ανά Ημέρα Εβδομάδας & Ομάδα",
    subtitle = "Invariant check: παρόμοια κατανομή → τυχαιοποίηση OK",
    x        = "Ημέρα",
    y        = "Ποσοστό χρηστών",
    fill     = "Ομάδα"
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

Σχολιασμός:

  • Αν η τυχαιοποίηση δούλεψε σωστά, οι δύο ομάδες πρέπει να έχουν παρόμοια κατανομή ανά ημέρα. Διακριτές ανισορροπίες θα υποδήλωναν confounding, το αποτέλεσμα θα μπορούσε να οφείλεται στην ημέρα και όχι στη διαφήμιση.

TODO 8

  • Υπολόγισε conversion rate, SE, 95% CI ανά ομάδα:

    • Διεξήγαγε prop.test() και χρησιμοποίησε broom::tidy() για μορφοποιημένο output
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
  )

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() με broom::tidy() για μορφοποιημένο output
real_test <- prop.test(
  x = c(real_stats$conversions[real_stats$group == "ad"],
              real_stats$conversions[real_stats$group == "psa"]),
  n  = c(real_stats$n[real_stats$group == "ad"],
              real_stats$n[real_stats$group == "psa"]),
  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.0255    0.0179      54.3 1.71e-13         1  0.00595   0.00943 2-sample …
## # ℹ 1 more variable: alternative <chr>

Σχολιασμός:

  • Το broom::tidy() μετατρέπει το αποτέλεσμα του prop.test() σε τακτοποιημένο tibble. Η στήλη estimate1 είναι το conversion rate της “ad”, estimate2 της “psa”. Αν p.value < 0.05 και το CI της διαφοράς δεν περιέχει το 0, η διαφορά είναι στατιστικά σημαντική.

TODO 9

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

    • Φτιάξε line plot με ribbon (95% CI) για κάθε ομάδα

    • Hint: geom_ribbon(aes(ymin = …, ymax = …), alpha = 0.15)

    • Ποια ημέρα έχει τη μεγαλύτερη διαφορά μεταξύ ομάδων;

day_conversion <- ads |>
  mutate(most_ads_day = factor(most_ads_day, levels = day_order)) |>
  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"
  )

# Βρίσκουμε την ημέρα με τη μεγαλύτερη διαφορά
max_diff_day <- day_conversion |>
  select(group, most_ads_day, conversion_rate) |>
  pivot_wider(names_from = group, values_from = conversion_rate) |>
  mutate(diff = ad - psa) |>
  slice_max(diff, n = 1)

ggplot(day_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) +
  annotate("text",
           x     = max_diff_day$most_ads_day,
           y     = max(day_conversion$ci_upper) + 0.002,
           label = paste0("Max diff:\n", max_diff_day$most_ads_day),
           size  = 3.5, color = "darkred", fontface = "bold") +
  scale_y_continuous(labels = percent) +
  scale_color_manual(values = c("psa" = "#7f8c8d", "ad" = "#3498db")) +
  scale_fill_manual(values  = c("psa" = "#7f8c8d", "ad" = "#3498db")) +
  labs(
    title    = "Conversion Rate ανά Ημέρα Εβδομάδας",
    subtitle = "Ribbon = 95% Confidence Interval | Μεγαλύτερο gap → δυνατότερο αποτέλεσμα",
    x        = "Ημέρα Εβδομάδας",
    y        = "Conversion Rate",
    color    = "Ομάδα",
    fill     = "Ομάδα"
  ) +
  theme_minimal(base_size = 13) +
  theme(axis.text.x = element_text(angle = 30, hjust = 1))

cat(sprintf("\nΗμέρα με μεγαλύτερη διαφορά: %s (diff = +%.3f%%)\n",
            max_diff_day$most_ads_day, max_diff_day$diff * 100))
## 
## Ημέρα με μεγαλύτερη διαφορά: Tuesday (diff = +1.599%)

Σχολιασμός:

  • Το line plot με ribbon επιτρέπει να δούμε αν η επίδραση της διαφήμισης είναι ομοιόμορφη όλες τις ημέρες ή αν υπάρχουν ημέρες που δουλεύει καλύτερα. Όταν το ribbon της “ad” ομάδας δεν επικαλύπτεται με αυτό της “psa”, η ημερήσια διαφορά είναι στατιστικά σημαντική. Αυτή η segmentation analysis είναι πολύτιμη για τη marketing ομάδα: ξέρουμε πότε να εντείνουμε τη διαφήμιση.

TODO 10 (BONUS)

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

    • Όρισε δ_min = 0.005 (κατώφλι επιχειρηματικής ουσίας)

    • Υπολόγισε το absolute lift και το relative lift

    • Ποια από τις 6 περιπτώσεις CI ισχύει; (A/B/C/D/E/F)

    • Ποια η τελική σύστασή σου;

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

# Absolute lift: η απόλυτη αύξηση στο conversion rate
p_ad  <- real_stats$conversion_rate[real_stats$group == "ad"]
p_psa <- real_stats$conversion_rate[real_stats$group == "psa"]

absolute_lift <- p_ad - p_psa
relative_lift <- absolute_lift / p_psa

# CI της διαφοράς (unpooled)
se_diff_real  <- sqrt(p_ad*(1-p_ad)/real_stats$n[real_stats$group=="ad"] +
                      p_psa*(1-p_psa)/real_stats$n[real_stats$group=="psa"])
ci_diff_lower <- absolute_lift - 1.96 * se_diff_real
ci_diff_upper <- absolute_lift + 1.96 * se_diff_real
## Επιχειρηματική Αξιολόγηση
## Conversion rate 'ad':    0.0255 (2.55%)
## Conversion rate 'psa':   0.0179 (1.79%)
## Absolute lift:           +0.0077 (+0.77 pp)
## Relative lift:           +43.1%
## 95% CI (διαφορά):       [0.0060, 0.0094]
## Κατώφλι ουσίας (δ_min): 0.005
# Αξιολόγηση σεναρίου CI
p_val <- real_test$p.value
practically_significant <- absolute_lift > delta_min
statistically_significant <- p_val < 0.05
ci_above_threshold <- ci_diff_lower > delta_min
## Κατάταξη Σεναρίου
## → Σενάριο A: Στατιστικά ΚΑΙ πρακτικά σημαντικό.Προχωράμε!

Βασικά συμπεράσματα

  • Το A/B test έδειξε στατιστικά σημαντική διαφορά στα conversion rates μεταξύ ad και psa ομάδας.

  • Το Μέρος Α επιβεβαίωσε ότι με n=8.000 ανά ομάδα και effect size +2pp, η στατιστική ισχύς είναι επαρκής.

  • Το invariants check αποκάλυψε ότι το dataset δεν είναι ισορροπημένο (ad >> psa), κάτι που πρέπει να λαμβάνεται υπόψη στην ερμηνεία.

  • Η segmentation ανά ημέρα δείχνει ότι η επίδραση της διαφήμισης δεν είναι ομοιόμορφη, κάποιες ημέρες αποδίδουν καλύτερα, παρέχοντας actionable insights για τη marketing ομάδα.

  • Η επιχειρηματική σύσταση εξαρτάται από το αν το lift υπερβαίνει το κατώφλι δ_min=0.005 και αν το ROI της καμπάνιας δικαιολογείται.