maybe:
https://stackoverflow.com/questions/36230790/missing-horizontal-scroll-bar-in-r-markdown-html-code-chunks-and-output
css: report_styles.css
pre, code {white-space:pre !important; overflow-x:scroll !important}
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

Lahman’s Baseball Database

http://www.seanlahman.com/

Package ‘Lahman’: https://cran.r-project.org/web/packages/Lahman/Lahman.pdf

Install Package ‘Lahman’:

#install.packages("Lahman")

Load Lahman

library(Lahman)

Create lahmanNames

Not required code

# install.packages("vcdExtra")
# vcdExtra::datasets(package="Lahman")
# data(AllstarFull) -- etc

Create names

name1 <- names(AllstarFull)
name2 <- names(Appearances)
name3 <- names(AwardsManagers)
name4 <- names(AwardsPlayers)
name5 <- names(AwardsShareManagers)
name6 <- names(AwardsSharePlayers)
name7 <- names(Batting)
name8 <- names(BattingPost)
name9 <- names(CollegePlaying)
name10 <- names(Fielding)
name11 <- names(FieldingOF)
name12 <- names(FieldingPost)
name13 <- names(HallOfFame)
name14 <- names(LahmanData)
name15 <- names(Managers)
name16 <- names(ManagersHalf)
name17 <- names(Master)
name18 <- names(Pitching)
name19 <- names(PitchingPost)
name20 <- names(Salaries)
name21 <- names(Schools)
name22 <- names(SeriesPost)
name23 <- names(Teams)
name24 <- names(TeamsFranchises)
name25 <- names(TeamsHalf)
name26 <- names(battingLabels)
name27 <- names(fieldingLabels)
name28 <- names(pitchingLabels)

Create lahmanNames

library(tibble)
lahmanNames <- list(
                    AllstarFull = tibble(var = name1),
                    Appearances = tibble(var = name2),
                    AwardsManagers = tibble(var = name3),
                    AwardsPlayers = tibble(var = name4),
                    AwardsShareManagers = tibble(var = name5),
                    AwardsSharePlayers = tibble(var = name6),
                    Batting = tibble(var = name7),
                    BattingPost = tibble(var = name8),
                    CollegePlaying = tibble(var = name9),
                    Fielding = tibble(var = name10),
                    FieldingOF = tibble(var = name11),
                    FieldingPost = tibble(var = name12),
                    HallOfFame = tibble(var = name13),
                    LahmanData = tibble(var = name14),
                    Managers = tibble(var = name15),
                    ManagersHalf = tibble(var = name16),
                    Master = tibble(var = name17),
                    Pitching = tibble(var = name18),
                    PitchingPost = tibble(var = name19),
                    Salaries = tibble(var = name20),
                    Schools = tibble(var = name21),
                    SeriesPost = tibble(var = name22),
                    Teams = tibble(var = name23),
                    TeamsFranchises = tibble(var = name24),
                    TeamsHalf = tibble(var = name25),
                    battingLabels = tibble(var = name26),
                    fieldingLabels = tibble(var = name27),
                    pitchingLabels = tibble(var = name28)
)

Examine lahmanNames

