I need your help!

I want your feedback to make the book better for you and other readers. If you find typos, errors, or places where the text may be improved, please let me know. The best ways to provide feedback are by GitHub or hypothes.is annotations.

You can leave a comment at the bottom of the page/chapter, or open an issue or submit a pull request on GitHub: https://github.com/isaactpetersen/Fantasy-Football-Analytics-Textbook

Hypothesis Alternatively, you can leave an annotation using hypothes.is. To add an annotation, select some text and then click the symbol on the pop-up menu. To see the annotations of others, click the symbol in the upper right-hand corner of the page.

3  Getting Started with R for Data Analysis

The book uses the software R (R Core Team, 2025) for statistical analyses (http://www.r-project.org). R is a free software environment; you can download it at no charge here: https://cran.r-project.org. This chapter provides an overview of how to install and learn the software R, how to troubleshoot code, and how to perform various data management operations.

3.1 Learning Statistics

Here are resources for learning statistics:

3.2 Learning R

Here are a various resources for learning R:

3.3 Getting Help with R

If you have R questions, you can ask them in a number of places:

Salmon (2018) provides additional resources and good guidance for getting help with R: https://masalmon.eu/2018/07/22/wheretogethelp/ (archived at https://perma.cc/4RRE-KL33).

When posting a question on forums or mailing lists, keep a few things in mind:

3.4 Initial Setup

To get started, follow the following steps:

  1. Install R: https://cran.r-project.org

  2. Install RStudio Desktop: https://posit.co/download/rstudio-desktop

  3. After installing RStudio, open RStudio and run the following code in the console to install the R packages used in this book (note: this will take a while):

    Code
    install.packages(c(
      "petersenlab","remotes","knitr","rmarkdown","tidyverse","nflverse",
      "tidymodels","easystats","broom","broom.mixed","psych","downlit","xml2",
      "gsisdecoder","progressr","DescTools","pwr","pwrss","WebPower","XICOR",
      "dagitty","ggdag","ggtext","gghighlight","ggExtra","grid","patchwork",
      "pROC", "lme4","lmerTest","MuMIn","emmeans","pbkrtest","sjstats",
      "AICcmodavg","rstan","brms","tidybayes","bbmle","fitdistrplus","sn",
      "mclust","magrittr","viridis","viridisLite","msir","plotly","webshot2",
      "quantmod","fPortfolio","NMOF","nFactors","xts","zoo","forecast","stringi",
      "parallelly","doParallel","missRanger","ggridges","powerjoin","caret",
      "LongituRF","gpboost","corrplot","mgcv","rms","car","lavaan","lavaanPlot",
      "lavaangui","mice","miceadds","interactions","robustbase","ordinal","MASS",
      "data.table","future","future.apply","SimDesign","domir","GGally"))
  4. Some necessary packages, including the ffanalytics package (Andersen et al., 2025), are hosted in GitHub (and are not hosted on the Comprehensive R Archive Network [CRAN]) and thus need to be installed using the following code (after installing the remotes package (Csárdi et al., 2024) above)1:

    Code
    remotes::install_github("DevPsyLab/petersenlab")
    remotes::install_github("FantasyFootballAnalytics/ffanalytics")
    remotes::install_github("stan-dev/cmdstanr")
Note 3.1: If you are in Professor Petersen’s class

If you are in Professor Petersen’s class, also perform the following steps:

  1. Download and install git: https://git-scm.com/downloads
  2. Set up a free account on GitHub.com.
  3. Download and install GitHub Desktop: https://desktop.github.com
  4. Make sure you are logged into your GitHub account on GitHub.com.
  5. Go to the following GitHub repository: https://github.com/isaactpetersen/QuartoBlogFantasyFootball and complete the following steps:
    1. Click “Use this Template” (in the top right of the screen) > “Create a new repository”
    2. Make sure the checkbox is selected for the following option: “Include all branches”
    3. Make sure your Owner account is selected
    4. Specify the repository name to whatever you want, such as FantasyFootballBlog
    5. Type a brief description, such as Files for my fantasy football blog
    6. Keep the repository public (this is necessary for generating your blog)
    7. Select “Create repository”
  6. After creating the new repository, make sure you are on the page of of your new repository and complete the following steps:
    1. Click “Settings” (in the top of the screen)
    2. Click “Actions” (in the left sidebar) > “General”
    3. Make sure the following are selected:
      • “Read and write permissions” (under “Workflow permissions”)
      • “Allow GitHub Actions to create and approve pull requests”
      • then click “Save”
    4. Click “Pages” (in the left sidebar)
    5. Make sure the following are selected:
      • “Deploy from a branch” (under “Source”)
      • “gh-pages/(root)” (under “Branch”)
      • then click “Save”
  7. Clone the repository to your local computer by clicking “Code” > “Open with GitHub Desktop”, select the folder where you want the repository to be saved on your local computer, and click “Clone”

3.5 Install Packages

You can install R packages using the following syntax:

Code
install.packages("INSERT_PACKAGE_NAME_HERE")

For instance, you can use the following code to install the tidyverse package (Wickham, 2023):

Code
install.packages("tidyverse")

You can also install multiple packages with one line of code (replacing each package with its respective name):

Code
install.packages(c("PACKAGE1","PACKAGE2","PACKAGE3"))

3.6 Load Packages

You can load a given library using the following syntax:

Code
library("INSERT_PACKAGE_NAME_HERE")

For instance, you can use the following code to load the tidyverse package (Wickham, 2023):

Code
library("tidyverse")

3.7 Using Functions and Arguments

A function often takes particular input(s) and produces some form of output. The name of the function is followed by parentheses; the inputs go in between the parentheses. The possible inputs that a function can accept are called “arguments”. You can learn about a particular function and its arguments by entering a question mark before the name of the function:

Code
?NAME_OF_FUNCTION()

Below, we provide examples for how to learn about and use functions and arguments, by using the seq() function as an example. The seq() function creates a sequence of numbers. To learn about the seq() function, which creates a sequence of numbers, you can execute the following command:

Code
?seq()

This is what the documentation shows for the seq() function in the Usage section:

Code
seq(
  from = 1,
  to = 1,
  by = ((to - from)/(length.out - 1)),
  length.out = NULL,
  along.with = NULL,
  ...)

Based on this information, we know that the seq() function takes the following arguments:

  • from
  • to
  • by
  • length.out
  • along.with
  • ...

The arguments have default values that are used if the user does not specify values for the arguments. The default values are provided in the Usage section and are in Table 3.1:

Table 3.1: Arguments and defaults for the seq() function. Arguments with a default of NULL are not used unless a value is provided by the user.
Argument Default Value for Argument
from 1
to 1
by ((to - from)/(length.out - 1))
length.out NULL
along.with NULL

What each argument represents (i.e., the meaning of from, to, by, etc.) is provided in the Arguments section of the documentation. You can specify a function and its arguments either by providing values for each argument in the order indicated by the function, or by naming its arguments. Naming arguments explicitly (rather than merely relying on order) is considered best practice because it is safer—doing so prevents you from accidentally assigning an input to the wrong argument.

Here is an example of providing values to the arguments in the order indicated by the function, to create a sequence of numbers from 1 to 9:

Code
seq(1, 9)
[1] 1 2 3 4 5 6 7 8 9

Here is an example of providing values to the arguments by naming its arguments:

Code
seq(
  from = 1,
  to = 9,
  by = 1)
[1] 1 2 3 4 5 6 7 8 9

If you provide values to arguments by naming the arguments, you can reorder the arguments and get the same answer:

Code
seq(
  by = 1,
  to = 9,
  from = 1)
[1] 1 2 3 4 5 6 7 8 9

There are various combinations of arguments that one could use to obtain the same result. For instance, here is code to generate a sequence from 1 to 9 by 2:

Code
seq(
  from = 1,
  to = 9,
  by = 2)
[1] 1 3 5 7 9

Or, alternatively, you could specify the length of the desired sequence (5 values):

Code
seq(
  from = 1,
  to = 9,
  length.out = 5)
[1] 1 3 5 7 9

If you want to generate a series with decimal values, you could specify a long desired sequence of 81 values:

Code
seq(
  from = 1,
  to = 9,
  length.out = 81)
 [1] 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8
[20] 2.9 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7
[39] 4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 6.0 6.1 6.2 6.3 6.4 6.5 6.6
[58] 6.7 6.8 6.9 7.0 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 8.0 8.1 8.2 8.3 8.4 8.5
[77] 8.6 8.7 8.8 8.9 9.0

This is equivalent to specifying a sequence from 1 to 9 by 0.1:

Code
seq(
  from = 1,
  to = 9,
  by = 0.1)
 [1] 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8
[20] 2.9 3.0 3.1 3.2 3.3 3.4 3.5 3.6 3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7
[39] 4.8 4.9 5.0 5.1 5.2 5.3 5.4 5.5 5.6 5.7 5.8 5.9 6.0 6.1 6.2 6.3 6.4 6.5 6.6
[58] 6.7 6.8 6.9 7.0 7.1 7.2 7.3 7.4 7.5 7.6 7.7 7.8 7.9 8.0 8.1 8.2 8.3 8.4 8.5
[77] 8.6 8.7 8.8 8.9 9.0

Hopefully, that provides an example for how to learn about a particular function, its arguments, and how to use them.

3.8 Create a Vector

A vector is a series of elements that can be numeric or character. Character elements should be specified in quotes. A vector has one dimension (length). To create a vector, use the c() function to combine elements into a vector (“c” stands for combine). And, we use the assignment operator (<-) to assign the vector to an object named exampleVector, so we can access it later. Anything on the right side of an assignment operator gets assigned to the object name on the left side of the assignment operator.

Code
exampleVector <- c(40, 30, 24, 20, 18, 23, 27, 32, 26, 23, NA, 37)

exampleVector2 <- c(
  "A","B1","B2","This is a sentence.","This is another sentence.")

We can then access the contents of the object by calling its name:

Code
exampleVector
 [1] 40 30 24 20 18 23 27 32 26 23 NA 37

3.9 Create a Data Frame

A data frame has two dimensions: rows and columns. Here is an example of creating a data frame, while using the assignment operator (<-) to assign the data frame to an object so we can access it later:

Code
players <- data.frame(
  ID = 1:12,
  name = c(
    "Ken Cussion",
    "Ben Sacked",
    "Chuck Downfield",
    "Ron Ingback",
    "Rhonda Ball",
    "Hugo Long",
    "Lionel Scrimmage",
    "Drew Blood",
    "Chase Emdown",
    "Justin Time",
    "Spike D'Ball",
    "Isac Ulooz"),
  position = c("QB","QB","QB","RB","RB","WR","WR","WR","WR","TE","TE","LB"),
  age = c(40, 30, 24, 20, 18, 23, 27, 32, 26, 23, NA, 37)
  )

fantasyPoints <- data.frame(
  ID = c(2, 7, 13, 14),
  fantasyPoints = c(250, 170, 65, 15)
)

fantasyPoints_weekly <- expand.grid(
  ID = 1:12,
  season = c(2022, 2023),
  week = 1:17
)

set.seed(52242)
fantasyPoints_weekly$fantasyPoints <- sample(
  0:35,
  size = nrow(fantasyPoints_weekly),
  replace = TRUE
)

3.10 Create a List

A list can store multiple data frames in one object:

Code
exampleList <- list(players, fantasyPoints, fantasyPoints_weekly)

3.11 Load a Data Frame

Here is how you load a .RData file using a relative path (i.e., a path relative to the working directory, where the working directory is represented by a period):

Code
load(file = "./data/nfl_players.RData")

The direction of the slashes matters—you should use forward slashes (not backslashes)! To determine where you working directory is, you can type:

Code
getwd()
[1] "/home/runner/work/Fantasy-Football-Analytics-Textbook/Fantasy-Football-Analytics-Textbook"

To change your working directory, you can type:

Code
setwd("C:/Users/myusername/")

The following code loads a file from an absolute path:

Code
nfl_players <- read.csv("C:/Users/myusername/nfl_players.RData")

Here is how you load a .csv file:

Code
nfl_players <- read.csv("./data/nfl_players.csv") # relative path
nfl_players <- read.csv("C:/Users/myusername/nfl_players.csv") # absolute path

3.12 Save a Data Frame

Here is how you save a .RData file using a relative path (i.e., relative to your working directory):

Code
save(
  nfl_players,
  file = "./data/nfl_players.RData")

The direction of the slashes matters—you should use forward slashes (not backslashes)! The following code saves a file to an absolute path:

Code
save(
  nfl_players,
  file = "C:/Users/myusername/nfl_players.RData")

Here is how you save a .csv file:

Code
write.csv(
  nfl_players,
  file = "./data/nfl_players.csv") # relative path

write.csv(
  nfl_players,
  file = "C:/Users/myusername/nfl_players.csv") # absolute path

3.13 Variable Names

To see the names of variables in a data frame, use the following syntax:

Code
names(nfl_players)
 [1] "gsis_id"                  "first_name"              
 [3] "last_name"                "position"                
 [5] "esb_id"                   "display_name"            
 [7] "rookie_year"              "college_conference"      
 [9] "current_team_id"          "draft_club"              
[11] "draft_number"             "draftround"              
[13] "entry_year"               "football_name"           
[15] "gsis_it_id"               "headshot"                
[17] "jersey_number"            "position_group"          
[19] "short_name"               "smart_id"                
[21] "status"                   "status_description_abbr" 
[23] "status_short_description" "team_abbr"               
[25] "uniform_number"           "height"                  
[27] "weight"                   "college_name"            
[29] "years_of_experience"      "birth_date"              
[31] "team_seq"                 "suffix"                  
Code
names(players)
[1] "ID"       "name"     "position" "age"     
Code
names(fantasyPoints)
[1] "ID"            "fantasyPoints"

3.14 Logical Operators

Logical Operators evaluate one or more elements for a condition, and return TRUE, FALSE, or if the element was missing, NA.

3.14.1 Is Equal To: ==

Code
players$position == "RB"
 [1] FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE

3.14.2 Is Not Equal To: !=

Code
players$position != "RB"
 [1]  TRUE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE

3.14.3 Is Greater Than: >

Code
players$age > 30
 [1]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE    NA  TRUE

3.14.4 Is Less Than: <

Code
players$age < 30
 [1] FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE    NA FALSE

3.14.5 Is Greater Than or Equal To: >=

Code
players$age >= 30
 [1]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE    NA  TRUE

3.14.6 Is Less Than or Equal To: <=

Code
players$age <= 30
 [1] FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE    NA FALSE

3.14.7 Is In a Value of Another Vector: %in%

Code
players$position %in% c("RB","WR")
 [1] FALSE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE FALSE

3.14.8 Is Not In a Value of Another Vector: !(%in%)

Code
!(players$position %in% c("RB","WR"))
 [1]  TRUE  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE  TRUE

The petersenlab package (Petersen, 2025) has a convenience function, %ni%, for the logical operator of determining whether an element is not in a value of another vector:

Code
players$position %ni% c("RB","WR")
Error in players$position %ni% c("RB", "WR"): could not find function "%ni%"

3.14.9 Is Missing: is.na()

Code
is.na(players$age)
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE

3.14.10 Is Not Missing: !is.na()

Code
!is.na(players$age)
 [1]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE

3.14.11 And: &

The “and” operator (&) is used to string together multiple logical operators that all must evaluate as TRUE in order for the combined evaluation to evaluate as TRUE; otherwise, the combined evaluation evaluates as FALSE.

Code
players$position == "WR" & players$age > 26
 [1] FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE

3.14.12 Or: |

The “or” operator (|) is used to string together multiple logical operators, any of which must evaluate as TRUE in order for the combined evaluation to evaluate as TRUE; otherwise, the combined evaluation evaluates as FALSE.

Code
players$position == "WR" | players$age > 23
 [1]  TRUE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE FALSE    NA  TRUE

3.14.13 which()

The operator, which(), can be used to obtain the TRUE indices of an object, which is useful for subsetting.

Code
which(players$position == "RB")
[1] 4 5

3.15 If…Else Conditions

We can use the construction, if()...else if()...else() if we want to perform conditional operations. The typical construction of if()...else if()...else() operates such that it first checks if the first if() condition is true. If the first if() condition is true, it performs the operation specified and terminates the process. If the first if() condition is not true, it checks the else if() conditions in order until one of them is true. There can be multiple else if() conditions. For the first true else if() condition, it performs the operation specified and terminates the process. If none of the else if() conditions is true, it performs the operation specified under else() and then terminates the process. The construction, if()...else if()...else() can only be used on one value at a time.

Code
player_rank <- 15

if(player_rank <= 10){ # check this condition first
  tier <- 1
  print(tier)
} else if(player_rank <= 20){ # if first condition was not met, check this condition next
  tier <- 2
  print(tier)
} else if(player_rank <= 30){ # if first two conditions were not met, check this condition next
  tier <- 3
  print(tier)
} else{ # if all other conditions were not met, then do this
  print("Don't draft this player!")
}
[1] 2

To apply conditional operations to a vector, we can use the ifelse() function.

Code
player_rank <- c(1, 10, 20, 40, 100)

tier <- ifelse(
  player_rank <= 10, # check this condition
  1, # assign this value if true
  2) # assign this value if false

tier
[1] 1 1 2 2 2

3.16 Piping

In base R, if you want to perform multiple operations, it is common to either a) nest the operations, or b) save the object at each step.

