The National Football League (NFL) has changed considerably over
time, particularly in scoring and team performance. For this project, I
used the spreadspoke_scores.csv dataset to analyze
historical NFL game results. The dataset includes seasons, game dates,
teams, final scores, and other information about each matchup. I used
five different visualizations to explore which teams score the most, how
scoring has changed, whether home teams have an advantage, and how team
success can change from year to year.
import os
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = 'C:/ProgramData/Anaconda3/Library/plugins/platforms'
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Load NFL dataset from Apporto U: drive
df = pd.read_csv('U:/spreadspoke_scores.csv')
df['schedule_date'] = pd.to_datetime(df['schedule_date'], errors='coerce')
df['score_home'] = pd.to_numeric(df['score_home'], errors='coerce')
df['score_away'] = pd.to_numeric(df['score_away'], errors='coerce')
df['schedule_season'] = pd.to_numeric(df['schedule_season'], errors='coerce')
# Remove games with missing scores
games = df.dropna(
subset=['score_home', 'score_away', 'schedule_season']
).copy()
games['total_points'] = games['score_home'] + games['score_away']
games['decade'] = (games['schedule_season'] // 10 * 10).astype(int)
print('Games with scores:', len(games))
## Games with scores: 13203
print('Seasons covered:',
int(games['schedule_season'].min()),
'-',
int(games['schedule_season'].max()))
## Seasons covered: 1966 - 2021
print(games[['score_home', 'score_away', 'total_points']]
.describe().round(2))
## score_home score_away total_points
## count 13203.00 13203.00 13203.00
## mean 22.44 19.75 42.19
## std 10.55 10.18 14.39
## min 0.00 0.00 3.00
## 25% 14.00 13.00 32.00
## 50% 22.00 20.00 41.00
## 75% 29.00 27.00 51.00
## max 72.00 62.00 113.00
After excluding games without recorded final scores, the dataset contains 13,203 games. The average combined score is 42.19 points, with a highest combined score of 113 and a lowest of 3. The descriptive statistics above also show the distribution of home, away, and combined scores, including the median and standard deviation. These statistics provide context for the five visualizations below.
# Combine home and away team scores
home_scores = games[['team_home', 'score_home']].rename(
columns={'team_home': 'Team', 'score_home': 'Points'}
)
away_scores = games[['team_away', 'score_away']].rename(
columns={'team_away': 'Team', 'score_away': 'Points'}
)
team_scores = pd.concat(
[home_scores, away_scores],
ignore_index=True
)
# Find top 10 teams
top_scoring = (
team_scores.groupby('Team')['Points']
.mean()
.nlargest(10)
.sort_values()
)
# Create horizontal bar chart
plt.figure(figsize=(11, 6))
colors = [
'navy' if i % 2 == 0 else 'firebrick'
for i in range(len(top_scoring))
]
plt.barh(top_scoring.index, top_scoring.values, color=colors)
plt.title('Top 10 Highest-Scoring NFL Teams')
plt.xlabel('Average Points per Game')
plt.xlim(0, max(top_scoring.values) + 4)
## (0.0, 28.353658536585368)
for i, value in enumerate(top_scoring.values):
plt.text(value + 0.15, i, f'{value:.2f}', va='center')
plt.tight_layout()
plt.show()
This horizontal bar chart shows the ten NFL teams with the highest average points scored per game throughout the dataset. The New England Patriots lead the rankings with an average of approximately 28 points per game, followed by the New Orleans Saints and Kansas City Chiefs. One of the most interesting things about this graph is how close many of the teams are in average scoring. Although the Patriots lead the group, the difference between several teams is relatively small. This shows how competitive the NFL can be, especially among teams that have consistently strong offenses. Another important takeaway is that scoring is only one part of a team’s overall success. While having a strong offense can help teams win games, other factors such as defense and turnovers also play important roles. Overall, this graph provides a good starting point for understanding which teams have been the most productive offensively.
# Calculate average combined points per season
season_scoring = (
games.groupby('schedule_season')['total_points'].mean()
)
plt.figure(figsize=(12, 6))
plt.plot(
season_scoring.index,
season_scoring.values,
color='navy',
linewidth=2.5
)
plt.title('Average NFL Points Per Game by Season')
plt.xlabel('NFL Season')
plt.ylabel('Average Combined Points')
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
This line graph shows how the average combined points scored in NFL games have changed over time. One of the most noticeable trends is that scoring has generally increased in recent decades. During the 1970s, average scoring dropped significantly, reaching below 35 points per game in the mid-1970s. Throughout the 1980s and 1990s, scoring fluctuated but generally remained between 38 and 44 points per game. Beginning in the 2000s, scoring started to increase more consistently, eventually reaching nearly 50 points per game around 2020. This increase could be related to changes in offensive strategies, passing offenses, and NFL rules that have made it easier for teams to score. Although there are noticeable increases and decreases throughout the dataset, the overall trend suggests that NFL games have become more offensively focused in recent years.
# Average home and away points by decade
decade_scores = games.groupby('decade')[
['score_home', 'score_away']
].mean()
# Create grouped bar chart
ax = decade_scores.plot(
kind='bar',
figsize=(12, 6),
color=['navy', 'firebrick'],
width=0.7
)
ax.set_title('Average Home vs. Away NFL Scoring by Decade')
ax.set_xlabel('Decade')
ax.set_ylabel('Average Points Scored')
ax.legend(['Home Teams', 'Away Teams'])
plt.xticks(rotation=0)
## (array([0, 1, 2, 3, 4, 5, 6]), [Text(0, 0, '1960'), Text(1, 0, '1970'), Text(2, 0, '1980'), Text(3, 0, '1990'), Text(4, 0, '2000'), Text(5, 0, '2010'), Text(6, 0, '2020')])
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.show()
This grouped bar chart compares the average points scored by home and away NFL teams across different decades. One of the most noticeable findings is that home teams consistently scored more points than away teams throughout every decade represented in the dataset. This suggests that playing at home may provide teams with an advantage, potentially because of crowd support, familiarity with the stadium, and reduced travel. Another interesting trend is that scoring has generally increased for both home and away teams over time. During the 1970s, scoring was noticeably lower than in more recent decades. By the 2020s, both home and away teams were averaging some of their highest point totals. The difference between home and away scoring also appears to have decreased in recent years. Overall, this graph highlights the potential importance of home-field advantage while also showing how NFL scoring has evolved over time.
# Count home wins, away wins, and ties
home_wins = (games['score_home'] > games['score_away']).sum()
away_wins = (games['score_away'] > games['score_home']).sum()
ties = (games['score_home'] == games['score_away']).sum()
outcomes = [home_wins, away_wins, ties]
labels = ['Home Wins', 'Away Wins', 'Ties']
colors = ['navy', 'firebrick', 'lightgray']
# Create donut chart
plt.figure(figsize=(9, 7))
plt.pie(
outcomes,
labels=labels,
colors=colors,
autopct='%1.1f%%',
startangle=90,
wedgeprops={'width': 0.4, 'edgecolor': 'white'}
)
## ([<matplotlib.patches.Wedge object at 0x000002B0BBD04610>, <matplotlib.patches.Wedge object at 0x000002B0BDBA4390>, <matplotlib.patches.Wedge object at 0x000002B0BDBA62D0>], [Text(-1.072337431975888, -0.2451375776647841, 'Home Wins'), Text(1.0772879128905717, 0.22237525208502013, 'Away Wins'), Text(0.023293148995011212, 1.099753349260595, 'Ties')], [Text(-0.5849113265323025, -0.13371140599897313, '57.2%'), Text(0.5876115888494027, 0.1212955920463746, '42.2%'), Text(0.012705353997278842, 0.5998654632330518, '0.7%')])
plt.title('Distribution of NFL Game Outcomes')
plt.tight_layout()
plt.show()
This donut chart shows the distribution of NFL game outcomes throughout the dataset. Home teams won approximately 57.2% of games, while away teams won 42.2%, and only 0.7% ended in ties. One of the most interesting findings is the difference between home and away victories. Home teams won a noticeably larger percentage of games, which supports the idea that playing at home can provide an advantage. This could be related to factors such as crowd support, familiarity with the stadium, and avoiding travel. These results also connect to the previous bar chart, which showed that home teams consistently averaged more points than away teams across different decades. These two graphs work well together because one looks at the actual points scored while the other focuses on the final outcomes of games. Overall, the results suggest that home-field advantage has played an important role in NFL games throughout the dataset.
# Focus on regular-season games from 2013-2021
recent_games = games[
(games['schedule_season'] >= 2013) &
(games['schedule_season'] <= 2021) &
(games['schedule_playoff'] == False)
].copy()
# Remove ties
recent_games = recent_games[
recent_games['score_home'] != recent_games['score_away']
].copy()
# Identify the winning team
recent_games['winner'] = np.where(
recent_games['score_home'] > recent_games['score_away'],
recent_games['team_home'],
recent_games['team_away']
)
# Count wins by team and season
wins = recent_games.groupby(
['schedule_season', 'winner']
).size().reset_index(name='Wins')
# Find the 10 teams with the most wins overall
top_teams = (
wins.groupby('winner')['Wins']
.sum()
.nlargest(10)
.index
)
# Rank teams each season
wins['Rank'] = wins.groupby('schedule_season')['Wins'].rank(
ascending=False,
method='min'
)
bump_data = wins[wins['winner'].isin(top_teams)]
# Create bump chart
plt.figure(figsize=(13, 7))
for i, team in enumerate(top_teams):
team_data = bump_data[
bump_data['winner'] == team
].sort_values('schedule_season')
plt.plot(
team_data['schedule_season'],
team_data['Rank'],
marker='o',
linewidth=2,
color=plt.cm.tab10.colors[i],
label=team
)
plt.gca().invert_yaxis()
plt.title('Top 10 NFL Teams: Season Win Rankings (2013–2021)')
plt.xlabel('NFL Season')
plt.ylabel('League Ranking by Wins')
plt.xticks(range(2013, 2022))
## ([<matplotlib.axis.XTick object at 0x000002B0BBD06C10>, <matplotlib.axis.XTick object at 0x000002B0BDBB1350>, <matplotlib.axis.XTick object at 0x000002B0BB21AF90>, <matplotlib.axis.XTick object at 0x000002B0BB3332D0>, <matplotlib.axis.XTick object at 0x000002B0BDBE7C90>, <matplotlib.axis.XTick object at 0x000002B0BDBE9410>, <matplotlib.axis.XTick object at 0x000002B0BDBEB190>, <matplotlib.axis.XTick object at 0x000002B0BDBED590>, <matplotlib.axis.XTick object at 0x000002B0BDBEF790>], [Text(2013, 0, '2013'), Text(2014, 0, '2014'), Text(2015, 0, '2015'), Text(2016, 0, '2016'), Text(2017, 0, '2017'), Text(2018, 0, '2018'), Text(2019, 0, '2019'), Text(2020, 0, '2020'), Text(2021, 0, '2021')])
plt.yticks(range(1, 33, 2))
## ([<matplotlib.axis.YTick object at 0x000002B0BDB75F90>, <matplotlib.axis.YTick object at 0x000002B0BDBB0D90>, <matplotlib.axis.YTick object at 0x000002B0BB330090>, <matplotlib.axis.YTick object at 0x000002B0BDBF8ED0>, <matplotlib.axis.YTick object at 0x000002B0BD900450>, <matplotlib.axis.YTick object at 0x000002B0BD902850>, <matplotlib.axis.YTick object at 0x000002B0BD904A50>, <matplotlib.axis.YTick object at 0x000002B0BD900C50>, <matplotlib.axis.YTick object at 0x000002B0BD907610>, <matplotlib.axis.YTick object at 0x000002B0BD909790>, <matplotlib.axis.YTick object at 0x000002B0BD90B810>, <matplotlib.axis.YTick object at 0x000002B0BD9159D0>, <matplotlib.axis.YTick object at 0x000002B0BD908D10>, <matplotlib.axis.YTick object at 0x000002B0BD91C650>, <matplotlib.axis.YTick object at 0x000002B0BD91E650>, <matplotlib.axis.YTick object at 0x000002B0BD924710>], [Text(0, 1, '1'), Text(0, 3, '3'), Text(0, 5, '5'), Text(0, 7, '7'), Text(0, 9, '9'), Text(0, 11, '11'), Text(0, 13, '13'), Text(0, 15, '15'), Text(0, 17, '17'), Text(0, 19, '19'), Text(0, 21, '21'), Text(0, 23, '23'), Text(0, 25, '25'), Text(0, 27, '27'), Text(0, 29, '29'), Text(0, 31, '31')])
plt.legend(
title='NFL Teams',
bbox_to_anchor=(1.02, 1),
loc='upper left'
)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()
This bump chart shows how the rankings of ten successful NFL teams changed over several seasons based on their regular-season wins. I think this is one of the most interesting visualizations because it highlights how unpredictable the NFL can be. Unlike the other graphs, which show more consistent patterns, this chart has lines constantly crossing as teams improve or decline from year to year. For example, the New England Patriots were consistently ranked near the top throughout much of the 2010s before experiencing a significant decline in 2020. On the other hand, the Buffalo Bills improved considerably toward the end of the period after ranking much lower in earlier seasons. Another interesting observation is that even teams with the most wins over the entire period can experience individual seasons where they perform poorly. Overall, this visualization demonstrates how quickly success can change in the NFL and how difficult it is for teams to remain consistently competitive. Teams with the same number of wins share a rank.
Overall, analyzing this NFL dataset helped me better understand how different visualizations can reveal patterns and trends that might not be obvious from looking at the data alone. One of the biggest takeaways from this project was how NFL scoring has generally increased over time, particularly in recent decades. Another important finding was the potential impact of home-field advantage, as home teams consistently scored more points and won a larger percentage of games than away teams. The final bump chart was especially interesting because it demonstrated how quickly teams can rise or fall in the rankings from one season to the next. While some teams have been consistently successful, maintaining that success over a long period is difficult. This project also helped me become more comfortable using Python to organize data, calculate statistics, and create different types of visualizations. By using multiple graphs to examine the same dataset, I was able to develop a more complete understanding of NFL scoring trends and team performance.