str(lahmanNames)
## List of 28
##  $ AllstarFull        :Classes 'tbl_df', 'tbl' and 'data.frame': 8 obs. of  1 variable:
##   ..$ var: chr [1:8] "playerID" "yearID" "gameNum" "gameID" ...
##  $ Appearances        :Classes 'tbl_df', 'tbl' and 'data.frame': 21 obs. of  1 variable:
##   ..$ var: chr [1:21] "yearID" "teamID" "lgID" "playerID" ...
##  $ AwardsManagers     :Classes 'tbl_df', 'tbl' and 'data.frame': 6 obs. of  1 variable:
##   ..$ var: chr [1:6] "playerID" "awardID" "yearID" "lgID" ...
##  $ AwardsPlayers      :Classes 'tbl_df', 'tbl' and 'data.frame': 6 obs. of  1 variable:
##   ..$ var: chr [1:6] "playerID" "awardID" "yearID" "lgID" ...
##  $ AwardsShareManagers:Classes 'tbl_df', 'tbl' and 'data.frame': 7 obs. of  1 variable:
##   ..$ var: chr [1:7] "awardID" "yearID" "lgID" "playerID" ...
##  $ AwardsSharePlayers :Classes 'tbl_df', 'tbl' and 'data.frame': 7 obs. of  1 variable:
##   ..$ var: chr [1:7] "awardID" "yearID" "lgID" "playerID" ...
##  $ Batting            :Classes 'tbl_df', 'tbl' and 'data.frame': 22 obs. of  1 variable:
##   ..$ var: chr [1:22] "playerID" "yearID" "stint" "teamID" ...
##  $ BattingPost        :Classes 'tbl_df', 'tbl' and 'data.frame': 22 obs. of  1 variable:
##   ..$ var: chr [1:22] "yearID" "round" "playerID" "teamID" ...
##  $ CollegePlaying     :Classes 'tbl_df', 'tbl' and 'data.frame': 3 obs. of  1 variable:
##   ..$ var: chr [1:3] "playerID" "schoolID" "yearID"
##  $ Fielding           :Classes 'tbl_df', 'tbl' and 'data.frame': 18 obs. of  1 variable:
##   ..$ var: chr [1:18] "playerID" "yearID" "stint" "teamID" ...
##  $ FieldingOF         :Classes 'tbl_df', 'tbl' and 'data.frame': 6 obs. of  1 variable:
##   ..$ var: chr [1:6] "playerID" "yearID" "stint" "Glf" ...
##  $ FieldingPost       :Classes 'tbl_df', 'tbl' and 'data.frame': 17 obs. of  1 variable:
##   ..$ var: chr [1:17] "playerID" "yearID" "teamID" "lgID" ...
##  $ HallOfFame         :Classes 'tbl_df', 'tbl' and 'data.frame': 9 obs. of  1 variable:
##   ..$ var: chr [1:9] "playerID" "yearID" "votedBy" "ballots" ...
##  $ LahmanData         :Classes 'tbl_df', 'tbl' and 'data.frame': 5 obs. of  1 variable:
##   ..$ var: chr [1:5] "file" "class" "nobs" "nvar" ...
##  $ Managers           :Classes 'tbl_df', 'tbl' and 'data.frame': 10 obs. of  1 variable:
##   ..$ var: chr [1:10] "playerID" "yearID" "teamID" "lgID" ...
##  $ ManagersHalf       :Classes 'tbl_df', 'tbl' and 'data.frame': 10 obs. of  1 variable:
##   ..$ var: chr [1:10] "playerID" "yearID" "teamID" "lgID" ...
##  $ Master             :Classes 'tbl_df', 'tbl' and 'data.frame': 26 obs. of  1 variable:
##   ..$ var: chr [1:26] "playerID" "birthYear" "birthMonth" "birthDay" ...
##  $ Pitching           :Classes 'tbl_df', 'tbl' and 'data.frame': 30 obs. of  1 variable:
##   ..$ var: chr [1:30] "playerID" "yearID" "stint" "teamID" ...
##  $ PitchingPost       :Classes 'tbl_df', 'tbl' and 'data.frame': 30 obs. of  1 variable:
##   ..$ var: chr [1:30] "playerID" "yearID" "round" "teamID" ...
##  $ Salaries           :Classes 'tbl_df', 'tbl' and 'data.frame': 5 obs. of  1 variable:
##   ..$ var: chr [1:5] "yearID" "teamID" "lgID" "playerID" ...
##  $ Schools            :Classes 'tbl_df', 'tbl' and 'data.frame': 5 obs. of  1 variable:
##   ..$ var: chr [1:5] "schoolID" "name_full" "city" "state" ...
##  $ SeriesPost         :Classes 'tbl_df', 'tbl' and 'data.frame': 9 obs. of  1 variable:
##   ..$ var: chr [1:9] "yearID" "round" "teamIDwinner" "lgIDwinner" ...
##  $ Teams              :Classes 'tbl_df', 'tbl' and 'data.frame': 48 obs. of  1 variable:
##   ..$ var: chr [1:48] "yearID" "lgID" "teamID" "franchID" ...
##  $ TeamsFranchises    :Classes 'tbl_df', 'tbl' and 'data.frame': 4 obs. of  1 variable:
##   ..$ var: chr [1:4] "franchID" "franchName" "active" "NAassoc"
##  $ TeamsHalf          :Classes 'tbl_df', 'tbl' and 'data.frame': 10 obs. of  1 variable:
##   ..$ var: chr [1:10] "yearID" "lgID" "teamID" "Half" ...
##  $ battingLabels      :Classes 'tbl_df', 'tbl' and 'data.frame': 2 obs. of  1 variable:
##   ..$ var: chr [1:2] "variable" "label"
##  $ fieldingLabels     :Classes 'tbl_df', 'tbl' and 'data.frame': 2 obs. of  1 variable:
##   ..$ var: chr [1:2] "variable" "label"
##  $ pitchingLabels     :Classes 'tbl_df', 'tbl' and 'data.frame': 2 obs. of  1 variable:
##   ..$ var: chr [1:2] "variable" "label"