Below is an example of nested operations:

Code
length(names(nfl_players))
[1] 32

Below is an example of saving the intermediate object at each step:

Code
variableNames <- names(nfl_players)
variableNames
 [1] "gsis_id"                  "first_name"              
 [3] "last_name"                "position"                
 [5] "esb_id"                   "display_name"            
 [7] "rookie_year"              "college_conference"      
 [9] "current_team_id"          "draft_club"              
[11] "draft_number"             "draftround"              
[13] "entry_year"               "football_name"           
[15] "gsis_it_id"               "headshot"                
[17] "jersey_number"            "position_group"          
[19] "short_name"               "smart_id"                
[21] "status"                   "status_description_abbr" 
[23] "status_short_description" "team_abbr"               
[25] "uniform_number"           "height"                  
[27] "weight"                   "college_name"            
[29] "years_of_experience"      "birth_date"              
[31] "team_seq"                 "suffix"                  
Code
lengthOfVariableNames <- length(variableNames)
lengthOfVariableNames
[1] 32

Code for performing nested operations can be challenging to read. Saving the intermediate object can be a waste of time to do if you are not interested in the intermediate object, and can take up unnecessary memory and computational resources. An alternative approach is to use piping. Piping allows taking the result from one computation and sending it to the next computation, thus allowing a chain of computations without saving the intermediate object at each step.

