0. CHARGEMENT DES PACKAGES

# ============================================================
# Chargement de tous les packages nécessaires
# Regroupés ici pour éviter les conflits entre fonctions
# ============================================================

library(tidyverse)    # manipulation des données et visualisation
library(readxl)       # import de fichiers Excel
library(ggcorrplot)   # matrice de corrélation graphique
library(GGally)       # scatter matrix
library(plotly)       # graphiques interactifs
library(lmtest)       # tests économétriques (Breusch-Pagan)
library(sandwich)     # erreurs standard robustes (correction de White)
library(stargazer)    # tableaux de régression académiques
library(psych)        # statistiques descriptives approfondies
library(knitr)        # mise en forme des tableaux
library(gridExtra)    # affichage de plusieurs graphiques côte à côte

# ---- Résolution des conflits entre packages ----
# psych et MASS ont des fonctions select() et filter() qui
# écrasent celles de dplyr — on force l'utilisation de dplyr
select    <- dplyr::select
filter    <- dplyr::filter
summarise <- dplyr::summarise

1. IMPORTATION ET PRÉPARATION DES DONNÉES

# ============================================================
# Import de la base de données brute depuis le fichier Excel
# ============================================================
data_brut <- read_excel("BD_F.xlsx")

# ============================================================
# Sélection des variables pertinentes et transformations
# ============================================================
data <- data_brut %>%
  
  # Sélection des colonnes retenues pour l'analyse
  dplyr::select(
    steamId, name, firstReleaseDate, earlyAccess,
    copiesSold, price, revenue, avgPlaytime,
    reviewScore, followers, wishlists,
    publisherClass, publishers
  ) %>%

  # ---- MUTATE 1 : Transformations logarithmiques ----
  # Les variables revenue, wishlists, followers et avgPlaytime
  # présentent des distributions fortement asymétriques
  # (skew > 1.94, kurtosis > 5) — la transformation log permet
  # de compresser les valeurs extrêmes et de normaliser les distributions
  # Le +1 évite log(0) = -Inf pour les observations nulles
  mutate(
    log_revenue   = log(revenue     + 1),
    log_price     = log(price       + 1),
    log_wishlists = log(wishlists   + 1),
    log_followers = log(followers   + 1),
    log_playtime  = log(avgPlaytime + 1),
    
    # Variable catégorielle : modèle de tarification
    f2p = ifelse(price == 0, "Free-to-Play", "Payant"),
    
    # Variable facteur ordonnée : catégorie de production
    # L'ordre Hobbyist < Indie < AA < AAA reflète la hiérarchie de budget
    pub_class = factor(publisherClass,
                       levels = c("Hobbyist", "Indie", "AA", "AAA"))
  ) %>%

  # ---- MUTATE 2 : Variables dummy pour la régression ----
  # Référence omise : AAA (catégorie de référence dans les modèles)
  # Les coefficients DUMY_Indie et DUMY_AA s'interpréteront
  # comme un écart de revenu PAR RAPPORT À UN JEU AAA
  mutate(
    DUMY_Hobbyist = ifelse(publisherClass == "Hobbyist", 1, 0),
    DUMY_Indie    = ifelse(publisherClass == "Indie",    1, 0),
    DUMY_AA       = ifelse(publisherClass == "AA",       1, 0),
    DUMY_AAA      = ifelse(publisherClass == "AAA",      1, 0),
    DUMY_F2P      = ifelse(price == 0, 1, 0),
    DUMY_Payant   = ifelse(price >  0, 1, 0)
  )

# Suppression d'une colonne résiduelle non utile
data$earlyAccessExitDate <- NULL

# ---- Vérification rapide ----
cat("Dimensions de la base :", nrow(data), "observations x", ncol(data), "variables\n")
## Dimensions de la base : 522 observations x 26 variables

2. STATISTIQUES DESCRIPTIVES

# ============================================================
# BLOC 1 — Statistiques descriptives approfondies
# psych::describe() fournit : mean, sd, median, trimmed,
# mad, min, max, range, skew, kurtosis, se
# Ces indicateurs justifient le recours aux transformations log
# ============================================================
data %>%
  dplyr::select(revenue, price, copiesSold, avgPlaytime,
                reviewScore, followers, wishlists) %>%
  psych::describe() %>%
  round(2)
##             vars   n        mean           sd      median     trimmed
## revenue        1 522 61564362.36 126711278.42 24208792.50 36280846.03
## price          2 522       22.57        21.41       19.99       20.25
## copiesSold     3 522  4107141.55   6177467.74  2114216.50  2729169.67
## avgPlaytime    4 522       28.71        29.86       20.72       23.36
## reviewScore    5 522       83.24        13.17       87.00       85.18
## followers      6 522   142066.78    174407.89    93104.00   108302.22
## wishlists      7 521   826721.36    879469.83   588500.00   674316.61
##                     mad     min          max        range  skew kurtosis
## revenue     29942407.98       0 1.676520e+09 1.676520e+09  6.47    60.60
## price             29.64       0 6.999000e+01 6.999000e+01  0.61    -0.76
## copiesSold   1314341.95 1000414 7.472485e+07 7.372443e+07  5.41    42.89
## avgPlaytime       19.24       0 3.015100e+02 3.015100e+02  2.92    15.59
## reviewScore       10.38      10 9.900000e+01 8.900000e+01 -1.56     3.32
## followers      85799.54       0 1.564041e+06 1.564041e+06  3.56    18.63
## wishlists     696377.22    1100 5.568200e+06 5.567100e+06  1.94     5.08
##                     se
## revenue     5546001.99
## price             0.94
## copiesSold   270380.42
## avgPlaytime       1.31
## reviewScore       0.58
## followers      7633.63
## wishlists     38530.27
# ============================================================
# BLOC 2 — Comparaison des moyennes F2P vs Payants
# Permet d'observer les différences structurelles entre
# les deux modèles de tarification
# ============================================================
data %>%
  group_by(f2p) %>%
  summarise(
    n              = n(),
    rev_moyen      = mean(revenue)     / 1e6,
    rev_median     = median(revenue)   / 1e6,
    price_moyen    = mean(price),
    score_moyen    = mean(reviewScore, na.rm = TRUE),
    wishlists_moy  = mean(wishlists),
    playtime_moyen = mean(avgPlaytime)
  ) %>%
  mutate(across(where(is.numeric), ~round(., 2)))