Do any variable names span all of the datasets?

Use purrr’s reduce() and a single set operation to return a data frame that contains every variable that appears in all 26 datasets (if any does).

library(purrr)
# Find variables in common
# ref intersect() set operation
reduce(lahmanNames, intersect) # returns an error

# output seen in course:

## A tibble: 0 × 1
## ... with 1 variables: var <chr>

No variables span all 26 datasets!

Attempt to use reduce() / interest()

reduce(list(lahmanNames$AllstarFull,lahmanNames$Appearances), intersect)

intersect(lahmanNames$AllstarFull,lahmanNames$Appearances)

as.vector(lahmanNames$AllstarFull)

intersect(as.vector(lahmanNames$AllstarFull),as.vector(lahmanNames$Appearances))

as.character(lahmanNames$AllstarFull)
## [1] "c(\"playerID\", \"yearID\", \"gameNum\", \"gameID\", \"teamID\", \"lgID\", \"GP\", \"startingPos\")"

KH1 <- as.character(lahmanNames$AllstarFull)
KH2 <- as.character(lahmanNames$Appearances)

unlist(KH2, use.names = FALSE)
## [1] "c(\"yearID\", \"teamID\", \"lgID\", \"playerID\", \"G_all\", \"GS\", \"G_batting\", \"G_defense\", \"G_p\", \"G_c\", \"G_1b\", \"G_2b\", \"G_3b\", \"G_ss\", \"G_lf\", \"G_cf\", \"G_rf\", \"G_of\", \"G_dh\", \"G_ph\", \"G_pr\")"

intersect(KH1, KH2)
## character(0)

intersect(c(1,2,3), c(3,4,5))
## [1] 3

intersect(as.vector(lahmanNames$AllstarFull),as.vector(lahmanNames$Appearances))

intersect(as.vector(lahmanNames$AllstarFull[1:2,1]),as.vector(lahmanNames$AllstarFull[5:6]))
## Error: Column indexes must be at most 1 if positive, not 5, 6

as.numeric(as.matrix(lahmanNames$AllstarFull))
## Warning: NAs introduced by coercion
## [1] NA NA NA NA NA NA NA NA

intersect(as.matrix(lahmanNames$AllstarFull),as.matrix(lahmanNames$Appearances))
## [1] "playerID" "yearID"   "teamID"   "lgID"

reduce(as.matrix(unlist(lahmanNames)), intersect)
## character(0)

Common keys

Which variables span more than one dataset?

These variables provide relationships between the various tables in Lahman

bind_rows()

lahmanNames %>%  

    # Bind the data frames in lahmanNames
    # into a single table 
    # that includes a new column
    # called dataframe, 
    # which contains the names of the data frames.
    bind_rows(.id = 'dataframe')

group_by()

lahmanNames %>%  

    # Bind the data frames in lahmanNames
    bind_rows(.id = 'dataframe') %>%
    # into a single table 
    # that includes a new column
    # called dataframe, 
    # which contains the names of the data frames.
    
        # Group the result by var
        group_by(var)

tally()

lahmanNames %>%  

    # Bind the data frames in lahmanNames
    bind_rows(.id = 'dataframe') %>%
    # into a single table 
    # that includes a new column
    # called dataframe, 
    # which contains the names of the data frames.
    
        # Group the result by var
        group_by(var) %>%

            # Tally the number of appearances
            tally() 

filter()

lahmanNames %>%  

    # Bind the data frames in lahmanNames
    bind_rows(.id = 'dataframe') %>%
    # into a single table 
    # that includes a new column
    # called dataframe, 
    # which contains the names of the data frames.
    
        # Group the result by var
        group_by(var) %>%

            # Tally the number of appearances
            tally() %>%
            
                # Filter the data
                # to only variables that appear more than once
                filter(n > 1)

arrange()