In base R, you can perform piping with the |> expression. In tidyverse you can perform piping with the %>% expression.

3.16.0.1 Base R

Code
nfl_players |>
  names() |>
  length()
[1] 32

3.16.0.2 Tidyverse

Code
nfl_players %>%
  names() %>%
  length()
[1] 32

3.17 Subset

To subset a vector, use brackets to specify the elements to keep:

Code
vector[elementsToKeep]

To subset a data frame, use brackets to specify the subset of rows and columns to keep, where the value/vector before the comma specifies the rows to keep, and the value/vector after the comma specifies the columns to keep:

Code
dataframe[rowsToKeep, columnsToKeep]

You can subset by using any of the following:

  • numeric indices of the elements/rows/columns to keep (or drop)
  • names of the rows/columns to keep (or drop)
  • values of TRUE and FALSE corresponding to which elements/rows/columns to keep

3.17.1 One Variable

To subset one variable, use the following syntax:

Code
players$name
 [1] "Ken Cussion"      "Ben Sacked"       "Chuck Downfield"  "Ron Ingback"     
 [5] "Rhonda Ball"      "Hugo Long"        "Lionel Scrimmage" "Drew Blood"      
 [9] "Chase Emdown"     "Justin Time"      "Spike D'Ball"     "Isac Ulooz"      

