Friday, July 24, 2026
Unique Sportz
No Result
View All Result
  • Home
  • NBA
  • MLB
  • NFL
  • NHL
  • NCAAF
  • Nascar
  • Premier
  • Other Sports
    • Tennis
    • Boxing
    • Golf
    • Formula 1
    • Cycling
    • Running
    • Swimming
    • Skiing
    • MMA
    • WWE
    • eSports
    • CrossFit
Unique Sportz
  • Home
  • NBA
  • MLB
  • NFL
  • NHL
  • NCAAF
  • Nascar
  • Premier
  • Other Sports
    • Tennis
    • Boxing
    • Golf
    • Formula 1
    • Cycling
    • Running
    • Swimming
    • Skiing
    • MMA
    • WWE
    • eSports
    • CrossFit
No Result
View All Result
Unique Sportz
No Result
View All Result
Home NCAAF

Your First CFBD Analysis: Build a Team Profile in Python

July 23, 2026
in NCAAF
Reading Time: 9 mins read
0 0
A A
0


An attention-grabbing first sports activities evaluation doesn’t want a mannequin, a big characteristic set, or an advanced dashboard. It wants a query, sufficient information to reply it, and a end result you’ll be able to clarify.

On this walkthrough, we are going to make three requests with the official CFBD Python consumer and switch them right into a profile of Michigan’s 2023 nationwide championship season. The completed chart compares Michigan with the total FBS throughout report, scoring, effectivity, explosiveness, and tempo.

Plan on about 20-Half-hour the primary time by way of. As soon as it really works, altering two variables will allow you to reuse the identical evaluation for one more group or accomplished season.

One essential word earlier than we begin: it is a descriptive profile, not a causal mannequin. It summarizes what occurred, however doesn’t show why it occurred.

Arrange a secure Python setting

This instance pins model 5.18.0 of the official CFBD Python consumer because the minimal model required. That is the model used to provide the outcomes on this publish. Additionally, you will want pandas for the tables and Matplotlib for the chart.

pip set up “cfbd>=5.18.0” “pandas>=2.2,=3.8,

If you don’t have already got an API key, request a CFBD key right here. Retailer it in an setting variable named CFBD_API_KEY. Don’t paste an actual key right into a pocket book, script, screenshot, or public repository.

On macOS or Linux:

export CFBD_API_KEY=”your-key-here”

In Home windows PowerShell:

$env:CFBD_API_KEY=”your-key-here”

Or you’ll be able to paste the important thing right into a .env file on the root of your challenge (my advice):

CFBD_API_KEY=your-key-here

Now import the packages, select a group and season, and throw a failure message if the bottom line is lacking or misconfigured:

import os
from pprint import pprint

import cfbd
import matplotlib.pyplot as plt
import pandas as pd

# Be at liberty to alter this to any group and 12 months
TEAM = “Michigan”
YEAR = 2023
EXCLUDE_GARBAGE_TIME = True

api_key = os.getenv(“CFBD_API_KEY”)
if not api_key:
elevate RuntimeError(
“CFBD_API_KEY will not be set. Add it to your setting, restart the ”
“kernel, and run the pocket book once more. Don’t paste the important thing into this pocket book.”
)

configuration = cfbd.Configuration(access_token=api_key)
print(f”Able to construct a {YEAR} profile for {TEAM}. API key loaded safely.”)

The secret’s read-only at runtime, and the code by no means prints it.

Select a small set of questions

The CFBD API exposes way more information than we want for a primary profile. Importing each out there subject would make the cleanup more durable with out essentially making the end result extra helpful.

As an alternative, we are going to reply 4 centered questions:

How profitable was the group general?How productive, environment friendly, and explosive have been the offense and protection?Did the group play quick or sluggish?What was one clear relative power and one clear relative weak point?

Eight metrics are sufficient for this model. This retains the end result readable and makes every alternative simpler to clarify.

Retrieve the smallest helpful dataset

We’ll make three requests:

TeamsApi.get_fbs_teams grabs the record of FBS groups from the 2023 season for comparability.GamesApi.get_games fetches accomplished regular-season and postseason scores.StatsApi.get_advanced_season_stats returns effectivity, explosiveness, and play counts.