## # A tibble: 2 × 8
##   f2p              n rev_moyen rev_median price_moyen score_moyen wishlists_moy
##   <chr>        <dbl>     <dbl>      <dbl>       <dbl>       <dbl>         <dbl>
## 1 Free-to-Play   158      47.0       4.35         0          77.7           NA 
## 2 Payant         364      67.9      32.7         32.4        85.7      1120750.
## # ℹ 1 more variable: playtime_moyen <dbl>
# ============================================================
# BLOC 3 — Statistiques par catégorie de production
# Montre la hiérarchie des revenus selon le type de jeu
# ============================================================
data %>%
  group_by(publisherClass) %>%
  summarise(
    n              = n(),
    rev_moyen      = round(mean(revenue)   / 1e6, 1),
    rev_median     = round(median(revenue) / 1e6, 1),
    pct_f2p        = round(mean(price == 0) * 100, 1),
    score_moy      = round(mean(reviewScore, na.rm = TRUE), 1),
    playtime_moyen = round(mean(avgPlaytime), 1)
  )
## # A tibble: 4 × 7
##   publisherClass     n rev_moyen rev_median pct_f2p score_moy playtime_moyen
##   <chr>          <int>     <dbl>      <dbl>   <dbl>     <dbl>          <dbl>
## 1 AA               162      59.4       41.4    14.2      85.9           31  
## 2 AAA              155     129.        61      21.3      79.9           38.8
## 3 Hobbyist          13       0          0     100        86.5           19  
## 4 Indie            192      13         10.6    46.4      83.5           19.3

3. ANALYSE GRAPHIQUE EXPLORATOIRE

3.1 Justification de la transformation logarithmique

# ============================================================
# Histogrammes avant / après transformation logarithmique
# Objectif : montrer visuellement pourquoi le log est nécessaire
# La distribution brute est écrasée à gauche par quelques outliers
# La distribution log est bien plus symétrique et lisible
# ============================================================
gridExtra::grid.arrange(

  ggplot(data, aes(x = revenue)) +
    geom_histogram(bins = 30, fill = "#7F77DD", color = "white") +
    scale_x_continuous(
      labels = scales::label_number(scale = 1e-6, suffix = "M$"),
      breaks = c(0, 10e6, 20e6, 30e6, 50e6, 84e6),
      limits = c(0, max(data$revenue, na.rm = TRUE) * 1.05)
    ) +
    labs(title = "Distribution du revenu brut",
         x = "Revenu (en dollars)", y = "Nombre de jeux") +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 10)),

  ggplot(data, aes(x = log_revenue)) +
    geom_histogram(bins = 30, fill = "#534AB7", color = "white") +
    labs(title = "Distribution après transformation log",
         x = "log(revenue + 1)", y = "Nombre de jeux") +
    theme_minimal(),

  ncol = 2
)

3.2 Distribution du revenu : Free-to-Play vs Payants

# ============================================================
# Density plot — comparaison F2P vs Payants
# L'axe X est en log mais relu en dollars via scale_x_continuous
# pour faciliter l'interprétation sans perdre l'échelle log
# ============================================================
Densite_F2P <- ggplot(data, aes(x = log_revenue, fill = f2p)) +
  geom_density(alpha = 0.5, color = "black") +
  scale_fill_manual(values = c("Free-to-Play" = "#A9CCE9",
                               "Payant"       = "#1F4E79")) +
  scale_x_continuous(
    breaks = log(c(1, 100000, 1e6, 5e6, 20e6, 50e6, 84e6) + 1),
    labels = c("0", "100K$", "1M$", "5M$", "20M$", "50M$", "84M$")
  ) +
  labs(title = "Distribution du revenu : Free-to-Play vs Payants",
       x     = "Revenu estimé",
       y     = "Densité",
       fill  = "Modèle de tarification") +
  theme_minimal() +
  theme(plot.title      = element_text(hjust = 0.5, face = "bold"),
        legend.position = "bottom",
        axis.text.x     = element_text(angle = 45, hjust = 1))

ggplotly(Densite_F2P)

3.3 Hiérarchie du revenu selon la catégorie de production

# ============================================================
# Boxplot revenu × score de critique
# Montre si la qualité perçue est liée au revenu
# ============================================================
data_box2 <- data %>%
  mutate(
    Niveau_score = case_when(
      reviewScore < 60 ~ "Mauvais",
      reviewScore < 75 ~ "Moyen",
      reviewScore < 90 ~ "Bon",
      TRUE             ~ "Excellent"
    ),
    Niveau_score = factor(Niveau_score,
                          levels = c("Mauvais", "Moyen", "Bon", "Excellent"))
  )

