Set UpΒΆ

Imports, Global Variables, and ConstantsΒΆ

InΒ [Β ]:
import os
import pickle
import numpy as np
import pandas as pd

import pygskin

# constants for saving and loading data
year = 2023
path_to_files = os.path.join(os.getcwd() + os.sep + "data")

# calculation constants
RANDOM_STATE = 42

# plot constants and variables
USA_BOUNDS = [-121, -75, 23, 50]
POINT_SIZE = 20
POINT_ZORDER = 2
LINE_ZORDER = 1.5

conference_colors = ["red", "blue", "green", "orange", "purple", "brown", "pink", "lime", "cyan", "magenta", "yellow", "gray", "olive", "maroon", "navy", "teal", "gold", "darkorange", "darkgreen", "darkred", "darkblue", "darkgray", "darkcyan", "darkmagenta", "darkkhaki", "darkgoldenrod", "darkslategray", "darkolivegreen", "darkseagreen", "darkslateblue", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "goldenrod", "greenyellow", "hotpink", "indianred"]

Load Season DataΒΆ

If season data is present in .cfb (pickled) form in the data/year folder, load it. Otherwise, use the CFBD API to download, save, and load it.

If the class definition of a Team, Season, or SeasonAnalyzer is altered, the loaded information will not function as intended. The first troubleshooting step is to delete your data folder and try again.

InΒ [Β ]:
if os.path.exists(path_to_files + os.sep + str(year) + os.sep + f"season_{year}.cfb"):
    print(f"Loading {year} season from file...")
    with open(path_to_files + os.sep + str(year) + os.sep + f"season_{year}.cfb", "rb") as f:
        season = pickle.load(f)
    print(f"Loaded {year} season from file.")
else:
    print(f"Loading {year} season from API...")
    season = pygskin.Season.from_cfbd_api(year)
    print(f"Loaded {year} season from API.")
    # create directory for season, if it doesn't exist
    if not os.path.exists(path_to_files + os.sep + str(year)):
        os.mkdir(path_to_files + os.sep + str(year))
    # pickle season
    with open(path_to_files + os.sep + str(year) + os.sep + f"season_{year}.cfb", "wb") as f:
        pickle.dump(season, f)
    # pickle all Teams in Season
    for school in season.teams.keys():
        with open(path_to_files + os.sep + str(year) + os.sep + f"team_{school}.cfb", "wb") as f:
            pickle.dump(season.teams[school], f)
    print(f"Saved {year} season to file.")
analysis = pygskin.SeasonAnalyzer(season)
Loading 2023 season from file...
Loaded 2023 season from file.

Data VisualizationΒΆ

This map shows the actual locations of all FBS schools in the continental United States. Hawai'i is the only school not visible with the default bounds, as it would greatly skew the scale of the map.

InΒ [Β ]:
import cartopy.mpl.geoaxes
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt

# Create a figure with an axes object on which we will plot. Pass the projection to that axes.
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection=ccrs.LambertConformal(central_longitude=-100, central_latitude=45)))
ax: cartopy.mpl.geoaxes.GeoAxes
# Set the extent of the map to the contiguous United States
ax.set_extent(USA_BOUNDS)
# Add state boundaries to plot
ax.add_feature(cfeature.STATES)
# Add country borders to plot
ax.add_feature(cfeature.BORDERS)
# Add ocean to plot
ax.add_feature(cfeature.OCEAN)
# Add lakes to plot
ax.add_feature(cfeature.LAKES)
# Add land to plot
ax.add_feature(cfeature.LAND)
# Add coastlines to plot
ax.coastlines()

# Add the locations of the schools to the plot with a different color for each conference
for conference in analysis.school_locations["conference"].unique():
    ax.scatter(analysis.school_locations[analysis.school_locations["conference"] == conference]["longitude"], analysis.school_locations[analysis.school_locations["conference"] == conference]["latitude"], color=conference_colors[conference], s=20, transform=ccrs.Geodetic(), linewidth=0.5, edgecolor="black", zorder=2)

# Add a circle around each conference with a radius of the max distance between the centroid and the schools in the conference
for conference in analysis.school_locations["conference"].unique():
    conf_schools = analysis.school_locations[analysis.school_locations["conference"] == conference]
    centroid = np.average(conf_schools[["longitude", "latitude"]], axis=0)
    max_distance = max([np.linalg.norm(np.array([school.longitude, school.latitude]) - centroid) for school in conf_schools.itertuples()])
    # ax.add_patch(plt.Circle((centroid[0], centroid[1]), max_distance, transform=ccrs.Geodetic(), fill=False, color=conference_colors[conference]))
    