You may confirm the out there arguments within the official consumer docs for TeamsApi, GamesApi, and StatsApi.

with cfbd.ApiClient(configuration) as api_client:
teams_api = cfbd.TeamsApi(api_client)
games_api = cfbd.GamesApi(api_client)
stats_api = cfbd.StatsApi(api_client)

fbs_teams = teams_api.get_fbs_teams(12 months=YEAR)
video games = games_api.get_games(
12 months=YEAR,
season_type=cfbd.SeasonType.BOTH,
classification=cfbd.DivisionClassification.FBS,
)
advanced_stats = stats_api.get_advanced_season_stats(
12 months=YEAR,
exclude_garbage_time=EXCLUDE_GARBAGE_TIME,
)

print(
f”Retrieved {len(fbs_teams)} FBS groups, {len(video games)} video games, ”
f”and {len(advanced_stats)} superior group rows.”
)

For this run, the three requests returned 133 FBS groups, 910 video games, and 133 superior group rows.

Earlier than reworking something, examine one returned object. The consumer returns typed Python fashions and calling .dict() will convert to a dictionary form:

sample_game = subsequent(
sport for sport in video games
if sport.home_team == TEAM or sport.away_team == TEAM
)
pprint(sample_game.dict())

The primary Michigan sport within the response consists of fields corresponding to home_team, away_team, home_points, away_points, accomplished, season_type, and week. Taking a look at one actual object earlier than writing transformation code is a straightforward behavior, however it prevents quite a lot of guessing about subject names and response construction.

Reshape video games into one row per group

A sport response has one residence group and one away group. Our evaluation might be simpler if every accomplished sport turns into one row per FBS group, so points_for, points_against, and win_value imply the identical factor no matter location.

fbs_names = {group.faculty for group in fbs_teams}

def win_value(points_for, points_against):
“””Return 1 for a win, 0.5 for a tie, and 0 for a loss.”””
if points_for > points_against:
return 1.0
if points_for == points_against:
return 0.5
return 0.0

team_game_rows = []
for sport in video games:
if not sport.accomplished:
proceed
if sport.home_points is None or sport.away_points is None:
proceed

if sport.home_team in fbs_names:
team_game_rows.append(
{
“game_id”: sport.id,
“group”: sport.home_team,
“points_for”: sport.home_points,
“points_against”: sport.away_points,
“win_value”: win_value(sport.home_points, sport.away_points),
}
)

if sport.away_team in fbs_names:
team_game_rows.append(
{
“game_id”: sport.id,
“group”: sport.away_team,
“points_for”: sport.away_points,
“points_against”: sport.home_points,
“win_value”: win_value(sport.away_points, sport.home_points),
}
)

team_games_df = pd.DataFrame(team_game_rows)
team_games_df.head()

Now summarize these rows into one season report per group:

season_results = team_games_df.groupby(“group”).agg(
video games=(“game_id”, “nunique”),
wins=(“win_value”, “sum”),
points_for=(“points_for”, “sum”),
points_against=(“points_against”, “sum”),
).reset_index()

season_results[“win_pct”] = season_results[“wins”] / season_results[“games”]
season_results[“points_per_game”] = (
season_results[“points_for”] / season_results[“games”]
)
season_results[“points_allowed_per_game”] = (
season_results[“points_against”] / season_results[“games”]
)

season_results[season_results[“team”] == TEAM]

The end result offers Michigan 15 video games, 15 wins, 35.9 factors per sport, and 10.4 factors allowed per sport.

There’s a season-definition element value calling out. SeasonType.BOTH consists of regular-season and postseason video games assigned to the 2023 season. The scoring averages additionally embody accomplished video games in opposition to non-FBS opponents when an FBS group participated. These decisions make sense for this profile, however one other evaluation may make completely different decisions.

Choose the superior fields we really need

Subsequent, copy a small set of fields from the superior response:

advanced_rows = []
for team_stats in advanced_stats:
if team_stats.group not in fbs_names:
proceed

