A key feature of the CPI is that prices are adjusted for quality changes. If the price of a car’s side mirror rises by $200, but $120 of that increase reflects the mirror becoming “smart” rather than “dumb,” only the remaining $80 is counted as inflation. Similarly, if the price of a medical procedure rises because new equipment improves the quality of care, the portion attributable to improved quality is removed from the inflation calculation. Consumers still pay for these quality improvements, whether they want them or not, so in many cases the CPI understates pure cost increases.
  There are eight major categories of the CPI, and each category has its own index: Food & Beverages, Housing, Apparel, Transportation, Medical Care, Recreation, Education & Communication, and Other. These are weighted to form the overall CPI, with the largest weights as Housing at about 44% of the total, Transportation at 17%, Food & Beverages at 14%, and Medical Care at 8%. (Each of these is further sub-divided into its own index; for example, Other includes Personal Care, and Personal Care has seprate indices for Cosmetics, Perfume, Bath, and Nail Preparations.) Of course the weights will not reflect your percentages of what you buy.
  Downloading historical CPI data from FRED (Federal Reserve Bank of St. Louis) was easier than I expected. You need an API key which you can get from https://fredaccount.stlouisfed.org/login
  Here is a line graph showing cumulative CPI growth for the overall CPI and each of the eight major categories through June 2026, indexed to December 2016 = 1.00. The overall CPI has risen 37.1% since December 2016. Housing (rent, insurance, energy, etc.) has increased the most at 44.8%. Transportation (vehicle purchases, fuel, maintenance, insurance, public transit fares) is next at 43.5%. Food (groceries and restaurants) is up 39.9%. Medical Care is lower at 25.9%. Medical Care includes out‑of‑pocket spending on providers, hospitals, and insurance, but excludes employer‑paid and government‑paid health insurance premiums.
The CPIs exclude a lot of things like the quality changes I mentioned above, and other items that you may pay but that the government does not classify as personal expenses. We all feel the cost of our groceries going up - I discussed this previously in groceries . Each category has its own reasons why it is increasing; I will leave that discussion to the economists, except to say that the cost of energy affects a lot of items in the cost of production and delivery.
Like many broad measures, the CPI is an attempt to estimate the overall cost of goods and services. But what ultimately matters to you is the actual cost of the things you buy.
Here is my R code:
library(tidyquant)
library(dplyr)
library(tidyr)
library(lubridate)
library(ggplot2)
library(ggrepel) # repel overlapping text labels
# Set your API environment variable
Sys.setenv(FRED_API_KEY = "xxxx")
# Define the official FRED database tracking codes
cpi_series <- c(
"CPIAUCSL", "CPIFABSL", "CPIHOSSL", "CPIAPPSL",
"CPITRNSL", "CPIMEDSL", "CPIRECNS", "CPIEDUNS", "CPIOGSNS"
)
# Download and clean data vectors
raw_data <- tq_get(cpi_series, get = "economic.data") # get from FRED
# 2. Clean, Filter, and Perform Group-Indexing
cpi_processed <- raw_data %>%
mutate(
Year = year(date),
Month = month(date)
) %>%
# Keep all Decembers from 2015 onward OR strictly isolate June 2026
filter((Month == 12 & Year >= 2015) | (Year == 2026 & Month == 6)) %>%
# Convert raw tracking codes into readable titles
mutate(Category = case_when(
symbol == "CPIAUCSL" ~ "Overall CPI",
symbol == "CPIFABSL" ~ "1. Food & Bev",
symbol == "CPIHOSSL" ~ "2. Housing",
symbol == "CPIAPPSL" ~ "3. Apparel",
symbol == "CPITRNSL" ~ "4. Transportation",
symbol == "CPIMEDSL" ~ "5. Medical Care",
symbol == "CPIRECNS" ~ "6. Recreation",
symbol == "CPIEDUNS" ~ "7. Education & Comm",
symbol == "CPIOGSNS" ~ "8. Other Goods"
)) %>%
# Chronologically sort each group, then anchor base-100 to the first row (Dec 2015)
group_by(Category) %>%
arrange(date, .by_group = TRUE) %>%
mutate(Indexed_Value = (price / first(price)) * 100) %>%
ungroup() %>%
# Convert Timeline to ordered categories for a clean discrete X-Axis
mutate(Period = if_else(Month == 6, paste0(Year, " (June)"), as.character(Year))) %>%
mutate(Period = factor(Period, levels = unique(Period[order(date)])))
# 3. Isolate final data point rows for the text tags
label_data <- cpi_processed %>%
group_by(Category) %>%
filter(date == max(date)) %>%
ungroup()
# 4. Generate the Chart with the Categorical String Axis Baseline
ggplot(cpi_processed, aes(x = Period, y = Indexed_Value, color = Category, group = Category)) +
geom_line(aes(linewidth = ifelse(Category == "Overall CPI", 1.5, 0.8))) +
geom_point(size = 2) +
# Non-overlapping direct text labels
geom_text_repel(
data = label_data,
aes(label = paste0(Category, " (", round(Indexed_Value, 1), ")")),
nudge_x = 0.5,
direction = "y",
hjust = 0,
segment.color = "grey50",
segment.size = 0.4,
force = 2,
fontface = "bold",
size = 4 # millimters
) +
# High-contrast visual color mapping matrix
scale_color_manual(values = c(
"Overall CPI" = "#000000",
"1. Food & Bev" = "#E64B35",
"2. Housing" = "#56B4E9",
"3. Apparel" = "#009E73",
"4. Transportation" = "#4D8805",
"5. Medical Care" = "#0072B2",
"6. Recreation" = "#D55E00",
"7. Education & Comm" = "#CC79A7",
"8. Other Goods" = "#999999"
)) +
# Format plot margins to prevent label cropping
scale_x_discrete(expand = expansion(mult = c(0.05, 0.35))) +
scale_linewidth_identity() +
labs(
title = "10.5-Year Cumulative CPI Growth Comparison",
subtitle = "Base Index: December 2016 = 100",
x = "Reporting Period",
y = "Indexed Value (Relative to 100)"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "none",
panel.grid.minor = element_blank(),
text = element_text(face = "bold"),
plot.title = element_text(face = "bold", size = 14),
axis.text = element_text(face = "bold")
)
# 5. Save the final graphic output file
# ggsave("cpi_growth_comparison.png", width = 12, height = 7, dpi = 300, bg = "white")
# Extract and print the final 10.5-year cumulative values
final_column_summary <- cpi_processed %>%
filter(date == max(date)) %>%
select(Category, Indexed_Value) %>%
mutate(Indexed_Value = round(Indexed_Value, 2)) %>%
arrange(desc(Indexed_Value)) # Sorts from highest inflation to lowest
print(as.data.frame(final_column_summary))
End