# Add a title to the plot
ax.set_title("2023 FBS Schools")

# create a legend with the names of the conferences
legend_elements = [plt.Line2D([0], [0], marker="o", color="w", label=analysis.conferences[conference], markerfacecolor=conference_colors[conference], markersize=10) for conference in analysis.school_locations["conference"].unique()]
ax.legend(title="Conference", bbox_to_anchor=(1.05, 1), loc="upper left", handles=legend_elements)
plt.show()
No description has been provided for this image

Mean and Median Distances within ConferencesΒΆ

In this section, we calculate the distances (as the crow flies) between every pair of schools within each conference. For each conference, we report the mean and median distance values.

Distance CalculationsΒΆ

InΒ [Β ]:
import math

def haversine(coord1: tuple[int, int], coord2: tuple[int, int]) -> float:
    """Returns the distance (in miles) between two points on the Earth's surface using the Haversine formula.

    Args:
        coord1 (tuple[int, int]): Longitude and latitude of the first point
        coord2 (tuple[int, int]): Longitude and latitude of the second point

    Returns:
        float: Distance between the two points in miles
    """
    R = 3958.8  # Radius of the Earth in miles

    # Coordinates in decimal degrees (e.g., 43.60, -79.49)
    lon1, lat1 = coord1
    lon2, lat2 = coord2

    # Convert decimal degrees to radians
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    delta_phi = math.radians(lat2 - lat1)
    delta_lambda = math.radians(lon2 - lon1)

    # Haversine formula
    a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))

    distance = R * c  # Output distance in miles
    return distance

conf_dist_list = []
for conference in analysis.school_locations["conference"].unique():
    for school in analysis.school_locations[analysis.school_locations["conference"] == conference].itertuples():
        for school_2 in analysis.school_locations[analysis.school_locations["conference"] == conference].itertuples():
            if school == school_2:  # don't calculate distance between the same school
                continue
            dist = haversine((school.longitude, school.latitude), (school_2.longitude, school_2.latitude))
            conf_dist_list.append([conference, school.school, school_2.school, dist])
conference_distances = pd.DataFrame(conf_dist_list, columns=["conference", "school_1", "school_2", "distance"])
for conference in conference_distances["conference"].unique():
    conference_distances["max_distance"] = conference_distances[conference_distances["conference"] == conference]["distance"].max()
    conference_distances["min_distance"] = conference_distances[conference_distances["conference"] == conference]["distance"].min()
    conference_distances["avg_distance"] = conference_distances[conference_distances["conference"] == conference]["distance"].mean()
    conference_distances["median_distance"] = conference_distances[conference_distances["conference"] == conference]["distance"].median()

Plot Mean vs. Median Distances in ConferencesΒΆ

InΒ [Β ]:
# create a scatter plot of the average distance between schools in a conference and the median distance between schools in a conference
fig, ax = plt.subplots()
ax: plt.Axes
for conference in conference_distances["conference"].unique():
    conf = conference_distances[conference_distances["conference"] == conference]
    mean = conference_distances[conference_distances["conference"] == conference]["distance"].mean()
    median = conference_distances[conference_distances["conference"] == conference]["distance"].median()
    ax.scatter(x=mean, y=median, label=analysis.conferences[conference], color=conference_colors[conference])
ax.set_xlabel("Mean Distance (miles)")
ax.set_ylabel("Median Distance (miles)")
ax.set_title("Mean vs. Median Distance Between Schools in a Conference")
ax.legend(title="Conference", bbox_to_anchor=(1.05, 1), loc="upper left")

plt.show()
No description has been provided for this image

Closest and Farthest School PairsΒΆ

This section displays the closest and farthest pairs of schools in each conference.

InΒ [Β ]:
closest_pairs = []
farthest_pairs = []
for conference in conference_distances["conference"].unique():
    min_dist = conference_distances[conference_distances["conference"] == conference]["distance"].min()
    max_dist = conference_distances[conference_distances["conference"] == conference]["distance"].max()
    closest_pairs.append((conference_distances[conference_distances['distance'] == min_dist]["school_1"].values[0], conference_distances[conference_distances['distance'] == min_dist]['school_2'].values[0]))
    farthest_pairs.append((conference_distances[conference_distances['distance'] == max_dist]["school_1"].values[0], conference_distances[conference_distances['distance'] == max_dist]['school_2'].values[0]))

