Purpose

The purpose of this project is to create a model that can predict a Pokemon’s type

Introduction

Pokemon is a role playing game in which the player can play as a trainer to catch and collect multiple Pokemon using Poke balls and then use them to fight in battles with other trainers. Pokemon have levels, and typically as they battle and gain more experience, they are able to evolve and get stronger.

Each pokemon has a type (or two types), essentially a “classification” of sorts of the the pokemon. Each type has its own strengths and weakness relative to other types, making it so that some pokemon match more favorably against others, based on their typing. This creates a complex web of interactions and strategies in pokemon battles. As of now, there are 18 different pokemon types.

List of Pokemon Types

The aim of this model is to predict the first type of a pokemon given information about the Pokemon itself, such as its height weight, base_stat total, etc…

Typically, when you look at a Pokemon, it can be somewhat obvious what type it is (red is associated with fire, green with grass, blue with water and so on), and other times not so much. I’m curious that given a lot of data, how well machine learning models can predict the type of a pokemon to a reasonable accuracy.

Packages and Data

First, we need to load in the packages and Pokemon data.

# Loading all necessary packages
library(readr)
library(magrittr)
library(dplyr)
library(janitor)
library(ggplot2)
library(corrplot)
library(ggbeeswarm)
library(rsample)
library(caret)
library(tidymodels)
library(tidyverse)
library(vip)
tidymodels_prefer()


knitr::opts_chunk$set(   # basic chunk settings
    echo = TRUE,
    fig.height = 10,
    fig.width = 10,
    tidy = TRUE,
    tidy.opts = list(width.cutoff = 60)
)

# Data is assigned to a variable
pokemon_data <- read_csv("pokemon.csv")

#Dimensions of dataset
dim(pokemon_data)
## [1] 801  41
# Print first few rows of the data
head(pokemon_data)

The data is from the Kaggle data set “The Complete Pokemon Dataset,” and it was scraped from Serebii by user Rounak Banik.

Cleaning the data

From the dataset, there are 801 observations and 41 columns. Since we are trying to predict type_1, this leaves 40 predictor variables (39 excluding type2 as well), so we will have to reduce the number of predictors.

The type effectiveness columns (against_bug, against_dark) all are directly correlated to the pokemon’s type, so we have to remove those 18 columns. There is no need to keep columns of the Japanese name of the pokemon and the pokemon’s pokedex number because we already have a unique identifier in the English name of the pokemon, leaving us with 20 columns left.

pokemon_data2 <- pokemon_data %>% 
  select(c("abilities", "attack","base_egg_steps", "base_happiness",  "base_total", "capture_rate", "classfication","defense", "experience_growth", "height_m", "hp", "name", "sp_attack", "sp_defense", "speed", "type1", "type2", "weight_kg", "generation", "is_legendary")) %>% 
  clean_names()

colnames(pokemon_data2)
##  [1] "abilities"         "attack"            "base_egg_steps"   
##  [4] "base_happiness"    "base_total"        "capture_rate"     
##  [7] "classfication"     "defense"           "experience_growth"
## [10] "height_m"          "hp"                "name"             
## [13] "sp_attack"         "sp_defense"        "speed"            
## [16] "type1"             "type2"             "weight_kg"        
## [19] "generation"        "is_legendary"

Data Tidying and Imputation

### Check for all missing values in all columns, fill them in
colSums(is.na(pokemon_data2))
##         abilities            attack    base_egg_steps    base_happiness 
##                 0                 0                 0                 0 
##        base_total      capture_rate     classfication           defense 
##                 0                 0                 0                 0 
## experience_growth          height_m                hp              name 
##                 0                20                 0                 0 
##         sp_attack        sp_defense             speed             type1 
##                 0                 0                 0                 0 
##             type2         weight_kg        generation      is_legendary 
##               384                20                 0                 0
# We see that a lot of values for type2 are missing, which is to be expected, since there are many Pokemon with only one type

#Replace NA in Pokemon types2 with "None"
pokemon_data2$type2 <- replace(pokemon_data2$type2, is.na(pokemon_data2$type2), "None")

