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.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
| 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
| 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