This code through demonstrates how to clean up messy column names and get a quick statistical overview of a real dataset, using the janitor and skimr packages in R.
Specifically, we’ll explain and demonstrate how to use the janitor and skimr packages to clean and format messy data so that it can be used in R for various purposes, using a real dataset of 2026 NFL quarterback stats pulled from Pro Football Reference.
This topic is valuable because real data is almost never ready to use as-is in R. In many cases, there are symbols in the columns, blank rows, and the formatting is built for the website it’s being displayed on, not R. For example, when I pulled stats from Pro Football Reference, the data would not cleanly show in R. Knowing how to clean this kind of data is something that could be consistently used going forward, since most of the actual work in analysis involves preparing the data before you do anything with it.
Take a messy CSV file with inconsistent column names and formatting, and load it into R
Translate those column names into a format R can work with cleanly,
using clean_names()
Get a clear, readable statistical summary of the dataset using
skim()
Here, we’ll show how to turn a messy, real-world dataset into something clean and ready to analyze in R.
This is based on the data cleaning type of work weve been doing all semester with dplyr, i just found a faster more automatic version of it to use with certain messy datasets. instead of renaming every column or writing out separate functions to check for missing values and summarize, ‘janitor’ and ‘skimr’ do it all for you in a few lines. Columns that have spaces or symbols like ‘%’ or ‘/’, like the original data I started with, don’t work very well in R. janitor::clean_names() fixes that automatically, and skimr::skim() gives you a full summary of the cleaned data, missing values, averages, distributions, all at once.
A basic example shows how clean_names() takes messy,
real-world column names and turns them into something R can work with
cleanly.
## [1] "Rk" "Player" "Age"
## [4] "Team" "Pos" "G"
## [7] "GS" "QBrec" "Cmp"
## [10] "Att" "Cmp." "Yds"
## [13] "TD" "TD." "Int"
## [16] "Int." "X1D" "Succ."
## [19] "Lng" "Y.A" "AY.A"
## [22] "Y.C" "Y.G" "Rate"
## [25] "QBR" "Sk" "Yds.1"
## [28] "Sk." "NY.A" "ANY.A"
## [31] "X4QC" "GWD" "Awards"
## [34] "Player.additional"
## [1] "rk" "player" "age"
## [4] "team" "pos" "g"
## [7] "gs" "q_brec" "cmp"
## [10] "att" "cmp_2" "yds"
## [13] "td" "td_2" "int"
## [16] "int_2" "x1d" "succ"
## [19] "lng" "y_a" "ay_a"
## [22] "y_c" "y_g" "rate"
## [25] "qbr" "sk" "yds_1"
## [28] "sk_2" "ny_a" "any_a"
## [31] "x4qc" "gwd" "awards"
## [34] "player_additional"
Notice the difference: Cmp., TD., and X1D become clean, lowercase names like cmp_2, td_2, and x1d. The messy names happened because R converted PFR’s % and / symbols into periods when the file was first read in.
More specifically, this can be used for getting a full statistical overview of a dataset in a single function call.
| Name | qb_clean |
| Number of rows | 57 |
| Number of columns | 34 |
| _______________________ | |
| Column type frequency: | |
| character | 5 |
| logical | 1 |
| numeric | 28 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| player | 0 | 1 | 6 | 18 | 0 | 57 | 0 |
| team | 0 | 1 | 0 | 3 | 1 | 33 | 0 |
| pos | 0 | 1 | 0 | 2 | 1 | 4 | 0 |
| q_brec | 0 | 1 | 0 | 5 | 17 | 14 | 0 |
| player_additional | 0 | 1 | 5 | 8 | 0 | 57 | 0 |
Variable type: logical
| skim_variable | n_missing | complete_rate | mean | count |
|---|---|---|---|---|
| awards | 57 | 0 | NaN | : |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| rk | 1 | 0.98 | 28.50 | 16.31 | 1.0 | 14.75 | 28.50 | 42.25 | 56.00 | ▇▇▇▇▇ |
| age | 1 | 0.98 | 28.95 | 4.71 | 22.0 | 25.00 | 28.00 | 31.25 | 43.00 | ▇▇▅▂▁ |
| g | 1 | 0.98 | 3.07 | 1.16 | 1.0 | 2.00 | 4.00 | 4.00 | 4.00 | ▂▃▁▂▇ |
| gs | 1 | 0.98 | 2.46 | 1.64 | 0.0 | 1.00 | 3.00 | 4.00 | 4.00 | ▃▂▃▁▇ |
| cmp | 1 | 0.98 | 48.39 | 38.49 | 0.0 | 8.00 | 44.00 | 81.25 | 121.00 | ▇▃▂▅▃ |
| att | 1 | 0.98 | 75.29 | 59.24 | 1.0 | 13.50 | 70.00 | 121.25 | 180.00 | ▇▅▂▅▅ |
| cmp_2 | 0 | 1.00 | 59.12 | 24.93 | 0.0 | 57.50 | 63.60 | 69.80 | 100.00 | ▂▁▃▇▁ |
| yds | 1 | 0.98 | 536.20 | 432.24 | 0.0 | 62.00 | 479.00 | 918.25 | 1268.00 | ▇▃▂▃▅ |
| td | 1 | 0.98 | 3.59 | 3.34 | 0.0 | 0.00 | 3.00 | 6.00 | 11.00 | ▇▃▃▂▂ |
| td_2 | 0 | 1.00 | 3.58 | 2.88 | 0.0 | 0.00 | 3.70 | 5.60 | 9.70 | ▇▆▆▃▂ |
| int | 1 | 0.98 | 1.62 | 1.78 | 0.0 | 0.00 | 1.00 | 3.00 | 7.00 | ▇▂▃▁▁ |
| int_2 | 0 | 1.00 | 3.55 | 13.20 | 0.0 | 0.00 | 1.70 | 2.60 | 100.00 | ▇▁▁▁▁ |
| x1d | 1 | 0.98 | 25.55 | 20.85 | 0.0 | 3.50 | 21.50 | 43.25 | 63.00 | ▇▅▃▅▃ |
| succ | 9 | 0.84 | 47.90 | 14.18 | 23.5 | 40.62 | 46.45 | 52.48 | 100.00 | ▃▇▂▁▁ |
| lng | 10 | 0.82 | 44.70 | 18.74 | 8.0 | 32.50 | 47.00 | 56.00 | 82.00 | ▅▅▇▆▂ |
| y_a | 0 | 1.00 | 6.16 | 3.29 | 0.0 | 5.60 | 6.90 | 7.50 | 16.00 | ▂▅▇▁▁ |
| ay_a | 0 | 1.00 | 5.27 | 7.65 | -45.0 | 4.90 | 6.40 | 8.40 | 16.00 | ▁▁▁▂▇ |
| y_c | 7 | 0.88 | 10.56 | 2.85 | 0.0 | 9.93 | 10.85 | 12.07 | 16.00 | ▁▁▂▇▁ |
| y_g | 0 | 1.00 | 156.30 | 101.58 | 0.0 | 63.00 | 181.50 | 239.50 | 317.00 | ▇▂▆▇▆ |
| rate | 0 | 1.00 | 84.02 | 27.68 | 0.0 | 73.00 | 86.50 | 105.00 | 127.30 | ▁▃▂▇▅ |
| qbr | 5 | 0.91 | 52.81 | 27.16 | 0.0 | 37.05 | 59.55 | 71.20 | 100.00 | ▃▃▅▇▃ |
| sk | 1 | 0.98 | 4.98 | 4.45 | 0.0 | 0.00 | 5.00 | 8.25 | 13.00 | ▇▂▃▃▃ |
| yds_1 | 1 | 0.98 | 30.88 | 29.39 | 0.0 | 0.00 | 27.00 | 51.25 | 107.00 | ▇▅▃▁▁ |
| sk_2 | 0 | 1.00 | 4.82 | 3.86 | 0.0 | 0.00 | 4.80 | 7.83 | 13.33 | ▇▅▆▃▂ |
| ny_a | 0 | 1.00 | 5.54 | 3.16 | 0.0 | 4.90 | 6.00 | 6.80 | 16.00 | ▃▇▆▁▁ |
| any_a | 0 | 1.00 | 4.66 | 7.53 | -45.0 | 4.20 | 6.00 | 7.40 | 16.00 | ▁▁▁▂▇ |
| x4qc | 1 | 0.98 | 0.36 | 0.59 | 0.0 | 0.00 | 0.00 | 1.00 | 2.00 | ▇▁▃▁▁ |
| gwd | 1 | 0.98 | 0.45 | 0.71 | 0.0 | 0.00 | 0.00 | 1.00 | 3.00 | ▇▃▁▁▁ |
What’s more, it can also be used for quickly counting and summarizing categorical columns, like how many players at each position appear in this dataset.
Most notably, it’s valuable for making the cleaned data usable in a real analysis, here, filtering out quarterbacks with very few pass attempts and ranking the rest by passer rating.
qb_clean %>%
filter(att >= 50) %>%
arrange(desc(rate)) %>%
select(player, team, att, cmp_2, rate) %>%
head(10)
Learn more about ‘janitor’ and ‘skimr’ with the following:
Resource I [https://cran.r-project.org/web/packages/janitor/index.html]
Resource II [https://cran.r-project.org/web/packages/skimr/index.html]
Resource III [https://www.pro-football-reference.com/years/2026/passing.htm]
This code through references and cites the following sources:
Firke S (2024). janitor: Simple Tools for Examining and Cleaning Dirty Data. doi:10.32614/CRAN.package.janitor https://doi.org/10.32614/CRAN.package.janitor. R package version 2.2.1, <https://CRAN.R-project.org/package=janitor
Waring, E., Quinn, M., McNamara, A., Arino de la Rubia, E., Zhu, H., & Ellis, S. (2022. skimr: Compact and Flexible Summaries of Data.) [https://cran.r-project.org/web/packages/skimr/index.html]
Pro Football Reference (2026). 2026 NFL Passing Stats.2026 NFL Passing Stats. [pro-football-reference.com] [https://www.pro-football-reference.com/]