Box_Revenue_Score <- ggplot(data_box2,
                             aes(x = Niveau_score, y = log_revenue,
                                 fill = Niveau_score)) +
  geom_boxplot(width = 0.7, color = "black", alpha = 0.9) +
  scale_fill_manual(values = c("Mauvais"   = "#F4CCCC",
                               "Moyen"     = "#FCE5CD",
                               "Bon"       = "#D9EAD3",
                               "Excellent" = "#274E13")) +
  scale_y_continuous(
    breaks = log(c(1, 100000, 1e6, 5e6, 20e6, 50e6, 84e6) + 1),
    labels = c("0", "100K$", "1M$", "5M$", "20M$", "50M$", "84M$")
  ) +
  labs(title = "Distribution du revenu selon le score de critique",
       x     = "Niveau de score critique",
       y     = "Revenu estimé") +
  theme_minimal() +
  theme(legend.position = "none",
        plot.title      = element_text(hjust = 0.5, face = "bold"))

ggplotly(Box_Revenue_Score)
# ============================================================
# Palette de couleurs fixe pour les catégories de production
# Définie une seule fois — réutilisée sur tous les graphiques suivants
# ============================================================
palette_segment <- c(
  "Hobbyist" = "#E07B54",
  "Indie"    = "#F2C14E",
  "AA"       = "#4AABDB",
  "AAA"      = "#2C6E49"
)
# ============================================================
# Boxplot hiérarchie des catégories de production
# Chaque boîte affiche la médiane et l'effectif sous la boîte
# L'axe Y est en log relu en dollars
# ============================================================
data_box <- data %>%
  mutate(pub_class = factor(pub_class,
                            levels = c("Hobbyist", "Indie", "AA", "AAA")))

Box_Hierarchie <- ggplot(data_box,
                         aes(x = pub_class, y = log_revenue, fill = pub_class)) +
  geom_boxplot(width = 0.6, color = "black", alpha = 0.9) +
  geom_jitter(aes(color = pub_class), width = 0.15, alpha = 0.35, size = 1.5) +
  stat_summary(
    fun.data = function(x) data.frame(
      y     = min(x) - 0.5,
      label = paste0("n=", length(x),
                     "\nméd=", round(exp(median(x)) / 1e6, 0), "M$")
    ),
    geom = "text", size = 3, color = "grey30", fontface = "italic"
  ) +
  scale_fill_manual(values  = palette_segment) +
  scale_color_manual(values = c("Hobbyist" = "#a84e2e", "Indie" = "#c49b3a",
                                "AA"       = "#2e7da8", "AAA"   = "#1a4a2e")) +
  scale_y_continuous(
    breaks = log(c(1, 100000, 1e6, 5e6, 20e6, 50e6, 200e6, 500e6) + 1),
    labels = c("0", "100K$", "1M$", "5M$", "20M$", "50M$", "200M$", "500M$")
  ) +
  labs(title    = "Hiérarchie du revenu selon le type de jeu",
       subtitle = "La médiane confirme-t-elle la hiérarchie AAA > AA > Indie > Hobbyist ?",
       x        = "Type de jeu",
       y        = "Revenu estimé",
       caption  = "Médiane et effectifs affichés sous chaque boîte") +
  theme_minimal() +
  theme(legend.position = "none",
        plot.title      = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle   = element_text(hjust = 0.5, color = "grey50"))

ggplotly(Box_Hierarchie)
# ============================================================
# Density plot par catégorie de production
# Complète le boxplot en montrant la forme entière des distributions
# ============================================================
Densite_Segment <- ggplot(data, aes(x = log_revenue, fill = pub_class)) +
  geom_density(alpha = 0.4, color = "black") +
  scale_fill_manual(values = palette_segment) +
  scale_x_continuous(
    breaks = log(c(1, 100000, 1e6, 5e6, 20e6, 50e6, 84e6) + 1),
    labels = c("0", "100K$", "1M$", "5M$", "20M$", "50M$", "84M$")
  ) +
  labs(title = "Distribution du revenu par type de jeu",
       x     = "Revenu estimé",
       y     = "Densité",
       fill  = "Type de jeu") +
  theme_minimal() +
  theme(plot.title      = element_text(hjust = 0.5, face = "bold"),
        legend.position = "bottom",
        axis.text.x     = element_text(angle = 45, hjust = 1))

ggplotly(Densite_Segment)
# ============================================================
# Répartition des jeux par catégorie — bar chart + camembert
# Montre le poids de chaque catégorie dans l'échantillon
# ============================================================
data_segment <- data %>%
  count(pub_class) %>%
  mutate(pct   = round(n / sum(n) * 100, 1),
         label = paste0(pub_class, "\n", n, " jeux (", pct, "%)"))

Bar_Segment <- ggplot(data_segment,
                      aes(x = pub_class, y = n, fill = pub_class)) +
  geom_col(color = "black", alpha = 0.9, width = 0.6) +
  geom_text(aes(label = paste0(n, " jeux\n(", pct, "%)")),
            vjust = -0.3, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = palette_segment) +
  scale_y_continuous(limits = c(0, max(data_segment$n) * 1.15)) +
  labs(title    = "Répartition des jeux par catégorie de production",
       subtitle = "Nombre de jeux et poids relatif dans la base",
       x        = "Catégorie de production",
       y        = "Nombre de jeux") +
  theme_minimal() +
  theme(legend.position = "none",
        plot.title      = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle   = element_text(hjust = 0.5, color = "grey50"))

Pie_Segment <- ggplot(data_segment,
                      aes(x = "", y = pct, fill = pub_class)) +
  geom_col(color = "white", width = 1) +
  coord_polar(theta = "y") +
  geom_text(aes(label = paste0(pub_class, "\n", pct, "%")),
            position = position_stack(vjust = 0.5),
            size = 4, fontface = "bold", color = "white") +
  scale_fill_manual(values = palette_segment) +
  labs(title    = "Poids des catégories de production dans la base",
       subtitle = "Part de chaque classe sur le total des jeux") +
  theme_void() +
  theme(legend.position = "none",
        plot.title      = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle   = element_text(hjust = 0.5, color = "grey50"))