Closest Pairs of Schools in Each ConferenceΒΆ

InΒ [Β ]:
# create a map of the closest pairs of schools in each conference, drawing a line between the two schools
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection=ccrs.LambertConformal(central_longitude=-100, central_latitude=45)))
ax: cartopy.mpl.geoaxes.GeoAxes
ax.set_extent(USA_BOUNDS)
ax.add_feature(cfeature.STATES)
ax.add_feature(cfeature.BORDERS)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.LAKES)
ax.add_feature(cfeature.LAND)
ax.coastlines()

for i, conference in enumerate(analysis.school_locations["conference"].unique()):
    # get closest pair of schools in conference
    ax.scatter(analysis.school_locations[analysis.school_locations["school"] == closest_pairs[i][0]]["longitude"], analysis.school_locations[analysis.school_locations["school"] == closest_pairs[i][0]]["latitude"], color=conference_colors[conference], s=20, transform=ccrs.Geodetic(), linewidth=0.5, edgecolor="black", zorder=2)
    ax.scatter(analysis.school_locations[analysis.school_locations["school"] == closest_pairs[i][1]]["longitude"], analysis.school_locations[analysis.school_locations["school"] == closest_pairs[i][1]]["latitude"], color=conference_colors[conference], s=20, transform=ccrs.Geodetic(), linewidth=0.5, edgecolor="black", zorder=2)

for conference in conference_distances["conference"].unique():
    conf = conference_distances[conference_distances["conference"] == conference]
    closest_pair = conf[conf["distance"] == conf["distance"].min()]
    school_1 = analysis.school_locations[analysis.school_locations["school"] == closest_pair["school_1"].values[0]].iloc[0]
    school_2 = analysis.school_locations[analysis.school_locations["school"] == closest_pair["school_2"].values[0]].iloc[0]
    ax.plot([school_1.longitude, school_2.longitude], [school_1.latitude, school_2.latitude], color=conference_colors[conference], transform=ccrs.Geodetic(), zorder=LINE_ZORDER)
    print(f"Closest pair of schools in {analysis.conferences[conference]}: {school_1.school} and {school_2.school}")

ax.set_title("Closest Pairs of Schools in Each Conference")
# add legend
handles, labels = ax.get_legend_handles_labels()
fig_legend = plt.figure(figsize=(2,2))
ax.legend(title="Conference", bbox_to_anchor=(1.05, 1), loc="upper left", handles=legend_elements)

plt.show()
Closest pair of schools in Mountain West: Colorado State and Wyoming
Closest pair of schools in Mid-American: Akron and Kent State
Closest pair of schools in SEC: Alabama and Mississippi State
Closest pair of schools in Sun Belt: South Alabama and Southern Mississippi
Closest pair of schools in Pac-12: UCLA and USC
Closest pair of schools in FBS Independents: Connecticut and UMass
Closest pair of schools in Big 12: Oklahoma and Oklahoma State
Closest pair of schools in ACC: Duke and North Carolina
Closest pair of schools in American Athletic: North Texas and SMU
Closest pair of schools in Conference USA: New Mexico State and UTEP
Closest pair of schools in Big Ten: Michigan and Michigan State
No description has been provided for this image
<Figure size 200x200 with 0 Axes>

Farthest Pair of Schools in Each ConferenceΒΆ

InΒ [Β ]:
# create a map of the farthest pairs of schools in each conference, drawing a line between the two schools
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection=ccrs.LambertConformal(central_longitude=-100, central_latitude=45)))
ax: cartopy.mpl.geoaxes.GeoAxes
ax.set_extent(USA_BOUNDS)
ax.add_feature(cfeature.STATES)
ax.add_feature(cfeature.BORDERS)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.LAKES)
ax.add_feature(cfeature.LAND)
ax.coastlines()

