In this analysis, I examine the April 1, 2025 Pokémon dataset from TidyTuesday and explore how different Pokémon types compare in key stats, especially Speed and Attack. Along the way, I highlight the fastest and strongest Pokémon of each type using sprite images, polished tables, and fun trivia.
I also created an interactive Shiny app to explore these metrics dynamically:
# Load TidyTuesday data
pokemon_data <- tidytuesdayR::tt_load(2025, week = 13)
## ---- Compiling #TidyTuesday Information for 2025-04-01 ----
## --- There is 1 file available ---
##
##
## ── Downloading files ───────────────────────────────────────────────────────────
##
## 1 of 1: "pokemon_df.csv"
pokemon_data <- pokemon_data$pokemon_df
# Clean column names
pokemon_data <- janitor::clean_names(pokemon_data)
# 1. # Compute total stats and Add Legendary flag
pokemon_df <- pokemon_data %>%
mutate(
total = hp + attack + defense + special_attack + special_defense + speed, legendary = ifelse(total > 600, "Legendary", "Not Legendary")
)
# 3. Fix missing or broken image URLs:
fallback_img <- "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png"
pokemon_df <- pokemon_df %>%
mutate(
url_image = ifelse(
is.na(url_image) | url_image == "" | !grepl("^https?://", url_image),
fallback_img,
url_image
)
)
# Count the number of Pokémon by their primary type
type_summary <- pokemon_data %>%
count(type_1, sort = TRUE)
kable(type_summary, caption = "Pokémon Count by Primary Type")
| type_1 | n |
|---|---|
| water | 126 |
| normal | 111 |
| grass | 84 |
| bug | 79 |
| rock | 65 |
| psychic | 64 |
| electric | 61 |
| fire | 59 |
| ghost | 40 |
| dragon | 39 |
| dark | 37 |
| ground | 36 |
| poison | 35 |
| fighting | 31 |
| steel | 30 |
| ice | 29 |
| fairy | 19 |
| flying | 4 |
Water‑types are the most common (126), followed by Normal (111) and Grass (84), while Flying‑only Pokémon are the rarest (4).
# Compute average stats per primary type
pokemon_summary <- pokemon_df %>%
group_by(type_1) %>%
summarise(
avg_speed = mean(speed, na.rm = TRUE),
avg_attack = mean(attack, na.rm = TRUE),
avg_height = mean(height, na.rm = TRUE),
avg_weight = mean(weight, na.rm = TRUE),
.groups = 'drop'
)
pokemon_summary
## # A tibble: 18 × 5
## type_1 avg_speed avg_attack avg_height avg_weight
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 bug 63.3 72.5 0.937 35.8
## 2 dark 76.6 84.7 1.22 65.8
## 3 dragon 82.9 109. 2.37 140.
## 4 electric 87.2 68.1 0.851 28.5
## 5 fairy 53.6 61.2 0.763 22.4
## 6 fighting 67.6 99.1 1.19 56.6
## 7 fire 73.5 84.3 1.19 68.6
## 8 flying 102. 78.8 1.23 54.8
## 9 ghost 65.6 76.3 1.22 66.1
## 10 grass 60.3 75.2 1.12 42.3
## 11 ground 64.6 96.2 1.30 147.
## 12 ice 64.6 73.1 1.17 98.3
## 13 normal 70.3 75.9 1.04 45.0
## 14 poison 64.2 73.5 1.16 35.7
## 15 psychic 80.9 72.5 1.2 62.3
## 16 rock 64.3 90.0 1.09 82.1
## 17 steel 56.1 93.1 2.13 226.
## 18 water 65.7 74.7 1.46 57.4
Dragon‑types boast the highest average Attack (~108.7), and Flying‑types dominate in average highest Speed (~102.5), whereas Fairy‑types sit at the bottom for both stats (≈61 Attack, ≈53.6 Speed) and Steel‑types are the heaviest (~225.6 kg).
# Using species_id to build sprite URLs
fastest_pokemon <- pokemon_df %>%
filter(!is.na(speed)) %>%
group_by(type_1) %>%
slice_max(order_by = speed, n = 1, with_ties = FALSE) %>%
select(type_1, pokemon, speed, url_image, species_id) %>%
ungroup()
fastest_pokemon
## # A tibble: 18 × 5
## type_1 pokemon speed url_image species_id
## <chr> <chr> <dbl> <chr> <dbl>
## 1 bug ninjask 160 https://raw.githubusercontent.com/… 291
## 2 dark weavile 125 https://raw.githubusercontent.com/… 461
## 3 dragon salamence-mega 120 https://raw.githubusercontent.com/… 373
## 4 electric electrode 150 https://raw.githubusercontent.com/… 101
## 5 fairy comfey 100 https://raw.githubusercontent.com/… 764
## 6 fighting marshadow 125 https://raw.githubusercontent.com/… 802
## 7 fire talonflame 126 https://raw.githubusercontent.com/… 663
## 8 flying noivern 123 https://raw.githubusercontent.com/… 715
## 9 ghost gengar-mega 130 https://raw.githubusercontent.com/… 94
## 10 grass sceptile-mega 145 https://raw.githubusercontent.com/… 254
## 11 ground dugtrio 120 https://raw.githubusercontent.com/… 51
## 12 ice froslass 110 https://raw.githubusercontent.com/… 478
## 13 normal lopunny-mega 135 https://raw.githubusercontent.com/… 428
## 14 poison crobat 130 https://raw.githubusercontent.com/… 169
## 15 psychic deoxys-speed 180 https://raw.githubusercontent.com/… 386
## 16 rock aerodactyl-mega 150 https://raw.githubusercontent.com/… 142
## 17 steel metagross-mega 110 https://raw.githubusercontent.com/… 376
## 18 water greninja-ash 132 https://raw.githubusercontent.com/… 658
Deoxys‑Speed tops all types with a Speed of 180, Ninjask rules Bug‑types at 160, and Electrode is the fastest Electric‑type at 150.
# Merge summary stats with fastest Pokémon info
pokemon_type_stats <- pokemon_summary %>%
left_join(fastest_pokemon, by = "type_1") %>%
rename(
fastest = pokemon,
fastest_sp = speed,
img = url_image
)
This combined table brings together each type’s average Speed and the single fastest Pokémon, making it easy to compare overall trends against individual standouts.
library(curl)
# Define fallback
fallback_img <- "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png"
# Create zero‑padded IDs
pokemon_type_stats <- pokemon_type_stats %>%
mutate(
sprite = sprintf("%03d", species_id),
sprite_url = paste0(
"https://raw.githubusercontent.com/HybridShivam/Pokemon/master/assets/images/",
sprite, ".png"
)
)
# Validate & fallback to Poké Ball
library(curl)
fallback <- "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/items/poke-ball.png"
valid <- map_lgl(pokemon_type_stats$sprite_url,
~ tryCatch(curl_fetch_memory(.x)$status_code == 200,
error = function(e) FALSE))
pokemon_type_stats <- pokemon_type_stats %>%
mutate(img = if_else(valid, sprite_url, fallback))
Needed accurate Pokemon images and make sure they are valid.
Generating Real Sprite URLs by species_id
library(ggplot2); library(ggimage)
ggplot(pokemon_type_stats,
aes(y = reorder(type_1, avg_speed), x = avg_speed)) +
geom_col(width = 0.6, fill = "#FFCB05", color = "#3B4CCA") +
geom_image(aes(image = img),
by = "height", size = 0.08,
nudge_x = max(pokemon_type_stats$avg_speed) * 0.02) +
geom_text(aes(label = fastest),
hjust = 0,
nudge_x = max(pokemon_type_stats$avg_speed) * 0.07,
size = 4) +
scale_x_continuous(expand = expansion(mult = c(0, 0.3))) +
coord_cartesian(clip = "off") +
labs(
title = "Pokémon Types by Average Speed",
subtitle = "Sprites: Fastest Pokémon per Type",
x = "Average Speed",
y = "Type"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(face = "bold", size = 20),
plot.subtitle = element_text(size = 16, color = "#555555"),
axis.title = element_text(face = "bold", size = 14),
axis.text = element_text(size = 12),
plot.margin = margin(20, 50, 20, 20)
)
In this bar chart above, Flying and Electric types (Noivern, Electrode) lead in average Speed, while Fairy‑types (Comfey) trail, with each bar labeled and annotated by the corresponding sprite.
pokemon_type_stats$hover_text <- paste(
"Type:", pokemon_type_stats$type_1,
"<br>Avg Speed:", pokemon_type_stats$avg_speed,
"<br>Fastest Pokémon:", pokemon_type_stats$fastest
)
p <- ggplot(pokemon_type_stats, aes(x = reorder(type_1, avg_speed), y = avg_speed)) +
geom_col(aes(fill = type_1, text = hover_text), color = "#3B4CCA") +
coord_flip() +
theme_minimal() + theme_pokemon() +
labs(
title = "Pokémon Types Ranked by Average Speed",
subtitle = "Hover to see Pokémon details",
x = "Primary Type",
y = "Average Speed"
)
## Warning in geom_col(aes(fill = type_1, text = hover_text), color = "#3B4CCA"):
## Ignoring unknown aesthetics: text
# Convert to interactive plot
ggplotly(p, tooltip = "text")
This interactive plotly version allows us hover over each type’s bar to see the exact average Speed and fastest Pokémon name.
# Identify strongest attackers
strongest_attackers <- pokemon_data %>%
group_by(type_1) %>%
filter(attack == max(attack, na.rm = TRUE)) %>%
slice(1) %>%
ungroup() %>%
select(type_1, species_id, strongest = pokemon)
pokemon_type_stats <- pokemon_type_stats %>%
left_join(strongest_attackers, by = "type_1")
# Join the strongest attackers to the pokemon_type_stats
pokemon_type_stats <- pokemon_type_stats %>%
left_join(strongest_attackers, by = "type_1") %>%
mutate(
img = paste0(
"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/",
species_id.y, ".png" # Use species_id.y or rename it appropriately
),
strongest = ifelse(is.na(strongest.y), "Unknown", strongest.y) # Fill missing values if any
) %>%
select(-contains(".x")) # Remove any columns that have ".x" suffix (duplicates)
# Plot
ggplot(pokemon_type_stats, aes(y = reorder(type_1, avg_attack), x = avg_attack)) +
geom_col(width = 0.6, fill = "#FF7F50", color = "#3B4CCA") +
geom_image(aes(image = img), by = "height", size = 0.08, nudge_x = max(pokemon_type_stats$avg_attack) * 0.02) +
geom_text(aes(label = strongest), hjust = 0, nudge_x = max(pokemon_type_stats$avg_attack) * 0.07, size = 4) +
scale_x_continuous(expand = expansion(mult = c(0, 0.3))) +
coord_cartesian(clip = "off") +
labs(
title = "Pokémon Types by Attack",
subtitle = "Sprites: Strongest Pokémon per Attack & Type",
x = "Average Attack",
y = "Type"
) +
theme_minimal(base_size = 15) +
theme(
plot.title = element_text(face = "bold", size = 20),
plot.subtitle = element_text(size = 16, color = "#555555"),
axis.title = element_text(face = "bold", size = 14),
axis.text = element_text(size = 12),
plot.margin = margin(20, 50, 20, 20)
)
Dragon and Fighting types (Rayquaza‑Mega, Lucario‑Mega) top in average Attack, while Fairy‑types (Xerneas) come in last, again highlighted by each type’s strongest attacker and sprite.
library(plotly)
plot_ly(
data = pokemon_type_stats,
x = ~avg_attack,
y = ~reorder(type_1, avg_attack),
type = 'bar',
orientation = 'h',
marker = list(color = "#FF7F50", line = list(color = "#3B4CCA", width = 1)),
text = ~paste0(
"<b>Type:</b> ", type_1, "<br>",
"<b>Avg Attack:</b> ", round(avg_attack, 1), "<br>",
"<b>Fastest:</b> ", fastest
),
hoverinfo = 'text'
) %>%
layout(
title = "Pokémon Types Ranked by Average Attack",
xaxis = list(title = "Avg Attack"),
yaxis = list(title = "Type", categoryorder = "array", categoryarray = rev(unique(pokemon_type_stats$type_1))),
hoverlabel = list(align = "left")
)
Hover around the plot to reveal each type’s average Attack and its strongest Pokémon. Dragon types again stand out with the highest averages coming from Salamence-Mega as the fatest.
pokemon_data <- pokemon_data %>%
mutate(total = hp + attack + defense + special_attack + special_defense + speed)
top_10 <- pokemon_data %>%
arrange(desc(total)) %>%
select(pokemon, type_1, type_2, total) %>%
head(10)
kable(top_10, caption = "Top 10 Pokémon by Total Stats")
| pokemon | type_1 | type_2 | total |
|---|---|---|---|
| mewtwo-mega-x | psychic | fighting | 780 |
| mewtwo-mega-y | psychic | NA | 780 |
| rayquaza-mega | dragon | flying | 780 |
| kyogre-primal | water | NA | 770 |
| groudon-primal | ground | fire | 770 |
| arceus | normal | NA | 720 |
| zygarde-complete | dragon | ground | 708 |
| kyurem-black | dragon | ice | 700 |
| kyurem-white | dragon | ice | 700 |
| tyranitar-mega | rock | dark | 700 |
The Mega Evolutions of Mewtwo and Rayquaza top the charts (780), followed by Primal Kyogre and Groudon.
pokemon_data <- pokemon_data %>%
mutate(legendary = ifelse(total > 600, "Legendary", "Not Legendary"))
legendary_summary <- pokemon_data %>%
group_by(generation_id, legendary) %>%
summarize(count = n()) %>%
ungroup()
## `summarise()` has grouped output by 'generation_id'. You can override using the
## `.groups` argument.
# Plot
ggplot(legendary_summary, aes(x = factor(generation_id), y = count, fill = legendary)) +
geom_col(position = "dodge") +
labs(title = "Legendary Pokémon by Generation",
x = "Generation", y = "Count")
After flagging any Pokémon with total stats > 600 as “Legendary,” I counted them by generation. Excluding those with no generation_id (mostly form variants), Generation 4 leads with 5 legendaries, followed by Generation 3 (with 4) and Gen 5 (with 3). Generations 2, 6, and 7 each have 2, and Generation 1 has just 1. This shows that the bulk of “core” legendary designs appeared in Generations 3–5, with Generation 4 peaking in pure legend count.
library(ggplot2)
library(ggimage)
img_df <- pokemon_type_stats %>% select(type_1, fastest, img)
ggplot(img_df, aes(x = 1, y = type_1)) +
geom_image(aes(image = img), size = 0.1, by = "height", asp = 1) +
geom_text(aes(label = fastest),
nudge_x = 0.12,
hjust = 0,
size = 3) +
scale_y_discrete(
limits = rev(unique(img_df$type_1)),
expand = expansion(add = c(1, 1))
) +
scale_x_continuous(limits = c(0.8, 1.6), expand = c(0, 0)) +
coord_cartesian(clip = "off") +
theme_void() +
theme(
plot.title = element_text(face = "bold", size = 16, hjust = 0.5),
plot.margin = margin(20, 20, 20, 20)
) +
labs(title = "Fastest Pokémon per Type: Image & Name")
This grid arranges each type vertically, showing the sprite(image) and name of its single fastest Pokémon for an at‑a‑glance comparison.
This exploration of Pokémon stats by type revealed several fun patterns:
- Speed vs. Attack: Flying and Electric types lead
in speed, Dragon and Fighting types in attack.
- Stat Leaders: Mega Mewtwo and Rayquaza top overall
stats (>780), Primal Kyogre/Groudon next.
- Generation Trends: Generation 4 introduced the most
Legendaries; later gens taper off.
Flying and Electric types are the fastest, while Dragon types pack the strongest punches. Although Fairy types are gentle in nature, their performance stats reflect that. These patterns highlight how certain type‑stat synergies (e.g. Dragon/Fighting) dominate competitive play.
Thank you for exploring Pokémon stats with me!