gridExtra::grid.arrange(Bar_Segment, Pie_Segment, ncol = 2)

# ============================================================
# Top 10 des éditeurs par revenu moyen
# Filtre les éditeurs avec au moins 2 jeux pour éviter les biais
# ============================================================
revenue_publisher <- data %>%
  group_by(publishers) %>%
  summarise(n              = n(),
            revenue_moyen  = mean(revenue, na.rm = TRUE),
            revenue_total  = sum(revenue,  na.rm = TRUE),
            revenue_median = median(revenue, na.rm = TRUE)) %>%
  filter(n >= 2) %>%
  arrange(desc(revenue_moyen))

top10 <- revenue_publisher %>%
  slice_head(n = 10) %>%
  mutate(publishers = factor(publishers, levels = rev(publishers)))

Bar_Top10 <- ggplot(top10, aes(x = publishers, y = revenue_moyen / 1e6)) +
  geom_col(fill = "#2C6E49", color = "black", alpha = 0.9) +
  geom_text(aes(label = paste0(round(revenue_moyen / 1e6, 1), "M$")),
            hjust = -0.1, size = 3) +
  coord_flip() +
  scale_y_continuous(labels = scales::label_number(suffix = "M$"),
                     limits = c(0, max(top10$revenue_moyen / 1e6) * 1.15)) +
  labs(title    = "Top 10 éditeurs — Revenu moyen par jeu",
       subtitle = "Éditeurs avec au moins 2 jeux dans la base",
       x        = "",
       y        = "Revenu moyen ($M)") +
  theme_minimal() +
  theme(plot.title      = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle   = element_text(hjust = 0.5, color = "grey50"),
        legend.position = "none")

ggplotly(Bar_Top10)
# ============================================================
# Boxplot revenu × temps de jeu moyen
# Montre si l'engagement des joueurs est lié au revenu
# Les tranches de temps permettent une lecture catégorielle
# ============================================================
data_box_playtime <- data %>%
  mutate(
    Tranche_playtime = case_when(
      avgPlaytime < 10  ~ "< 10h",
      avgPlaytime < 30  ~ "10h - 30h",
      avgPlaytime < 60  ~ "30h - 60h",
      TRUE              ~ "> 60h"
    ),
    Tranche_playtime = factor(Tranche_playtime,
                              levels = c("< 10h", "10h - 30h",
                                         "30h - 60h", "> 60h"))
  )

Box_Revenue_Playtime <- ggplot(data_box_playtime,
                                aes(x = Tranche_playtime, y = log_revenue,
                                    fill = Tranche_playtime)) +
  geom_boxplot(width = 0.6, color = "black", alpha = 0.9) +
  geom_jitter(width = 0.15, alpha = 0.4, size = 1.5, color = "grey30") +
  scale_fill_manual(values = c("< 10h"     = "#E07B54",
                               "10h - 30h" = "#F2C14E",
                               "30h - 60h" = "#4AABDB",
                               "> 60h"     = "#2C6E49")) +
  scale_y_continuous(
    breaks = log(c(1, 100000, 1e6, 5e6, 20e6, 50e6, 84e6) + 1),
    labels = c("0", "100K$", "1M$", "5M$", "20M$", "50M$", "84M$")
  ) +
  stat_summary(
    fun.data = function(x) data.frame(y = min(x) - 0.3,
                                      label = paste0("n = ", length(x))),
    geom = "text", size = 3, color = "grey40", fontface = "italic"
  ) +
  labs(title    = "Distribution du revenu selon le temps de jeu moyen",
       subtitle = "Un temps de jeu élevé reflète-t-il un meilleur revenu ?",
       x        = "Temps de jeu moyen",
       y        = "Revenu estimé") +
  theme_minimal() +
  theme(legend.position = "none",
        plot.title      = element_text(hjust = 0.5, face = "bold"),
        plot.subtitle   = element_text(hjust = 0.5, color = "grey50"))

ggplotly(Box_Revenue_Playtime)

4. ANALYSE DES OUTLIERS

# ============================================================
# Détection visuelle des outliers sur les variables continues
# L'échelle log permet de voir les distributions malgré
# les écarts de valeurs très importants
# Les points rouges sont les outliers selon la règle de Tukey (1.5 × IQR)
# ============================================================
data %>%
  dplyr::select(revenue, copiesSold, followers, wishlists) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "valeur") %>%
  ggplot(aes(x = variable, y = valeur)) +
  geom_boxplot(fill = "#4AABDB", color = "black",
               outlier.color = "red", outlier.size = 2) +
  scale_y_log10(labels = scales::label_number(scale = 1e-6, suffix = "M")) +
  labs(title = "Détection des outliers par variable",
       x = "", y = "Valeur (échelle log)") +
  theme_minimal()

# ============================================================
# Courbe de Pareto — concentration du revenu
# Montre combien de jeux génèrent quelle part du revenu total
# La ligne rouge à 80% permet d'identifier le seuil critique
# ============================================================
data %>%
  arrange(desc(revenue)) %>%
  mutate(rang       = row_number(),
         part_cumul = cumsum(revenue) / sum(revenue) * 100) %>%
  ggplot(aes(x = rang, y = part_cumul)) +
  geom_line(color = "#2C6E49", linewidth = 1.2) +
  geom_hline(yintercept = 80, linetype = "dashed", color = "red") +
  annotate("text", x = 400, y = 82,
           label = "80% du revenu total", color = "red", size = 3.5) +
  labs(title = "Courbe de concentration du revenu (Pareto)",
       x     = "Rang du jeu (du plus au moins rentable)",
       y     = "Part cumulée du revenu (%)") +
  theme_minimal()

