MakeoverMonday: Global Oil Production - Who Still Moves the Map

MakeoverMonday
Python
Data Viz
US overtakes Russia and Saudi Arabia while OPEC share shrinks - how 50 years of oil production reshaped the global energy map
Author

chokotto

Published

April 14, 2026

Overview

Oil production has been one of the most consequential variables in geopolitics and economics for over half a century. The US shale revolution upended a decades-old order where OPEC nations set the pace, and today three countries alone account for roughly 40% of the world’s output. This makeover explores how the production landscape shifted from 1970 to the present.

Dataset

Code
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
Code
url = "https://raw.githubusercontent.com/owid/energy-data/master/owid-energy-data.csv"
df = pd.read_csv(url)
df.columns = df.columns.str.replace(" ", "_")

aggregates = [
    "World", "OPEC", "Non-OPEC", "Africa", "Asia Pacific", "CIS",
    "Central America", "Europe", "European Union (27)", "High-income countries",
    "Low-income countries", "Lower-middle-income countries",
    "Upper-middle-income countries", "Middle East", "North America",
    "Oceania", "South America", "South & Central America",
    "Asia", "USSR",
]
df = df[~df["country"].isin(aggregates)].copy()

oil = df[["country", "year", "oil_production"]].dropna(subset=["oil_production"])
oil = oil[(oil["year"] >= 1970) & (oil["year"] <= 2024)].copy()

My Makeover

The US shale revolution rewrote the leaderboard after 2010

Code
top5 = ["United States", "Saudi Arabia", "Russia", "Canada", "China"]
top5_df = oil[oil["country"].isin(top5)].copy()

latest_year = top5_df["year"].max()
latest_vals = top5_df[top5_df["year"] == latest_year].set_index("country")["oil_production"]

fig = go.Figure()
for country in top5:
    cdf = top5_df[top5_df["country"] == country].sort_values("year")
    is_us = country == "United States"
    color = ACCENT if is_us else MUTED
    width = 3 if is_us else 1.5

    fig.add_trace(go.Scatter(
        x=cdf["year"], y=cdf["oil_production"],
        mode="lines",
        name=country,
        line=dict(color=color, width=width),
        hovertemplate=f"{country}<br>%{{x}}: %{{y:,.0f}} TWh<extra></extra>",
    ))

    label_val = latest_vals.get(country, None)
    if label_val is not None:
        label = f"<b>{country}</b>" if is_us else country
        fig.add_annotation(
            x=latest_year + 0.5, y=label_val,
            text=label, showarrow=False,
            xanchor="left",
            font=dict(size=11, color=color),
        )

us_latest = latest_vals.get("United States", 0)
sa_latest = latest_vals.get("Saudi Arabia", 0)
ru_latest = latest_vals.get("Russia", 0)
world_latest = oil[oil["year"] == latest_year]["oil_production"].sum()
top3_share = (us_latest + sa_latest + ru_latest) / world_latest * 100 if world_latest > 0 else 0

fig.update_layout(
    **THEME,
    title=make_title(
        f"US overtakes Russia and Saudi Arabia: 3 producers now control {top3_share:.0f}% of global oil"
    ),
    height=450,
    margin=dict(l=60, r=140, t=100, b=100),
    xaxis=dict(title=""),
    yaxis=dict(title="Oil production (TWh)"),
    showlegend=False,
)

add_source(fig)
assert_no_title_overlap(fig)
fig.show()
try:
    fig.write_image("chart-1.png", width=1200, height=600, scale=2)
except Exception:
    pass

OPEC’s grip loosened as shale and new entrants grew

Code
opec_members = [
    "Saudi Arabia", "Iraq", "Iran", "United Arab Emirates", "Kuwait",
    "Venezuela", "Nigeria", "Libya", "Algeria", "Angola", "Congo",
    "Equatorial Guinea", "Gabon",
]

yearly = oil.groupby("year")["oil_production"].sum().reset_index()
yearly.columns = ["year", "world_total"]

opec_yearly = (
    oil[oil["country"].isin(opec_members)]
    .groupby("year")["oil_production"].sum()
    .reset_index()
)
opec_yearly.columns = ["year", "opec_total"]

share = yearly.merge(opec_yearly, on="year", how="left").fillna(0)
share["non_opec_total"] = share["world_total"] - share["opec_total"]
share["opec_pct"] = share["opec_total"] / share["world_total"] * 100
share["non_opec_pct"] = 100 - share["opec_pct"]

fig2 = go.Figure()

fig2.add_trace(go.Scatter(
    x=share["year"], y=share["opec_pct"],
    fill="tozeroy",
    name="OPEC",
    mode="lines",
    line=dict(color=ACCENT, width=2),
    fillcolor="rgba(230, 57, 70, 0.25)",
    hovertemplate="OPEC: %{y:.1f}%<extra></extra>",
))

fig2.add_trace(go.Scatter(
    x=share["year"], y=[100] * len(share),
    fill="tonexty",
    name="Non-OPEC",
    mode="lines",
    line=dict(color=SECONDARY, width=0),
    fillcolor="rgba(59, 130, 246, 0.15)",
    hovertemplate="Non-OPEC: %{customdata:.1f}%<extra></extra>",
    customdata=share["non_opec_pct"],
))

peak_row = share.loc[share["opec_pct"].idxmax()]
latest_row = share.iloc[-1]

