Θέμα 6 — Πείραμα στη θέση «πύλης» σε mobile παιχνίδι


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

library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.3
## Warning: package 'ggplot2' was built under R version 4.5.3
## Warning: package 'dplyr' was built under R version 4.5.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.1     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.3     ✔ 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(42)

# --- Φόρτωση δεδομένων ---
ads <- read_csv("cookie_cats.csv") |>
  janitor::clean_names() |>
  mutate(
    group     = factor(version, levels = c("gate_30", "gate_40")),
    converted = as.integer(retention_7),
  )
## Rows: 90189 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): version
## dbl (2): userid, sum_gamerounds
## lgl (2): retention_1, retention_7
## 
## ℹ 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.
glimpse(ads)
## Rows: 90,189
## Columns: 7
## $ userid         <dbl> 116, 337, 377, 483, 488, 540, 1066, 1444, 1574, 1587, 1…
## $ version        <chr> "gate_30", "gate_30", "gate_40", "gate_40", "gate_40", …
## $ sum_gamerounds <dbl> 3, 38, 165, 1, 179, 187, 0, 2, 108, 153, 3, 0, 30, 39, …
## $ retention_1    <lgl> FALSE, TRUE, TRUE, FALSE, TRUE, TRUE, FALSE, FALSE, TRU…
## $ retention_7    <lgl> FALSE, FALSE, FALSE, FALSE, TRUE, TRUE, FALSE, FALSE, T…
## $ group          <fct> gate_30, gate_30, gate_40, gate_40, gate_40, gate_40, g…
## $ converted      <int> 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1…


Επιλογή μεθόδου: Ποια μέθοδο θα επιλέξετε με βάση τα παραπάνω;

Επιλέγω A/B testing, διότι έχουμε τυχαία κατανομή χρηστών σε δύο διαφορετικά group και επιδιώκουμε να δούμε αν η αλλαγή αυτή επηρεάζει πραγματικά τη διατήρηση των χρηστών.


# Αναλογία στις ομάδες

ads |>
  count(group) |>
  mutate(pct = n / sum(n))
## # A tibble: 2 × 3
##   group       n   pct
##   <fct>   <int> <dbl>
## 1 gate_30 44700 0.496
## 2 gate_40 45489 0.504

Η κατανομή των χρηστών μεταξύ των ομάδων είναι σχεδόν ισομερής. Συγκεκριμένα, περίπου το 49% των χρηστών ανήκει στο gate_30, ενώ περίπου το 50% στο gate_40.

Επομένως, το experiment ακολουθεί αναλογία 50/50.


# Διατήρηση 1ης ημέρας

ads |>
  count(group, retention_1) |>
  group_by(group) |>
  mutate(pct = n / sum(n)) |>
  ggplot(aes(x = retention_1, y = pct, fill = group)) +
  geom_col(position = "dodge") +
  scale_y_continuous(labels = percent) +
  labs(title = "Invariant check: κατανομή 1η ημέρα",
       x = NULL, y = "% της ομάδας") +
  theme_minimal()

# Διατήρηση 7ης ημέρας

ads |>
  count(group, retention_7
) |>
  group_by(group) |>
  mutate(pct = n / sum(n)) |>
  ggplot(aes(x = retention_7, y = pct, fill = group)) +
  geom_col(position = "dodge") +
  scale_y_continuous(labels = percent) +
  labs(title = "Invariant check: κατανομή 7η ημέρα",
       x = NULL, y = "% της ομάδας") +
  theme_minimal()

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


# Ανάλυση αποτελέσματος με tidyverse

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 gate_30 44700        8502           0.190 0.00186    0.187    0.194
## 2 gate_40 45489        8279           0.182 0.00181    0.178    0.186
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.190     0.182      10.0 0.00155  0.00312    0.0133

Το dataset δείχνει ότι η ομάδα gate_30 παρουσιάζει υψηλότερο conversion rate σε σχέση με την ομάδα gate_40.

Συγκεκριμένα:

η ομάδα gate_30 έχει conversion rate περίπου 19%

η ομάδα gate_40 έχει conversion rate περίπου 18%

Αυτό σημαίνει ότι οι χρήστες που τοποθετήθηκαν στο νέο group πραγματοποίησαν λιγότερα conversions.

Τα 95% confidence intervals είναι:

gate_30 : περίπου [18.65%, 19.38%]

gate_40: περίπου [17.84%, 18.55%]

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

Ωστόσο, ο έλεγχος αναλογιών (prop.test) έδωσε p-value = 0.0015, το οποίο είναι μικρότερο από το επίπεδο σημαντικότητας α = 0.05 γεγονός που δείχνει ότι η διαφορά μεταξύ των δύο ομάδων είναι στατιστικά σημαντική και δύσκολα οφείλεται σε τυχαία διακύμανση.

Ωστόσο, επειδή η ομάδα gate_40 έχει μικρότερο ποσοστό conversion_rate, δεν θα πρότεινα μετακίνηση της πύλης

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.190     0.182      10.0 0.00155  0.00312    0.0133
# CI για τη διαφορά (lift)
conf_int <- broom::tidy(test)
lift_pct <- (conf_int$estimate1 - conf_int$estimate2) /
             conf_int$estimate2 * 100

cat(sprintf("Absolute lift: %.2f ποσοστ. μονάδες\n",
            (conf_int$estimate1 - conf_int$estimate2) * 100))
## Absolute lift: 0.82 ποσοστ. μονάδες
cat(sprintf("Relative lift: %+.1f%%\n", lift_pct))
## Relative lift: +4.5%
cat(sprintf("95%% CI for difference: [%.4f, %.4f]\n",
            conf_int$conf.low, conf_int$conf.high))
## 95% CI for difference: [0.0031, 0.0133]

Το absolute lift είναι περίπου 0.82 ποσοστιαίες μονάδες και το relative lift 4.5%. Το 95% confidence interval της διαφοράς είναι [0.0031, 0.0133], γεγονός που δείχνει ότι η διαφορά μεταξύ των δύο ομάδων είναι στατιστικά σημαντική. Επιπλέον, το μέγεθος της διαφοράς (Absolute lift) υπερβαίνει το επιχειρηματικό κατώφλι d min=0.005, καθώς ακόμη και το όριο του confidence interval που βρίσκεται πλησιέστερα στο μηδέν έχει απόλυτη τιμή μεγαλύτερη από 0.005. Επομένως, η επίδραση είναι τόσο στατιστικά όσο και επιχειρηματικά σημαντική.


Με βάση τα ευρήματα, θα προτείνατε τη μετακίνηση της πύλης;

Δεν θα πρότεινα μετακίνηση της πύλης, καθώς η πύλη gate_40 έχει μικρότερο ποσοστό conversion_rate από ότι η πύλη gate_30.