lahmanNames %>%  

    # Bind the data frames in lahmanNames
    bind_rows(.id = 'dataframe') %>%
    # into a single table 
    # that includes a new column
    # called dataframe, 
    # which contains the names of the data frames.
    
        # Group the result by var
        group_by(var) %>%

            # Tally the number of appearances
            tally() %>%
            
                # Filter the data
                filter(n > 1) %>% 
                # to only variables that appear more than once
                
                    # Arrange the results
                    # most frequently used variables appear before less frequently used variables.
                    arrange(desc(n))

playerID

most widely used keys

  • Q: Which datasets use playerID?
    • A: The playerID variable appears in 19 of the 26 datasets in Lahman.

bind_rows()

lahmanNames %>% 
    # Bind the data frames
    # into a single table
    # that includes the column dataframe, 
    # which contains the names of the data frames.
    bind_rows(.id = 'dataframe') 

filter()

lahmanNames %>% 
    # Bind the data frames
    # into a single table
    # that includes the column dataframe, 
    # which contains the names of the data frames.
    bind_rows(.id = 'dataframe') %>%
  
        # Filter the results
        filter(var == "playerID") 

$

lahmanNames %>% 
    # Bind the data frames
    # into a single table
    # that includes the column dataframe, 
    # which contains the names of the data frames.
    bind_rows(.id = 'dataframe') %>%
  
        # Filter the results
        filter(var == "playerID") %>% 
            
            # Extract the dataframe variable
            `$`("dataframe")
##  [1] "AllstarFull"         "Appearances"         "AwardsManagers"     
##  [4] "AwardsPlayers"       "AwardsShareManagers" "AwardsSharePlayers" 
##  [7] "Batting"             "BattingPost"         "CollegePlaying"     
## [10] "Fielding"            "FieldingOF"          "FieldingPost"       
## [13] "HallOfFame"          "Managers"            "ManagersHalf"       
## [16] "Master"              "Pitching"            "PitchingPost"       
## [19] "Salaries"

Players

verify salary information for all players i.e. no systematic holes in our coverage

=> need: list of every player (to compare against salaries)

distinct()

distinct() – find unique rows for all columns in a table.


players <- Master %>%  # may contain multiple rows 
                       # for each player
  
  # Return one row for each distinct player
  # & three columns:
  #                playerID
  #                nameFirst
  #                nameLast


  distinct(playerID, nameFirst, nameLast)

Missing Salaries

  • Salary coverage: How many players are completely missing salary information?

anti_join()

players %>% 

    # Find all players who do not appear in Salaries
    anti_join(Salaries) 
## Joining, by = "playerID"

count()

players %>% 

    # Find all players who do not appear in Salaries
    anti_join(Salaries) %>%

        # how many players do not appear in Salaries
        # dplyr function count()
        count()
## Joining, by = "playerID"

        # i.e. we are missing the salaries for 13,888 players

Unpaid games?

Explain the hole in the salary data!

Is it possible that these players somehow did not play (and hence did not earn a salary)?

Appearances contains information about every game played in major league baseball. (i.e. if a player played a game, it would show up as a row in Appearances)

anti_join()

players %>% 
  
    anti_join(Salaries, by = "playerID")

semi_join()

players %>% 
  
    anti_join(Salaries, by = "playerID") %>% 
  
        # Add a join to the pipe 
        #   to determine how many (if any) unsalaried players
        #   played at least one game.
        # 
        # How many unsalaried players appear in Appearances?
        semi_join(Appearances, by = "playerID")

count()

players %>% 
  
    anti_join(Salaries, by = "playerID") %>% 
  
        # Add a join to the pipe 
        #   to determine how many (if any) unsalaried players
        #   played at least one game.
        # 
        # How many unsalaried players appear in Appearances?
        semi_join(Appearances, by = "playerID") %>%
        count()

        # 13,695 players played a game but are missing salary information.

        # Interestingly, 193 players neither played a game nor have a recorded salary.

How many games?

  • maybe: the unsalaried players only played one or two games
    • thus, did not earn a full salary.

how many games each of these unsalaried players played?

anti_join

players %>% 

  # Find all players who do not appear in Salaries
  anti_join(Salaries, by = "playerID")

left_join()

players %>% 

  # Find all players who do not appear in Salaries
  anti_join(Salaries, by = "playerID") %>% 

    # Join them to Appearances
    left_join(Appearances, by = "playerID")

group_by()

players %>% 

  # Find all players who do not appear in Salaries
  anti_join(Salaries, by = "playerID") %>% 

    # Join them to Appearances
    left_join(Appearances, by = "playerID") %>%

      # Calculate total_games for each player
      group_by(playerID)