# There are 20 missing values for height and weight, so we need to fill them in
pokemon_data2$name[which(is.na(pokemon_data2$height_m))]
##  [1] "Rattata"   "Raticate"  "Raichu"    "Sandshrew" "Sandslash" "Vulpix"   
##  [7] "Ninetales" "Diglett"   "Dugtrio"   "Meowth"    "Persian"   "Geodude"  
## [13] "Graveler"  "Golem"     "Grimer"    "Muk"       "Exeggutor" "Marowak"  
## [19] "Hoopa"     "Lycanroc"
pokemon_data2$name[which(is.na(pokemon_data2$weight_kg))]
##  [1] "Rattata"   "Raticate"  "Raichu"    "Sandshrew" "Sandslash" "Vulpix"   
##  [7] "Ninetales" "Diglett"   "Dugtrio"   "Meowth"    "Persian"   "Geodude"  
## [13] "Graveler"  "Golem"     "Grimer"    "Muk"       "Exeggutor" "Marowak"  
## [19] "Hoopa"     "Lycanroc"

All of the Pokemon with missing values in height and weight are the same, so we can impute their values through a simple Google search and fill in the values. Fun fact: All of these Pokemon have Alolan counterparts (except for Hoopa and Lycanroc, which have separate forms), which have differing physical characteristics and typing, so I chose to use values from their original form.

pokemon_data2[pokemon_data2$name == "Rattata", "height_m"] <- 0.3
pokemon_data2[pokemon_data2$name == "Rattata", "weight_kg"] <- 3.5

pokemon_data2[pokemon_data2$name == "Raticate", "height_m"] <- 0.7
pokemon_data2[pokemon_data2$name == "Raticate", "weight_kg"] <- 18.5

pokemon_data2[pokemon_data2$name == "Raichu", "height_m"] <- 0.8
pokemon_data2[pokemon_data2$name == "Raichu", "weight_kg"] <- 30.0

pokemon_data2[pokemon_data2$name == "Sandshrew", "height_m"] <- 0.6
pokemon_data2[pokemon_data2$name == "Sandshrew", "weight_kg"] <- 12

pokemon_data2[pokemon_data2$name == "Sandslash", "height_m"] <- 1.0
pokemon_data2[pokemon_data2$name == "Sandslash", "weight_kg"] <- 29.5

pokemon_data2[pokemon_data2$name == "Vulpix", "height_m"] <- 0.6
pokemon_data2[pokemon_data2$name == "Vulpix", "weight_kg"] <- 21.8

pokemon_data2[pokemon_data2$name == "Ninetales", "height_m"] <- 1.1
pokemon_data2[pokemon_data2$name == "Ninetales", "weight_kg"] <- 19.9

pokemon_data2[pokemon_data2$name == "Diglett", "height_m"] <- 0.2
pokemon_data2[pokemon_data2$name == "Diglett", "weight_kg"] <- 0.8

pokemon_data2[pokemon_data2$name == "Dugtrio", "height_m"] <- 0.7
pokemon_data2[pokemon_data2$name == "Dugtrio", "weight_kg"] <- 33.3

pokemon_data2[pokemon_data2$name == "Meowth", "height_m"] <- 0.4
pokemon_data2[pokemon_data2$name == "Meowth", "weight_kg"] <- 4.2

pokemon_data2[pokemon_data2$name == "Persian", "height_m"] <- 1.0
pokemon_data2[pokemon_data2$name == "Persian", "weight_kg"] <- 32.0

pokemon_data2[pokemon_data2$name == "Geodude", "height_m"] <- 0.4
pokemon_data2[pokemon_data2$name == "Geodude", "weight_kg"] <- 10.0

pokemon_data2[pokemon_data2$name == "Graveler", "height_m"] <- 1.0
pokemon_data2[pokemon_data2$name == "Graveler", "weight_kg"] <- 105.0

pokemon_data2[pokemon_data2$name == "Golem", "height_m"] <- 1.4
pokemon_data2[pokemon_data2$name == "Golem", "weight_kg"] <- 300.0

pokemon_data2[pokemon_data2$name == "Grimer", "height_m"] <- 0.9
pokemon_data2[pokemon_data2$name == "Grimer", "weight_kg"] <- 30.0

pokemon_data2[pokemon_data2$name == "Muk", "height_m"] <- 1.2
pokemon_data2[pokemon_data2$name == "Muk", "weight_kg"] <- 30.0

pokemon_data2[pokemon_data2$name == "Exeggutor", "height_m"] <- 2.0
pokemon_data2[pokemon_data2$name == "Exeggutor", "weight_kg"] <- 120.0