advanced_rows.append(
{
“group”: team_stats.group,
“convention”: team_stats.convention,
“offensive_success_rate”: team_stats.offense.success_rate,
“defensive_success_rate”: team_stats.protection.success_rate,
“offensive_explosiveness”: team_stats.offense.explosiveness,
“defensive_explosiveness”: team_stats.protection.explosiveness,
“offensive_plays”: team_stats.offense.performs,
}
)

advanced_df = pd.DataFrame(advanced_rows)
advanced_df.head()

The items and instructions matter:

Success fee is a share of performs, so 0.45 means 45%. Larger is best for offense; decrease is best for protection.Explosiveness is CFBD’s superior explosiveness worth. Larger is best for offense; decrease is best for protection.Performs per sport might be derived as non-garbage-time offensive performs divided by accomplished video games. It describes tempo. Quicker will not be robotically higher.

As a result of the superior request makes use of exclude_garbage_time=True, these superior fields deliberately omit CFBD-defined garbage-time performs.

Be a part of the tables and deal with lacking values

Be a part of the scoring and superior tables by group, derive performs per sport, and explicitly take away any group that’s lacking a metric required by the chart:

team_seasons = season_results.merge(advanced_df, on=”group”, how=”interior”)
team_seasons[“plays_per_game”] = (
team_seasons[“offensive_plays”] / team_seasons[“games”]
)

metric_columns = [
“win_pct”,
“points_per_game”,
“points_allowed_per_game”,
“offensive_success_rate”,
“defensive_success_rate”,
“offensive_explosiveness”,
“defensive_explosiveness”,
“plays_per_game”,
]

rows_before = len(team_seasons)
team_seasons = team_seasons.dropna(subset=metric_columns).reset_index(drop=True)
rows_removed = rows_before – len(team_seasons)

if TEAM not in set(team_seasons[“team”]):
elevate ValueError(f”{TEAM!r} will not be current within the cleaned {YEAR} FBS information.”)

print(
f”Clear baseline: {len(team_seasons)} FBS groups ”
f”({rows_removed} incomplete rows eliminated).”
)

On this 2023 run, all 133 FBS groups had the fields required for the profile. The missing-value step remains to be essential. If protection adjustments for one more season, incomplete rows is not going to silently be included within the comparability.

Convert uncooked values into FBS percentiles

Uncooked values reply what occurred. Percentile ranks add context by answering how uncommon a end result was amongst FBS groups.

A Ninetieth-percentile worth means the group ranked higher than roughly 90% of the groups within the comparability group. For factors allowed, defensive success fee, and defensive explosiveness, decrease uncooked values are higher, so we reverse their rating course. Tempo stays completely different: the next percentile means quicker, not higher.

# Larger uncooked values rank increased for these metrics.
team_seasons[“win_pct_percentile”] = (
team_seasons[“win_pct”].rank(pct=True) * 100
)
team_seasons[“points_per_game_percentile”] = (
team_seasons[“points_per_game”].rank(pct=True) * 100
)
team_seasons[“offensive_success_rate_percentile”] = (
team_seasons[“offensive_success_rate”].rank(pct=True) * 100
)
team_seasons[“offensive_explosiveness_percentile”] = (
team_seasons[“offensive_explosiveness”].rank(pct=True) * 100
)

# Decrease uncooked values rank increased for defensive metrics.
team_seasons[“points_allowed_per_game_percentile”] = (
team_seasons[“points_allowed_per_game”].rank(pct=True, ascending=False) * 100
)
team_seasons[“defensive_success_rate_percentile”] = (
team_seasons[“defensive_success_rate”].rank(pct=True, ascending=False) * 100
)
team_seasons[“defensive_explosiveness_percentile”] = (
team_seasons[“defensive_explosiveness”].rank(pct=True, ascending=False) * 100
)

# For tempo, the next percentile means quicker—not higher.
team_seasons[“plays_per_game_percentile”] = (
team_seasons[“plays_per_game”].rank(pct=True) * 100
)

Acquire the chosen group’s uncooked worth, the FBS median, and its percentile into one compact desk:

selected_team = team_seasons[team_seasons[“team”] == TEAM].iloc[0]

# Every tuple accommodates: label, DataFrame column, course, show scale.
metric_specs = [
(“Win percentage (%)”, “win_pct”, “higher”, 100),
(“Points per game”, “points_per_game”, “higher”, 1),
(“Points allowed per game”, “points_allowed_per_game”, “lower”, 1),
(“Offensive success rate (%)”, “offensive_success_rate”, “higher”, 100),
(“Defensive success rate allowed (%)”, “defensive_success_rate”, “lower”, 100),
(“Offensive explosiveness”, “offensive_explosiveness”, “higher”, 1),
(“Defensive explosiveness allowed”, “defensive_explosiveness”, “lower”, 1),
(“Non-garbage-time plays per game”, “plays_per_game”, “faster”, 1),
]

profile_rows = []
for label, column, course, scale in metric_specs:
profile_rows.append(
{
“metric”: label,
“team_value”: selected_team[column] * scale,
“fbs_median”: team_seasons[column].median() * scale,
“percentile”: selected_team[f”{column}_percentile”],
“course”: course,
}
)

profile = pd.DataFrame(profile_rows)

profile_table = profile[[“metric”, “team_value”, “fbs_median”, “percentile”]].copy()
profile_table = profile_table.rename(
columns={“team_value”: TEAM, “percentile”: “FBS percentile”}
)
profile_table.spherical(1)

Michigan’s profile features a A hundredth-percentile win proportion, Ninetieth-percentile scoring, 92nd-percentile offensive success fee, and A hundredth-percentile marks for each factors allowed and defensive success fee allowed. Its offensive explosiveness ranks a lot decrease, on the thirty eighth percentile, whereas its tempo is among the many slowest within the comparability group.

These comparisons are unadjusted. Opponent high quality, sport rely, postseason path, and play-sample measurement all have an effect on the profile. Percentiles make the groups simpler to match, however they don’t take away schedule power or flip a descriptive metric into proof of trigger.

Construct the chart

The plotting code makes use of one horizontal bar per metric and a dashed reference line on the FBS median. High quality metrics at or above the median are inexperienced, these beneath it are purple, and tempo is grey as a result of quick and sluggish are kinds slightly than grades.