for i, conference in enumerate(analysis.school_locations["conference"].unique()):
    # get farthest pair of schools in conference
    ax.scatter(analysis.school_locations[analysis.school_locations["school"] == farthest_pairs[i][0]]["longitude"], analysis.school_locations[analysis.school_locations["school"] == farthest_pairs[i][0]]["latitude"], color=conference_colors[conference], s=20, transform=ccrs.Geodetic(), linewidth=0.5, edgecolor="black", zorder=2)
    ax.scatter(analysis.school_locations[analysis.school_locations["school"] == farthest_pairs[i][1]]["longitude"], analysis.school_locations[analysis.school_locations["school"] == farthest_pairs[i][1]]["latitude"], color=conference_colors[conference], s=20, transform=ccrs.Geodetic(), linewidth=0.5, edgecolor="black", zorder=2)
    print(f"Farthest pair of schools in {analysis.conferences[conference]}: {farthest_pairs[i][0]} and {farthest_pairs[i][1]}")

for conference in conference_distances["conference"].unique():
    conf = conference_distances[conference_distances["conference"] == conference]
    farthest_pair = conf[conf["distance"] == conf["distance"].max()]
    school_1 = analysis.school_locations[analysis.school_locations["school"] == farthest_pair["school_1"].values[0]].iloc[0]
    school_2 = analysis.school_locations[analysis.school_locations["school"] == farthest_pair["school_2"].values[0]].iloc[0]
    ax.plot([school_1.longitude, school_2.longitude], [school_1.latitude, school_2.latitude], color=conference_colors[conference], transform=ccrs.Geodetic(), zorder=LINE_ZORDER)

ax.set_title("Farthest Pairs of Schools in Each Conference")
handles, labels = ax.get_legend_handles_labels()
fig_legend = plt.figure(figsize=(2,2))
ax.legend(title="Conference", bbox_to_anchor=(1.05, 1), loc="upper left", handles=legend_elements)

plt.show()
Farthest pair of schools in Mountain West: Air Force and Hawai'i
Farthest pair of schools in Mid-American: Buffalo and Northern Illinois
Farthest pair of schools in SEC: South Carolina and Texas A&M
Farthest pair of schools in Sun Belt: Old Dominion and Texas State
Farthest pair of schools in Pac-12: Arizona and Washington
Farthest pair of schools in FBS Independents: Notre Dame and UMass
Farthest pair of schools in Big 12: BYU and UCF
Farthest pair of schools in ACC: Boston College and Miami
Farthest pair of schools in American Athletic: Temple and UT San Antonio
Farthest pair of schools in Conference USA: Florida International and New Mexico State
Farthest pair of schools in Big Ten: Nebraska and Rutgers
No description has been provided for this image
<Figure size 200x200 with 0 Axes>

K-Nearest Neighbors ModelΒΆ

Use the k-nearest neighbors algorithm to predict teams' conference based exclusively on longitude and latitude.

The k-nearest neighbors algorithm is a classification algorithm that attempts to determine which class (conference) a data point belongs to. Because college football conferences are no longer geographically based, it becomes apparent that exclusively using longititude and latitude to predict a school's conference is barely more accurate than random chance.

Due to the way that team information is reported by the CFBD API, it is necessary to drop all schools with a conference value of "FBS Independents". These schools do not actually belong to a conference, so they should not be included in the training data.

InΒ [Β ]:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix

print("Training KNN model...")

# Preprocess the data. Drop school and conference columns, use conference as target
df = analysis.school_locations.copy()
# remove schools with the conference name "FBS Independents"
df = df[df["conference"] != analysis.conferences.index("FBS Independents")]

X = df.drop(columns=["school", "conference"])
y = df["conference"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=RANDOM_STATE)
knn = KNeighborsClassifier(n_neighbors=5)
# train the model
knn.fit(X_train, y_train)

# X has columns longitude and latitude
# y has column conference

# test the model
y_pred = knn.predict(X_test)
# print accuracy
print(f"Accuracy: {accuracy_score(y_test, y_pred)}")

knn_df = pd.DataFrame(columns=["school", "longitude", "latitude", "conference", "predicted", "correct"])
# use the coordinates from the test set to find the school name
knn_df["school"] = df[df["longitude"].isin(X_test["longitude"]) & df["latitude"].isin(X_test["latitude"])]["school"]
knn_df["longitude"] = X_test["longitude"]
knn_df["latitude"] = X_test["latitude"]
knn_df["conference"] = y_test
knn_df["predicted"] = y_pred
knn_df["correct"] = knn_df["conference"] == knn_df["predicted"]