pokemon_data2[pokemon_data2$name == "Marowak", "height_m"] <- 1.0
pokemon_data2[pokemon_data2$name == "Marowak", "weight_kg"] <- 45.0

pokemon_data2[pokemon_data2$name == "Hoopa", "height_m"] <- 0.5
pokemon_data2[pokemon_data2$name == "Hoopa", "weight_kg"] <- 9.0

pokemon_data2[pokemon_data2$name == "Lycanroc", "height_m"] <- 0.8
pokemon_data2[pokemon_data2$name == "Lycanroc", "weight_kg"] <- 25.0

Picture of Alolan Pokemon

Change the capture rate for one pokemon (Minior) so that capture_rate is numeric rather than character

str(pokemon_data2$capture_rate)
##  chr [1:801] "45" "45" "45" "45" "45" "45" "45" "45" "45" "255" "120" "45" ...
pokemon_data2[pokemon_data2$name == "Minior", "capture_rate"] <- "30"
pokemon_data2$capture_rate <- as.numeric(pokemon_data2$capture_rate)
str(pokemon_data2$capture_rate)
##  num [1:801] 45 45 45 45 45 45 45 45 45 255 ...

Exploratory Data Analysis

Distribution of Type 1

In this graph, we see a bar plot of the distribution of the types of all the Pokemon in the data set. Some things to note is that water, normal and bug tend to be the most common types, as type 1, and fairy and flying tend to be the least common types, as type 1. I added a color scheme for each of the types for their corresponding colors so that they are more fun to look at!

# Define the color scheme
color_scheme <- c("#A8B820", "#705848", "#7038F8", "#F8D030", "#EE99AC", "#C03028", "#F08030", "#A890F0", "#705898", "#78C850", "#E0C068", "#98D8D8", "#A8A878", "#A040A0", "#F85888", "#B8A038", "#B8B8D0", "#6890F0")

ggplot(pokemon_data, aes(type1)) +
  geom_bar(fill = color_scheme) +
  labs(
    title = "Distribution of Type 1"  ) +
  coord_flip()

Distribution of Pokemon Type 1 vs. Base Total

In this graph, we see a scatter plot of the base totals for each pokemon for each type. I arranged it from least to greatest by mean to see which types of pokemon tend to have the greatest base stat total. We see that the bug type has the lowest average, which makes sense, since bugs tend to be pokemon that reach their final evolution sooner but are weaker overall as a result. Unsurprisingly, dragon has the highest base total average. Dragon pokemon tend to be associated with power and are rarer and there are more legendary dragon pokemon than other types, so it makes sense why the dragon type would have the highest base total average.

One thing to note is that I used only type1, so inferences about type 2 cannot be made. The flying type has one of the higher averages for type 1, but this is only based on 3 observations, since very few pokemon have their type 1 as a flying type and a large number of pokemon have their type 2 as a flying type.

types_ordered <- c("bug", "dark", "dragon", "electric", "fairy", "fighting", "fire", "flying", "ghost", "grass", "ground", "ice", "normal", "poision", "psychic", "rock", "steel", "water")

# Create a named vector where names are the types and values are the colors
type_colors <- setNames(color_scheme, types_ordered)

ggplot(pokemon_data, aes(x=reorder(type1, base_total, FUN = mean), y=base_total, color=type1)) + 
  geom_point() +
  stat_summary(fun = mean, geom = "line", aes(group = 1), color = "black") +
  xlab("Pokemon Type") + 
  ylab("Base Total") + 
  ggtitle("Scatter Plot of Pokemon Types over Base Totals") +
  scale_color_manual(values = type_colors)

Distribution of Type 1 and Type 2

In this graph, we see the distribution of type 2 across each of the individual type 1s. Some interesting patterns emerge, such that normal and flying is a very common combination of types, as well as grass and poison. psychic and fairy, and bug and flying.

As a side note, I made all of the bar plots horizontally aligned so that it would be easier to read the type name than if it were on the x-axis.

ggplot(pokemon_data, aes(x = type1, fill = type2)) +
  geom_bar() +
  scale_fill_manual(values = c("#A8B820", "#705848", "#7038F8", "#F8D030", "#EE99AC", "#C03028", "#F08030", "#A890F0", "#705898", "#78C850", "#E0C068", "#98D8D8", "#A8A878", "#A040A0", "#F85888", "#B8A038", "#B8B8D0", "#6890F0")) +
  labs(title = "Distribution of Type 1 with Type 2") + 
  scale_y_continuous(breaks=seq(0,115,10)) +
  coord_flip()

