Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
A very short post today. During the global fuel crisis relating to the conflict in Iran, I have been monitoring this chart, every Thursday morning my time:
It shows the amount of crude oil in the Cushing facility in Oklahoma and is one of the most timely and frequently updating (every Wednesday) indicators of the overall health of the crude oil market in the USA. As the chart says, below 20 million barrels is widely cited as problematic, “tank bottom”, or the “operational floor”. Below this level is expected to cause risks to oil quality, to the ability of infrastructure to move oil around, and to service the commodities markets that rely on this facility for actual delivery.
We’ve been under 20 million for a few weeks now and yet the oil markets in the USA have not blown up or ground to a halt (choose your metaphor), so clearly there is a bit of slack built into that “minimum”. Perhaps the real minimum is 18 million, 17 million, who knows? But it can’t be far below the current level and this is clearly worth monitoring.
There is of course a web page where you can check the data directly, but naturally I wanted to draw my own polished chart and label it appropriately. The code below runs a complete pipeline to download the data, process it (including preparing some smart uncluttered automatic labelling of points) and drawing the plot.
library(tidyverse)
library(scales)
library(readxl)
library(ggrepel)
library(glue)
# Download data
download.file("https://www.eia.gov/dnav/pet/hist_xls/W_EPC0_SAX_YCUOK_MBBLw.xls",
mode = "wb", destfile = "cushing.xls")
# Import and process data
cushing <- read_excel("cushing.xls", sheet = "Data 1", skip = 2) |>
rename(value = `Weekly Cushing, OK Ending Stocks excluding SPR of Crude Oil (Thousand Barrels)`,
end_date = Date) |>
# we want a label for a point only if it is at least 500 different from
# the subsequent point (this is to avoid clutter when the line is
# basically horizontal)
mutate(label = ifelse(is.na(lead(value)) |
abs(value - lead(value)) > 500,
comma(value), ""),
# We also want the label to disappear if the value is really close to
# 20,000, which is going to be a clearly labelled line anyway so would
# just be unnecessary clutter.
label = ifelse(abs(value - 20000) < 200, "", label))
# Draw chart
cushing |>
filter(end_date > as.Date("2025-12-31")) |>
# original data was in thousands but it's better to have it in millioms
# visually:
ggplot(aes(x = end_date, y = value / 1000)) +
geom_hline(yintercept = 20, colour = "darkred") +
geom_line(colour = "steelblue") +
geom_point(colour = "steelblue") +
geom_text(data = filter(cushing, end_date > as.Date("2026-05-01")),
aes(label = label, x = end_date + 150000),
size = 2.8, hjust = 0) +
annotate("text", x = as.Date("2026-03-02"), y = 20.500,
label = "Widely cited minimum working level - 20 million barrels",
colour = "darkred") +
scale_x_date(date_breaks = "1 month", date_labels = "%b") +
scale_y_continuous(label = comma) +
theme(panel.grid.minor = element_blank()) +
labs(x = "Month (2026)",
y = "Million barrels",
title = "Stocks of crude oil at Cushing, Oklahoma",
subtitle = "Cushing is the main US crude oil storage and pipeline hub, and the delivery point for the West Texas Intermediary (WTI) oil benchmark.",
caption = glue("Source: US Energy Information Administration (EIA) https://www.eia.gov/dnav/pet/hist/LeafHandler.ashx?n=PET&s=W_EPC0_SAX_YCUOK_MBBL&f=W. Accessed {Sys.Date()}."))R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