printable_knn_df = knn_df.drop(columns=["longitude", "latitude"])
printable_knn_df["conference"] = [analysis.conferences[conference] for conference in printable_knn_df["conference"]]
printable_knn_df["predicted"] = [analysis.conferences[conference] for conference in printable_knn_df["predicted"]]

print(printable_knn_df)
Training KNN model...
Accuracy: 0.2692307692307692
               school         conference          predicted  correct
4             Arizona             Pac-12  American Athletic    False
11             Baylor             Big 12            Big Ten    False
12        Boise State      Mountain West       Mid-American    False
19          Charlotte  American Athletic             Pac-12    False
20         Cincinnati             Big 12      Mountain West    False
28   Eastern Michigan       Mid-American                SEC    False
29            Florida                SEC     Conference USA    False
33       Fresno State      Mountain West            Big Ten    False
38            Hawai'i      Mountain West             Pac-12    False
42               Iowa            Big Ten       Mid-American    False
47       Kansas State             Big 12                ACC    False
57           Maryland            Big Ten           Sun Belt    False
58            Memphis  American Athletic             Big 12    False
66           Missouri                SEC     Conference USA    False
71         New Mexico      Mountain West            Big Ten    False
83           Ole Miss                SEC                ACC    False
84             Oregon             Pac-12             Pac-12     True
87         Pittsburgh                ACC                SEC    False
96     South Carolina                SEC           Sun Belt    False
98      South Florida  American Athletic       Mid-American    False
100          Syracuse                ACC                ACC     True
103         Tennessee                SEC             Big 12    False
108            Toledo       Mid-American                ACC    False
118              Utah             Pac-12             Pac-12     True
123          Virginia                ACC                SEC    False
128  Western Kentucky     Conference USA             Big 12    False

Actual vs. Predicted ClustersΒΆ

Evaluate the results of the k-nearest neighbors algorithm. Plot the points with a correctly predicted conference in green and incorrectly predicted in red. Output the results.

InΒ [Β ]:
# Create a figure with an axes object on which we will plot. Pass the projection to that axes.
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection=ccrs.LambertConformal(central_longitude=-100, central_latitude=45)))
ax: cartopy.mpl.geoaxes.GeoAxes
# Set the extent of the map to the contiguous United States
ax.set_extent(USA_BOUNDS)
# Add state boundaries to plot
ax.add_feature(cfeature.STATES)
# Add country borders to plot
ax.add_feature(cfeature.BORDERS)
# Add ocean to plot
ax.add_feature(cfeature.OCEAN)
# Add lakes to plot
ax.add_feature(cfeature.LAKES)
# Add land to plot
ax.add_feature(cfeature.LAND)
# Add coastlines to plot
ax.coastlines()

# Plot each school with a color corresponding to the success of the prediction
for index, row in knn_df.iterrows():
    # add a label to the plot for the correctly predicted schools
    ax.scatter(row["longitude"], row["latitude"], transform=ccrs.Geodetic(), s=POINT_SIZE, color="green" if row["correct"] else "red", linewidth=0.5, edgecolor="black", zorder=2)

# Add text to the plot
for index, school in knn_df.iterrows():
    ax.text(school["longitude"], school["latitude"], analysis.conferences[school["conference"]], transform=ccrs.Geodetic(), horizontalalignment="left", verticalalignment="bottom")

# Add a legend to the plot
ax.legend(handles=[plt.Line2D([0], [0], color="green", lw=4), plt.Line2D([0], [0], color="red", lw=4)], labels=["Correct", "Incorrect"])

# Add a title to the plot
ax.set_title("2023 FBS Conference Members - K-Nearest Neighbors Predictions")

plt.show()
No description has been provided for this image

K-Means ClusteringΒΆ

The k-means algorithm is a cluster analysis algorithm, a mathematical process used to group data points based on similarity. The clusters it creates contain data points which are more similar to one another than to the data points outside of the cluster. Here, it is used to form geographically-based clusters (conferences) of FBS schools. This clusters represent potential conferences that maintain the geographical similarity that was originally present in college athletic conferences while using the current set of FBS schools.

In the year 2023, there were 11 conferences in the FBS. The k-means algorithm uses a k value equal to the number of conferences in the FBS that season, not counting FBS Independents. Because FBS conferences typically have 8 to 18 members, this is the range of acceptable points per cluster.