Correlation plot

In this plot we can see the correlation between all of the predictor variables with each other. I used hclust so that the more obvious patterns can be easily seen. A couple of things that I found interesting was that height and weight are moderately correlated with each other, as well as with legendary status, which does make sense, since legendary pokemon tend to be bigger than normal pokemon. I also found it surprising that the correlation between legendary status and base stat total is only 0.49; I was expecting it to be larger than that.

# Selecting only numeric values
pokemon_numeric_data <- pokemon_data2 %>%
  select_if(is.numeric)

# Correlation matrix
pokemon_cor <- cor(pokemon_numeric_data)


# Visualization of correlation matrix
pokemon_corrplot <- corrplot(pokemon_cor, method = "circle", addCoef.col = 1, number.cex = 0.7, order = 'hclust')

Pokemon type correlation with legendary status

In this graph, we can see the distibution of legendary pokemon by type. We see that a lot of legendary pokemon have a dual type, rather than a single type, and interestingly there is a very high number of psychic legendary pokemon, especially without a secondary type. This could be due to the fact that the psychic type is connected to concepts of power of the mind and intelligence/ spirituality, which are ideas that are also commonly associatef with legendary pokemon.

#Subset by legendary
pokemon_legendary <- subset(pokemon_data2, is_legendary == 1)


pokemon_legendary %>%
  ggplot(aes(x = type1, y = type2)) +
  geom_count(aes(size = ..n.., color = ..n..)) +
  scale_size_continuous(range = c(1, 10)) +
  scale_color_gradient(low = "blue", high = "red") +
  labs(title = "Legendary Pokemon by Type")
## Warning: The dot-dot notation (`..n..`) was deprecated in ggplot2 3.4.0.
## i Please use `after_stat(n)` instead.

Setting Up Models

Now we need to start setting up our models. First we need to split the data, then create the recipe based on the predictors we have, and then establish folds for k-fold cross validation.

Splitting the Data

Before we fit the model, we need to split our data into a training data set and a testing data set. The testing data set will be used to test the effectiveness on the model, based on the results from the training data set. The testing data remains untouched so that we have a good estimate on the model’s performance based on its results from the training data set. An advantage by splitting the data into training and testing data sets is that we can avoid overfitting, because the model isn’t being trained on all of the available data. I have chosen a 75/25 split, which means that 75% of the data is in the training data set, and 25% is in the testing data set. In addition, both the training and testing data sets are stratified on type1 so that there is an equal distribution in both data sets.

# Setting the seed for reproducibility
set.seed(565667)

# Splitting the data (75/25 split, stratify on type1)
pokemon_split <- initial_split(pokemon_data2, prop = 0.75, strata = type1)
pokemon_train <- training(pokemon_split)
pokemon_test <- testing(pokemon_split)


#Checking if the data was split correctly

nrow(pokemon_train)/nrow(pokemon_data)
## [1] 0.7490637
nrow(pokemon_test)/nrow(pokemon_data)
## [1] 0.2509363

Recipe Creation

Since we are using the exact same, predictors, outcome variable and conditions for each model, we need to have one general recipe for all of our models.

Out of the the remaining 20 remaining variables, we’ll be selecting only 9 of them as predictors: base_egg_steps, base_total, base_happiness, capture_rate, experience_growth, height_m, weight_kg, generation, is_legendary. 6 of the other predictors hp, attack, defense, sp_attack, sp_defense and speed are all directly correlating to base_total since base_total is the sum of them, so we don’t need to include them.

# Creating recipe
pokemon_recipe <- recipe(type1 ~ base_egg_steps + base_total + base_happiness + capture_rate + experience_growth + height_m + weight_kg + generation + is_legendary, data = pokemon_train) %>% 
  # Convert variables to factors
  step_mutate(generation = as.factor(generation),
              is_legendary = as.factor(is_legendary)) %>%
  # Dummy coding nominal variables
  step_dummy(generation, is_legendary) %>% 
  # Normalizing
  step_center(all_predictors()) %>% 
  step_scale(all_predictors())

K-Fold Cross Validation

For K-Fold Cross Validation, we’ll be selecting 10 folds, which means that the data from the training data set will be placed into one of 10 folds. And for each fold, a testing will be created that is composed of that specific fold and the remaining folds.