summarize()

players %>% 

  # Find all players who do not appear in Salaries
  anti_join(Salaries, by = "playerID") %>% 

    # Join them to Appearances
    left_join(Appearances, by = "playerID") %>%

      # Calculate total_games for each player
      group_by(playerID) %>% 
      summarize(total_games = sum(G_all, na.rm = TRUE))

arrange()

players %>% 

  # Find all players who do not appear in Salaries
  anti_join(Salaries, by = "playerID") %>% 

    # Join them to Appearances
    left_join(Appearances, by = "playerID") %>%

      # Calculate total_games for each player
      group_by(playerID) %>% 
      summarize(total_games = sum(G_all, na.rm = TRUE)) %>%

        # Arrange in descending order by total_games
        arrange(desc(total_games))

How many at-bats?

Is it possible that the unsalaried players did not actually play in the games that they appeared in?

with Batting (instead of Appearances)

players %>%

    # Find unsalaried players
    anti_join(Salaries, by = "playerID") %>% 
      
      # Join Batting to the unsalaried players
      left_join(Batting, by = "playerID") %>%
  
          # Group by player
          group_by(playerID) %>% 
  
              # Sum at-bats for each player
              summarize(total_at_bat = sum(AB, na.rm = TRUE)) %>%
                  
                  # Arrange in descending order
                  arrange(desc(total_at_bat))


# The unpaid players definitely participated in the games
# Ty Cobb and Hank Aaron in the top three at-bats
# i.e. dealing with missing data here and not unsalaried players

Hall of Fame

collection of distinguished baseball players selected by a committee of baseball experts

names()

names(HallOfFame)
## [1] "playerID"    "yearID"      "votedBy"     "ballots"     "needed"     
## [6] "votes"       "inducted"    "category"    "needed_note"

head()

head(HallOfFame)

str()

str(HallOfFame)
## 'data.frame':    4156 obs. of  9 variables:
##  $ playerID   : chr  "cobbty01" "ruthba01" "wagneho01" "mathech01" ...
##  $ yearID     : int  1936 1936 1936 1936 1936 1936 1936 1936 1936 1936 ...
##  $ votedBy    : chr  "BBWAA" "BBWAA" "BBWAA" "BBWAA" ...
##  $ ballots    : int  226 226 226 226 226 226 226 226 226 226 ...
##  $ needed     : int  170 170 170 170 170 170 170 170 170 170 ...
##  $ votes      : int  222 215 215 205 189 146 133 111 105 80 ...
##  $ inducted   : Factor w/ 2 levels "N","Y": 2 2 2 2 2 1 1 1 1 1 ...
##  $ category   : Factor w/ 4 levels "Manager","Pioneer/Executive",..: 3 3 3 3 3 3 3 3 3 3 ...
##  $ needed_note: chr  NA NA NA NA ...

Hall of fame nominations

nominated

distinct()

# Find the distinct players (by playerID) that appear in HallOfFame
nominated <- HallOfFame %>% 
    
  distinct(playerID)


head(nominated)

count()

nominated %>% 
    # Count the number of players in nominated
    count()

# there were 1,239 nominees for the Hall of Fame. 

nominated_full

left_join()

nominated_full <- nominated %>% 
    
    # Join to Master
    left_join(Master, by = "playerID")

select()


nominated_full <- nominated %>% 
    
    # Join to Master
    left_join(Master, by = "playerID") %>% 
    
        # Return playerID, nameFirst, nameLast
        select(playerID, nameFirst, nameLast)

head(nominated_full)

# We now have a dataset of everyone nominated! 

Hall of fame inductions

# Repeat the process in the last exercise
inducted <- HallOfFame %>% 
    filter(inducted == "Y") %>% 
        distinct(playerID)
    # call filter() before distinct() 
    # to get the distinct players 
    # that were inducted into the hall of fame
  
head(inducted)



inducted %>% 
    # Count the number of players in inducted
    count()

# 312 players have been inducted into the Hall of Fame out of 1,239 nominees

inducted_full <- inducted %>% 
    # Join to Master
    left_join(Master, by = "playerID") %>% 
        # Return playerID, nameFirst, nameLast
        select(playerID, nameFirst, nameLast)
  
  head(inducted_full)

  # We now also have datasets of everyone inducted and everyone nominated
    

Awards