fig2.add_annotation(
    x=peak_row["year"], y=peak_row["opec_pct"],
    text=f"Peak: {peak_row['opec_pct']:.0f}% ({int(peak_row['year'])})",
    showarrow=True, arrowhead=2, arrowcolor=ACCENT,
    font=dict(size=11, color=ACCENT),
    ax=40, ay=-30,
)

fig2.update_layout(
    **THEME,
    title=make_title(
        f"OPEC's share fell from {peak_row['opec_pct']:.0f}% to {latest_row['opec_pct']:.0f}% "
        f"as non-OPEC producers filled the gap"
    ),
    height=450,
    margin=dict(l=60, r=30, t=100, b=100),
    xaxis=dict(title=""),
    yaxis=dict(title="Share of global oil production (%)", range=[0, 105]),
    showlegend=True,
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)

add_source(fig2)
assert_no_title_overlap(fig2)
fig2.show()

The top 3 produce more than the next 12 combined

Code
latest = oil[oil["year"] == oil["year"].max()].copy()
top15 = latest.nlargest(15, "oil_production").sort_values("oil_production", ascending=True)

colors = [
    ACCENT if i >= len(top15) - 3 else MUTED
    for i in range(len(top15))
]

fig3 = go.Figure()
fig3.add_trace(go.Bar(
    y=top15["country"],
    x=top15["oil_production"],
    orientation="h",
    marker_color=colors,
    text=[f"{v:,.0f}" for v in top15["oil_production"]],
    textposition="outside",
    textfont=dict(size=11),
    hovertemplate="%{y}<br>%{x:,.0f} TWh<extra></extra>",
))

top3_total = top15.nlargest(3, "oil_production")["oil_production"].sum()
rest_total = top15.nsmallest(12, "oil_production")["oil_production"].sum()

fig3.update_layout(
    **THEME,
    title=make_title(
        f"Top 3 producers ({top3_total:,.0f} TWh) outproduce the next 12 ({rest_total:,.0f} TWh)"
    ),
    height=500,
    margin=dict(l=10, r=30, t=100, b=100),
    xaxis=dict(title=f"Oil production (TWh) - {int(oil['year'].max())}"),
    yaxis=dict(title=""),
    showlegend=False,
)

add_legend_note(fig3, "Red = top 3 producers")
add_source(fig3)
assert_no_title_overlap(fig3)
fig3.show()

Shale nations surged while traditional producers stalled

Code
top10_countries = (
    oil[oil["year"] == oil["year"].max()]
    .nlargest(10, "oil_production")["country"]
    .tolist()
)

decade_start = oil["year"].max() - 10
decade_end = oil["year"].max()

start_vals = (
    oil[(oil["country"].isin(top10_countries)) & (oil["year"] == decade_start)]
    .set_index("country")["oil_production"]
)
end_vals = (
    oil[(oil["country"].isin(top10_countries)) & (oil["year"] == decade_end)]
    .set_index("country")["oil_production"]
)

growth = pd.DataFrame({
    "country": top10_countries,
    "start": [start_vals.get(c, np.nan) for c in top10_countries],
    "end": [end_vals.get(c, np.nan) for c in top10_countries],
})
growth = growth.dropna()
growth["change_pct"] = (growth["end"] - growth["start"]) / growth["start"] * 100
growth = growth.sort_values("change_pct", ascending=True)

bar_colors = [
    ACCENT if v > 0 else SECONDARY for v in growth["change_pct"]
]

fig4 = go.Figure()
fig4.add_trace(go.Bar(
    y=growth["country"],
    x=growth["change_pct"],
    orientation="h",
    marker_color=bar_colors,
    text=[f"{v:+.1f}%" for v in growth["change_pct"]],
    textposition="outside",
    textfont=dict(size=11),
    hovertemplate="%{y}<br>Change: %{x:+.1f}%<extra></extra>",
))

fig4.add_vline(x=0, line_width=1, line_color="#475569")

fig4.update_layout(
    **THEME,
    title=make_title(
        f"Production change {decade_start}-{decade_end}: US and Canada led growth, "
        "traditional producers stalled"
    ),
    height=450,
    margin=dict(l=10, r=30, t=100, b=100),
    xaxis=dict(title=f"% change in oil production ({decade_start}-{decade_end})"),
    yaxis=dict(title=""),
    showlegend=False,
)

add_legend_note(fig4, "Red = growth, Blue = decline")
add_source(fig4)
assert_no_title_overlap(fig4)
fig4.show()

Key Takeaways

  1. The US is now the world’s largest oil producer. The shale revolution pushed US output past both Russia and Saudi Arabia, a position unthinkable before 2010.

  2. OPEC’s market share has structurally declined. From a peak above 50% in the 1970s, OPEC members now account for a much smaller share as non-OPEC supply – led by the US and Canada – expanded relentlessly.

  3. Concentration at the top is extreme. The top 3 producers generate more oil than the next 12 combined, making global supply highly sensitive to decisions in Washington, Riyadh, and Moscow.

  4. Growth is unevenly distributed. Over the past decade, shale nations (US, Canada) saw double-digit production growth while several traditional producers either stalled or declined due to sanctions, conflict, or underinvestment.


This post is part of the MakeoverMonday weekly data visualization project.

CautionDisclaimer

This analysis is for educational and practice purposes only. Data visualizations and interpretations are based on the provided dataset and may not represent complete or current information.