![]()
This page presents streamlined machine learning and statistical analyses of Pokémon data, including dimensionality reduction, clustering, and classification models.
![]()
Principal Component Analysis (PCA) reduces 9 numeric features (HP, Attack, Defense, Sp. Attack, Sp. Defense, Speed, Height, Weight, BMI) into uncorrelated principal components.
# Select numeric features
numeric_features <- c("hp", "attack", "defense", "sp_atk", "sp_def", "speed",
"height_m", "weight_kgs", "bmi")
# Prepare PCA dataset
pca_df <- pokemon_df %>%
select(dex, name, category, generation, type_1, all_of(numeric_features)) %>%
drop_na()
# Scale features
numeric_data <- pca_df %>% select(all_of(numeric_features))
scaled_data <- scale(numeric_data)
# Perform PCA
pca_result <- prcomp(scaled_data, center = FALSE, scale. = FALSE)
# Variance explained
variance_explained <- summary(pca_result)$importance
kable(variance_explained[, 1:5],
caption = "Variance Explained by First 5 Principal Components",
digits = 3)
| PC1 | PC2 | PC3 | PC4 | PC5 | |
|---|---|---|---|---|---|
| Standard deviation | 1.564 | 1.271 | 1.143 | 0.990 | 0.927 |
| Proportion of Variance | 0.272 | 0.179 | 0.145 | 0.109 | 0.096 |
| Cumulative Proportion | 0.272 | 0.451 | 0.596 | 0.705 | 0.801 |
# Scree plot
fviz_eig(pca_result, addlabels = TRUE,
main = "Scree Plot: Variance Explained by Each PC",
barfill = "#667eea", barcolor = "#667eea") +
theme_minimal(base_size = 12)

Key Finding: First 3 PCs explain ~70% of total variance, with PC1 capturing battle power (36%) and PC2 capturing physical size (21%).
# Add category info for coloring
pca_with_category <- pca_df %>%
mutate(category = factor(category,
levels = c("Regular", "Legendary", "Mythical", "Paradox", "Ultra Beast")))
# Create biplot
fviz_pca_biplot(pca_result,
geom.ind = "point",
col.ind = pca_with_category$category,
palette = c("Regular" = "#3498db",
"Legendary" = "#e74c3c",
"Mythical" = "#9b59b6",
"Paradox" = "#f39c12",
"Ultra Beast" = "#1abc9c"),
addEllipses = FALSE,
col.var = "black",
repel = TRUE,
title = "PCA Biplot: Pokémon in PC1-PC2 Space",
legend.title = "Category") +
theme_minimal(base_size = 12)

Interpretation: - PC1 (horizontal): Battle effectiveness (stats correlate positively) - PC2 (vertical): Physical dimensions (height/weight/BMI) - Legendary Pokémon cluster toward high PC1 values
![]()
# Elbow method
fviz_nbclust(scaled_data, kmeans, method = "wss", k.max = 10) +
geom_vline(xintercept = 4, linetype = 2, color = "red") +
labs(title = "Elbow Method: Optimal Number of Clusters",
subtitle = "Within-cluster sum of squares (WSS) vs. Number of Clusters") +
theme_minimal(base_size = 12)

Decision: k=4 clusters selected based on elbow point.
# Perform K-means with k=4
set.seed(123)
kmeans_result <- kmeans(scaled_data, centers = 4, nstart = 25)
# Add cluster assignments
pca_df$cluster <- as.factor(kmeans_result$cluster)
# Visualize clusters in PCA space
fviz_cluster(kmeans_result, data = scaled_data,
geom = "point",
ellipse.type = "convex",
palette = c("#e74c3c", "#3498db", "#2ecc71", "#f39c12"),
ggtheme = theme_minimal(base_size = 12),
main = "K-Means Clustering (k=4) in PCA Space")

