Introduction

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:

View the Shiny App »

Load Pokémon Data

# 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)

Data Cleaning and Preparation

# 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
    )
  )

Summary of Pokémon by Type

# 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")
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).

Step 1: Summary Stats by Type

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

Step 2: Identify Top Pokémon per Type

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

Step 3: Combine Summaries

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

Fixing: Ensure All Images Are Valid

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.

Step 4: Plotting Average Speed by Type with Images

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.

Interactive Plotly

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.

5 Average Attack by Type

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

Interactive Plot

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.

Top 10 Pokémon by Total Stats

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

Legendary Pokémon by Generation

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.

Conclusion

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!