Project 2: U.S. Inflation Rates

Author

Ummay Rukiya

Published

October 9, 2026

Overview

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.

Data Source

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.

Loading Required Packages

Code
library(rvest)
library(dplyr)
library(tidyr)
library(stringr)
library(ggplot2)
library(knitr)

Inspecting the Online Data Source

I loaded the webpage directly into R and checked how many HTML tables were available before selecting the annual inflation-rate table.

Code
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
Code
lapply(
  inflation_tables,
  dim
)
[[1]]
[1] 28 14

[[2]]
[1] 153   3
Code
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"
Code
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>
Code
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>

Creating the Raw Wide-Format Data File

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.

Code
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
Code
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>
Code
getwd()
[1] "/Users/ummayrukiya/Desktop"

Importing the Raw Data from GitHub

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.

Code
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
Code
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

Data Structure Before Tidying

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.

Code
data.frame(
  rows = nrow(inflation_wide),
  columns = ncol(inflation_wide)
)
  rows columns
1   27      14
Code
names(inflation_wide)
 [1] "year"           "jan"            "feb"            "mar"           
 [5] "apr"            "may"            "jun"            "jul"           
 [9] "aug"            "sep"            "oct"            "nov"           
[13] "dec"            "annual_average"
Code
inflation_wide |>
  select(
    year,
    jan,
    feb,
    mar,
    apr,
    may,
    jun
  ) |>
  head(6) |>
  kable(
    caption = "Sample of the Original Wide-Format Inflation Data"
  )
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

Transforming the Data from Wide to Long Format

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.

Code
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
Code
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

Handling Missing and Inconsistent Values

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.

Code
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"
  )
Missing Values in the Tidy Monthly Inflation Data
total_rows missing_values percent_missing
324 5 1.54
Code
inflation_tidy |>
  filter(
    is.na(inflation_rate)
  ) |>
  select(
    year,
    month,
    inflation_rate_raw
  ) |>
  kable(
    caption = "Monthly Values Recorded as Unavailable"
  )
Monthly Values Recorded as Unavailable
year month inflation_rate_raw
2025 Oct – (*)
2026 Sep Avail.Oct.14
2026 Oct
2026 Nov
2026 Dec

Monthly Inflation Trend

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.

Code
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()

The 2021–2022 Inflation Increase

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.

Code
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"
  )
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

Interpretation of the Inflation Trend

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.

Annual Average Compared with December Inflation

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.

Code
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"
  )
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

Interpretation of the Annual Comparison

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.

Average Inflation Rate by Month

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.

Code
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()

Interpretation of the Monthly Pattern

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.

Conclusion

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.