Below is the code for the creating of the metric Spin_Plus which looks to normalize spin by pitch by year. Data was gathered from csv dumps from baseballsavant.com and expected spin/mean spin was calculated and manually entered into a dataframe.


library(baseballr)
library(dplyr)
library(ggplot2)

pitcher_data <- read.csv("C:\\Users\\james\\Downloads\\spin_stats.csv")

let’s add the spin table

mean_spin_data <- data.frame(
  pitch_type = c("ff", "sl", "ch", "cu", "si", "fc", "fs", "kn", "st", "sv", "fo", "sc"),
  "2015" = c(2208, 2080, 1728.9, 2245.7, 2124.7, 2192.2, 1460.7, 1359, NA, NA, 1994, NA),
  "2016" = c(2233.6, 2258.3, 1745.6, 2426.8, 2151.6, 2282.1, 1529.5, 1659, 2750, 2695, 1936, NA),
  "2017" = c(2236, 2355.9, 1700.9, 2476, 2163.4, 2315.8, 1556.2, 1530, 2683.3, 2270, NA, NA),
  "2018" = c(2234.9, 2362.9, 1745, 2440, 2139.5, 2300.4, 1542.5, NA, 2690, 2405, NA, 1116),
  "2019" = c(2260, 2388.3, 1797.7, 2454, 2150.7, 2316.1, 1522.5, NA, 2684, 2617, NA, 1126),
  "2020" = c(2278.3, 2449.1, 1574.8, 2530.9, 2190.2, 2374.7, 1572.6, NA, 2619, 2552, NA, NA),
  "2021" = c(2245.5, 2386.3, 1717.5, 2445.7, 2126.2, 2322.8, 1493.5, NA, 2477.3, 2532, NA, NA),
  "2022" = c(2286.4, 2366.3, 1697.1, 2472.1, 2126.2, 2339.6, 1520.8, NA, 2484.5, 2731, NA, NA),
  "2023" = c(2270.9, 2391.2, 1759.7, 2498.7, 2171.5, 2366.1, 1470.6, NA, 2492.9, 2482, NA, NA),
  "2024" = c(2265.9, 2380.4, 1774.5, 2497.9, 2167.5, 2394.8, 1370.5, 199.5, 2517.6, 2559, NA, NA),
  "2025" = c(2285.6, 2354, 1658, 2545.7, 2194, 2374.8, 1348.2, 2136, NA, NA, NA, NA),
  check.names = FALSE
)

let’s reformat

mean_spin_long <- mean_spin_data %>%
  tidyr::pivot_longer(cols = starts_with("20"), names_to = "year", values_to = "expected_spin_rate") %>%
  filter(!is.na(expected_spin_rate)) %>%
  mutate(pitch_type = tolower(pitch_type))

let’s prepare the data


# Reshape pitcher table into long format
pitcher_long <- pitcher_data %>%
  pivot_longer(
    cols = ends_with("_avg_spin"),  # Select columns like ff_avg_spin, sl_avg_spin, etc.
    names_to = "pitch_type",
    values_to = "observed_spin_rate"
  ) %>%
  mutate(
    pitch_type = sub("_avg_spin", "", pitch_type),  # Remove "_avg_spin" to get pitch type (e.g., "ff")
    pitch_type = tolower(pitch_type),
    year = as.character(year)
  ) %>%
  filter(!is.na(observed_spin_rate))  # Remove rows with NA spin rates

# Merge with mean spin table and calculate Spin+
pitcher_spin_plus <- pitcher_long %>%
  left_join(mean_spin_long, by = c("pitch_type", "year")) %>%
  mutate(spin_plus = (observed_spin_rate / expected_spin_rate) * 100) %>%
  filter(!is.na(spin_plus))  # Remove rows where Spin+ couldn't be calculated

let’s make it wide form again

filtered_table <- pitcher_spin_plus %>%
  select(
    last_name..first_name, year, pitch_type, observed_spin_rate, expected_spin_rate, spin_plus
  )
# Loop through each year to convert the corresponding table to wide format
for (yr in unique_years) {
  # Get the long-format table for the current year
  long_table <- get(paste0("table_", yr), envir = .GlobalEnv)
  
  # Convert to wide format
  wide_table <- long_table %>%
    # Select relevant columns (we only need pitcher, year, and spin_plus for the pivot)
    select(last_name..first_name, year, pitch_type, spin_plus) %>%
    # Pivot to wide format: create columns like ff_spin_plus, sl_spin_plus, etc.
    pivot_wider(
      names_from = pitch_type,
      values_from = spin_plus,
      names_prefix = "pitch_type_",  # Adds prefix to pitch type (e.g., pitch_type_ff)
      names_glue = "{pitch_type}_spin_plus"  # More explicit: creates ff_spin_plus, sl_spin_plus
    )
  
  # Create a permanent wide-format table with the name "wide_table_YEAR"
  assign(paste0("wide_table_", yr), wide_table, envir = .GlobalEnv)
}