or:

Code
players[,"name"]
 [1] "Ken Cussion"      "Ben Sacked"       "Chuck Downfield"  "Ron Ingback"     
 [5] "Rhonda Ball"      "Hugo Long"        "Lionel Scrimmage" "Drew Blood"      
 [9] "Chase Emdown"     "Justin Time"      "Spike D'Ball"     "Isac Ulooz"      

3.17.2 Particular Rows of One Variable

To subset one variable, use the following syntax:

Code
players$name[which(players$position == "RB")]
[1] "Ron Ingback" "Rhonda Ball"

or:

Code
players[which(players$position == "RB"), "name"]
[1] "Ron Ingback" "Rhonda Ball"

3.17.3 Particular Columns (Variables)

To subset particular columns/variables, use the following syntax:

3.17.3.1 Base R

Code
subsetVars <- c("name","age")

players[,c(2,4)]
Code
players[,c("name","age")]
Code
players[,subsetVars]

Or, to drop columns:

Code
dropVars <- c("name","age")

players[,-c(2,4)]
Code
players[,!(names(players) %in% c("name","age"))]
Code
players[,!(names(players) %in% dropVars)]

3.17.3.2 Tidyverse

Code
players %>%
  dplyr::select(name, age)
Code
players %>%
  dplyr::select(name:age)
Code
players %>%
  dplyr::select(all_of(subsetVars))

Or, to drop columns:

Code
players %>%
  dplyr::select(-name, -age)
Code
players %>%
  dplyr::select(-c(name:age))
Code
players %>%
  dplyr::select(-all_of(dropVars))

3.17.4 Particular Rows

To subset particular rows, use the following syntax:

3.17.4.1 Base R

Code
subsetRows <- c(4,5)

players[c(4,5),]
Code
players[subsetRows,]
Code
players[which(players$position == "RB"),]

3.17.4.2 Tidyverse

Code
players %>%
  filter(position == "WR")
Code
players %>%
  filter(position == "WR", age <= 26)
Code
players %>%
  filter(position == "WR" | age >= 26)

3.17.5 Particular Rows and Columns

To subset particular rows and columns, use the following syntax:

3.17.5.1 Base R

Code
players[c(4,5), c(2,4)]
Code
players[subsetRows, subsetVars]
Code
players[which(players$position == "RB"), subsetVars]

3.17.5.2 Tidyverse

Code
players %>%
  dplyr::filter(position == "RB") %>%
  dplyr::select(all_of(subsetVars))

3.18 View Data

3.18.1 All Data

To view data, use the following syntax:

Code
View(players)

3.18.2 First 6 Rows/Elements

To view only the first six rows (if a data frame) or elements (if a vector), use the following syntax:

Code
head(nfl_players)
Code
head(nfl_players$display_name)
[1] "'Omar Ellison"    "A'Shawn Robinson" "A.J. Arcuri"      "A.J. Barner"     
[5] "A.J. Bouye"       "A.J. Brown"      

3.19 Sort Data

Code
players %>% 
  arrange(position, age) #sort by position (ascending) then by age (ascending)
Code
players %>% 
  arrange(position, -age) #sort by position (ascending) then by age (descending)

3.20 Data Characteristics

3.20.1 Data Structure

