library(tidyverse) # load the tidyverse
library(Lahman) # load the Lahman package to access its datasets
Batting <- as_tibble(Batting) # initialize the Batting datasettidyverse
CMSACamp 2026
Department of Statistics & Data Science
Carnegie Mellon University
First few lectures: data wrangling and data visualization
Aspects of data wrangling:
Import: load in data (e.g., read_csv())
Tidy: each row is an observation, each column is a variable (i.e. tabular data)
Transform: filter observations, create new variables, etc.
Generate questions about data
Answer via data wrangling and visualization
Data can be explored numerically (tables, descriptive statistics, etc.) or visually (graphs)
Examples of questions:
What type of variation do the variables display?
What type of relationships exist between variables?
Import Batting table of historical MLB batting statistics from the Lahman package.
Always look at your data: view the first 6 (by default) rows with head()
# A tibble: 6 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 aardsda01 2004 1 SFN NL 11 0 0 0 0 0 0
2 aardsda01 2006 1 CHN NL 45 2 0 0 0 0 0
3 aardsda01 2007 1 CHA AL 25 0 0 0 0 0 0
4 aardsda01 2008 1 BOS AL 47 1 0 0 0 0 0
5 aardsda01 2009 1 SEA AL 73 0 0 0 0 0 0
6 aardsda01 2010 1 SEA AL 53 0 0 0 0 0 0
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
Is the Batting dataset tidy?
Each row = a player’s season stint with a team (i.e. players can play for multiple teams in year)
Each column = different measurement or recording about the player-team-season observation (get all column names with colnames(Batting) or names(Batting))
We summarize quantitative (e.g. yearID, AB) and categorical (e.g. teamID, lgID) variables in different ways
summary() function:table() function:dplyrdplyr packagedplyr is a package within the tidyverse with functions for data wrangling
“Grammar of data manipulation”:
dplyr functions are verbs
datasets are nouns
function arguments are adverbs
The six main dplyr verbs for data wrangling:
filter()
select()
arrange()
mutate()
group_by()
summarize()
filter()We can use filter() to extract rows (i.e. observations) that meet certain conditions
Note: These conditions must be logical conditions (aka boolean expressions)
filter()Example 1: Extract batting stats for the American League (AL) and National League (NL)
[1] 15042
Example 2: Extract batting stats for Pirates players in 2022
# A tibble: 3 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 alforan01 2022 1 PIT NL 2 4 0 1 0 0 0
2 alldrca01 2022 1 PIT NL 1 0 0 0 0 0 0
3 allengr01 2022 1 PIT NL 46 118 17 22 4 0 2
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
x < y: less thanx <= y: less than or equal tox == y: equal tox != y: not equal tox > y: greater thanx >= y: greater than or equal tox %in% y: whether the value is present in a given vectoris.na(x): is missingx & y: andx | y: or!x: not… and basically anything that returns a TRUE/FALSE value
select()We can use select() to extract columns (variables) of interest
We simply specify the column names:
# A tibble: 128,598 × 8
playerID yearID G AB R H HR BB
<chr> <int> <int> <int> <int> <int> <int> <int>
1 aardsda01 2004 11 0 0 0 0 0
2 aardsda01 2006 45 2 0 0 0 0
3 aardsda01 2007 25 0 0 0 0 0
4 aardsda01 2008 47 1 0 0 0 0
5 aardsda01 2009 73 0 0 0 0 0
6 aardsda01 2010 53 0 0 0 0 0
7 aardsda01 2012 1 0 0 0 0 0
8 aardsda01 2013 43 0 0 0 0 0
9 aardsda01 2015 33 1 0 0 0 0
10 aaronha01 1954 122 468 58 131 13 28
# ℹ 128,588 more rows
mutate()We use mutate() to create new variables
New variables created via mutate() are usually based on existing variables
Make sure to give your new variable a name
Naming the new variable the same as the existing variable will overwrite the original column
Example: Get the batting average and strikeout-to-walk ratio for every player
# A tibble: 1 × 24
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 aardsda01 2004 1 SFN NL 11 0 0 0 0 0 0
# ℹ 12 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>, batting_avg <dbl>,
# so_bb_ratio <dbl>
arrange()We use arrange() to sort the observations (rows) by variables (columns)
Default is ascending order:
Low to high for numeric columns
Alphabetical order for character columns
Example 1: Who holds the single-season home run record?
# A tibble: 3 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 bondsba01 2001 1 SFN NL 153 476 129 156 32 2 73
2 mcgwima01 1998 1 SLN NL 155 509 130 152 21 0 70
3 sosasa01 1998 1 CHN NL 159 643 134 198 20 0 66
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
arrange()Example 2: we can arrange by multiple columns at once:
At bats from high to low
Home runs from low to high
Variable order matters!
# A tibble: 3 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 rolliji01 2007 1 PHI NL 162 716 139 212 38 20 30
2 wilsowi02 1980 1 KCA AL 161 705 133 230 28 15 3
3 suzukic01 2004 1 SEA AL 161 704 101 262 24 5 8
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
What if we want to perform several tasks using multiple dplyr verbs?
Example: Which Pirates players had the highest batting average in 2022, among those with at least 50 at bats?
# A tibble: 23 × 3
playerID AB batting_avg
<chr> <int> <dbl>
1 newmake01 288 0.274
2 reynobr01 542 0.262
3 hayeske01 505 0.244
4 marisja01 77 0.234
5 perezro02 60 0.233
6 castrro01 253 0.233
7 cruzon01 331 0.233
8 gamelbe01 371 0.232
9 chavimi01 401 0.229
10 vogelda01 237 0.228
# ℹ 13 more rows
That’s awfully annoying to write, and also difficult to read…
Introducing the pipe operator |>
Note: You might have seen the magrittr pipe %>%…
This is from the maggritr package, automatically loaded when loading tidyverse
Recently, many people (including Hadley Wickham) have switched to |>, the built-in “native” pipe to base R
We use |> to perform a sequence of operations (think of as: “and then…”)
The pipe takes an object (e.g., tibble, data frame, matrix, vector) on the left and passes it as the first argument of the function on the right
Recall our example: Which Pirates players had the highest batting average in 2022, among those with at least 50 at bats?
Our list of tasks:
filter(): only Pirates players in 2022 with at least 50 at bats
mutate(): create a new column for batting average
arrange(): sort by batting average in descending order
select(): report player name, at bats, and batting average
# A tibble: 23 × 3
playerID AB batting_avg
<chr> <int> <dbl>
1 newmake01 288 0.274
2 reynobr01 542 0.262
3 hayeske01 505 0.244
4 marisja01 77 0.234
5 perezro02 60 0.233
6 castrro01 253 0.233
7 cruzon01 331 0.233
8 gamelbe01 371 0.232
9 chavimi01 401 0.229
10 vogelda01 237 0.228
# ℹ 13 more rows
This is much easier to write and read!
summarize() (by itself)We use summarize() to collapse the data down to a single row (per group) by aggregating variables into single values
This is useful for computing summaries (e.g., mean, median, max, min, correlation)
Example 1: Extracting the median at bats
group_by() and summarize()We can use group_by() to covert the data into a “grouped tbl”, where operations are performed by group
That is, group_by() splits the data into groups based on column values
Powerful when combined with summarize()
After the operation is done at the group-level, you may have to use ungroup() to remove leftover groupings
group_by() and summarize()Example: How many home runs, strike outs, and walks did each team accumulate in each season from 2015 to 2019?
# A tibble: 30 × 4
teamID total_hr total_so total_bb
<fct> <int> <int> <int>
1 NYA 1209 6659 2839
2 HOU 1159 6294 2759
3 TOR 1139 6741 2752
4 LAN 1111 6751 2991
5 BAL 1103 6914 2162
6 TEX 1041 7008 2572
7 SEA 1036 6693 2489
8 MIN 1035 6694 2604
9 OAK 1033 6474 2610
10 MIL 1031 7434 2724
# ℹ 20 more rows
dplyrcount()count() returns the number of observations in each group
count()slice_*() family for subsetting rowsslice(): extract rows (observations) based on the row index
# A tibble: 4 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 aardsda01 2004 1 SFN NL 11 0 0 0 0 0 0
2 abbotfr01 1904 1 CLE AL 41 130 14 22 4 2 0
3 abbotgl01 1973 1 OAK AL 5 0 0 0 0 0 0
4 adamsbo03 1951 1 CIN NL 125 403 57 107 12 5 5
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
slice_*() family for subsetting rowsslice_head() / slice_tail(): extract the first / last n rows
# A tibble: 5 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 aardsda01 2004 1 SFN NL 11 0 0 0 0 0 0
2 aardsda01 2006 1 CHN NL 45 2 0 0 0 0 0
3 aardsda01 2007 1 CHA AL 25 0 0 0 0 0 0
4 aardsda01 2008 1 BOS AL 47 1 0 0 0 0 0
5 aardsda01 2009 1 SEA AL 73 0 0 0 0 0 0
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
slice_*() family for subsetting rowsslice_min() / slice_max(): extract rows with the smallest or largest values of a variable
# A tibble: 5 × 22
playerID yearID stint teamID lgID G AB R H X2B X3B HR
<chr> <int> <int> <fct> <fct> <int> <int> <int> <int> <int> <int> <int>
1 bondsba01 2001 1 SFN NL 153 476 129 156 32 2 73
2 mcgwima01 1998 1 SLN NL 155 509 130 152 21 0 70
3 sosasa01 1998 1 CHN NL 159 643 134 198 20 0 66
4 mcgwima01 1999 1 SLN NL 153 521 118 145 21 1 65
5 sosasa01 2001 1 CHN NL 160 577 146 189 34 5 64
# ℹ 10 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, SO <int>,
# IBB <int>, HBP <int>, SH <int>, SF <int>, GIDP <int>
slice_*() family for subsetting rowsslice_sample(): randomly sample a specified number / fraction of observation in the data
Useful for performing resampling (e.g., bootstrap, cross-validation, etc.)
Example 1: Get batting stats for each year: each row is a year with the following variables
yearly_batting <- Batting |>
filter(lgID %in% c("AL", "NL")) |>
group_by(yearID) |>
summarize(total_h = sum(H, na.rm = TRUE),
total_hr = sum(HR, na.rm = TRUE),
total_so = sum(SO, na.rm = TRUE),
total_bb = sum(BB, na.rm = TRUE),
total_ab = sum(AB, na.rm = TRUE)) |>
mutate(batting_avg = total_h / total_ab)Example 2: What are the top three years with the most HRs?
# A tibble: 3 × 7
yearID total_h total_hr total_so total_bb total_ab batting_avg
<int> <int> <int> <int> <int> <int> <dbl>
1 2019 42039 6776 42823 15895 166651 0.252
2 2017 42215 6105 40104 15829 165567 0.255
3 2021 39484 5944 42145 15794 161941 0.244
# A tibble: 3 × 7
yearID total_h total_hr total_so total_bb total_ab batting_avg
<int> <int> <int> <int> <int> <int> <dbl>
1 2019 42039 6776 42823 15895 166651 0.252
2 2017 42215 6105 40104 15829 165567 0.255
3 2021 39484 5944 42145 15794 161941 0.244
Example 3: Which years have the best and worst strikeout to walk ratios?
# A tibble: 2 × 8
yearID total_h total_hr total_so total_bb total_ab batting_avg so_bb_ratio
<int> <int> <int> <int> <int> <int> <dbl> <dbl>
1 1893 15913 460 3341 6143 56898 0.280 0.544
2 1879 6171 58 1843 508 24155 0.255 3.63
Using rename() and gt():
| Year | Batting Average |
|---|---|
| 1894 | 0.309 |
| 1895 | 0.296 |
| 1930 | 0.296 |
| 1908 | 0.239 |
| 1888 | 0.239 |
| 1968 | 0.237 |
gt()yearly_batting |>
select(yearID, batting_avg) |>
rename(Year = yearID,
`Batting Average` = batting_avg) |>
arrange(desc(`Batting Average`)) |>
slice(c(1:3, (n()-2):n())) |>
gt(rowname_col = "Year") |>
# add table header
tab_header(title = "Best / Worst MLB Seasons by Batting Average") |>
# create row groups by location
tab_row_group(
label = "Bottom 3 Years",
rows = 4:6
) |>
tab_row_group(
label = "Top 3 Years",
rows = 1:3
) |>
# recolor row group labels
tab_style(
style = cell_fill(color = "bisque"),
locations = cells_row_groups(groups = "Bottom 3 Years")
) |>
tab_style(
style = cell_fill(color = "darkseagreen1"),
locations = cells_row_groups(groups = "Top 3 Years")) |>
# round "Batting Average" to 3 decimals
fmt_number(columns = `Batting Average`, decimals = 3)| Best / Worst MLB Seasons by Batting Average | |
| Batting Average | |
|---|---|
| Top 3 Years | |
| 1894 | 0.309 |
| 1895 | 0.296 |
| 1930 | 0.296 |
| Bottom 3 Years | |
| 1908 | 0.239 |
| 1888 | 0.239 |
| 1968 | 0.237 |
Enough with tables (for now)!
DATA VISUALIZATION
The simple graph has brought more information to the data analyst’s mind than any other device. — John Tukey
Use ggplot2 (and the grammar of graphics) to visually explore data
More intuitive than base R plotting
Different types of visualizations for categorical and quantitative data, faceting, etc.
dplyr verbs and |> leads to natural pipeline for EDA

A “data wrangler” illustration of the dplyr package, courtesy of Allison Horst