plot_data = profile.iloc[::-1].reset_index(drop=True)
colours = []
for row in plot_data.itertuples():
if row.course == “quicker”:
colours.append(“#6B7280”)
elif row.percentile >= 50:
colours.append(“#009C27”)
else:
colours.append(“#BB0019”)

fig, ax = plt.subplots(figsize=(10, 6.5))
bars = ax.barh(
plot_data[“metric”],
plot_data[“percentile”],
colour=colours,
peak=0.64,
)
ax.axvline(50, colour=”#9CA3AF”, linewidth=1.2, linestyle=”–“)

labels = [f”{value:.0f}” for value in plot_data[“percentile”]]
ax.bar_label(bars, labels=labels, padding=3, fontsize=10, fontweight=”daring”)

ax.set_xlim(0, 105)
ax.set_xticks([0, 20, 40, 60, 80, 100])
ax.set_xlabel(“FBS percentile (increased = stronger; tempo = quicker)”, fontsize=10)
ax.set_title(
f”{TEAM} {YEAR} Workforce Profile”,
loc=”left”,
fontsize=18,
fontweight=”daring”,
colour=”#00274C”,
)

for facet in [“top”, “right”, “left”]:
ax.spines[side].set_visible(False)
ax.tick_params(axis=”y”, size=0)
ax.grid(axis=”x”, colour=”#E5E7EB”, linewidth=0.8)
ax.set_axisbelow(True)
plt.tight_layout()

output_path = f”{TEAM.decrease().exchange(‘ ‘, ‘_’)}_{YEAR}_team_profile.png”
fig.savefig(output_path, dpi=200, bbox_inches=”tight”, facecolor=”white”)
plt.present()
print(f”Saved chart to {output_path}”)

The saved PNG is able to use in a dashboard, article, class challenge, or social publish.

Michigan 2023 group profile. Knowledge: CollegeFootballData.com.

Interpret the end result

The chart helps a couple of direct observations:

Michigan’s report, scoring protection, and defensive success fee allowed all sit on the prime of this FBS comparability.Offensive explosiveness is the lowest-ranked high quality metric within the profile on the thirty eighth percentile. That makes it a relative weak point inside this set of metrics, not proof that the offense was poor.Michigan’s non-garbage-time tempo is across the 2nd FBS percentile. That describes tempo and shouldn’t be learn as a high quality grade.

These statements describe the place Michigan landed. They don’t set up that tempo, explosiveness, or some other single metric induced the 15–0 season. Teaching, personnel, opponent high quality, sport state, and different components overlap.

Run it in your group

Change the 2 parameters close to the highest and run the total script once more:

TEAM = “Indiana”
YEAR = 2025

Use a accomplished season for the cleanest first comparability. Workforce names should match the names returned by TeamsApi.get_fbs_teams.

As soon as this model works, there are a number of completely different instructions you’ll be able to take it:

Add an opponent adjustment or schedule-strength management.Put two groups facet by facet.Add an end_week filter and refresh the profile through the season.Transfer the saved chart right into a dashboard or an extended evaluation.

Begin with the straightforward model. Construct the profile in your personal group, then share the chart or the one end result that shocked you most.



Source link

Tags: AnalysisBuildCFBDprofilePythonTeam
Previous Post

NHL Rumors: Chicago Blackhawks, and the Pittsburgh Penguins

Next Post

Pirates-Yankees rained out Tuesday; split doubleheader set for Wednesday

Related Posts

USC doing well with two-way ’28 ATH Jaion Smith but Kansas, Minnesota and UCLA involved as well
NCAAF

USC doing well with two-way ’28 ATH Jaion Smith but Kansas, Minnesota and UCLA involved as well

July 23, 2026
NCAA shouldn't take Kirby Smart's SEC stance seriously … yet
NCAAF

NCAA shouldn't take Kirby Smart's SEC stance seriously … yet

July 22, 2026
Georgia's Kirby Smart on why Bulldogs favor high school recruiting over the portal
NCAAF

Georgia's Kirby Smart on why Bulldogs favor high school recruiting over the portal

July 21, 2026
USA TODAY used College Football 27 to predict CFP national champion
NCAAF

USA TODAY used College Football 27 to predict CFP national champion

July 19, 2026
How can Michigan move on from Sherrone Moore? Be transparent and make a Warde Manuel decision
NCAAF

How can Michigan move on from Sherrone Moore? Be transparent and make a Warde Manuel decision

July 18, 2026
Bill Belichick sends strong message about North Carolina football's progress
NCAAF

Bill Belichick sends strong message about North Carolina football's progress

July 19, 2026
Next Post
Pirates-Yankees rained out Tuesday; split doubleheader set for Wednesday

Pirates-Yankees rained out Tuesday; split doubleheader set for Wednesday

Road To Utah Ski Resort Closed Indefinitely One Month After Devastating Wildfire

Road To Utah Ski Resort Closed Indefinitely One Month After Devastating Wildfire

7Mesh Guardian Air Shell Review: High Performance, Ultralight, PFAS Free Gore-Tex

7Mesh Guardian Air Shell Review: High Performance, Ultralight, PFAS Free Gore-Tex

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • Disclaimer
  • DMCA
  • Privacy Policy
  • Cookie Privacy Policy
  • Terms and Conditions
  • Contact us
  • About Us
UNIQUE SPORTZ

Copyright © 2024 Unique Sportz.
Unique Sportz is not responsible for the content of external sites.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
  • Home
  • NBA
  • MLB
  • NFL
  • NHL
  • NCAAF
  • Nascar
  • Premier
  • Other Sports
    • Tennis
    • Boxing
    • Golf
    • Formula 1
    • Cycling
    • Running
    • Swimming
    • Skiing
    • MMA
    • WWE
    • eSports
    • CrossFit

Copyright © 2024 Unique Sportz.
Unique Sportz is not responsible for the content of external sites.