InΒ [Β ]:
from sklearn.cluster import KMeans

k_means_df = analysis.school_locations.copy()
X = k_means_df.drop(columns=["school", "conference"])

allowed_cluster_size = range(8, 19)
min_cluster_size, max_cluster_size = 0, 0
# print(f"Attempting to create clusters with sizes in [{allowed_cluster_size[0]}, {allowed_cluster_size[-1]}]")
while min_cluster_size not in allowed_cluster_size and max_cluster_size not in allowed_cluster_size:
    # Create an instance of the KMeans class. Iterate up 1000 times to find the best clusters
    k_means = KMeans(n_clusters=analysis.num_conferences, n_init='auto', max_iter=50)    # random_state=RANDOM_STATE
    # Fit the data to the model
    k_means.fit(X)
    # find the size of the smallest cluster
    conf_members = [len(k_means.labels_[k_means.labels_ == i]) for i in range(analysis.num_conferences)]
    min_cluster_size = min(conf_members)
    max_cluster_size = max(conf_members)
    # print(f"Min: {min_cluster_size}, Max: {max_cluster_size}")

# Get the cluster labels for each data point
labels = k_means.labels_

# Get the cluster centers
centers = k_means.cluster_centers_

# Add the cluster labels to the dataframe
k_means_df["predicted_cluster"] = labels

# Print information about the results
# print(f"Iterations executed: {k_means.n_iter_}")

Visualize the K-Means ClustersΒΆ

Each cluster represents one potential conference created by the k-means algorithm. The circles around each cluster are centered on the centroid and intersect the data point furthest from the centroid. Clusters are color-coded according to the legend below the map.

The clusters generated by the k-means algorithm are not an attempt to recreate any existing conference. They represent potential conferences based solely on geographic locality.

InΒ [Β ]:
# Create a figure with an axes object on which we will plot. Pass the projection to that axes.
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection=ccrs.LambertConformal(central_longitude=-100, central_latitude=45)))
ax: cartopy.mpl.geoaxes.GeoAxes
# Set the extent of the map to the contiguous United States
ax.set_extent(USA_BOUNDS)
# Add state boundaries to plot
ax.add_feature(cfeature.STATES)
# Add country borders to plot
ax.add_feature(cfeature.BORDERS)
# Add ocean to plot
ax.add_feature(cfeature.OCEAN)
# Add lakes to plot
ax.add_feature(cfeature.LAKES)
# Add land to plot
ax.add_feature(cfeature.LAND)
# Add coastlines to plot
ax.coastlines()

# Add all the schools to the plot with a different color for each conference
for cluster_label in range(analysis.num_conferences):
    cluster_data = k_means_df[k_means_df["predicted_cluster"] == cluster_label]
    ax.scatter(cluster_data["longitude"], cluster_data["latitude"], transform=ccrs.Geodetic(), s=20, label=f"Cluster {cluster_label}", color=conference_colors[cluster_label], linewidth=0.5, edgecolor="black", zorder=2)

# add circles around the clusters
for cluster_label in range(analysis.num_conferences):
    cluster_data = k_means_df[k_means_df["predicted_cluster"] == cluster_label]
    centroid = centers[cluster_label][::1] # longitude, latitude
    distances = [np.linalg.norm(np.array([centroid[0], centroid[1]], dtype=np.float64) - np.array([row[3], row[4]], dtype=np.float64)) for row in cluster_data.itertuples()]
    radius = max(distances)
    # print(f"Cluster {cluster_label} centroid: {cluster_center[1]}, {cluster_center[0]}, radius: {radius}")
    ax.add_patch(plt.Circle((centroid[1], centroid[0]), radius, edgecolor=conference_colors[cluster_label], fill=False, linewidth=1.5, transform=ccrs.Geodetic(), zorder=1.5))

# Add a title to the plot
ax.set_title("2023 FBS Conference Suggestions - K-Means Clusters")
ax.legend(title="Conference", bbox_to_anchor=(1.05, 1), loc="upper left", handles=legend_elements)
plt.show()
No description has been provided for this image

ConclusionsΒΆ

With this analysis, it becomes apparent that the athletic conferences of the FBS member schools is no longer based primarily on geography. In fact, conferences have very little relation to the location of their member schools. Including 2023 realignments, the conferences have moved even further away from their geographic roots.

© 2024 Scott Ratchford - All Rights Reserved