# ============================================================
# Tableau des 10 jeux les plus rentables — outliers potentiels
# Ces 10 productions représentent 24% du revenu total
# ============================================================
data %>%
  arrange(desc(revenue)) %>%
  slice_head(n = 10) %>%
  dplyr::select(name, publishers, publisherClass, revenue, copiesSold, reviewScore) %>%
  mutate(revenue = scales::dollar(revenue, scale = 1e-6, suffix = "M$")) %>%
  knitr::kable(caption = "Top 10 jeux par revenu — outliers potentiels")
Top 10 jeux par revenu — outliers potentiels
name publishers publisherClass revenue copiesSold reviewScore
Apex Legends™ Electronic Arts AAA \(1,676.52M\) 74724847 68
Destiny 2 Bungie AAA \(909.72M\) 36869480 77
Baldur’s Gate 3 Larian Studios AAA \(794.51M\) 16471582 97
ELDEN RING FromSoftware, Inc.,Bandai Namco Entertainment AAA \(782.95M\) 19527144 93
Cyberpunk 2077 CD PROJEKT RED AAA \(769.82M\) 22805942 86
Black Myth: Wukong Game Science AAA \(694.30M\) 16052084 97
Battlefield™ 6 Electronic Arts AAA \(589.59M\) 10516577 66
Call of Duty® Activision AAA \(480.54M\) 41198255 59
HELLDIVERS™ 2 PlayStation Publishing LLC AAA \(474.77M\) 13870499 77
Red Dead Redemption 2 Rockstar Games AAA \(454.16M\) 21978376 92

5. MATRICE DE CORRÉLATION

# ============================================================
# Matrice de corrélation entre les variables continues du modèle
# Note : les variables dummy ne sont pas incluses ici car leur
# nature binaire (0/1) rend leur corrélation peu informative
# Elles seront introduites directement dans les modèles de régression
# ============================================================
vars_corr <- data %>%
  dplyr::select(log_revenue, price, log_wishlists,
                log_followers, avgPlaytime, reviewScore)

cor_matrix <- cor(vars_corr, use = "complete.obs")

ggcorrplot(cor_matrix,
           method   = "circle",
           type     = "lower",
           lab      = TRUE,
           lab_size = 3,
           colors   = c("#E07B54", "white", "#2C6E49"),
           title    = "Matrice de corrélation — variables du modèle") +
  theme(plot.title = element_text(hjust = 0.5, face = "bold"))


6. RÉGRESSIONS BIVARIÉES (NUAGES DE POINTS)

# ============================================================
# Palette de couleurs pour les graphiques de régression
# ============================================================
couleur_payant  <- "#1F4E79"   # bleu foncé — jeux payants
couleur_f2p     <- "#2C6E49"   # vert forêt — jeux F2P
couleur_complet <- "#534AB7"   # violet — droite de régression
# ============================================================
# Création des sous-groupes Payants et F2P
# Ces datasets sont utilisés dans tous les graphiques suivants
# et dans les modèles de régression
# ============================================================
data_payant <- data %>% filter(price > 0)
data_f2p    <- data %>% filter(price == 0)
# ============================================================
# NUAGES DE POINTS — Modèles séparés Payants vs F2P
# Pour chaque variable explicative, on trace deux graphiques
# côte à côte pour comparer les deux marchés
# La droite de régression simple permet de visualiser la relation
# avec le revenu avant la modélisation multiple
# ============================================================

tracer_nuage <- function(var_x, label_x) {

  formule    <- as.formula(paste("log_revenue ~", var_x))
  mod_payant <- lm(formule, data = data_payant)
  mod_f2p    <- lm(formule, data = data_f2p)

  par(mfrow = c(1, 2), mar = c(5, 4, 5, 2))

  # Graphique 1 : Jeux Payants
  plot(x = data_payant[[var_x]], y = data_payant$log_revenue,
       main = paste("Payants —", label_x, "→ revenu"),
       xlab = label_x, ylab = "log(Revenu)",
       col  = adjustcolor(couleur_payant, alpha.f = 0.5),
       pch  = 16, cex = 0.8, cex.main = 0.95, cex.lab = 0.90, cex.axis = 0.85)
  abline(mod_payant, col = couleur_payant, lwd = 2)
  r2_payant <- round(summary(mod_payant)$r.squared, 3)
  legend("topleft", legend = paste("R² =", r2_payant), bty = "n", cex = 0.9)

  # Graphique 2 : Jeux F2P
  plot(x = data_f2p[[var_x]], y = data_f2p$log_revenue,
       main = paste("F2P —", label_x, "→ revenu"),
       xlab = label_x, ylab = "log(Revenu)",
       col  = adjustcolor(couleur_f2p, alpha.f = 0.5),
       pch  = 16, cex = 0.8, cex.main = 0.95, cex.lab = 0.90, cex.axis = 0.85)
  abline(mod_f2p, col = couleur_f2p, lwd = 2)
  r2_f2p <- round(summary(mod_f2p)$r.squared, 3)
  legend("topleft", legend = paste("R² =", r2_f2p), bty = "n", cex = 0.9)
}