Did nominees who were inducted earn more awards than nominees who were not inducted?

  • We shall use AwardsPlayers:
    • lists the playerID’s of players who won baseball awards
    • contains one row for each award awarded in major league baseball

AwardsPlayers

head(AwardsPlayers)
AwardsPlayers

nAwards

# Tally the number of awards in AwardsPlayers by playerID
nAwards <- AwardsPlayers %>% 
  group_by(playerID) %>% 
  tally()
  # resulting column containing the number of awards is called n by default

head(nAwards)
nAwards

inducted

nAwards %>% 
  # Filter to just the players in inducted 
  semi_join(inducted, by = "playerID") 

avg_n

nAwards %>% 
  # Filter to just the players in inducted 
  semi_join(inducted, by = "playerID") %>% 
  # Calculate the mean number of awards per player
  summarize(avg_n = mean(n, na.rm = TRUE))

nominated (& NOT present in inducted)

nAwards %>% 
  # Filter to just the players in nominated 
  semi_join(nominated, by = "playerID") %>%
  # Filter to players NOT in inducted 
  anti_join(inducted, by = "playerID")

avg_n

nAwards %>% 
  # Filter to just the players in nominated 
  semi_join(nominated, by = "playerID") %>%
  # Filter to players NOT in inducted 
  anti_join(inducted, by = "playerID") %>%
  # Calculate the mean number of awards per player
  summarize(avg_n = mean(n, na.rm = TRUE))
# On Average, inductees had 11.95 - 4.23 = 7.72 more awards than non-inductees

Salary

i.e. another way to differentiate inductees from non-inductees

Does the maximum salary earned by inductees tend to be greater than the maximum salary earned by nominees who were not inducted?

notInducted

# Find the players who are in nominated, but not inducted
notInducted <- nominated %>% 
  setdiff(inducted) # using a set operation

head(notInducted)

Filter Salaries

# Filter Salaries against notInducted to return only the salaries of nominees who were not inducted
Salaries %>% 
  # Find the players who are in notInducted
  semi_join(notInducted, by = "playerID") 

summarize()

Salaries %>% 
  # Find the players who are in notInducted
  semi_join(notInducted, by = "playerID") %>%
  # Calculate the max salary by player
  group_by(playerID) %>% 
  summarize(max_salary = max(salary, na.rm = TRUE)) %>% 
  # Calculate the average of the max salaries
  summarize(avg_salary = mean(max_salary, na.rm = TRUE))

inducted

# Repeat for players who were inducted
Salaries %>% 
  semi_join(inducted, by = "playerID")

summarize()

# Repeat for players who were inducted
Salaries %>% 
  semi_join(inducted, by = "playerID") %>% 
  group_by(playerID) %>% 
  summarize(max_salary = max(salary, na.rm = TRUE)) %>% 
  summarize(avg_salary = mean(max_salary, na.rm = TRUE))
# average salary of players who were inducted was $5,079,720 - $4,677,737 = $401,983 more per year

# We know that we have some missing salary information, 

# but what else could be wrong with this analysis?

Retirement

One of the rules of the Hall of Fame is that players cannot be nominated until five years after they retire

Is this reflected in our data?

Appearances

head(Appearances)

Appearances for all nominated players

Appearances %>% 
  # Filter Appearances against nominated
  semi_join(nominated, by = "playerID")

Year of the last appearance of each player nominated to the Hall of Fame

Appearances %>% 
  # Filter Appearances against nominated
  semi_join(nominated, by = "playerID") %>% 
  # Find last year played by player - before retiring
  group_by(playerID) %>% 
  summarize(last_year = max(yearID))

join to HallOfFame

Appearances %>% 
  # Filter Appearances against nominated
  semi_join(nominated, by = "playerID") %>% 
  # Find last year played by player
  group_by(playerID) %>% 
  summarize(last_year = max(yearID)) %>% 
  # Join to full HallOfFame
  left_join(HallOfFame, by = "playerID")

spot unusual observations: nominated before retired

Appearances %>% 
  # Filter Appearances against nominated
  semi_join(nominated, by = "playerID") %>% 
  # Find last year played by player
  group_by(playerID) %>% 
  summarize(last_year = max(yearID)) %>% 
  # Join to full HallOfFame
  left_join(HallOfFame, by = "playerID") %>% 
  # Filter for unusual observations
  # players were nominated before they completely retired
  filter(last_year >= yearID) # yearID variable from HallOfFame
# quite a few players have been nominated before they retired

# but this practice seems much less frequent in recent years.