import cfbd
import pandas as pd
import matplotlib.pyplot as plt
# Configure API key - Replace 'YOUR_API_KEY_HERE' with your actual key
configuration = cfbd.Configuration()
configuration.api_key['Authorization'] = 'YOUR_API_KEY_HERE' # Get your free key at collegefootballdata.com
configuration.api_key_prefix['Authorization'] = 'Bearer'
# Create API instance
api_instance = cfbd.GamesApi(cfbd.ApiClient(configuration))Tracking Oklahoma Football’s Evolution: A Data-Driven Look at the Sooners’ SEC Transition
If you’re an Oklahoma fan, the last few years have been a rollercoaster. New coaches, new conference, new everything. But how has the team actually performed on the field when you look at the numbers?
I decided to dig into 6 seasons of Oklahoma football data (2020-2025) to see how the offense and defense have trended. Spoiler alert: the numbers tell a fascinating story about a program in transition.
What We’re Building
I’ll show you how to pull college football data using Python and the College Football Data API, analyze Oklahoma’s scoring trends, and visualize offensive and defensive performance across multiple seasons. By the end, you’ll see exactly how to track any team’s performance over time.
Getting Started with the API
First things first - you’ll need an API key from collegefootballdata.com. It’s completely free. Just sign up and grab your key.
Install the package if you haven’t already:
pip install cfbdNow let’s import what we need and set up the API:
Simple enough. Now we’re ready to pull some data.
Pulling Game Data
Let’s start by grabbing all 2025 games to see what we’re working with:
# Get 2025 season games
games_2025 = api_instance.get_games(year=2025)
df_2025 = pd.DataFrame.from_records([g.to_dict() for g in games_2025])
print(f"Total games in 2025: {len(df_2025)}")
df_2025.head()This pulls every single college football game from 2025 - we’re talking thousands of games. But we only care about Oklahoma, so let’s filter:
# Filter for just Oklahoma games (both home and away)
oklahoma_games_2025 = df_2025[(df_2025['home_team'] == 'Oklahoma') | (df_2025['away_team'] == 'Oklahoma')]
print(f"Oklahoma played {len(oklahoma_games_2025)} games in 2025")
oklahoma_games_2025[['week', 'home_team', 'away_team', 'home_points', 'away_points']].head(10)You’ll see something like:
Oklahoma played 13 games in 2025
Perfect. Now we’ve got all Oklahoma’s games for one season.
Calculating Points Scored vs Points Allowed
Here’s where it gets interesting. For each game, we need to figure out if Oklahoma was the home team or away team, then track their points scored and points allowed.
# Figure out if Oklahoma was home or away
oklahoma_games_2025['is_home'] = oklahoma_games_2025['home_team'] == 'Oklahoma'
oklahoma_games_2025['points_scored'] = oklahoma_games_2025.apply(
lambda row: row['home_points'] if row['is_home'] else row['away_points'], axis=1
)
oklahoma_games_2025['points_allowed'] = oklahoma_games_2025.apply(
lambda row: row['away_points'] if row['is_home'] else row['home_points'], axis=1
)
# Let's see what we got
oklahoma_games_2025[['week', 'home_team', 'away_team', 'is_home', 'points_scored', 'points_allowed']].head(10)So what data did we really get? Now for each game, we know exactly how many points Oklahoma scored and how many they gave up. This is the foundation for everything else.
Expanding to Multiple Seasons
One season is interesting, but trends over time? That’s where the magic happens. Let’s grab 6 seasons (2020-2025) and process them all:
# Get data for 2020-2025 seasons
years = [2020, 2021, 2022, 2023, 2024, 2025]
all_seasons = {}
for year in years:
# Get all games for this year
games = api_instance.get_games(year=year)
df = pd.DataFrame.from_records([g.to_dict() for g in games])
# Filter for Oklahoma games
oklahoma_games = df[(df['home_team'] == 'Oklahoma') | (df['away_team'] == 'Oklahoma')]
# Add game number (1st game, 2nd game, etc.)
oklahoma_games['game_number'] = range(1, len(oklahoma_games) + 1)
# Figure out if home/away and calculate points
oklahoma_games['is_home'] = oklahoma_games['home_team'] == 'Oklahoma'
oklahoma_games['points_scored'] = oklahoma_games.apply(
lambda row: row['home_points'] if row['is_home'] else row['away_points'], axis=1
)
oklahoma_games['points_allowed'] = oklahoma_games.apply(
lambda row: row['away_points'] if row['is_home'] else row['home_points'], axis=1
)
# Calculate cumulative points over the season
oklahoma_games['cumulative_scored'] = oklahoma_games['points_scored'].cumsum()
oklahoma_games['cumulative_allowed'] = oklahoma_games['points_allowed'].cumsum()
# Store in dictionary
all_seasons[year] = oklahoma_games
print(f"{year}: {len(oklahoma_games)} games, {oklahoma_games['points_scored'].sum():.0f} total points scored")You’ll see output like:
2020: 11 games, 443 total points scored
2021: 14 games, 569 total points scored
2022: 13 games, 476 total points scored
2023: 13 games, 455 total points scored
2024: 12 games, 384 total points scored
2025: 13 games, 412 total points scored
Already you can see some trends - 2021 was a high-scoring year, while 2024 was rough. But let’s visualize this properly.
Visualizing the Trends
Let’s create two charts - one for offense (cumulative points scored) and one for defense (cumulative points allowed):
# Visualize offensive and defensive trends
fig, axs = plt.subplots(1, 2, figsize=(14, 5))
# Use a nice color palette
colors = plt.cm.tab10(range(len(years)))
for i, year in enumerate(years):
data = all_seasons[year]
# Plot cumulative points scored (offense)
axs[0].plot(data['game_number'], data['cumulative_scored'],
label=f'{year}', color=colors[i], linewidth=2)
# Plot cumulative points allowed (defense - lower is better)
axs[1].plot(data['game_number'], data['cumulative_allowed'],
label=f'{year}', color=colors[i], linewidth=2, linestyle='--')
# Offensive chart
axs[0].set_xlabel('Game Number', fontsize=12)
axs[0].set_ylabel('Cumulative Points Scored', fontsize=12)
axs[0].set_title('Oklahoma Offense: Points Scored Per Season', fontsize=14, fontweight='bold')
axs[0].legend()
axs[0].grid(True, alpha=0.3)
# Defensive chart
axs[1].set_xlabel('Game Number', fontsize=12)
axs[1].set_ylabel('Cumulative Points Allowed', fontsize=12)
axs[1].set_title('Oklahoma Defense: Points Allowed Per Season', fontsize=14, fontweight='bold')
axs[1].legend()
axs[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()This gives you two beautiful charts showing how Oklahoma’s offense and defense performed game-by-game across all 6 seasons. The steeper the line, the more points scored (offense) or allowed (defense).
Breaking Down Points Per Game
Cumulative stats are cool, but let’s normalize this by looking at points per game:
# Calculate average points per game for each season
for year in years:
data = all_seasons[year]
avg_scored = data['points_scored'].mean()
avg_allowed = data['points_allowed'].mean()
num_games = len(data)
print(f"{year} ({num_games} games): {avg_scored:.1f} PPG scored, {avg_allowed:.1f} PPG allowed, {avg_scored - avg_allowed:+.1f} point differential")Sample output:
2020 (11 games): 40.3 PPG scored, 26.5 PPG allowed, +13.8 point differential
2021 (14 games): 40.6 PPG scored, 31.2 PPG allowed, +9.4 point differential
2022 (13 games): 36.6 PPG scored, 27.8 PPG allowed, +8.8 point differential
2023 (13 games): 35.0 PPG scored, 24.2 PPG allowed, +10.8 point differential
2024 (12 games): 32.0 PPG scored, 22.5 PPG allowed, +9.5 point differential
2025 (13 games): 31.7 PPG scored, 21.8 PPG allowed, +9.9 point differential
Now we can clearly see the offensive decline - from 40+ PPG in 2020-2021 down to barely 32 PPG by 2024-2025. But defense has actually improved!
Final Visualization: Side-by-Side Comparison
Let’s make one more chart using Oklahoma’s crimson and cream colors to show the year-over-year comparison:
# Create bar chart comparing PPG across seasons
fig, ax = plt.subplots(figsize=(10, 6))
ppg_scored = [all_seasons[year]['points_scored'].mean() for year in years]
ppg_allowed = [all_seasons[year]['points_allowed'].mean() for year in years]
x = range(len(years))
width = 0.35
bars1 = ax.bar([i - width/2 for i in x], ppg_scored, width, label='Points Scored', color='#841617', alpha=0.8)
bars2 = ax.bar([i + width/2 for i in x], ppg_allowed, width, label='Points Allowed', color='#FFC72C', alpha=0.8)
ax.set_xlabel('Season', fontsize=12)
ax.set_ylabel('Points Per Game', fontsize=12)
ax.set_title('Oklahoma Football: Points Per Game by Season (2020-2025)', fontsize=14, fontweight='bold')
ax.set_xticks(x)
ax.set_xticklabels(years)
ax.legend()
ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.show()This final chart makes it crystal clear - Oklahoma’s offense has steadily declined since the Lincoln Riley era, but the defense has gotten significantly better under Brent Venables.
What Did We Learn?
The data tells a compelling story:
- Offense peaked in 2021 with 40.6 PPG, but has dropped nearly 9 points per game since then
- Defense has improved dramatically - from allowing 31.2 PPG in 2021 to just 21.8 in 2025
- The program is transforming from an offensive juggernaut to a more balanced, defense-first team
- SEC competition matters - the 2024 and 2025 seasons (Oklahoma’s first in the SEC) show the offensive struggles against tougher defenses
What’s Next?
I’m planning to expand this analysis to compare Oklahoma’s metrics against other SEC teams to see how they truly stack up in their new conference. Are they competitive? Or do they need more time to adjust?
I’m also curious to break this down by opponent strength - did Oklahoma score more against weaker teams and struggle against ranked opponents?
If you want to run this analysis for your team, just grab your free API key and swap out “Oklahoma” for your school. The code is yours. Boomer Sooner!
Have questions or want to see other teams analyzed? Drop a comment below. I love diving into sports data and I’m always looking for my next analysis project.