High School Swimming State-Off Tournament Florida (3) vs. Illinois (6)

[This article was first published on Welcome on Swimming + Data Science, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

This week in the High School Swimming State-Off Tournament we have the third seeded Sunshine State, Florida (3) taking on the sixth seeded Illinois (6) aka the Prairie State.

library(SwimmeR)
library(dplyr)
library(stringr)
library(purrr)
library(flextable)

Please note the following analysis was updated November 22nd 2020 to reflect changes beginning with SwimmeR v0.6.0 released via CRAN on November 22nd 2020. Please make sure your version of SwimmeR is up-to-date.


Florida Results

Florida has a nice results repository, with Hy-Tek real-time results. This will then be similar to last week when we first scraped a Hy-Tek real-time results page. This time though we’re going to scrape four, one for each of Florida’s four divisions.

Please note, Florida has changed their results repository since this post was written and have made results unavailable. Rather than reading in the now unavailable results directly from the FHSAA I’m downloading them from github, here I had archived them.

FL_Link <- "https://raw.githubusercontent.com/gpilgrim2670/Pilgrim_Data/master/FL_States_2020.csv"

FL_Results <- read.csv(url(FL_Link)) %>% # reads CSV file
  select(Name, "Team" = School, Finals_Time, Event, State)

### original code for posterity
# base_4A <- "https://www.fhsaa.org/sites/default/files/orig_uploads/sports/swimming-diving/archives/2019-20/state/4A/191115F0"
# 
# base_3A <- "https://www.fhsaa.org/sites/default/files/orig_uploads/sports/swimming-diving/archives/2019-20/state/3A/191116F0"
# 
# base_2A <- "https://www.fhsaa.org/sites/default/files/orig_uploads/sports/swimming-diving/archives/2019-20/state/191108F0"
# 
# base_1A <- "https://www.fhsaa.org/sites/default/files/orig_uploads/sports/swimming-diving/archives/2019-20/state/1A/191109F0"
# 
# real_time_links <- function(base, event_numbers) { # function to make list of results links
#   event_numbers <-
#     sprintf("%02d", as.numeric(event_numbers)) # adds leading zeros where needed on event numbers
#   links <-
#     map(base, paste0, event_numbers, ".htm") # combines link base with event numbers
#   links <- unlist(links, recursive = FALSE)
#   return(links)
# }
# 
# FL_Links <-
#   real_time_links(base = c(base_4A, base_3A, base_2A, base_1A),
#                   event_numbers = 1:24)
# 
# FL_Results <-
#   map(FL_Links, read_results, node = "pre") %>% # map SwimmeR::read_results over the list of links
#   map(swim_parse) %>%
#   bind_rows() %>% # bind together results from each link
#   select(Name, Team, Finals_Time, Event) %>% # only the columns we need
#   mutate(State = "FL") # add column for state since we'll be combining results with IL

Illinois Results

Illinois splits their results repositories into boys and girls. Final results from the Illinois girls state meet was just a scan of a printout, or as it’s sometime called .norm format (as of 8/12/2020 this file has been moved by IHSA, so now there are no results). Not a good look Illinois, not a good look at all. Illinois will be penalized for this egregious data-foul by being made to use their preliminary results on the girls side. Frankly it’s the only way to include them in the meet at all, short of transcribing that miserable scan. Even I’m not that interested.

Both Illinois boys and Illinois girls (prelim) results are available as .pdf files, which SwimmeR::read_results can handle. There are some inconsistencies in spacing for class designators (“Sr”, “Jr” etc.) as well as a few leading spaces. There’s also an issue with a region name, which follows school names, but is placed in parentheses. We can clean these up with the typo and replacement arguments of swim_parse.

Please note: Illinois has moved their repository since this article was first posted. Links have been updated.

IL_Boys_Link <- "https://www.ihsa.org/archive/swb/2019-20/StateResults.pdf"
IL_Girls_Link <-
  "https://www.ihsa.org/Portals/0/prelim%20results.pdf"

IL_Results <-
  map(c(IL_Girls_Link, IL_Boys_Link), read_results) %>% # map SwimmeR::read_results over the list of links
  map(
    swim_parse,
    avoid = c("IHSA", "NFHS", "POOL", "NATIONAL"),
    typo = c(
      "Sr\\s{2,}",
      # fix issue with some class designation strings
      "Jr\\s{2,}",
      "So\\s{2,}",
      "Fr\\s{2,}",
      "\\s{2,}\\(",
      # region designation fix
      "\\s\\d{1,2}\\s{2,}" # fix leading spaces
    ),
    replacement = c("Sr ",
                    "Jr ",
                    "So ",
                    "Fr ",
                    " \\)",
                    " ")
  ) %>%
  bind_rows() %>% # bind together results from each link
  mutate(Ind = case_when(str_detect(Event, "Relay") == FALSE ~ 1,
                         TRUE ~ 0)) %>%
  mutate(
    Age = case_when(
      is.na(Age) == TRUE &
        Ind == 1 ~ str_extract(Team, "^Fr|^So|^Jr|^Sr")
    ),
    Team = case_when(is.na(Age) == FALSE ~ str_remove(Team, Age),
                       TRUE ~ Team)
  ) %>%
  select(Name, Team, Finals_Time, Event) %>% # only the columns we need
  mutate(State = "IL")

Joining Up Results

Having collected results from Florida and Illinois we just need to join them up, add a column for gender and remove events outside the standard 12 event program.

Results <- bind_rows(FL_Results, IL_Results) %>%
  mutate(Gender = case_when(
    str_detect(Event, "Girls") == TRUE ~ "Girls",
    # coding gender
    str_detect(Event, "Boys") == TRUE ~ "Boys"
  )) %>%
  filter(str_detect(Event, "Swim-off") == FALSE,
         # removing extra events
         str_detect(Event, "AWD") == FALSE)

Analysis

Last week to make a point about reusable code I copied my code from the week prior. Copy and pasting code is all well and good, but even better is to functionalize code. So this week we’re going to start working on function for scoring each event type (diving, relay and individual swim). The eventual goal, over the next few weeks, is to develop this code into a robust function for inclusion in a future release of the SwimmeR package.

Below is our starting point. It’s basically just the code from previous weeks wrapped in curly brackets. Inside the curly brackets there are still cases, determined by if, to break up the input results into diving, relays, and individual swims, and then to score them exactly as before. The difference is that this function does not assume there actually are any diving, relay or individual swims. It has else conditions following each if that insure the function does not fault if a particular event type isn’t represented.


Scoring Function

results_score <-
  function(results, events, point_values, max_place) {
    point_values <- c(point_values, 0)
    names(point_values) <- 1:length(point_values)
    
    results <- results %>%
      filter(str_detect(Event, paste(events, collapse = "|")) == TRUE)
    
    if (any(str_detect(results$Event, "Diving"))) {
      # break out diving results
      diving_results <- results %>%
        filter(str_detect(Event, "Diving")) %>%
        mutate(Finals_Time = as.numeric(Finals_Time)) %>%
        group_by(Event, Name) %>%
        slice(1) %>% # first instance of every diver
        ungroup() %>%
        group_by(Event) %>%
        mutate(
          Place = rank(desc(Finals_Time), ties.method = "min"),
          # highest score gets rank 1
          Finals_Time = as.character(Finals_Time)
        ) %>%
        filter(Place <= max_place)
      
    } else {
      diving_results  <-
        results[0, ] # if there are no diving results create a blank dataframe called diving_results, with the same columns as results
    }
    
    if (any(str_detect(results$Event, "Relay"))) {
      # break out relay results
      relay_results <- results %>%
        filter(str_detect(Event, "Relay") == TRUE) %>% # only want relays
        group_by(Event, Team) %>%
        slice(1) %>% # select first occurrence of team in each event
        ungroup() %>%
        mutate(Finals_Time_sec = sec_format(Finals_Time)) %>% # convert time to seconds
        group_by(Event) %>%
        mutate(Place = rank(Finals_Time_sec, ties.method = "min")) %>% # places, low number wins
        filter(Place <= max_place)
      
    } else {
      relay_results  <-
        results[0, ] # if there are no relay results create a blank dataframe called relay_results, with the same columns as results
    }
    
    if (any(str_detect(results$Event, "Diving|Relay") == FALSE)) {
      # break out non diving/relay events
      ind_results <- results %>%
        filter(str_detect(Event, "Diving|Relay") == FALSE) %>%
        group_by(Event, Name) %>%
        slice(1) %>% # first instance of every swimmer
        ungroup() %>%
        group_by(Event) %>%
        mutate(Finals_Time_sec = sec_format(Finals_Time)) %>% # time as seconds
        mutate(Place = rank(Finals_Time_sec, ties.method = "min")) %>% # places, low number wins
        filter(Place <= max_place)
      
    } else {
      ind_results <-
        results[0, ] # if there are no non relay/diving results create a blank dataframe called ind_results, with the same columns as results
    }
    
    results <-
      bind_rows(ind_results, diving_results, relay_results) # bind together all the dataframes.  This is why we needed blank dataframes in the case of no relay/diving results
    
    results <- results %>% # deal with ties
      group_by(Event) %>%
      mutate(New_Place = rank(Place, ties.method = "first"),
             Points = point_values[New_Place]) %>%
      group_by(Place, Event) %>%
      summarize(Points = mean(Points)) %>%
      inner_join(results) %>%
      mutate(Points = case_when(str_detect(Event, "Relay") == TRUE ~ Points * 2,
                                TRUE ~ Points)) %>%
      ungroup()
    
  }


In future installments of the Swim-Off we’ll add peices of code to this function to sanitize inputs, provide error messages, and eventually to score other meet formats.
Because results_score can score arbitrary events, we don’t actually have to score an entire meet. We can select only a few events. Say we were interested in short axis strokes, butterfly and breaststroke. Those events can be passed to results_score in place of the full suite of events.

Results_Short_Axis <-
  results_score(
    results = Results,
    events = c("Butterfly", "Breaststroke"),
    point_values = c(20, 17, 16, 15, 14, 13, 12, 11, 9, 7, 6, 5, 4, 3, 2, 1),
    max_place = 16
  )

Results_Short_Axis %>%
  filter(Place <= 2) %>%
  select(Place, Name, State, Event, Points) %>%
  arrange(Event, Place) %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

Place

Name

State

Event

Points

1

Iida, Max

IL

Boys 100 Yard Breaststroke

20

2

Alderson, Justin

IL

Boys 100 Yard Breaststroke

17

1

Filipovic, Aleksej

IL

Boys 100 Yard Butterfly

20

2

Jones, William

FL

Boys 100 Yard Butterfly

17

1

Gridley, Kaelyn

IL

Girls 100 Yard Breaststroke

20

2

Schwab, Carly

FL

Girls 100 Yard Breaststroke

17

1

Peoples, Olivia

FL

Girls 100 Yard Butterfly

20

2

Stone, McKenna

IL

Girls 100 Yard Butterfly

17



Scored Results

we are interested in scoring a full meet though, so let’s pass all the events from Florida vs. Illinois to results_score and get the scores for each event.

Results_Final <-
  results_score(
    results = Results,
    events = unique(Results$Event),
    point_values = c(20, 17, 16, 15, 14, 13, 12, 11, 9, 7, 6, 5, 4, 3, 2, 1),
    max_place = 16
  )

Scores <- Results_Final %>%
  group_by(State, Gender) %>%
  summarise(Score = sum(Points))


Florida wins both the boys and girls meets, but it’s close, especially on the boys side. On the girls side Illinois was likely hampered by not having their finals swims available. Serves them right for their lousy data practices.

Scores %>%
  arrange(Gender, desc(Score)) %>%
  ungroup() %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

State

Gender

Score

FL

Boys

1296

IL

Boys

1029

FL

Girls

1408

IL

Girls

917


Scores %>%
  group_by(State) %>%
  summarise(Score = sum(Score)) %>%
  arrange(desc(Score)) %>%
  ungroup() %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

State

Score

FL

2704

IL

1946


Let’s take a closer look, and see how many events each state won, by gender.

Results_Final %>%
  filter(Place == 1) %>%
  select(Event, State, Gender) %>%
  group_by(Gender, State) %>%
  summarise(Total = n()) %>%
  arrange(Gender, desc(Total)) %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

Gender

State

Total

Boys

IL

11

Boys

FL

2

Girls

FL

7

Girls

IL

5


The Illinois boys won 11 of 12 events – that’s impressive! Let’s see that listed out:

Results_Final <-
  Results_Final[order(match(Results_Final$Event, Results$Event)),] # to order events correctly

Results_Final %>%
  filter(Gender == "Boys",
         Place == 1) %>%
  select(Name, Team, State, Finals_Time, Event) %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

Name

Team

State

Finals_Time

Event

Glenview (Glenbrook South)

IL

1:31.31

Boys 200 Yard Medley Relay

Hinsdale (Central)

IL

1:31.31

Boys 200 Yard Medley Relay

Maurer, Luke

Wilmette (Loyola Academy)

IL

1:36.35

Boys 200 Yard Freestyle

Zuchowski, Joshua

King’s Academy

FL

1:47.44

Boys 200 Yard IM

Boyle, Connor

Naperville (Neuqua Valley)

IL

20.04

Boys 50 Yard Freestyle

Nolan, Harrison

Riverside (R.-Brookfield)

IL

522.95

Boys 1 mtr Diving

Filipovic, Aleksej

St. Charles (North)

IL

48.25

Boys 100 Yard Butterfly

Boyle, Connor

Naperville (Neuqua Valley)

IL

43.82

Boys 100 Yard Freestyle

Andrew, Everet

Wilmette (Loyola Academy)

IL

4:24.50

Boys 500 Yard Freestyle

LaGrange (Lyons)

IL

1:22.40

Boys 200 Yard Freestyle Relay

Zuchowski, Joshua

King’s Academy

FL

47.85

Boys 100 Yard Backstroke

Iida, Max

Glenview (Glenbrook South)

IL

54.62

Boys 100 Yard Breaststroke

Wilmette (Loyola Academy)

IL

3:02.80

Boys 400 Yard Freestyle Relay



Swimmers of the Meet

Just to remind everyone of the Swimmer of the Meet criteria first we’ll look for athletes who won two events, thereby scoring a the maximum possible forty points. We’ll also grab the All-American cuts to use as a tiebreaker, in case multiple athletes win two events.

Cuts_Link <-
  "https://raw.githubusercontent.com/gpilgrim2670/Pilgrim_Data/master/State_Cuts.csv"
Cuts <- read.csv(url(Cuts_Link))

'%!in%' <- function(x, y)
  ! ('%in%'(x, y)) # "not in" function

Cuts <- Cuts %>% # clean up Cuts
  filter(Stroke %!in% c("MR", "FR", "11 Dives")) %>%
  rename(Gender = Sex) %>%
  mutate(
    Event = case_when((Distance == 200 & #match events
                         Stroke == 'Free') ~ "200 Yard Freestyle",
                      (Distance == 200 &
                         Stroke == 'IM') ~ "200 Yard IM",
                      (Distance == 50 &
                         Stroke == 'Free') ~ "50 Yard Freestyle",
                      (Distance == 100 &
                         Stroke == 'Fly') ~ "100 Yard Butterfly",
                      (Distance == 100 &
                         Stroke == 'Free') ~ "100 Yard Freestyle",
                      (Distance == 500 &
                         Stroke == 'Free') ~ "500 Yard Freestyle",
                      (Distance == 100 &
                         Stroke == 'Back') ~ "100 Yard Backstroke",
                      (Distance == 100 &
                         Stroke == 'Breast') ~ "100 Yard Breaststroke",
                      TRUE ~ paste(Distance, "Yard", Stroke, sep = " ")
    ),
    
    Event = case_when(
      Gender == "M" ~ paste("Boys", Event, sep = " "),
      Gender == "F" ~ paste("Girls", Event, sep = " ")
    )
  )

Ind_Swimming_Results <- Results_Final %>%
  filter(str_detect(Event, "Diving|Relay") == FALSE) %>% # join Ind_Swimming_Results and Cuts
  left_join(Cuts %>% filter((Gender == "M" &
                               Year == 2020) |
                              (Gender == "F" &
                                 Year == 2019)) %>%
              select(AAC_Cut, AA_Cut, Event),
            by = 'Event')

Swimmer_Of_Meet <- Ind_Swimming_Results %>%
  mutate(
    AA_Diff = (Finals_Time_sec - sec_format(AA_Cut)) / sec_format(AA_Cut),
    Name = str_to_title(Name)
  ) %>%
  group_by(Name) %>%
  filter(n() == 2) %>% # get swimmers that competed in two events
  summarise(
    Avg_Place = sum(Place) / 2,
    AA_Diff_Avg = round(mean(AA_Diff, na.rm = TRUE), 3),
    Gender = unique(Gender),
    State = unique(State)
  ) %>%
  arrange(Avg_Place, AA_Diff_Avg) %>%
  group_split(Gender) # split out a dataframe for boys (1) and girls (2)



Boys

As we’ve already seen, Illinois boys won 11 of 12 events, with Connor Boyle and Max Iida both winning two apiece. That means we’ll be going to the All-American standard tiebreaker.

Swimmer_Of_Meet[[1]] %>% 
  slice_head(n = 5) %>% 
  select(-Gender) %>% 
  ungroup() %>%
  flextable() %>% 
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>% 
  autofit()

Name

Avg_Place

AA_Diff_Avg

State

Boyle, Connor

1.0

-0.025

IL

Zuchowski, Joshua

1.0

-0.025

FL

Maurer, Luke

1.5

-0.022

IL

Iida, Max

1.5

-0.018

IL

Tirheimer, Logan

3.0

-0.013

FL


It’s Connor Boyle by a nose, just squeaking past Max Iida. Interestingly Florida’s lone event winner, Joshua Zuchowski, had the same All-American score as Connor, but only won one event, finishing second to Max in the 200 IM. He’s young though, so maybe next year.

Results_Final %>%
  filter(Name == "Boyle, Connor") %>%
  select(Place, Name, Team, Finals_Time, Event) %>%
  arrange(desc(Event)) %>%
  ungroup() %>%
  flextable() %>%
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>%
  autofit()

Place

Name

Team

Finals_Time

Event

1

Boyle, Connor

Naperville (Neuqua Valley)

20.04

Boys 50 Yard Freestyle

1

Boyle, Connor

Naperville (Neuqua Valley)

43.82

Boys 100 Yard Freestyle



Girls

Swimmer_Of_Meet[[2]] %>%
  slice_head(n = 5) %>%
  select(-Gender) %>%
  ungroup() %>%
  flextable() %>% 
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>% 
  autofit()

Name

Avg_Place

AA_Diff_Avg

State

Cronk, Micayla

1.0

-0.040

FL

Weyant, Emma

1.0

-0.033

FL

Cooper, Grace

1.5

-0.027

IL

Stege, Rachel

2.0

-0.020

IL

Novelline, Carly

2.5

-0.016

IL


The girls meet was overall much stronger, with the top three girls swimmers further under the All-American cuts than any of the boys. Micayla Cronk was especially excellent, not only winning two events, but doing so with extremely impressive times. Emma Weyant also won two events, but was pipped by Micayla on the All-American tie-breaker.

Results_Final %>%
  filter(Name == "Cronk, Micayla") %>%
  select(Place, Name, Team, Finals_Time, Event) %>%
  arrange(desc(Event)) %>% 
  ungroup() %>%
  flextable() %>% 
  bold(part = "header") %>%
  bg(bg = "#D3D3D3", part = "header") %>% 
  autofit()

Place

Name

Team

Finals_Time

Event

1

Cronk, Micayla

Flagler

1:44.39

Girls 200 Yard Freestyle

1

Cronk, Micayla

Flagler

48.20

Girls 100 Yard Freestyle



In Closing

Thanks for joining us for another installment of the High School Swimming State-Off here at Swimming + Data Science. Come back next week for Texas (2) vs. Ohio (7), where we’ll be making further improvements to our new results_score function.

To leave a comment for the author, please follow the link and comment on their blog: Welcome on Swimming + Data Science.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)