Performance is measured from taking the average accuracy across the testing set of each fold. The advantage of k-fold cross validation is that it gives a better estimate of the testing accuracy than just one sample, and thus variation is reduced.

Basically, the model is “practiced” on a portion of the data, then “tested” on a different portion. By doing this multiple times (the “K” folds), we can get a more reliable assessment of the level of performance of the model.

pokemon_folds <- vfold_cv(pokemon_train, v = 10, strata = type1)

Model Creation

For creating the models, we’ll be using 4 different ones: LDA, KNN, Decision Tree, and Random Forest. The results from each of the models has been saved from an RDA file and then loaded in below in order to save time. I decided to choose accuracy as the primary measure for evaluating the performance for my model, and ROC as the secondary measure, because they aren’t as efficient in a multilabel classification model.

Steps to Create the Models

The steps to create the models is outlined as such:

  1. First, we set up the model by specifying what type it is, the engine it has, as well as its mode (which is classification in this case)

  2. Then, we set up the workflow, add the new model, and add the pokemon recipe we created earlier

We will skip the steps 3-5 for LDA, as it does not requre any hyperparameters

  1. Then we set up the tuning grid with the parameters that need to be tuned, as well as the different levels for tuning.

  2. We tune the model the the parameters in mind

  3. After tuning the model, we select the most accurate model and finalize the worflow using the parameters.

  4. We then fit the model with our workflow to the training data set.

  5. Lastly, we save our results to an RDA file so that we’re able to load it back.

Loading Data From the Models

load("C:/Users/pramu_v8voor1/OneDrive/Documents/Pokemon Project/Pokemon_Linear_Discriminant.rda")
load("C:/Users/pramu_v8voor1/OneDrive/Documents/Pokemon Project/Pokemon_KNN.rda")
load("C:/Users/pramu_v8voor1/OneDrive/Documents/Pokemon Project/Pokemon_Decision_Tree.rda")
load("C:/Users/pramu_v8voor1/OneDrive/Documents/Pokemon Project/Pokemon_Random_Forest.rda")

Model Results

pokemon_accuracy <- c(rf_best_accuracy["mean"]$mean,
                      dt_best_accuracy["mean"]$mean,
                      knn_best_accuracy["mean"]$mean,
                      lda_best_accuracy["mean"]$mean)

pokemon_roc_aucs <- c(rf_roc["mean"]$mean,
                     dt_roc["mean"]$mean,
                     knn_roc["mean"]$mean,
                     lda_roc["mean"]$mean)

pokemon_mod_names <- c("Random Forest",
                      "Decision Tree",
                      "KNN",
                      "LDA")



pokemon_results_tibble <- tibble(Model = pokemon_mod_names,
                          Accuracy = pokemon_accuracy, ROC_AUC = pokemon_roc_aucs)


pokemon_results_tibble

Model Autoplots

Decision Tree Autoplot

From the decision tree autoplot, we can see that accuracy peaks. around the cost-complexity parameter of 0.005, and then gradually decreases. And for its roc, it peaks around 0.008 and then decreases at a slightly larger rate than accuracy

autoplot(pokemon_dt_tune_res)

KNN Autoplot

In the KNN autoplot, interestingly, the highest accuracy is attained when the the number of nearest neighbors is the lowest, at 1. And from there, the accuracy only gets lower. And for the roc, it suddenly increases from 1 nearest neighbors to 2, and then it slightly goes down as the number of nearest neighbors increases.

autoplot(pokemon_knn_tune)

Random Forest Autoplot

In the Random Forest autoplot, we see that as the number of trees increases, the roc_auc score increases as well, for node sizes of both 2 and 3. There doesn’t appear to be an increase in roc_auc given an increase in the number of predictors. The tree with the best roc_auc score has 18 trees, the maximum amount, but only 1 predictor.

autoplot(pokemon_rf_tune_res_auc)

Results of the Best Model

Random Forest Model #118 performed the best out of all of the models, specifically with 13 predictors, 11 trees and a minimal node size of 3 ended up having 0.28 accuracy.

rf_best_accuracy <- collect_metrics(pokemon_rf_tune_res) %>% 
  arrange(mean) %>% 
  slice(128)

rf_best_accuracy

Fitting to the training data

Here we take the best model and fit it to the entire training data set. Once it has been fit, then it should be ready for testing.