# Graphique Prix — payants uniquement (F2P price = 0 par définition)
par(mfrow = c(1, 1), mar = c(5, 4, 5, 2))
mod_price <- lm(log_revenue ~ price, data = data_payant)
plot(x = data_payant$price, y = data_payant$log_revenue,
     main = "Payants — Prix → revenu", xlab = "Prix ($)",
     ylab = "log(Revenu)", col = adjustcolor(couleur_payant, alpha.f = 0.5),
     pch = 16, cex = 0.8, cex.main = 0.95, cex.lab = 0.90, cex.axis = 0.85)
abline(mod_price, col = couleur_payant, lwd = 2)
legend("topleft", legend = paste("R² =", round(summary(mod_price)$r.squared, 3)),
       bty = "n", cex = 0.9)

# Variables communes aux deux groupes
tracer_nuage("log_wishlists", "log(Wishlists)")

tracer_nuage("log_playtime",  "log(Temps de jeu)")

tracer_nuage("log_followers", "log(Followers)")

tracer_nuage("reviewScore",   "Score critique (/100)")

# ---- FONCTION RÉUTILISABLE ----
tracer_nuage_complet <- function(var_x, label_x) {

  formule     <- as.formula(paste("log_revenue ~", var_x))
  mod_complet <- lm(formule, data = data)

  par(mfrow = c(1, 1), mar = c(11, 4, 4, 2))  # marge basse = 11

  plot(
    x    = data[[var_x]],
    y    = data$log_revenue,
    main = paste("Modèle complet —", label_x, "→ revenue"),
    xlab = label_x,
    ylab = "log(Revenue)",
    col  = ifelse(data$price == 0,
                  adjustcolor(couleur_f2p,    alpha.f = 0.5),
                  adjustcolor(couleur_payant, alpha.f = 0.5)),
    pch  = 16,
    cex  = 0.8
  )

  abline(mod_complet, col = couleur_complet, lwd = 2)
  r2 <- round(summary(mod_complet)$r.squared, 3)

  par(xpd = TRUE)
  legend(
    x      = par("usr")[1],
    y      = par("usr")[3] - (par("usr")[4] - par("usr")[3]) * 0.45,
    legend = c(
      paste("R² =", r2),
      "Régression complète",
      "F2P",
      "Payants"
    ),
    col    = c(
      "black",
      couleur_complet,
      adjustcolor(couleur_f2p,    alpha.f = 0.8),
      adjustcolor(couleur_payant, alpha.f = 0.8)
    ),
    pch       = c(NA,  NA, 16, 16),
    lty       = c(NA,   1, NA, NA),
    lwd       = c(NA,   2, NA, NA),
    bty       = "n",
    cex       = 0.70,
    x.intersp = 0.5,
    y.intersp = 0.8
  )
  par(xpd = FALSE)
}

# ---- Variable 1 : Prix ----
mod_price <- lm(log_revenue ~ price, data = data_payant)

par(mfrow = c(1, 1), mar = c(11, 4, 4, 2))
plot(
  x    = data_payant$price,
  y    = data_payant$log_revenue,
  main = "Modèle complet — Prix → revenue (jeux payants)",
  xlab = "Prix ($)",
  ylab = "log(Revenue)",
  col  = adjustcolor(couleur_payant, alpha.f = 0.5),
  pch  = 16, cex = 0.8
)
abline(mod_price, col = couleur_complet, lwd = 2)
r2_price <- round(summary(mod_price)$r.squared, 3)

par(xpd = TRUE)
legend(
  x      = par("usr")[1],
  y      = par("usr")[3] - (par("usr")[4] - par("usr")[3]) * 0.45,
  legend = c(
    paste("R² =", r2_price),
    "Régression",
    "Payants"
  ),
  col    = c("black", couleur_complet,
             adjustcolor(couleur_payant, alpha.f = 0.8)),
  pch       = c(NA,  NA, 16),
  lty       = c(NA,   1, NA),
  lwd       = c(NA,   2, NA),
  bty       = "n",
  cex       = 0.70,
  x.intersp = 0.5,
  y.intersp = 0.8
)

par(xpd = FALSE)

# Variable 2 : Wishlists
tracer_nuage_complet("log_wishlists", "log(Wishlists)")

# Variable 3 : Temps de jeu
tracer_nuage_complet("log_playtime", "log(Temps de jeu)")

# Variable 4 : Followers
tracer_nuage_complet("log_followers", "log(Followers)")

# Variable 5 : Score critique
tracer_nuage_complet("reviewScore", "Score critique (/100)")


---

# 7. MODÈLES DE RÉGRESSION MULTIPLE

## 7.1 Choix de la spécification du prix