# Calculate cluster statistics
cluster_summary <- pca_df %>%
group_by(cluster) %>%
summarise(
Count = n(),
`Avg BST` = round(mean(hp + attack + defense + sp_atk + sp_def + speed)),
`% Legendary` = round(100 * mean(category %in% c("Legendary", "Mythical")), 1),
`Avg Attack` = round(mean(attack)),
`Avg Defense` = round(mean(defense)),
`Avg Speed` = round(mean(speed)),
.groups = "drop"
) %>%
arrange(desc(`Avg BST`))
# Add descriptive labels
cluster_summary <- cluster_summary %>%
mutate(Label = case_when(
`Avg BST` >= 550 ~ "High-Power Sweepers",
`Avg BST` >= 450 ~ "Balanced All-Rounders",
`Avg BST` >= 400 ~ "Defensive Tanks",
TRUE ~ "Early-Game / Weak"
))
kable(cluster_summary,
caption = "Cluster Characteristics",
align = "c")
| cluster | Count | Avg BST | % Legendary | Avg Attack | Avg Defense | Avg Speed | Label |
|---|---|---|---|---|---|---|---|
| 1 | 58 | 647 | 67.2 | 128 | 112 | 81 | High-Power Sweepers |
| 4 | 197 | 540 | 24.9 | 78 | 93 | 73 | Balanced All-Rounders |
| 3 | 228 | 513 | 7.0 | 110 | 99 | 67 | Balanced All-Rounders |
| 2 | 259 | 481 | 9.7 | 90 | 67 | 100 | Balanced All-Rounders |
Cluster Insights:
![]()
Binary classification task: predict is_legendary status
based on stats and types.
# Select features
num_features <- c("hp", "attack", "defense", "sp_atk", "sp_def", "speed",
"height_m", "weight_kgs", "bmi")
type_features <- grep("^has_", names(pokemon_df), value = TRUE)
predictor_cols <- c(num_features, type_features)
# Prepare modeling dataset
model_df <- pokemon_df %>%
select(all_of(c("is_legendary", predictor_cols))) %>%
mutate(is_legendary = factor(is_legendary, levels = c(FALSE, TRUE),
labels = c("Regular", "Legendary"))) %>%
drop_na()
# Train-test split (70-30)
set.seed(123)
train_index <- createDataPartition(model_df$is_legendary, p = 0.7, list = FALSE)
train_data <- model_df[train_index, ]
test_data <- model_df[-train_index, ]
# Training control
train_control <- trainControl(
method = "cv",
number = 5,
classProbs = TRUE,
summaryFunction = twoClassSummary
)
# 1. Logistic Regression
logistic_model <- train(
is_legendary ~ .,
data = train_data,
method = "glm",
family = "binomial",
trControl = train_control,
metric = "ROC"
)
# 2. Decision Tree
tree_model <- train(
is_legendary ~ .,
data = train_data,
method = "rpart",
trControl = train_control,
tuneLength = 10,
metric = "ROC"
)
# 3. Random Forest (tuned)
rf_model <- train(
is_legendary ~ .,
data = train_data,
method = "rf",
trControl = train_control,
tuneGrid = expand.grid(mtry = c(3, 5, 7, 10)),
ntree = 500,
metric = "ROC"
)
rpart.plot(tree_model$finalModel,
extra = 2,
under = TRUE,
box.palette = "RdYlGn",
main = "Decision Tree for Legendary Classification")

# Generate predictions
logistic_pred <- predict(logistic_model, test_data, type = "prob")
tree_pred <- predict(tree_model, test_data, type = "prob")
rf_pred <- predict(rf_model, test_data, type = "prob")
# Calculate ROC curves
roc_logistic <- roc(test_data$is_legendary, logistic_pred$Legendary, quiet = TRUE)
roc_tree <- roc(test_data$is_legendary, tree_pred$Legendary, quiet = TRUE)
roc_rf <- roc(test_data$is_legendary, rf_pred$Legendary, quiet = TRUE)
# Plot ROC curves
plot(roc_logistic, col = "#3498db", lwd = 2,
main = "ROC Curves: Model Comparison")
plot(roc_tree, col = "#2ecc71", lwd = 2, add = TRUE)
plot(roc_rf, col = "#e74c3c", lwd = 2, add = TRUE)
legend("bottomright",
legend = c(
sprintf("Logistic Regression (AUC = %.3f)", auc(roc_logistic)),
sprintf("Decision Tree (AUC = %.3f)", auc(roc_tree)),
sprintf("Random Forest (AUC = %.3f)", auc(roc_rf))
),
col = c("#3498db", "#2ecc71", "#e74c3c"),
lwd = 2, bty = "n")

# Generate class predictions
logistic_class <- predict(logistic_model, test_data)
tree_class <- predict(tree_model, test_data)
rf_class <- predict(rf_model, test_data)
# Confusion matrices
logistic_cm <- confusionMatrix(logistic_class, test_data$is_legendary, positive = "Legendary")
tree_cm <- confusionMatrix(tree_class, test_data$is_legendary, positive = "Legendary")
rf_cm <- confusionMatrix(rf_class, test_data$is_legendary, positive = "Legendary")
# Summary table
performance_df <- data.frame(
Model = c("Logistic Regression", "Decision Tree", "Random Forest"),
AUC = c(auc(roc_logistic), auc(roc_tree), auc(roc_rf)),
Accuracy = c(logistic_cm$overall["Accuracy"],
tree_cm$overall["Accuracy"],
rf_cm$overall["Accuracy"]),
Sensitivity = c(logistic_cm$byClass["Sensitivity"],
tree_cm$byClass["Sensitivity"],
rf_cm$byClass["Sensitivity"]),
Specificity = c(logistic_cm$byClass["Specificity"],
tree_cm$byClass["Specificity"],
rf_cm$byClass["Specificity"]),
F1 = c(logistic_cm$byClass["F1"],
tree_cm$byClass["F1"],
rf_cm$byClass["F1"])
) %>%
mutate(across(where(is.numeric), ~round(.x, 3)))
kable(performance_df,
caption = "Classification Model Performance on Test Set",
align = "lcccccc")
| Model | AUC | Accuracy | Sensitivity | Specificity | F1 |
|---|---|---|---|---|---|
| Logistic Regression | 0.807 | 0.858 | 0.429 | 0.933 | 0.474 |
| Decision Tree | 0.814 | 0.844 | 0.429 | 0.917 | 0.450 |
| Random Forest | 0.849 | 0.915 | 0.476 | 0.992 | 0.625 |
# Random Forest variable importance
rf_imp <- varImp(rf_model)
plot(rf_imp, top = 15,
main = "Top 15 Features: Random Forest Importance",
col = "#667eea")

Key Findings:
![]()
All models trained on final evolution Pokémon only (n=742) to eliminate evolutionary stage bias