rf_final_workflow <- finalize_workflow(pokemon_rand_forest_wf, rf_best_accuracy)
rf_final_fit_train <- fit(rf_final_workflow, data = pokemon_train)

Testing the Model

Comparing Predicted vs. Actual

# Creating the predicted vs. actual value type values
pokemon_pred_test <- predict(rf_final_fit_train, new_data = pokemon_test %>% select(-type1))
pokemon_pred_test <- bind_cols(pokemon_pred_test, pokemon_test %>% select(type1))

pokemon_pred_test$type1 <- as.factor(pokemon_pred_test$type1)

pokemon_pred_test
#Removed flying from pred_class because it wasn't in the type1 column
pokemon_pred_test_filtered <- pokemon_pred_test %>% 
  filter(.pred_class != "flying") %>% 
  droplevels()

pokemon_metric <- metric_set(accuracy)

pokemon_pred_accuracy <- pokemon_metric(pokemon_pred_test_filtered, truth = type1, estimate = .pred_class)
pokemon_pred_accuracy

From testing the model, we see that it predicts an accuracy of 0.25 or around 25%, which is less than the value from the training data set which was around 28%. This is to be expected, since there may have been some level of over fitting in the training data set.

Variable Importance Plot

The Variable Importance Plot (VIP) shows us which predictor variables were the most important in predicting type1 in our random forest model.

rf_final_fit_train %>% 
  extract_fit_engine() %>% 
  vip(aesthetics = list(fill = "blue", color = "black"))

Interestingly, the most important predictor was weight, which I did not expect at all. It does make sense why weight would be an important predictor, because Pokemon of different types do have different physical characteristics. Pokemon that are rock, ground or steel types tend to be a lot heavier and Pokemon that are fairy, bug or ghost tend to be lighter. Base_total is second most important predictor, which I thought would be the most important predictor for the reasons outlined in the scatter plot of base totals for each Pokemon type above. And the third highest predictor is height, which I would assume is one of the top predictors for similar reasons due to weight, in that Pokemon of certain types tend to be taller or shorter relative to others.

Conclusion

In our effort to predict the primary type of Pokemon based on various features, we developed and tested four machine learning models: Linear Discriminant Analysis (LDA), K-Nearest Neighbors (KNN), Decision Tree, and Random Forest. Among these, the Random Forest model emerged as the top performer, with accuracy of 28.2% and a ROC_AUC score of 0.69. Though the accuracy may seem low, it’s definitely higher than random guessing which would yield around 5.6% accuracy (1 in 18).

Overall, I am not surprised that Random Forest was the top performer. Random Forest’s strength lies within its ability to handle numerous features through the creation of multiple decision trees and aggregating its results. This makes it good for dealing with Pokemon data that contains many different types of information and makes it a reliable and robust model.

The Random Forest model still presented some surprises. For instance, the best model had 13 predictors, 11 trees, and a minimal node size of 3, which wasn’t an anticipated combination. The number of trees used was notably fewer than the maximum tested (18 trees), demonstrating that a greater number of trees does not always equate to better model performance, specifically for predicted Pokemon types.

The Decision Tree model also performed well, with an accuracy of 25.5% and a ROC_AUC score of 0.71. However, the LDA and KNN models were less successful, with the LDA model producing an accuracy of 16.5% and a ROC_AUC score of 0.66, and the KNN model only achieving an accuracy of 17.8% and a ROC_AUC score of 0.57.

For future steps, we could try to improve our models by adding more features or refining our selection of predictors. One such way I was thinking would be to use the actual images of Pokemon as predictors for types, which I feel would be a major improvement to the model, since the colors of a Pokemon are typically a strong indicator of what their type is.

We could also consider other modeling approaches, such as support vector machines or neural networks. One such possible improvement would be to use ensemble methods, where we combine predictions from multiple models to obtain better predictive performance than could be obtained from any of the models alone.

In general, my findings illustrate that machine learning models can indeed predict Pokemon types to a reasonable degree of accuracy, given a suitable set of data. The project was a fascinating exploration of the application of these models in the context of Pokemon, offering insights that could be applicable in a broad range of other domains. I definitely had a lot of fun working on this project and it was amazing to see machine learning in action!

Sources

The data is from the Kaggle data set “The Complete Pokemon Dataset,” and it was scraped from Serebii by user Rounak Banik.

The information about missing height and weight values from the 20 Pokemon were found here: Pokemondb.net.