``` r
# ============================================================
# Comparaison log_price vs price en niveau
# On compare les R² ajustés des deux spécifications
# pour choisir la forme fonctionnelle la plus adaptée au prix
# ============================================================
modele_log_price <- lm(log_revenue ~ log_price + reviewScore +
                                     log_wishlists + log_playtime +
                                     DUMY_Indie + DUMY_AA + DUMY_AAA,
                       data = data_payant)

modele_niv_price <- lm(log_revenue ~ price + reviewScore +
                                     log_wishlists + log_playtime +
                                     DUMY_Indie + DUMY_AA + DUMY_AAA,
                       data = data_payant)

cat("R² ajusté — log(price) :", round(summary(modele_log_price)$adj.r.squared, 4), "\n")
## R² ajusté — log(price) : 0.7529
cat("R² ajusté — price      :", round(summary(modele_niv_price)$adj.r.squared, 4), "\n")
## R² ajusté — price      : 0.7579
cat("=> La spécification retenue est celle avec le R² ajusté le plus élevé\n")
## => La spécification retenue est celle avec le R² ajusté le plus élevé

7.2 Modèles finaux

# ============================================================
# MODÈLES DE RÉGRESSION FINALS
#
# Deux modèles distincts justifiés par les analyses descriptives
# qui montrent des dynamiques différentes entre F2P et Payants
#
# Référence des dummies : AAA (catégorie omise)
# => DUMY_Indie et DUMY_AA mesurent l'écart par rapport à AAA
#
# Note : DUMY_F2P n'est pas incluse car chaque modèle est déjà
# filtré sur un seul type de tarification
# ============================================================

# ---- Modèle Payant ----
# Variables : prix, score critique, wishlists, temps de jeu, segment
# Référence dummies : AAA
modele_payant <- lm(log_revenue ~ price +
                                  reviewScore +
                                  log_wishlists +
                                  log_playtime +
                                  DUMY_Indie +
                                  DUMY_AA,
                    data = data_payant)

# ---- Modèle F2P ----
# Variables : score critique, wishlists, temps de jeu, segment
# Le prix n'est pas inclus car tous les jeux F2P ont price = 0
# DUMY_Hobbyist est incluse car cette catégorie est présente dans le marché F2P
# Référence dummies : AAA
modele_f2p <- lm(log_revenue ~ reviewScore +
                               log_wishlists +
                               log_playtime +
                               DUMY_Hobbyist +
                               DUMY_Indie +
                               DUMY_AA,
                 data = data_f2p)

# ---- Affichage des résultats ----
summary(modele_payant)
## 
## Call:
## lm(formula = log_revenue ~ price + reviewScore + log_wishlists + 
##     log_playtime + DUMY_Indie + DUMY_AA, data = data_payant)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.10362 -0.38366 -0.06837  0.32910  1.99154 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    5.590230   0.559657   9.989  < 2e-16 ***
## price          0.011853   0.002392   4.956 1.11e-06 ***
## reviewScore   -0.002546   0.002777  -0.917  0.35989    
## log_wishlists  0.789362   0.044190  17.863  < 2e-16 ***
## log_playtime   0.344078   0.036934   9.316  < 2e-16 ***
## DUMY_Indie    -0.417034   0.100006  -4.170 3.83e-05 ***
## DUMY_AA       -0.203421   0.077822  -2.614  0.00933 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.5406 on 357 degrees of freedom
## Multiple R-squared:  0.7619, Adjusted R-squared:  0.7579 
## F-statistic: 190.4 on 6 and 357 DF,  p-value: < 2.2e-16
summary(modele_f2p)
## 
## Call:
## lm(formula = log_revenue ~ reviewScore + log_wishlists + log_playtime + 
##     DUMY_Hobbyist + DUMY_Indie + DUMY_AA, data = data_f2p)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -11.2807  -1.4303   0.7974   2.0315   9.0103 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   -6.83976    3.74984  -1.824   0.0701 .  
## reviewScore   -0.02435    0.02070  -1.176   0.2413    
## log_wishlists  1.33785    0.26255   5.096 1.03e-06 ***
## log_playtime   2.56577    0.27948   9.181 3.21e-16 ***
## DUMY_Hobbyist -8.30884    1.25066  -6.644 5.30e-10 ***
## DUMY_Indie     0.75035    0.79397   0.945   0.3461    
## DUMY_AA        1.54998    0.98996   1.566   0.1195    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 3.59 on 150 degrees of freedom
##   (1 observation effacée parce que manquante)
## Multiple R-squared:  0.6743, Adjusted R-squared:  0.6612 
## F-statistic: 51.75 on 6 and 150 DF,  p-value: < 2.2e-16
# ============================================================
# Tableau comparatif des deux modèles — format académique
# stargazer affiche les coefficients, erreurs standard et
# significativité côte à côte pour faciliter la comparaison
# ============================================================
stargazer(modele_payant, modele_f2p,
          type             = "text",
          title            = "Déterminants du revenu — Jeux Payants vs F2P",
          column.labels    = c("Jeux Payants", "Jeux F2P"),
          covariate.labels = c("Prix", "Score critique", "log(Wishlists)",
                               "log(Temps de jeu)", "Hobbyist (réf. AAA)",
                               "Indie (réf. AAA)", "AA (réf. AAA)"),
          dep.var.labels   = "log(Revenu)",
          star.cutoffs     = c(0.05, 0.01, 0.001),
          digits           = 3,
          no.space         = TRUE,
          omit.stat        = c("ser", "f"),
          add.lines        = list(
            c("R² ajusté",
              round(summary(modele_payant)$adj.r.squared, 3),
              round(summary(modele_f2p)$adj.r.squared,    3)),
            c("Observations", nrow(data_payant), nrow(data_f2p)),
            c("Référence dummies", "AAA", "AAA")
          ))
## 
## Déterminants du revenu — Jeux Payants vs F2P
## ==================================================
##                          Dependent variable:      
##                     ------------------------------
##                              log(Revenu)          
##                       Jeux Payants     Jeux F2P   
##                           (1)             (2)     
## --------------------------------------------------
## Prix                    0.012***                  
##                         (0.002)                   
## Score critique           -0.003         -0.024    
##                         (0.003)         (0.021)   
## log(Wishlists)          0.789***       1.338***   
##                         (0.044)         (0.263)   
## log(Temps de jeu)       0.344***       2.566***   
##                         (0.037)         (0.279)   
## Hobbyist (réf. AAA)                    -8.309***  
##                                         (1.251)   
## Indie (réf. AAA)       -0.417***         0.750    
##                         (0.100)         (0.794)   
## AA (réf. AAA)           -0.203**         1.550    
##                         (0.078)         (0.990)   
## Constant                5.590***        -6.840    
##                         (0.560)         (3.750)   
## --------------------------------------------------
## R² ajusté                0.758           0.661    
## Observations              364             158     
## Référence dummies         AAA             AAA     
## Observations              364             157     
## R2                       0.762           0.674    
## Adjusted R2              0.758           0.661    
## ==================================================
## Note:                *p<0.05; **p<0.01; ***p<0.001

8. TESTS DIAGNOSTICS ET ROBUSTESSE

# ============================================================
# TESTS DIAGNOSTICS DES HYPOTHÈSES DE LA RÉGRESSION
#
# Deux hypothèses classiques sont vérifiées :
# 1. Homoscédasticité — test de Breusch-Pagan
#    H0 : variance des résidus constante
#    Si p < 0.05 : hétéroscédasticité → correction de White
#
# 2. Normalité des résidus — test de Shapiro-Wilk
#    H0 : résidus normalement distribués
#    Si p < 0.05 : non normalité détectée
# ============================================================

bp_payant <- bptest(modele_payant)
bp_f2p    <- bptest(modele_f2p)
sw_payant <- shapiro.test(residuals(modele_payant))
sw_f2p    <- shapiro.test(residuals(modele_f2p))

# ---- Tableau de synthèse des tests ----
tableau_tests <- data.frame(
  Test = c("Breusch-Pagan (homoscédasticité)",
           "Shapiro-Wilk (normalité résidus)"),
  Statistique_Payant = c(round(bp_payant$statistic, 3),
                         round(sw_payant$statistic, 4)),
  Pvaleur_Payant     = c(formatC(bp_payant$p.value, format = "e", digits = 3),
                         formatC(sw_payant$p.value, format = "e", digits = 3)),
  Statistique_F2P    = c(round(bp_f2p$statistic, 3),
                         round(sw_f2p$statistic, 4)),
  Pvaleur_F2P        = c(formatC(bp_f2p$p.value, format = "e", digits = 3),
                         formatC(sw_f2p$p.value, format = "e", digits = 3)),
  Conclusion         = c("p < 0.05 → Hétéroscédasticité → Correction de White",
                         "p < 0.05 → Non normalité détectée")
)

knitr::kable(tableau_tests,
             caption   = "Tableau de synthèse des tests diagnostics",
             col.names = c("Test", "Stat. Payant", "p-val. Payant",
                           "Stat. F2P", "p-val. F2P", "Conclusion"),
             align     = c("l", "c", "c", "c", "c", "l"))
Tableau de synthèse des tests diagnostics
Test Stat. Payant p-val. Payant Stat. F2P p-val. F2P Conclusion
BP Breusch-Pagan (homoscédasticité) 21.0860 1.770e-03 28.4580 7.703e-05 p < 0.05 → Hétéroscédasticité → Correction de White
W Shapiro-Wilk (normalité résidus) 0.9703 8.690e-07 0.9216 1.580e-07 p < 0.05 → Non normalité détectée
# ============================================================
# Q-Q Plots — vérification visuelle de la normalité des résidus
# Si les points s'alignent sur la diagonale : normalité respectée
# Les écarts aux extrémités indiquent des queues épaisses
# ============================================================
par(mfrow = c(1, 2))
plot(modele_payant, which = 2, main = "Q-Q Plot — Modèle Payant")
plot(modele_f2p,    which = 2, main = "Q-Q Plot — Modèle F2P")

# ============================================================
# CORRECTION DE WHITE — Erreurs standard robustes (HC1)
#
# Suite à la détection d'hétéroscédasticité (Breusch-Pagan p < 0.05)
# on recalcule les erreurs standard avec la correction de White
# Cette correction ne modifie pas les coefficients mais garantit
# la validité des tests de significativité malgré l'hétéroscédasticité
# ============================================================

cat("=== CORRECTION DE WHITE — MODÈLE PAYANT ===\n")
## === CORRECTION DE WHITE — MODÈLE PAYANT ===
coeftest(modele_payant, vcov = vcovHC(modele_payant, type = "HC1"))
## 
## t test of coefficients:
## 
##                 Estimate Std. Error t value  Pr(>|t|)    
## (Intercept)    5.5902304  0.7149369  7.8192 6.045e-14 ***
## price          0.0118527  0.0026337  4.5003 9.195e-06 ***
## reviewScore   -0.0025460  0.0032504 -0.7833  0.433965    
## log_wishlists  0.7893619  0.0511832 15.4223 < 2.2e-16 ***
## log_playtime   0.3440777  0.0539254  6.3806 5.475e-10 ***
## DUMY_Indie    -0.4170339  0.0997862 -4.1793 3.682e-05 ***
## DUMY_AA       -0.2034214  0.0736746 -2.7611  0.006059 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
cat("\n=== CORRECTION DE WHITE — MODÈLE F2P ===\n")
## 
## === CORRECTION DE WHITE — MODÈLE F2P ===
coeftest(modele_f2p, vcov = vcovHC(modele_f2p, type = "HC1"))
## 
## t test of coefficients:
## 
##                Estimate Std. Error t value  Pr(>|t|)    
## (Intercept)   -6.839759   4.388931 -1.5584   0.12124    
## reviewScore   -0.024346   0.024220 -1.0052   0.31641    
## log_wishlists  1.337849   0.242068  5.5267 1.409e-07 ***
## log_playtime   2.565774   0.367460  6.9824 8.770e-11 ***
## DUMY_Hobbyist -8.308836   1.827954 -4.5454 1.121e-05 ***
## DUMY_Indie     0.750346   0.867933  0.8645   0.38868    
## DUMY_AA        1.549980   0.751804  2.0617   0.04096 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1