Code
str(nfl_players)
nflvrs_d [21,419 × 32] (S3: nflverse_data/tbl_df/tbl/data.table/data.frame)
 $ gsis_id                 : chr [1:21419] "00-0004866" "00-0032889" "00-0037845" "00-0039793" ...
 $ first_name              : chr [1:21419] "'Omar" "A'Shawn" "A.J." "A.J." ...
 $ last_name               : chr [1:21419] "Ellison" "Robinson" "Arcuri" "Barner" ...
 $ position                : chr [1:21419] "WR" "DE" "T" "TE" ...
 $ esb_id                  : chr [1:21419] "ELL711319" "ROB367960" "ARC716900" "BAR235889" ...
 $ display_name            : chr [1:21419] "'Omar Ellison" "A'Shawn Robinson" "A.J. Arcuri" "A.J. Barner" ...
 $ rookie_year             : int [1:21419] NA 2016 2022 2024 2013 2019 2015 2019 NA NA ...
 $ college_conference      : chr [1:21419] NA "Southeastern Conference" "Big Ten Conference" "Big Ten Conference" ...
 $ current_team_id         : chr [1:21419] "4400" "0750" "2510" "4600" ...
 $ draft_club              : chr [1:21419] NA "DET" "LA" "SEA" ...
 $ draft_number            : int [1:21419] NA 46 261 121 NA 51 67 NA NA NA ...
 $ draftround              : int [1:21419] NA 2 7 4 NA 2 3 NA NA NA ...
 $ entry_year              : int [1:21419] NA 2016 2022 2024 2013 2019 2015 2019 NA NA ...
 $ football_name           : chr [1:21419] NA "A'Shawn" "A.J." "A.J." ...
 $ gsis_it_id              : int [1:21419] NA 43335 54726 57242 40688 47834 42410 48335 NA NA ...
 $ headshot                : chr [1:21419] NA "https://static.www.nfl.com/image/upload/f_auto,q_auto/league/erkfjohq7j2kqigny727" "https://static.www.nfl.com/image/upload/f_auto,q_auto/league/rqisaua66gwc8wxgajz1" "https://static.www.nfl.com/image/upload/f_auto,q_auto/league/msnzbeyjoemcas9dm8vt" ...
 $ jersey_number           : int [1:21419] 84 94 61 88 24 11 60 6 81 63 ...
 $ position_group          : chr [1:21419] "WR" "DL" "OL" "TE" ...
 $ short_name              : chr [1:21419] NA "A.Robinson" "A.Arcuri" "A.Barner" ...
 $ smart_id                : chr [1:21419] "3200454c-4c71-1319-728e-d49d3d236f8f" "3200524f-4236-7960-bf20-bc060ac0f49c" "32004152-4371-6900-5185-8cdd66b2ad11" "32004241-5223-5889-95d9-0ba3aeeb36ed" ...
 $ status                  : chr [1:21419] "RET" "ACT" "ACT" "ACT" ...
 $ status_description_abbr : chr [1:21419] NA "A01" "A01" "A01" ...
 $ status_short_description: chr [1:21419] NA "Active" "Active" "Active" ...
 $ team_abbr               : chr [1:21419] "LAC" "CAR" "LA" "SEA" ...
 $ uniform_number          : chr [1:21419] NA "94" "61" "88" ...
 $ height                  : num [1:21419] 73 76 79 78 72 72 75 76 69 76 ...
 $ weight                  : int [1:21419] 200 330 320 251 191 226 325 220 190 280 ...
 $ college_name            : chr [1:21419] NA "Alabama" "Michigan State" "Michigan" ...
 $ years_of_experience     : chr [1:21419] "2" "9" "2" "0" ...
 $ birth_date              : chr [1:21419] "1971-10-08" "1995-03-21" "1997-08-13" "2002-05-03" ...
 $ team_seq                : int [1:21419] NA 1 NA NA 1 1 1 1 NA NA ...
 $ suffix                  : chr [1:21419] NA NA NA NA ...
 - attr(*, "nflverse_type")= chr "players"
 - attr(*, "nflverse_timestamp")= chr "2025-05-31 22:15:12 EDT"
 - attr(*, ".internal.selfref")=<externalptr> 

3.20.2 Data Dimensions

Number of rows and columns:

Code
dim(nfl_players)
[1] 21419    32

Number of rows:

Code
nrow(nfl_players)
[1] 21419

Number of columns:

Code
ncol(nfl_players)
[1] 32

3.20.3 Number of Elements

Code
length(nfl_players$display_name)
[1] 21419

3.20.4 Number of Missing Elements

Missing elements are stored in R as NA. You can determine how many missing elements there are in a data frame using the following syntax:

Code
length(which(is.na(nfl_players)))
[1] 239935

You can determine how many missing elements there are in a variable using the following syntax:

Code
length(nfl_players$college_name[which(is.na(nfl_players$college_name))])
[1] 12127

3.20.5 Number of Non-Missing Elements

You can determine how many non-missing elements there are in a data frame using the following syntax:

Code
length(which(!is.na(nfl_players)))
[1] 445473

You can determine how many rows in the data frame have no missing data using the following syntax:

Code
nrow(na.omit(nfl_players))
[1] 176

You can determine how many non-missing elements there are in a vector using the following syntax:

Code
length(nfl_players$college_name[which(!is.na(nfl_players$college_name))])
[1] 9292
Code
length(na.omit(nfl_players$college_name))
[1] 9292

3.21 Create New Variables

To create a new variable, use the following syntax:

Code
players$newVar <- NA

Here is an example of creating a new variable:

Code
players$newVar <- 1:nrow(players)

3.22 Recode Variables

Here is an example of recoding a variable:

Code
players$oldVar1 <- NA
players$oldVar1[which(players$position == "QB")] <- "quarterback"
players$oldVar1[which(players$position == "RB")] <- "running back"
players$oldVar1[which(players$position == "WR")] <- "wide receiver"
players$oldVar1[which(players$position == "TE")] <- "tight end"

players$oldVar2 <- NA
players$oldVar2[which(players$age < 30)] <- "young"
players$oldVar2[which(players$age >= 30)] <- "old"

Recode multiple variables:

Code
players %>%
  dplyr::mutate(across(c(
    oldVar1:oldVar2),
    ~ case_match(
      .,
      c("quarterback","old","running back") ~ 0,
      c("wide receiver","tight end","young") ~ 1)))

3.23 Rename Variables

Code
players <- players %>% 
  dplyr::rename(
    newVar1 = oldVar1,
    newVar2 = oldVar2)

Using a vector of variable names:

Code
varNamesFrom <- c("oldVar1","oldVar2")
varNamesTo <- c("newVar1","newVar2")

players <- players %>% 
  dplyr::rename_with(~ varNamesTo, all_of(varNamesFrom))

3.24 Convert the Types of Variables

One variable:

Code
players$factorVar <- factor(players$ID)
players$numericVar <- as.numeric(players$age)
players$integerVar <- as.integer(players$newVar1)
players$characterVar <- as.character(players$newVar2)

Multiple variables:

Code
players %>%
  dplyr::mutate(across(c(
    ID,
    age),
    as.numeric))
Code
players %>%
  dplyr::mutate(across(
    age:newVar1,
    as.character))
Code
players %>%
  dplyr::mutate(across(
    where(is.factor),
    as.character))

3.25 Merging/Joins

3.25.1 Overview

