Code
library(rvest)
library(dplyr)
library(tidyr)
library(stringr)
library(ggplot2)
library(knitr)For this part of Project 2, I will tidy and analyze annual U.S. inflation-rate data reported by the U.S. Inflation Calculator using Consumer Price Index data from the Bureau of Labor Statistics. The original table is in a wide format because every month is stored in a separate column, along with an annual-average column.
I will transform the monthly columns into a tidy year-month structure and examine how inflation changed over time. I am especially interested in the increase during 2021 and 2022, how quickly inflation decreased afterward, and whether the annual average sometimes hides important monthly changes.
The data comes from the Annual Inflation Rates table published by the U.S. Inflation Calculator. The site reports Consumer Price Index inflation rates based on data from the U.S. Bureau of Labor Statistics. The table contains one row for each year and separate columns for January through December and the annual average.
The monthly values represent the percentage change in consumer prices compared with the same month one year earlier. Some recent values are unavailable because the reporting period is not yet complete or the original source did not publish a value.
library(rvest)
library(dplyr)
library(tidyr)
library(stringr)
library(ggplot2)
library(knitr)I loaded the webpage directly into R and checked how many HTML tables were available before selecting the annual inflation-rate table.
data_url <- paste0(
"https://www.usinflationcalculator.com/",
"inflation/current-inflation-rates/"
)
inflation_page <- read_html(data_url)
inflation_tables <- inflation_page |>
html_elements("table") |>
html_table(fill = TRUE)
length(inflation_tables)[1] 2
lapply(
inflation_tables,
dim
)[[1]]
[1] 28 14
[[2]]
[1] 153 3
inflation_source_wide <- inflation_tables[[1]]
names(inflation_source_wide) [1] "X1" "X2" "X3" "X4" "X5" "X6" "X7" "X8" "X9" "X10" "X11" "X12"
[13] "X13" "X14"
head(inflation_source_wide)# A tibble: 6 × 14
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13
<chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 Year Jan Feb Mar Apr May Jun Jul Aug Sep "Oct" "Nov" "Dec"
2 2026 2.4 2.4 3.3 3.8 4.2 3.5 3.4 3.4 Avail… "" "" ""
3 2025 3.0 2.8 2.4 2.3 2.4 2.7 2.7 2.9 3.0 "– (… "2.7" "2.7"
4 2024 3.1 3.2 3.5 3.4 3.3 3.0 2.9 2.5 2.4 "2.6" "2.7" "2.9"
5 2023 6.4 6.0 5.0 4.9 4.0 3.0 3.2 3.7 3.7 "3.2" "3.1" "3.4"
6 2022 7.5 7.9 8.5 8.3 8.6 9.1 8.5 8.3 8.2 "7.7" "7.1" "6.5"
# ℹ 1 more variable: X14 <chr>
tail(inflation_source_wide)# A tibble: 6 × 14
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10 X11 X12 X13
<chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 2005 3.0 3.0 3.1 3.5 2.8 2.5 3.2 3.6 4.7 4.3 3.5 3.4
2 2004 1.9 1.7 1.7 2.3 3.1 3.3 3.0 2.7 2.5 3.2 3.5 3.3
3 2003 2.6 3.0 3.0 2.2 2.1 2.1 2.1 2.2 2.3 2.0 1.8 1.9
4 2002 1.1 1.1 1.5 1.6 1.2 1.1 1.5 1.8 1.5 2.0 2.2 2.4
5 2001 3.7 3.5 2.9 3.3 3.6 3.2 2.7 2.7 2.6 2.1 1.9 1.6
6 2000 2.7 3.2 3.8 3.1 3.2 3.7 3.7 3.4 3.5 3.4 3.4 3.4
# ℹ 1 more variable: X14 <chr>
The webpage table was imported without recognizing its first row as the header. I assigned clear column names, removed the repeated header row, and saved the remaining wide-format table as a CSV file. At this stage, I did not tidy the monthly columns or replace any unavailable values.
inflation_wide <- inflation_source_wide |>
slice(-1)
names(inflation_wide) <- c(
"year",
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
"annual_average"
)
write.csv(
inflation_wide,
"us_inflation_rates_wide.csv",
row.names = FALSE,
na = ""
)
dim(inflation_wide)[1] 27 14
head(inflation_wide)# A tibble: 6 × 14
year jan feb mar apr may jun jul aug sep oct nov dec
<chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
1 2026 2.4 2.4 3.3 3.8 4.2 3.5 3.4 3.4 Avail… "" "" ""
2 2025 3.0 2.8 2.4 2.3 2.4 2.7 2.7 2.9 3.0 "– (… "2.7" "2.7"
3 2024 3.1 3.2 3.5 3.4 3.3 3.0 2.9 2.5 2.4 "2.6" "2.7" "2.9"
4 2023 6.4 6.0 5.0 4.9 4.0 3.0 3.2 3.7 3.7 "3.2" "3.1" "3.4"
5 2022 7.5 7.9 8.5 8.3 8.6 9.1 8.5 8.3 8.2 "7.7" "7.1" "6.5"
6 2021 1.4 1.7 2.6 4.2 5.0 5.4 5.4 5.3 5.4 "6.2" "6.8" "7.0"
# ℹ 1 more variable: annual_average <chr>
getwd()[1] "/Users/ummayrukiya/Desktop"
After creating the wide-format CSV, I uploaded it to my public GitHub repository. I downloaded that saved version directly from GitHub so the tidying and analysis can be reproduced without relying on a file stored on my computer.
raw_data_url <- paste0(
"https://raw.githubusercontent.com/UR-71/",
"DATA607-Project2/main/",
"us_inflation_rates_wide.csv"
)
raw_csv_file <- tempfile(fileext = ".csv")
download.file(
raw_data_url,
raw_csv_file,
mode = "wb",
method = "libcurl"
)
inflation_wide <- read.csv(
raw_csv_file,
colClasses = "character",
check.names = FALSE
)
dim(inflation_wide)[1] 27 14
head(inflation_wide) year jan feb mar apr may jun jul aug sep oct nov dec
1 2026 2.4 2.4 3.3 3.8 4.2 3.5 3.4 3.4 Avail.Oct.14
2 2025 3.0 2.8 2.4 2.3 2.4 2.7 2.7 2.9 3.0 – (*) 2.7 2.7
3 2024 3.1 3.2 3.5 3.4 3.3 3.0 2.9 2.5 2.4 2.6 2.7 2.9
4 2023 6.4 6.0 5.0 4.9 4.0 3.0 3.2 3.7 3.7 3.2 3.1 3.4
5 2022 7.5 7.9 8.5 8.3 8.6 9.1 8.5 8.3 8.2 7.7 7.1 6.5
6 2021 1.4 1.7 2.6 4.2 5.0 5.4 5.4 5.3 5.4 6.2 6.8 7.0
annual_average
1
2 2.6
3 2.9
4 4.1
5 8.0
6 4.7
The raw dataset contains 27 rows and 14 columns. Each row represents one year, while January through December are stored in separate columns. The final column contains the annual average. This is a wide structure because the months are being used as column names instead of values within a month column.
data.frame(
rows = nrow(inflation_wide),
columns = ncol(inflation_wide)
) rows columns
1 27 14
names(inflation_wide) [1] "year" "jan" "feb" "mar"
[5] "apr" "may" "jun" "jul"
[9] "aug" "sep" "oct" "nov"
[13] "dec" "annual_average"
inflation_wide |>
select(
year,
jan,
feb,
mar,
apr,
may,
jun
) |>
head(6) |>
kable(
caption = "Sample of the Original Wide-Format Inflation Data"
)| year | jan | feb | mar | apr | may | jun |
|---|---|---|---|---|---|---|
| 2026 | 2.4 | 2.4 | 3.3 | 3.8 | 4.2 | 3.5 |
| 2025 | 3.0 | 2.8 | 2.4 | 2.3 | 2.4 | 2.7 |
| 2024 | 3.1 | 3.2 | 3.5 | 3.4 | 3.3 | 3.0 |
| 2023 | 6.4 | 6.0 | 5.0 | 4.9 | 4.0 | 3.0 |
| 2022 | 7.5 | 7.9 | 8.5 | 8.3 | 8.6 | 9.1 |
| 2021 | 1.4 | 1.7 | 2.6 | 4.2 | 5.0 | 5.4 |
I used pivot_longer() to move the twelve month columns into rows. The tidy dataset contains separate columns for the year, month, month number, date, and inflation rate. I kept the annual-average values in a separate dataframe because they describe the entire year rather than one individual month.
inflation_tidy <- inflation_wide |>
pivot_longer(
cols = jan:dec,
names_to = "month",
values_to = "inflation_rate_raw"
) |>
mutate(
year = as.integer(year),
month_number = match(
month,
tolower(month.abb)
),
month = factor(
str_to_title(month),
levels = month.abb
),
inflation_rate = suppressWarnings(
as.numeric(inflation_rate_raw)
),
date = as.Date(
sprintf(
"%d-%02d-01",
year,
month_number
)
)
) |>
select(
year,
month,
month_number,
date,
inflation_rate_raw,
inflation_rate
) |>
arrange(date)
annual_inflation <- inflation_wide |>
transmute(
year = as.integer(year),
annual_average_raw = annual_average,
annual_average = suppressWarnings(
as.numeric(annual_average)
)
) |>
arrange(year)
dim(inflation_tidy)[1] 324 6
head(inflation_tidy)# A tibble: 6 × 6
year month month_number date inflation_rate_raw inflation_rate
<int> <fct> <int> <date> <chr> <dbl>
1 2000 Jan 1 2000-01-01 2.7 2.7
2 2000 Feb 2 2000-02-01 3.2 3.2
3 2000 Mar 3 2000-03-01 3.8 3.8
4 2000 Apr 4 2000-04-01 3.1 3.1
5 2000 May 5 2000-05-01 3.2 3.2
6 2000 Jun 6 2000-06-01 3.7 3.7
The original table contains blank cells and text such as Avail.Oct.14 and – (*) where numerical inflation rates were unavailable. These values became NA during the numerical conversion. I did not replace them with zero because zero would represent an actual inflation rate and would change the analysis.
missing_value_summary <- inflation_tidy |>
summarise(
total_rows = n(),
missing_values = sum(
is.na(inflation_rate)
),
percent_missing = round(
mean(is.na(inflation_rate)) * 100,
2
)
)
missing_value_summary |>
kable(
caption = "Missing Values in the Tidy Monthly Inflation Data"
)| total_rows | missing_values | percent_missing |
|---|---|---|
| 324 | 5 | 1.54 |
inflation_tidy |>
filter(
is.na(inflation_rate)
) |>
select(
year,
month,
inflation_rate_raw
) |>
kable(
caption = "Monthly Values Recorded as Unavailable"
)| year | month | inflation_rate_raw |
|---|---|---|
| 2025 | Oct | – (*) |
| 2026 | Sep | Avail.Oct.14 |
| 2026 | Oct | |
| 2026 | Nov | |
| 2026 | Dec |
The following chart shows the reported 12-month inflation rate for every available month from 2000 through 2026. Missing observations are excluded from the line but remain recorded as NA in the tidy dataset.
inflation_tidy |>
filter(
!is.na(inflation_rate)
) |>
ggplot(
aes(
x = date,
y = inflation_rate
)
) +
geom_hline(
yintercept = 0,
color = "gray60",
linetype = "dashed"
) +
geom_line(
color = "steelblue",
linewidth = 0.8
) +
labs(
title = "U.S. Inflation Rate Over Time",
subtitle = "Twelve-month CPI inflation rate, 2000–2026",
x = "Year",
y = "Inflation Rate (%)"
) +
theme_minimal()To measure the inflation spike, I identified the month with the highest rate in the dataset and compared it with the latest month containing an available value.
peak_month <- inflation_tidy |>
filter(
!is.na(inflation_rate)
) |>
slice_max(
inflation_rate,
n = 1,
with_ties = FALSE
)
latest_month <- inflation_tidy |>
filter(
!is.na(inflation_rate)
) |>
slice_max(
date,
n = 1,
with_ties = FALSE
)
inflation_change_summary <- bind_rows(
peak_month |>
transmute(
measurement = "Highest rate",
date,
inflation_rate
),
latest_month |>
transmute(
measurement = "Latest available rate",
date,
inflation_rate
)
)
inflation_change_summary |>
kable(
col.names = c(
"Measurement",
"Date",
"Inflation Rate (%)"
),
caption = "Peak and Latest Available Inflation Rates"
)| Measurement | Date | Inflation Rate (%) |
|---|---|---|
| Highest rate | 2022-06-01 | 9.1 |
| Latest available rate | 2026-08-01 | 3.4 |
The highest inflation rate in the dataset was 9.1% in June 2022. This followed a rapid increase that began during 2021. By August 2026, the latest available rate had decreased to 3.4%, which was 5.7 percentage points below the peak. Inflation had therefore slowed substantially after 2022, although the latest rate was still above many of the rates observed before the 2021 increase.
The annual average summarizes all twelve months, while the December rate shows the inflation rate at the end of the year. Comparing them helps show whether inflation was increasing or decreasing during the year.
december_rates <- inflation_tidy |>
filter(
month == "Dec",
!is.na(inflation_rate)
) |>
select(
year,
december_rate = inflation_rate
)
annual_comparison <- annual_inflation |>
left_join(
december_rates,
by = "year"
) |>
filter(
!is.na(annual_average),
!is.na(december_rate)
) |>
mutate(
december_minus_average = round(
december_rate - annual_average,
2
)
)
annual_comparison |>
filter(
year >= 2019
) |>
select(
year,
annual_average,
december_rate,
december_minus_average
) |>
kable(
col.names = c(
"Year",
"Annual Average (%)",
"December Rate (%)",
"December Minus Average"
),
caption = "Annual Average and December Inflation Rates"
)| Year | Annual Average (%) | December Rate (%) | December Minus Average |
|---|---|---|---|
| 2019 | 1.8 | 2.3 | 0.5 |
| 2020 | 1.2 | 1.4 | 0.2 |
| 2021 | 4.7 | 7.0 | 2.3 |
| 2022 | 8.0 | 6.5 | -1.5 |
| 2023 | 4.1 | 3.4 | -0.7 |
| 2024 | 2.9 | 2.9 | 0.0 |
| 2025 | 2.6 | 2.7 | 0.1 |
The comparison shows why the annual average does not always describe what was happening at the end of a year. In 2021, the annual average was 4.7%, but the December rate had already reached 7.0%. This shows that inflation was increasing quickly and the yearly average hid part of that rise.
The opposite pattern appeared in 2022. Although the annual average was 8.0%, the December rate had fallen to 6.5% after reaching its peak in June. In 2023, the December rate was also lower than the annual average, showing that the decline continued. By 2024, the annual average and December rate were both 2.9%, indicating a more stable pattern during that year.
To look for a possible seasonal pattern, I calculated the average inflation rate for each calendar month using the complete years from 2000 through 2024.
monthly_pattern <- inflation_tidy |>
filter(
year <= 2024,
!is.na(inflation_rate)
) |>
group_by(
month,
month_number
) |>
summarise(
average_inflation = round(
mean(inflation_rate),
2
),
.groups = "drop"
) |>
arrange(month_number)
ggplot(
monthly_pattern,
aes(
x = month,
y = average_inflation
)
) +
geom_col(
fill = "darkorange"
) +
labs(
title = "Average U.S. Inflation Rate by Month",
subtitle = "Average twelve-month inflation rate, 2000–2024",
x = "Month",
y = "Average Inflation Rate (%)"
) +
theme_minimal()The bars are nearly the same height, which suggests that the inflation rate did not follow a strong or consistent seasonal pattern across the calendar months. No single month was regularly much higher or lower than the others. The larger changes in inflation appear to be connected more strongly to particular economic periods, such as the increase during 2021 and 2022, than to the month of the year.
The original inflation table was successfully collected from an online source, saved in its original wide format, uploaded to GitHub, and transformed into a tidy dataset containing one row for each year and month. The unavailable text and blank cells were converted to NA rather than zero so they would not distort the results.
The analysis shows that inflation increased rapidly during 2021 and reached a peak of 9.1% in June 2022. By August 2026, it had decreased to 3.4%, although this was still higher than many of the rates observed before the increase. Comparing the annual averages with the December rates also showed that yearly averages can hide important changes. The 2021 average was lower than the December rate because inflation was rising, while the 2022 average was higher than the December rate because inflation had already begun to decline.
The calendar-month averages were very similar, so the data did not show a strong seasonal pattern. Overall, the largest movements were connected to changes across years and economic periods rather than a particular month repeatedly having higher inflation.