Pokemon_clean$gender_cat <- factor(Pokemon_clean$gender_cat )
Pokemon_clean$gender_cat <-relevel(Pokemon_clean$gender_cat, ref = "Equal")
Pokemon_clean$happiness_cat <-relevel(factor(Pokemon_clean$happiness_cat), ref = "Normal")
mv_fit_step <- lm(formula = log2(weight_kg) ~ log2(height_m) + height_m + gender_cat +
I(hp/10) + I(defense/10) + I(sp_attack/10) + I(speed/10) +
special_status + happiness_cat, data = Pokemon_clean, x = T,
y = T)
The aim of this analysis project is to study the variability in the weight of Pokémon species. The statistical analysis plan (SAP) specifies that linear regression models will be used to estimate the association between height and weight, both univariable and multivariable. The document Regression_Report addresses the problem with univariable and multivariable analyses.
In this document we assess also the association of type with the outcome. We assume that all the models described in the document Regression_Report were fitted and analyzed.
We start from the multivariable model selected with the backward AIC based method and we add the 18 Pokémon types as explanatory variables. Each type is defined as a binary variable, indicating if the species belongs or does not belong to the type. (See additional IDA report for more details about this variable).
mv_fit_type <- update(mv_fit_step, .~. + is_rock + is_dragon + is_bug + is_steel + is_ground+ is_water+is_fire+is_grass+is_ice+is_flying+is_fairy+is_ghost+is_poison+is_electric+is_normal+is_dark+is_psychic+is_fighting)
summary(mv_fit_type) %>% tab_model()
| log 2(weight kg) | |||
|---|---|---|---|
| Predictors | Estimates | CI | p |
| (Intercept) | 4.31 | 3.81 – 4.81 | <0.001 |
| height m [log2] | 1.73 | 1.60 – 1.86 | <0.001 |
| height m | -0.31 | -0.40 – -0.21 | <0.001 |
|
gender cat [Mostly female] |
-0.24 | -0.56 – 0.08 | 0.140 |
| gender cat [Mostly male] | 0.17 | -0.05 – 0.38 | 0.129 |
| gender cat [Genderless] | 0.45 | 0.14 – 0.75 | 0.004 |
| hp/10 | 0.12 | 0.08 – 0.15 | <0.001 |
| defense/10 | 0.07 | 0.04 – 0.10 | <0.001 |
| sp attack/10 | -0.04 | -0.07 – -0.01 | 0.019 |
| speed/10 | -0.04 | -0.07 – -0.01 | 0.021 |
|
special status [sub-legendary] |
-0.44 | -0.86 – -0.02 | 0.042 |
|
special status [legendary] |
0.17 | -0.36 – 0.69 | 0.532 |
| special status [mythical] | -0.62 | -1.21 – -0.03 | 0.041 |
| happiness cat [Happy] | -0.38 | -0.84 – 0.08 | 0.103 |
| happiness cat [Unhappy] | 0.17 | -0.11 – 0.44 | 0.232 |
| is rock | 0.67 | 0.35 – 0.99 | <0.001 |
| is dragon | 0.13 | -0.19 – 0.46 | 0.430 |
| is bug | -0.04 | -0.32 – 0.25 | 0.799 |
| is steel | 0.81 | 0.47 – 1.14 | <0.001 |
| is ground | 0.40 | 0.10 – 0.71 | 0.010 |
| is water | -0.04 | -0.30 – 0.22 | 0.771 |
| is fire | 0.24 | -0.07 – 0.56 | 0.131 |
| is grass | -0.37 | -0.63 – -0.10 | 0.007 |
| is ice | 0.51 | 0.16 – 0.87 | 0.005 |
| is flying | -0.38 | -0.63 – -0.13 | 0.003 |
| is fairy | -0.62 | -0.96 – -0.28 | <0.001 |
| is ghost | -1.11 | -1.44 – -0.78 | <0.001 |
| is poison | -0.43 | -0.72 – -0.14 | 0.004 |
| is electric | 0.11 | -0.24 – 0.46 | 0.541 |
| is normal | 0.05 | -0.24 – 0.34 | 0.719 |
| is dark | 0.17 | -0.14 – 0.48 | 0.292 |
| is psychic | -0.04 | -0.33 – 0.26 | 0.806 |
| is fighting | 0.15 | -0.16 – 0.47 | 0.342 |
| Observations | 1025 | ||
| R2 / R2 adjusted | 0.760 / 0.752 | ||
RESULTS: Including type increases the R^2 (from 0.71 to 0.75) and decreases the residual standard error. Many types have a statistically significant association with weight. Rock, ice, steel and ground are statistically significantly positively associated, grass, flying, fairy, ghost and poison are statistically significantly negatively associated. The estimated coefficients of other explanatory variables change compared to the model without type, and the associations are generally attenuated (as might be expected, as type is associated to the other variables).
We fit also a model where the interaction between height (linear + log2) and all types are considered. The joint distribution of type and height was examined during IDA (see additional IDA report); no type was found to be so sparse or so narrowly distributed in height as to make its interaction term inestimable, though estimates for less numerous types are expected to be less precise. To limit the number of terms, backward elimination in the interaction model is performed based on AIC. The backward elimination drops both interactions together for a given type (pairwise), or none. The procedure only allows dropping a main effect after both interactions are eliminated (hierarchical rule).
# first fit a model with all interactions between types and height
mv_fit_type_int <- lm(
log2(weight_kg) ~ log2(height_m) + height_m + gender_cat +
I(hp/10) + I(attack/10) + I(defense/10) +
I(sp_attack/10) + I(sp_defense/10) + I(speed/10) +
happiness_cat +
is_rock + is_ice + is_dragon + is_bug + is_steel + is_ground +
is_water + is_fire + is_grass + is_flying + is_fairy + is_ghost +
is_poison + is_electric + is_normal + is_dark + is_psychic + is_fighting +
# Interactions
is_rock*log2(height_m) + is_ice*log2(height_m) + is_dragon*log2(height_m) +
is_bug*log2(height_m) + is_steel*log2(height_m) + is_ground*log2(height_m) +
is_water*log2(height_m) + is_fire*log2(height_m) + is_grass*log2(height_m) +
is_flying*log2(height_m) + is_fairy*log2(height_m) + is_ghost*log2(height_m) +
is_poison*log2(height_m) + is_electric*log2(height_m) + is_normal*log2(height_m) +
is_dark*log2(height_m) + is_psychic*log2(height_m) + is_fighting*log2(height_m) +
is_rock*height_m + is_ice*height_m + is_dragon*height_m +
is_bug*height_m + is_steel*height_m + is_ground*height_m +
is_water*height_m + is_fire*height_m + is_grass*height_m +
is_flying*height_m + is_fairy*height_m + is_ghost*height_m +
is_poison*height_m + is_electric*height_m + is_normal*height_m +
is_dark*height_m + is_psychic*height_m + is_fighting*height_m,
data = Pokemon_clean
)
## -------------------------------
## Blocked, hierarchical backward AIC for type–height interactions
## -------------------------------
# types you use as 0/1 indicators in the model:
type_names <- c(
"rock","ice","dragon","bug","steel","ground","water","fire","grass",
"flying","fairy","ghost","poison","electric","normal","dark","psychic","fighting"
)
# helper: given term labels from terms(model), detect presence of main & interactions (either order)
present_terms_for_type <- function(terms_in_model, type) {
main <- paste0("is_", type)
int_lin_a <- paste0("is_", type, ":height_m")
int_lin_b <- paste0("height_m:is_", type)
int_log_a <- paste0("is_", type, ":log2(height_m)")
int_log_b <- paste0("log2(height_m):is_", type)
list(
main_present = main %in% terms_in_model,
main_label = main,
lin_present = any(c(int_lin_a, int_lin_b) %in% terms_in_model),
lin_label = intersect(c(int_lin_a, int_lin_b), terms_in_model),
log2_present = any(c(int_log_a, int_log_b) %in% terms_in_model),
log2_label = intersect(c(int_log_a, int_log_b), terms_in_model)
)
}
# helper: update formula dropping a vector of exact term labels
drop_terms_from_formula <- function(fml, drops) {
if (length(drops) == 0) return(fml)
rhs <- paste(". ~ . -", paste(drops, collapse = " - "))
update(fml, rhs)
}
# main routine
step_blocked_types <- function(model, data, type_names,
strict_pairs = TRUE, # if TRUE, only drop both interactions together
verbose = TRUE) {
current_model <- model
current_aic <- AIC(current_model)
log_df <- data.frame(iter = integer(), type = character(),
dropped = character(), old_AIC = numeric(),
new_AIC = numeric(), dAIC = numeric(),
stringsAsFactors = FALSE)
improved <- TRUE
iter <- 0
while (improved) {
iter <- iter + 1
improved <- FALSE
best_model <- current_model
best_aic <- current_aic
best_drop <- NULL
best_type <- NULL
terms_in_model <- attr(terms(current_model), "term.labels")
for (tp in type_names) {
pres <- present_terms_for_type(terms_in_model, tp)
# Which drops are allowed under hierarchy?
# Case A: both interactions present -> propose dropping BOTH interactions together
if (pres$lin_present && pres$log2_present) {
drop_vec <- c(pres$lin_label, pres$log2_label)
drop_vec <- unique(as.character(drop_vec))
# Try dropping both interactions
cand_formula <- drop_terms_from_formula(formula(current_model), drop_vec)
cand_model <- lm(cand_formula, data = data)
cand_aic <- AIC(cand_model)
if (cand_aic < best_aic) {
best_model <- cand_model
best_aic <- cand_aic
best_drop <- drop_vec
best_type <- tp
improved <- TRUE
}
} else {
# Case B (optional): only one interaction present
if (!strict_pairs) {
# allow dropping whichever single interaction remains
single <- unique(as.character(c(pres$lin_label, pres$log2_label)))
single <- single[nchar(single) > 0]
if (length(single) == 1) {
cand_formula <- drop_terms_from_formula(formula(current_model), single)
cand_model <- lm(cand_formula, data = data)
cand_aic <- AIC(cand_model)
if (cand_aic < best_aic) {
best_model <- cand_model
best_aic <- cand_aic
best_drop <- single
best_type <- tp
improved <- TRUE
}
}
}
}
# Case C: interactions absent, main present -> main is eligible for drop
if (!pres$lin_present && !pres$log2_present && pres$main_present) {
drop_vec <- pres$main_label
cand_formula <- drop_terms_from_formula(formula(current_model), drop_vec)
cand_model <- lm(cand_formula, data = data)
cand_aic <- AIC(cand_model)
if (cand_aic < best_aic) {
best_model <- cand_model
best_aic <- cand_aic
best_drop <- drop_vec
best_type <- tp
improved <- TRUE
}
}
# Case D: interactions present but main absent -> OK, we already handle dropping interactions above.
# We never try to drop a main that isn't in the model.
} # end for tp
if (improved) {
if (verbose) {
cat(sprintf("Iter %d: drop (%s): %s ΔAIC = %.3f\n",
iter, best_type, paste(best_drop, collapse=", "),
best_aic - current_aic))
}
log_df <- rbind(log_df, data.frame(
iter = iter,
type = best_type,
dropped = paste(best_drop, collapse = ", "),
old_AIC = current_aic,
new_AIC = best_aic,
dAIC = best_aic - current_aic,
stringsAsFactors = FALSE
))
current_model <- best_model
current_aic <- best_aic
}
} # while
list(model = current_model, log = log_df)
}
# mv_fit_type_int (defined above) is the full model with all type×height interactions
# Example call (strict pair-drop on interactions):
res <- step_blocked_types(mv_fit_type_int, data = Pokemon_clean,
type_names = type_names,
strict_pairs = TRUE, # only drop both interactions together
verbose = FALSE)
final_model <- res$model
cat("\nFinal AIC:", AIC(final_model), "\n")
##
## Final AIC: 3219.621
summary(final_model)
##
## Call:
## lm(formula = cand_formula, data = data)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.4102 -0.5164 0.0581 0.6297 9.8383
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.47045 0.24599 18.174 < 2e-16 ***
## log2(height_m) 1.83904 0.07838 23.462 < 2e-16 ***
## height_m -0.40111 0.06285 -6.382 2.68e-10 ***
## gender_catMostly female -0.26470 0.16188 -1.635 0.102335
## gender_catMostly male 0.13606 0.10412 1.307 0.191580
## gender_catGenderless 0.20753 0.13778 1.506 0.132302
## I(hp/10) 0.11007 0.01863 5.907 4.78e-09 ***
## I(attack/10) 0.01199 0.01755 0.683 0.494688
## I(defense/10) 0.04348 0.01866 2.330 0.020007 *
## I(sp_attack/10) -0.05899 0.01632 -3.614 0.000317 ***
## I(sp_defense/10) 0.04788 0.01908 2.509 0.012249 *
## I(speed/10) -0.04052 0.01597 -2.537 0.011337 *
## happiness_catHappy -0.62109 0.21441 -2.897 0.003853 **
## happiness_catUnhappy 0.21474 0.12564 1.709 0.087736 .
## is_rock 0.55701 0.31564 1.765 0.077926 .
## is_ice 0.45692 0.16773 2.724 0.006559 **
## is_steel 0.84240 0.16087 5.237 1.99e-07 ***
## is_ground 0.43987 0.14422 3.050 0.002350 **
## is_fire 0.22803 0.14014 1.627 0.104027
## is_grass -0.41087 0.11450 -3.588 0.000349 ***
## is_flying -0.40552 0.12161 -3.334 0.000886 ***
## is_fairy -0.68936 0.16127 -4.275 2.10e-05 ***
## is_ghost -2.25585 0.46530 -4.848 1.45e-06 ***
## is_poison -0.90020 0.19313 -4.661 3.57e-06 ***
## log2(height_m):is_rock -0.38153 0.26927 -1.417 0.156815
## log2(height_m):is_ghost -0.59590 0.26467 -2.251 0.024572 *
## log2(height_m):is_poison -0.34890 0.18125 -1.925 0.054525 .
## height_m:is_rock 0.06776 0.22237 0.305 0.760650
## height_m:is_ghost 0.84672 0.33430 2.533 0.011467 *
## height_m:is_poison 0.31471 0.10149 3.101 0.001985 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.146 on 995 degrees of freedom
## Multiple R-squared: 0.7635, Adjusted R-squared: 0.7566
## F-statistic: 110.8 on 29 and 995 DF, p-value: < 2.2e-16
res$log # shows the elimination path
## iter type dropped old_AIC
## 1 1 normal height_m:is_normal, log2(height_m):is_normal 3271.235
## 2 2 fighting height_m:is_fighting, log2(height_m):is_fighting 3267.327
## 3 3 ground height_m:is_ground, log2(height_m):is_ground 3263.581
## 4 4 fairy height_m:is_fairy, log2(height_m):is_fairy 3259.806
## 5 5 dark height_m:is_dark, log2(height_m):is_dark 3256.119
## 6 6 psychic height_m:is_psychic, log2(height_m):is_psychic 3252.718
## 7 7 dragon height_m:is_dragon, log2(height_m):is_dragon 3249.191
## 8 8 steel height_m:is_steel, log2(height_m):is_steel 3246.744
## 9 9 fire height_m:is_fire, log2(height_m):is_fire 3243.830
## 10 10 ice height_m:is_ice, log2(height_m):is_ice 3241.569
## 11 11 psychic is_psychic 3239.289
## 12 12 bug height_m:is_bug, log2(height_m):is_bug 3237.303
## 13 13 bug is_bug 3235.536
## 14 14 flying height_m:is_flying, log2(height_m):is_flying 3233.669
## 15 15 water height_m:is_water, log2(height_m):is_water 3232.105
## 16 16 water is_water 3229.614
## 17 17 normal is_normal 3227.642
## 18 18 grass height_m:is_grass, log2(height_m):is_grass 3226.007
## 19 19 fighting is_fighting 3224.724
## 20 20 dragon is_dragon 3223.596
## 21 21 dark is_dark 3222.600
## 22 22 electric height_m:is_electric, log2(height_m):is_electric 3221.519
## 23 23 electric is_electric 3220.426
## new_AIC dAIC
## 1 3267.327 -3.9078560
## 2 3263.581 -3.7463262
## 3 3259.806 -3.7747486
## 4 3256.119 -3.6873993
## 5 3252.718 -3.4008783
## 6 3249.191 -3.5272207
## 7 3246.744 -2.4467203
## 8 3243.830 -2.9145009
## 9 3241.569 -2.2606623
## 10 3239.289 -2.2798241
## 11 3237.303 -1.9858642
## 12 3235.536 -1.7675395
## 13 3233.669 -1.8669335
## 14 3232.105 -1.5641473
## 15 3229.614 -2.4908816
## 16 3227.642 -1.9722943
## 17 3226.007 -1.6348519
## 18 3224.724 -1.2826499
## 19 3223.596 -1.1280716
## 20 3222.600 -0.9961329
## 21 3221.519 -1.0813083
## 22 3220.426 -1.0920398
## 23 3219.621 -0.8053959
summary(final_model) %>% tab_model()
| Dependent variable | |||
|---|---|---|---|
| Predictors | Estimates | CI | p |
| (Intercept) | 4.47 | 3.99 – 4.95 | <0.001 |
| log2(height_m) | 1.84 | 1.69 – 1.99 | <0.001 |
| height_m | -0.40 | -0.52 – -0.28 | <0.001 |
| gender_catMostly female | -0.26 | -0.58 – 0.05 | 0.102 |
| gender_catMostly male | 0.14 | -0.07 – 0.34 | 0.192 |
| gender_catGenderless | 0.21 | -0.06 – 0.48 | 0.132 |
| I(hp/10) | 0.11 | 0.07 – 0.15 | <0.001 |
| I(attack/10) | 0.01 | -0.02 – 0.05 | 0.495 |
| I(defense/10) | 0.04 | 0.01 – 0.08 | 0.020 |
| I(sp_attack/10) | -0.06 | -0.09 – -0.03 | <0.001 |
| I(sp_defense/10) | 0.05 | 0.01 – 0.09 | 0.012 |
| I(speed/10) | -0.04 | -0.07 – -0.01 | 0.011 |
| happiness_catHappy | -0.62 | -1.04 – -0.20 | 0.004 |
| happiness_catUnhappy | 0.21 | -0.03 – 0.46 | 0.088 |
| is_rock | 0.56 | -0.06 – 1.18 | 0.078 |
| is_ice | 0.46 | 0.13 – 0.79 | 0.007 |
| is_steel | 0.84 | 0.53 – 1.16 | <0.001 |
| is_ground | 0.44 | 0.16 – 0.72 | 0.002 |
| is_fire | 0.23 | -0.05 – 0.50 | 0.104 |
| is_grass | -0.41 | -0.64 – -0.19 | <0.001 |
| is_flying | -0.41 | -0.64 – -0.17 | 0.001 |
| is_fairy | -0.69 | -1.01 – -0.37 | <0.001 |
| is_ghost | -2.26 | -3.17 – -1.34 | <0.001 |
| is_poison | -0.90 | -1.28 – -0.52 | <0.001 |
| log2(height_m):is_rock | -0.38 | -0.91 – 0.15 | 0.157 |
| log2(height_m):is_ghost | -0.60 | -1.12 – -0.08 | 0.025 |
| log2(height_m):is_poison | -0.35 | -0.70 – 0.01 | 0.055 |
| height_m:is_rock | 0.07 | -0.37 – 0.50 | 0.761 |
| height_m:is_ghost | 0.85 | 0.19 – 1.50 | 0.011 |
| height_m:is_poison | 0.31 | 0.12 – 0.51 | 0.002 |
| Observations | 1025 | ||
| R2 / R2 adjusted | 0.764 / 0.757 | ||
RESULTS: The interactions retained in the model after variable selection are those for rock, ghost and poison. The main effects are those of rock, ice, steel, ground, fire, grass, flying, fairy, ghost and poison. The R2 is similar to that of the model without interactions.
The graphical presentation of the estimated association between height and weight among different types is shown below. The types that were omitted from variable selection are grouped in the Other class, the weight is estimated using the reference value for categorical variables and the median value for numerical variables.
library(dplyr)
library(ggplot2)
library(RColorBrewer)
library(scales)
library(ggrepel)
# --- 1. Define all types and those kept in final_model ---
all_types <- c("is_rock","is_ice","is_dragon","is_bug","is_steel","is_ground",
"is_water","is_fire","is_grass","is_flying","is_fairy","is_ghost",
"is_poison","is_electric","is_normal","is_dark","is_psychic","is_fighting")
kept_types <- intersect(all_types, attr(terms(final_model), "term.labels"))
# --- 2. Reference values for other predictors ---
ref_values <- Pokemon_clean %>%
summarise(across(where(is.numeric), median, na.rm = TRUE))
ref_values$gender_cat <- levels(Pokemon_clean$gender_cat)[1]
ref_values$happiness_cat <- levels(Pokemon_clean$happiness_cat)[1]
ref_values$PokemonPhase_5cat <- levels(Pokemon_clean$PokemonPhase_5cat)[1]
# --- 3. Build predictions per type within observed ranges ---
pred_list <- list()
for (t in all_types) {
obs_range <- Pokemon_clean %>%
filter(.data[[t]] == 1) %>%
summarise(min_h = min(height_m, na.rm = TRUE),
max_h = max(height_m, na.rm = TRUE))
if (nrow(obs_range) == 0 || is.na(obs_range$min_h)) next
height_seq <- seq(obs_range$min_h, obs_range$max_h, length.out = 100)
newdata <- data.frame(
height_m = height_seq,
log2_height_m = log2(height_seq)
)
for (col in setdiff(names(ref_values), c("height_m","log2_height_m"))) {
newdata[[col]] <- ref_values[[col]]
}
for (tt in all_types) {
newdata[[tt]] <- ifelse(tt == t, 1, 0)
}
newdata$type <- ifelse(t %in% kept_types, t, "Other")
newdata$pred <- predict(final_model, newdata)
newdata$pred_kg <- 2^(newdata$pred)
newdata$line_type <- ifelse(newdata$type == "Other", "dashed", "solid")
pred_list[[t]] <- newdata
}
pred_all <- bind_rows(pred_list)
# --- 4. Assign Dark2 colors ---
main_types <- unique(pred_all$type)
kept_colors <- brewer.pal(min(length(kept_types), 8), "Dark2") # max 8 colors in Dark2
# If more than 8 kept types, repeat colors
kept_colors <- rep(kept_colors, length.out = length(kept_types))
colors_vec <- c(kept_colors, "gray50") # last color for Other
names(colors_vec) <- c(kept_types, "Other")
# --- 5. Prepare labels at endpoints ---
end_points <- pred_all %>%
group_by(type) %>%
filter(height_m == max(height_m)) %>%
ungroup()
# --- 6. Plot ---
ggplot(pred_all, aes(x = height_m, y = pred_kg, color = type, linetype = line_type)) +
geom_line(size = 1.2) +
geom_text_repel(data = end_points,
aes(label = type),
nudge_x = 0.1,
segment.color = NA,
direction = "y",
hjust = 0,
size = 4,
box.padding = 0.2,
max.overlaps = 20) +
scale_x_continuous(trans = "log2",
breaks = c(0.25, 0.5, 1, 2, 4, 8),
labels = c("0.25","0.5","1","2","4","8")) +
scale_y_continuous(trans = "log2",
breaks = c(1, 2, 5, 10, 20, 50, 100, 200, 500),
labels = c("1","2","5","10","20","50","100","200","500")) +
scale_color_manual(values = colors_vec) +
scale_linetype_identity() +
labs(x = "Height (m)",
y = "Predicted Weight (kg)") +
theme_minimal(base_size = 14) +
theme(legend.position = "none")
RESULTS: The differences between types are substantial. Ghost type has a slightly different shape than the other types. The growth for some types is very steep (steel, ice, ground, fire, rock); other types as fairy, grass and flying increase weight at lower rate, as height increases. The distinctive shape of the poison type is determined by the extreme values of height and weight of Eternatus (poison and dragon type).