Merging (also called joining) merges two data objects using a shared set of variables called “keys.” The keys are the variable(s) that are used to align the rows from the two objects. The data for the given key(s) in the first object get paired with (i.e., get placed in the same row as) the data for that same key in the second object. In general, each row should have a value on each of the keys; there should be no missingness in the keys. To merge two objects, the key(s) that will be used to match the records must be present in both objects. The keys are used to merge the variables in object 1 (x) with the variables in object 2 (y). Different merge types select different rows to merge.

For some data objects, you might want to combine information for the same player from multiple data objects. If each data object is in player form (i.e., player_id uniquely identifies each row), you might merge by the player’s identification number (e.g., player_id). In this case, the key uniquely identifies each row.

However, some data objects have multiple keys. For instance, in long form data objects, each player may have multiple rows corresponding to multiple seasons. In this case, the keys may be player_id and season—that is, the data are in player-season form. If object 1 and object 2 are both in player-season form, we would use player_id and season as the keys to merge the two objects. In this case, the keys uniquely identify each row; that is, they account for the levels of nesting.

However, if the data objects are of different form, we would select the keys as the variable(s) that represent the lowest common denominator of variables used to join the data objects that are present in both objects. For instance, assume that object 1 is in player-season form. For object 2, each player has multiple rows corresponding to seasons and games/weeks—in this case, object 2 is in player-season-week form. Object 1 does not have the week variable, so it cannot be used to join the objects. Thus, we would use player_id and season as the keys to merge the two objects, because both variables are present in both objects.

It is important not to have rows with duplicate values on the keys. For instance, if there is more than one row with the same player_id in each object (or multiple rows in object 2 with the same combination of player_id, season, and week), then each row with that player_id in object 1 gets paired with each row with that player_id in object 2. The many possible combinations can lead to the resulting object greatly expanding in terms of the number of rows. Thus, you want the keys to uniquely identify each row. In the example below, player is present in each object, so we can merge by player; however, each object has multiple rows with the same player. For example, mergeExample1A has three rows for player A; mergeExample1B has two rows for player A. Thus, when we merge them, the resulting object has many more rows than each respective object (even though neither object has players that the other object does not).

Code
mergeExample1A <- data.frame(
  player = c("A","A","A","B","B"),
  age = c(20,22,24,26,28)
)

mergeExample1B <- data.frame(
  player = c("A","A","B","B"),
  points = c(10,15,20,25)
)

mergeExample1 <- dplyr::full_join(
  mergeExample1A,
  mergeExample1B,
  by = "player")

mergeExample1
Code
dim(mergeExample1)
[1] 10  3

Note: if the two objects include variables with the same name (apart from the keys), R will not know how you want each to appear in the merged object. So, it will add a suffix (e.g., .x, .y) to each common variable to indicate which object (i.e., object x or object y) the variable came from, where object x is the first object—i.e., the object to which object y (the second object) is merged. In general, apart from the keys, you should not include variables with the same name in two objects to be merged. To prevent this, either remove or rename the shared variable in one of the objects, or include the shared variable as a key. However, as described above, you should include it as a key only if you want to use its values to align the rows from each object. Below is an example of merging two objects with the same variable name (i.e., points) that is not used as a key.

Code
mergeExample2A <- data.frame(
  player = c("A","B","C","D","E"),
  points = c(20,22,24,26,28)
)

mergeExample2B <- data.frame(
  player = c("A","B","C","F"),
  points = c(10,15,20,25)
)

mergeExample2 <- dplyr::full_join(
  mergeExample2A,
  mergeExample2B,
  by = "player")

mergeExample2

When two objects are merged that have different formats, the resulting data object inherits the format of the data object that has more levels of nesting. For instance, consider that you want to merge two objects, object A and object B. Object A is in player form and object B is in player-season-week form. When you merge them, the resulting data object will be in player-season-week form.

Code
mergeExample3A <- data.frame(
  player = c("A","B","C","D","E"),
  weight = c(225,250,275,300,325)
)

mergeExample3B <- data.frame(
  player = c("A","A","A","A","B","B"),
  season = c(2023,2023,2024,2024,2024,2024),
  week = c(1,2,1,2,3,4),
  points = c(10,15,20,25,30,35)
)

mergeExample3 <- dplyr::full_join(
  mergeExample3A,
  mergeExample3B,
  by = "player")

mergeExample3

3.25.2 Data Before Merging

Here are the data in the players object:

Code
players
Code
dim(players)
[1] 12 10

The data are structured in ID form. That is, every row in the dataset is uniquely identified by the variable, ID.

Here are the data in the fantasyPoints object:

Code
fantasyPoints
Code
dim(fantasyPoints)
[1] 4 2

3.25.3 Types of Joins

3.25.3.1 Visual Overview of Join Types

Figure 3.1 depicts various types of merges/joins. Object x is the circle labeled as x. Object y is the circle labeled as y. The area of overlap in the Venn diagram indicates the rows on the keys that are shared between the two objects (e.g., the same player_id, season, and week). The non-overlapping area indicates the rows on the keys that are unique to each object. The shaded blue area indicates which rows (on the keys) are kept in the merged object from each of the two objects, when using each of the merge types. For instance, a left outer join keeps the shared rows and the rows that are unique to object x, but it drops the rows that are unique to object y.

Types of Merges/Joins.
Figure 3.1: Types of Merges/Joins.

3.25.3.2 Full Outer Join

A full outer join includes all rows in x or y. It returns columns from x and y. Here is how to merge two data frames using a full outer join (i.e., “full join”):

Code
fullJoinData <- dplyr::full_join(
  players,
  fantasyPoints,
  by = "ID")

fullJoinData
Code
dim(fullJoinData)
[1] 14 11

3.25.3.3 Left Outer Join

A left outer join includes all rows in x. It returns columns from x and y. Here is how to merge two data frames using a left outer join (“left join”):

Code
leftJoinData <- dplyr::left_join(
  players,
  fantasyPoints,
  by = "ID")

leftJoinData
Code
dim(leftJoinData)
[1] 12 11

3.25.3.4 Right Outer Join

A right outer join includes all rows in y. It returns columns from x and y. Here is how to merge two data frames using a right outer join (“right join”):

Code
rightJoinData <- dplyr::right_join(
  players,
  fantasyPoints,
  by = "ID")

rightJoinData
Code
dim(rightJoinData)
[1]  4 11