wide_table_2025 <- wide_table_2025 %>%
  arrange(desc(ff_spin_plus))

print(wide_table_2025)
LS0tDQp0aXRsZTogIlNwaW5fUGx1cyBNZXRyaWMiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpCZWxvdyBpcyB0aGUgY29kZSBmb3IgdGhlIGNyZWF0aW5nIG9mIHRoZSBtZXRyaWMgU3Bpbl9QbHVzIHdoaWNoIGxvb2tzIHRvIG5vcm1hbGl6ZSBzcGluIGJ5IHBpdGNoIGJ5IHllYXIuIERhdGEgd2FzIGdhdGhlcmVkIGZyb20gY3N2IGR1bXBzIGZyb20gYmFzZWJhbGxzYXZhbnQuY29tIGFuZCBleHBlY3RlZCBzcGluL21lYW4gc3BpbiB3YXMgY2FsY3VsYXRlZCBhbmQgbWFudWFsbHkgZW50ZXJlZCBpbnRvIGEgZGF0YWZyYW1lLiANCg0KYGBge3J9DQoNCmxpYnJhcnkoYmFzZWJhbGxyKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoZ2dwbG90MikNCg0KcGl0Y2hlcl9kYXRhIDwtIHJlYWQuY3N2KCJDOlxcVXNlcnNcXGphbWVzXFxEb3dubG9hZHNcXHNwaW5fc3RhdHMuY3N2IikNCmBgYA0KDQpsZXQncyBhZGQgdGhlIHNwaW4gdGFibGUNCg0KYGBge3J9DQptZWFuX3NwaW5fZGF0YSA8LSBkYXRhLmZyYW1lKA0KICBwaXRjaF90eXBlID0gYygiZmYiLCAic2wiLCAiY2giLCAiY3UiLCAic2kiLCAiZmMiLCAiZnMiLCAia24iLCAic3QiLCAic3YiLCAiZm8iLCAic2MiKSwNCiAgIjIwMTUiID0gYygyMjA4LCAyMDgwLCAxNzI4LjksIDIyNDUuNywgMjEyNC43LCAyMTkyLjIsIDE0NjAuNywgMTM1OSwgTkEsIE5BLCAxOTk0LCBOQSksDQogICIyMDE2IiA9IGMoMjIzMy42LCAyMjU4LjMsIDE3NDUuNiwgMjQyNi44LCAyMTUxLjYsIDIyODIuMSwgMTUyOS41LCAxNjU5LCAyNzUwLCAyNjk1LCAxOTM2LCBOQSksDQogICIyMDE3IiA9IGMoMjIzNiwgMjM1NS45LCAxNzAwLjksIDI0NzYsIDIxNjMuNCwgMjMxNS44LCAxNTU2LjIsIDE1MzAsIDI2ODMuMywgMjI3MCwgTkEsIE5BKSwNCiAgIjIwMTgiID0gYygyMjM0LjksIDIzNjIuOSwgMTc0NSwgMjQ0MCwgMjEzOS41LCAyMzAwLjQsIDE1NDIuNSwgTkEsIDI2OTAsIDI0MDUsIE5BLCAxMTE2KSwNCiAgIjIwMTkiID0gYygyMjYwLCAyMzg4LjMsIDE3OTcuNywgMjQ1NCwgMjE1MC43LCAyMzE2LjEsIDE1MjIuNSwgTkEsIDI2ODQsIDI2MTcsIE5BLCAxMTI2KSwNCiAgIjIwMjAiID0gYygyMjc4LjMsIDI0NDkuMSwgMTU3NC44LCAyNTMwLjksIDIxOTAuMiwgMjM3NC43LCAxNTcyLjYsIE5BLCAyNjE5LCAyNTUyLCBOQSwgTkEpLA0KICAiMjAyMSIgPSBjKDIyNDUuNSwgMjM4Ni4zLCAxNzE3LjUsIDI0NDUuNywgMjEyNi4yLCAyMzIyLjgsIDE0OTMuNSwgTkEsIDI0NzcuMywgMjUzMiwgTkEsIE5BKSwNCiAgIjIwMjIiID0gYygyMjg2LjQsIDIzNjYuMywgMTY5Ny4xLCAyNDcyLjEsIDIxMjYuMiwgMjMzOS42LCAxNTIwLjgsIE5BLCAyNDg0LjUsIDI3MzEsIE5BLCBOQSksDQogICIyMDIzIiA9IGMoMjI3MC45LCAyMzkxLjIsIDE3NTkuNywgMjQ5OC43LCAyMTcxLjUsIDIzNjYuMSwgMTQ3MC42LCBOQSwgMjQ5Mi45LCAyNDgyLCBOQSwgTkEpLA0KICAiMjAyNCIgPSBjKDIyNjUuOSwgMjM4MC40LCAxNzc0LjUsIDI0OTcuOSwgMjE2Ny41LCAyMzk0LjgsIDEzNzAuNSwgMTk5LjUsIDI1MTcuNiwgMjU1OSwgTkEsIE5BKSwNCiAgIjIwMjUiID0gYygyMjg1LjYsIDIzNTQsIDE2NTgsIDI1NDUuNywgMjE5NCwgMjM3NC44LCAxMzQ4LjIsIDIxMzYsIE5BLCBOQSwgTkEsIE5BKSwNCiAgY2hlY2submFtZXMgPSBGQUxTRQ0KKQ0KYGBgDQoNCmxldCdzIHJlZm9ybWF0DQpgYGB7cn0NCm1lYW5fc3Bpbl9sb25nIDwtIG1lYW5fc3Bpbl9kYXRhICU+JQ0KICB0aWR5cjo6cGl2b3RfbG9uZ2VyKGNvbHMgPSBzdGFydHNfd2l0aCgiMjAiKSwgbmFtZXNfdG8gPSAieWVhciIsIHZhbHVlc190byA9ICJleHBlY3RlZF9zcGluX3JhdGUiKSAlPiUNCiAgZmlsdGVyKCFpcy5uYShleHBlY3RlZF9zcGluX3JhdGUpKSAlPiUNCiAgbXV0YXRlKHBpdGNoX3R5cGUgPSB0b2xvd2VyKHBpdGNoX3R5cGUpKQ0KYGBgDQoNCmxldCdzIHByZXBhcmUgdGhlIGRhdGENCg0KYGBge3J9DQoNCiMgUmVzaGFwZSBwaXRjaGVyIHRhYmxlIGludG8gbG9uZyBmb3JtYXQNCnBpdGNoZXJfbG9uZyA8LSBwaXRjaGVyX2RhdGEgJT4lDQogIHBpdm90X2xvbmdlcigNCiAgICBjb2xzID0gZW5kc193aXRoKCJfYXZnX3NwaW4iKSwgICMgU2VsZWN0IGNvbHVtbnMgbGlrZSBmZl9hdmdfc3Bpbiwgc2xfYXZnX3NwaW4sIGV0Yy4NCiAgICBuYW1lc190byA9ICJwaXRjaF90eXBlIiwNCiAgICB2YWx1ZXNfdG8gPSAib2JzZXJ2ZWRfc3Bpbl9yYXRlIg0KICApICU+JQ0KICBtdXRhdGUoDQogICAgcGl0Y2hfdHlwZSA9IHN1YigiX2F2Z19zcGluIiwgIiIsIHBpdGNoX3R5cGUpLCAgIyBSZW1vdmUgIl9hdmdfc3BpbiIgdG8gZ2V0IHBpdGNoIHR5cGUgKGUuZy4sICJmZiIpDQogICAgcGl0Y2hfdHlwZSA9IHRvbG93ZXIocGl0Y2hfdHlwZSksDQogICAgeWVhciA9IGFzLmNoYXJhY3Rlcih5ZWFyKQ0KICApICU+JQ0KICBmaWx0ZXIoIWlzLm5hKG9ic2VydmVkX3NwaW5fcmF0ZSkpICAjIFJlbW92ZSByb3dzIHdpdGggTkEgc3BpbiByYXRlcw0KDQojIE1lcmdlIHdpdGggbWVhbiBzcGluIHRhYmxlIGFuZCBjYWxjdWxhdGUgU3BpbisNCnBpdGNoZXJfc3Bpbl9wbHVzIDwtIHBpdGNoZXJfbG9uZyAlPiUNCiAgbGVmdF9qb2luKG1lYW5fc3Bpbl9sb25nLCBieSA9IGMoInBpdGNoX3R5cGUiLCAieWVhciIpKSAlPiUNCiAgbXV0YXRlKHNwaW5fcGx1cyA9IChvYnNlcnZlZF9zcGluX3JhdGUgLyBleHBlY3RlZF9zcGluX3JhdGUpICogMTAwKSAlPiUNCiAgZmlsdGVyKCFpcy5uYShzcGluX3BsdXMpKSAgIyBSZW1vdmUgcm93cyB3aGVyZSBTcGluKyBjb3VsZG4ndCBiZSBjYWxjdWxhdGVkDQpgYGANCg0KbGV0J3MgbWFrZSBpdCB3aWRlIGZvcm0gYWdhaW4NCmBgYHtyfQ0KZmlsdGVyZWRfdGFibGUgPC0gcGl0Y2hlcl9zcGluX3BsdXMgJT4lDQogIHNlbGVjdCgNCiAgICBsYXN0X25hbWUuLmZpcnN0X25hbWUsIHllYXIsIHBpdGNoX3R5cGUsIG9ic2VydmVkX3NwaW5fcmF0ZSwgZXhwZWN0ZWRfc3Bpbl9yYXRlLCBzcGluX3BsdXMNCiAgKQ0KYGBgDQoNCmBgYHtyfQ0KIyBMb29wIHRocm91Z2ggZWFjaCB5ZWFyIHRvIGNvbnZlcnQgdGhlIGNvcnJlc3BvbmRpbmcgdGFibGUgdG8gd2lkZSBmb3JtYXQNCmZvciAoeXIgaW4gdW5pcXVlX3llYXJzKSB7DQogICMgR2V0IHRoZSBsb25nLWZvcm1hdCB0YWJsZSBmb3IgdGhlIGN1cnJlbnQgeWVhcg0KICBsb25nX3RhYmxlIDwtIGdldChwYXN0ZTAoInRhYmxlXyIsIHlyKSwgZW52aXIgPSAuR2xvYmFsRW52KQ0KICANCiAgIyBDb252ZXJ0IHRvIHdpZGUgZm9ybWF0DQogIHdpZGVfdGFibGUgPC0gbG9uZ190YWJsZSAlPiUNCiAgICAjIFNlbGVjdCByZWxldmFudCBjb2x1bW5zICh3ZSBvbmx5IG5lZWQgcGl0Y2hlciwgeWVhciwgYW5kIHNwaW5fcGx1cyBmb3IgdGhlIHBpdm90KQ0KICAgIHNlbGVjdChsYXN0X25hbWUuLmZpcnN0X25hbWUsIHllYXIsIHBpdGNoX3R5cGUsIHNwaW5fcGx1cykgJT4lDQogICAgIyBQaXZvdCB0byB3aWRlIGZvcm1hdDogY3JlYXRlIGNvbHVtbnMgbGlrZSBmZl9zcGluX3BsdXMsIHNsX3NwaW5fcGx1cywgZXRjLg0KICAgIHBpdm90X3dpZGVyKA0KICAgICAgbmFtZXNfZnJvbSA9IHBpdGNoX3R5cGUsDQogICAgICB2YWx1ZXNfZnJvbSA9IHNwaW5fcGx1cywNCiAgICAgIG5hbWVzX3ByZWZpeCA9ICJwaXRjaF90eXBlXyIsICAjIEFkZHMgcHJlZml4IHRvIHBpdGNoIHR5cGUgKGUuZy4sIHBpdGNoX3R5cGVfZmYpDQogICAgICBuYW1lc19nbHVlID0gIntwaXRjaF90eXBlfV9zcGluX3BsdXMiICAjIE1vcmUgZXhwbGljaXQ6IGNyZWF0ZXMgZmZfc3Bpbl9wbHVzLCBzbF9zcGluX3BsdXMNCiAgICApDQogIA0KICAjIENyZWF0ZSBhIHBlcm1hbmVudCB3aWRlLWZvcm1hdCB0YWJsZSB3aXRoIHRoZSBuYW1lICJ3aWRlX3RhYmxlX1lFQVIiDQogIGFzc2lnbihwYXN0ZTAoIndpZGVfdGFibGVfIiwgeXIpLCB3aWRlX3RhYmxlLCBlbnZpciA9IC5HbG9iYWxFbnYpDQp9DQoNCndpZGVfdGFibGVfMjAyNSA8LSB3aWRlX3RhYmxlXzIwMjUgJT4lDQogIGFycmFuZ2UoZGVzYyhmZl9zcGluX3BsdXMpKQ0KDQpwcmludCh3aWRlX3RhYmxlXzIwMjUpDQpgYGANCg0K