3.25.3.5 Inner Join

An inner join includes all rows that are in both x and y. An inner join will return one row of x for each matching row of y, and can duplicate values of records on either side (left or right) if x and y have more than one matching record. It returns columns from x and y. Here is how to merge two data frames using an inner join:

Code
innerJoinData <- dplyr::inner_join(
  players,
  fantasyPoints,
  by = "ID")

innerJoinData
Code
dim(innerJoinData)
[1]  2 11

3.25.3.6 Semi Join

A semi join is a filter. A left semi join returns all rows from x with a match in y. That is, it filters out records from x that are not in y. Unlike an inner join, a left semi join will never duplicate rows of x, and it includes columns from only x (not from y). Here is how to merge two data frames using a left semi join:

Code
semiJoinData <- dplyr::semi_join(
  players,
  fantasyPoints,
  by = "ID")

semiJoinData
Code
dim(semiJoinData)
[1]  2 10

3.25.3.7 Anti Join

An anti join is a filter. A left anti join returns all rows from x without a match in y. That is, it filters out records from x that are in y. It returns columns from only x (not from y). Here is how to merge two data frames using a left anti join:

Code
antiJoinData <- dplyr::anti_join(
  players,
  fantasyPoints,
  by = "ID")

antiJoinData
Code
dim(antiJoinData)
[1] 10 10

3.25.3.8 Cross Join

A cross join combines each row in x with each row in y.

Code
crossJoinData <- dplyr::cross_join(
  players,
  fantasyPoints)

crossJoinData
Code
dim(crossJoinData)
[1] 48 12

3.26 Transform Data from Long to Wide

Depending on the analysis, it may be important to restructure the data to be in long or wide form. When the data are in wide form, each player has only one row. When the data are in long form, each player has multiple rows—e.g., a row for each game. The data structure is called wide or long form because a dataset in wide form has more columns and fewer rows (i.e., it appears wider and shorter), whereas a dataset in long form has more rows and fewer columns (i.e., it appears narrower and taller).

Here are the original data in long form. The data are structured in “player-season-week form”. That is, every row in the dataset is uniquely identified by the combination of variables, ID, season, and week—these are the keys. This is an example of long form, because each player has multiple rows.

Code
dataLong <- dplyr::full_join(
  players %>% dplyr::select(-age),
  fantasyPoints_weekly,
  by = c("ID")
) %>% 
  arrange(ID, season, week)

dataLong
Code
dim(dataLong)
[1] 408  12
Code
names(dataLong)
 [1] "ID"            "name"          "position"      "newVar1"      
 [5] "newVar2"       "factorVar"     "numericVar"    "integerVar"   
 [9] "characterVar"  "season"        "week"          "fantasyPoints"

Below, we widen the data by two variables (season and week), using tidyverse, so that the data are now in “player form” (where each row is uniquely identified by the ID variable):

Code
dataWide <- dataLong %>% 
  tidyr::pivot_wider(
    names_from = c(season, week),
    names_glue = "{.value}_{season}_week{week}",
    values_from = fantasyPoints)

dataWide
Code
dim(dataWide)
[1] 12 43
Code
names(dataWide)
 [1] "ID"                        "name"                     
 [3] "position"                  "newVar1"                  
 [5] "newVar2"                   "factorVar"                
 [7] "numericVar"                "integerVar"               
 [9] "characterVar"              "fantasyPoints_2022_week1" 
[11] "fantasyPoints_2022_week2"  "fantasyPoints_2022_week3" 
[13] "fantasyPoints_2022_week4"  "fantasyPoints_2022_week5" 
[15] "fantasyPoints_2022_week6"  "fantasyPoints_2022_week7" 
[17] "fantasyPoints_2022_week8"  "fantasyPoints_2022_week9" 
[19] "fantasyPoints_2022_week10" "fantasyPoints_2022_week11"
[21] "fantasyPoints_2022_week12" "fantasyPoints_2022_week13"
[23] "fantasyPoints_2022_week14" "fantasyPoints_2022_week15"
[25] "fantasyPoints_2022_week16" "fantasyPoints_2022_week17"
[27] "fantasyPoints_2023_week1"  "fantasyPoints_2023_week2" 
[29] "fantasyPoints_2023_week3"  "fantasyPoints_2023_week4" 
[31] "fantasyPoints_2023_week5"  "fantasyPoints_2023_week6" 
[33] "fantasyPoints_2023_week7"  "fantasyPoints_2023_week8" 
[35] "fantasyPoints_2023_week9"  "fantasyPoints_2023_week10"
[37] "fantasyPoints_2023_week11" "fantasyPoints_2023_week12"
[39] "fantasyPoints_2023_week13" "fantasyPoints_2023_week14"
[41] "fantasyPoints_2023_week15" "fantasyPoints_2023_week16"
[43] "fantasyPoints_2023_week17"

3.27 Transform Data from Wide to Long

Conversely, we can also restructure data from wide to long. Here are the data in long form, after they have been transformed from wide form using tidyverse:

Code
dataLong <- dataWide %>% 
  tidyr::pivot_longer(
    cols = fantasyPoints_2022_week1:fantasyPoints_2023_week17,
    names_to = c("season", "week"),
    names_pattern = "fantasyPoints_(.*)_week(.*)",
    values_to = "fantasyPoints")

dataLong
Code
dim(dataLong)
[1] 408  12
Code
names(dataLong)
 [1] "ID"            "name"          "position"      "newVar1"      
 [5] "newVar2"       "factorVar"     "numericVar"    "integerVar"   
 [9] "characterVar"  "season"        "week"          "fantasyPoints"

3.28 Loops

If you want to perform the same computation multiple times, it can be faster to do it in a loop compared to writing out the same computation many times. For instance, here is a loop that runs from 1 to 12 (the number of players in the players object), incrementing by 1 after each iteration. The loop prints each element of a vector (i.e., the player’s name) and the loop index (i) that indicates where the loop is in terms of its iterations:

Code
for(i in 1:length(players$ID)){
  print(paste("The loop is at index:", i, sep = " "))
  print(paste("My favorite player is:", players$name[i], sep = " "))
}
[1] "The loop is at index: 1"
[1] "My favorite player is: Ken Cussion"
[1] "The loop is at index: 2"
[1] "My favorite player is: Ben Sacked"
[1] "The loop is at index: 3"
[1] "My favorite player is: Chuck Downfield"
[1] "The loop is at index: 4"
[1] "My favorite player is: Ron Ingback"
[1] "The loop is at index: 5"
[1] "My favorite player is: Rhonda Ball"
[1] "The loop is at index: 6"
[1] "My favorite player is: Hugo Long"
[1] "The loop is at index: 7"
[1] "My favorite player is: Lionel Scrimmage"
[1] "The loop is at index: 8"
[1] "My favorite player is: Drew Blood"
[1] "The loop is at index: 9"
[1] "My favorite player is: Chase Emdown"
[1] "The loop is at index: 10"
[1] "My favorite player is: Justin Time"
[1] "The loop is at index: 11"
[1] "My favorite player is: Spike D'Ball"
[1] "The loop is at index: 12"
[1] "My favorite player is: Isac Ulooz"

Loops are fine for basic computations, but other approaches (such as the apply() family of functions) can be even faster than loops.

3.29 Create a Function

Now, let’s put together what we have learned to create a useful function. Functions are useful if you want to perform an operation multiple times, especially if you want to be able to manipulate various settings to make slight modifications each time. Any operation that you want to perform multiple times, you can create a function to accomplish. Use of a function can save you time without needed to retype out all of the code each time. For instance, let’s say you want to convert temperature between Fahrenheit and Celsius, you could create a function to do that. In this case, our function has two arguments: temperature (in degrees) and unit of the original temperature (F for Fahrenheit or C for Celsius, where the default unit is Fahrenheit). That allows us to make the slight modification of which unit is the input temperature and to change the calculation accordingly.

Code
convert_temperature <- function(temperature, unit = "F"){ # specify the arguments and any defaults
  if(unit == "F"){ # if the input temperature(s) in Fahrenheit
    newtemp <- (temperature - 32) / (9/5)
  } else if(unit == "C"){ # if the input temperature(s) in Celsius
    newtemp <- (temperature * (9/5)) + 32
  }
  
  return(newtemp) # this is what is returned by the function
}

Now we can use the function to convert temperatures between Fahrenheit and Celsius. A temperature of 32°F is equal to 0°C. A temperature of 0°C is equal to 89.6°F.

Code
convert_temperature(
  temperature = 32,
  unit = "F"
)
[1] 0
Code
convert_temperature(
  temperature = 32,
  unit = "C"
)
[1] 89.6

We can also convert the temperature for a vector of values at once:

Code
convert_temperature(
  temperature = c(0, 10, 20, 30, 40, 50),
  unit = "F"
)
[1] -17.777778 -12.222222  -6.666667  -1.111111   4.444444  10.000000
Code
convert_temperature(
  temperature = c(0, 10, 20, 30, 40, 50),
  unit = "C"
)
[1]  32  50  68  86 104 122

Because the default unit is “F”, we do not need to specify the unit if our input temperatures are in Fahrenheit:

Code
convert_temperature(
  c(0, 10, 20, 30, 40, 50)
)
[1] -17.777778 -12.222222  -6.666667  -1.111111   4.444444  10.000000

3.30 Commenting Code

To comment your code, using the # sign. Anything on the line that appears after the # sign will be interpreted as a comment and will not be evaluated by R. It is important to comment your code frequently with what you are doing doing and why so that you and others know how to read it.

Code
x <- 1 # this line will run
       # this line will NOT run: x <- 2

x
[1] 1

4 Conclusion

This chapter provided an overview of how to install and learn the software R, how to troubleshoot code, and how to perform various data management operations. Most of the code throughout the book that performs data management operations leverages the techniques implemented in this chapter.

4.1 Session Info

At the end of each chapter in which R code is used, I provide the session information, which describes the system and operating system the code was run on and the versions of each package. That way, if you get different results from me, you can see which session settings differ, to help with reproducibility. If you run the (all of) the exact same code as is provided in the text, in the exact same order, with the exact same setup (platform, operating system, package versions, etc.) and the exact same data, you should get the exact same answer as is in the text. That is the idea of reproducibility—getting the exact same result with the exact same inputs. Reproducibility is crucial for studies to achieve greater confidence in their findings and to ensure better replicability of findings across studies.

Code
sessionInfo()
R version 4.5.1 (2025-06-13)
Platform: x86_64-pc-linux-gnu
Running under: Ubuntu 24.04.2 LTS

Matrix products: default
BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
 [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
 [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
[10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   

time zone: UTC
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] lubridate_1.9.4 forcats_1.0.0   stringr_1.5.1   dplyr_1.1.4    
 [5] purrr_1.1.0     readr_2.1.5     tidyr_1.3.1     tibble_3.3.0   
 [9] ggplot2_3.5.2   tidyverse_2.0.0

loaded via a namespace (and not attached):
 [1] gtable_0.3.6       jsonlite_2.0.0     compiler_4.5.1     tidyselect_1.2.1  
 [5] scales_1.4.0       yaml_2.3.10        fastmap_1.2.0      R6_2.6.1          
 [9] generics_0.1.4     knitr_1.50         htmlwidgets_1.6.4  pillar_1.11.0     
[13] RColorBrewer_1.1-3 tzdb_0.5.0         rlang_1.1.6        stringi_1.8.7     
[17] xfun_0.52          timechange_0.3.0   cli_3.6.5          withr_3.0.2       
[21] magrittr_2.0.3     digest_0.6.37      grid_4.5.1         hms_1.1.3         
[25] lifecycle_1.0.4    vctrs_0.6.5        evaluate_1.0.4     glue_1.8.0        
[29] farver_2.1.2       rmarkdown_2.29     tools_4.5.1        pkgconfig_2.0.3   
[33] htmltools_0.5.8.1 

  1. Although the petersenlab package (Petersen, 2025) is hosted on CRAN, installing from GitHub will ensure you have the latest version.↩︎

Feedback

Please consider providing feedback about this textbook, so that I can make it as helpful as possible. You can provide feedback at the following link: https://forms.gle/LsnVKwqmS1VuxWD18

Email Notification

The online version of this book will remain open access. If you want to know when the print version of the book is for sale, enter your email below so I can let you know.