最近在整理历史足球数据时,发现08-09赛季是一个充满故事的赛季。无论是英超的“四大豪门”争霸、西甲的“梦三”巴萨启航,还是意甲的国米连冠,其背后都隐藏着海量的数据故事。单纯看比分和集锦,我们只能知道“发生了什么”;但结合数据,我们才能理解“为什么会发生”以及“如何发生的”。本文将以08-09赛季为背景,手把手带你从零开始,搭建一个足球数据分析项目。我们将使用Python作为主要工具,涵盖数据获取、清洗、存储、分析和可视化全流程,最终目标是能通过数据复盘那个经典赛季,并挖掘出一些有趣的洞察。无论你是足球爱好者想深入了解历史,还是数据分析初学者想找一个有趣的项目练手,这篇文章都能为你提供一套完整的、可复现的实战方案。
1. 背景与核心概念:为什么是08-09赛季的数据?
在开始敲代码之前,我们有必要先明确分析对象的价值。2008-2009赛季在欧洲足坛是一个承上启下的关键节点。
从足球技战术演变看:这个赛季是“控球至上”哲学开始统治欧洲的起点。巴塞罗那在瓜迪奥拉的带领下,开启了“梦三王朝”,其极高的控球率和精准的传球网络,改变了人们对足球比赛的认知。同时,穆里尼奥的国际米兰展示了极致的防守反击,英超则延续着高节奏、强对抗的风格。多种战术流派同台竞技,为数据分析提供了丰富的对比样本。
从数据可得性看:08-09赛季处于现代足球数据统计开始系统化、但尚未爆炸性增长的时期。像“预期进球(xG)”这类高阶数据在当时还未普及,但基础的比赛数据(射门、传球、犯规等)已经相对完善。这为我们进行“传统数据”分析提供了良好的基础,同时也可以尝试用现代的数据思维去重新审视这些历史数据,比如计算球队的“控球效率”或“射门转化率”。
从项目学习角度看:分析一个完整的历史赛季,涉及从数据采集到结论呈现的全链路。你将会遇到真实数据中常见的各种问题,如数据缺失、格式不一致、定义模糊等,并学习如何解决它们。最终,我们不仅能得到关于08-09赛季的结论,更能掌握一套处理时序体育数据的方法论,这套方法可以轻松迁移到分析其他赛季或其他体育项目上。
简单来说,数据是凝固的历史,分析是让历史开口说话的工具。我们本项目要做的,就是为08-09这个精彩的赛季,打造一套专属的“数据翻译器”。
2. 环境准备与版本说明
工欲善其事,必先利其器。为了保证教程的可复现性,下面列出本项目推荐的环境配置。如果你的环境有所不同,请根据实际情况调整依赖库的版本。
操作系统:Windows 10/11, macOS, 或 Linux (如 Ubuntu) 均可。本文示例命令以 macOS/Linux 的终端为主,Windows 用户可在 PowerShell 或 WSL 中执行相应命令。
编程语言与核心版本:
- Python 3.8+:这是我们的主力编程语言。建议使用 3.8 及以上版本,以确保所有库的兼容性。
- Pip:Python 的包管理工具,通常随 Python 安装。
主要第三方库: 我们将使用以下库,请通过pip安装。创建一个requirements.txt文件来管理依赖是个好习惯。
# requirements.txt pandas==1.5.3 # 数据分析核心库,用于数据清洗、处理和分析 numpy==1.24.3 # 数值计算基础库,pandas的依赖,也用于数学运算 requests==2.31.0 # 用于从网络API或网页获取数据 beautifulsoup4==4.12.2 # 用于解析HTML网页,爬取数据 lxml==4.9.3 # BeautifulSoup的解析器,速度较快 sqlalchemy==2.0.23 # ORM工具,用于将数据方便地存入数据库 matplotlib==3.7.2 # 基础绘图库,用于创建静态图表 seaborn==0.12.2 # 基于matplotlib的统计图表库,绘图更美观 jupyter==1.0.0 # 可选,用于在笔记本环境中交互式分析在项目根目录下,执行以下命令一键安装所有依赖:
pip install -r requirements.txt开发工具:
- 代码编辑器/IDE:强烈推荐使用VS Code或PyCharm。它们对Python、Jupyter Notebook和数据科学有很好的支持。
- 数据库:为了持久化存储数据,我们将使用轻量级的SQLite。它无需安装,Python 内置
sqlite3模块即可操作。这对于学习和中小型项目完全足够。
项目结构: 在开始前,建议先建立如下清晰的目录结构,这有助于管理代码和数据。
football-data-0809/ ├── data/ # 存放原始和清洗后的数据文件 │ ├── raw/ # 原始数据(如爬取的CSV、JSON) │ └── processed/ # 清洗后的数据 ├── src/ # 源代码 │ ├── crawler.py # 数据爬取脚本 │ ├── cleaner.py # 数据清洗脚本 │ ├── analyzer.py # 数据分析脚本 │ └── visualizer.py # 数据可视化脚本 ├── notebooks/ # Jupyter Notebook文件,用于探索性分析 ├── output/ # 生成的图表、报告 ├── requirements.txt # 项目依赖列表 └── README.md # 项目说明文档现在,你的基础环境已经就绪。接下来,我们将进入最关键的环节:获取数据。
3. 核心步骤一:数据获取与爬虫实践
对于历史赛季数据,一个可靠的来源至关重要。我们可以从一些公开的足球数据网站获取。请注意:在爬取任何网站数据前,务必查看该网站的robots.txt文件和使用条款,尊重网站的爬虫规则,避免高频请求对服务器造成压力。本教程将以教育和学习为目的,演示基础爬虫思路。
我们计划爬取08-09赛季英超联赛的球队基本数据,例如积分、胜平负、进球、失球等。这里以一个假设的、结构简单的公开数据页面为例。
3.1 分析目标页面结构
假设我们目标页面的URL结构为:http://example.com/league/EPL/2009。首先,我们需要用浏览器开发者工具(F12)查看网页结构,找到包含联赛积分表的HTML元素。
通常,这类数据会放在一个<table>标签中。我们需要找到这个table的独特标识,比如id或class。
3.2 编写爬虫脚本
在src/crawler.py中,我们编写爬取逻辑。
# src/crawler.py import requests from bs4 import BeautifulSoup import pandas as pd import time import os def scrape_premier_league_0809(): """ 爬取08-09赛季英超联赛积分榜数据。 这是一个示例函数,实际URL和解析规则需要根据真实网站调整。 """ # 目标URL (示例,请替换为真实可用的数据源) url = 'http://example.com/league/EPL/2009' # 设置请求头,模拟浏览器访问 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } try: print(f"正在请求页面: {url}") response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() # 如果状态码不是200,抛出HTTPError异常 except requests.exceptions.RequestException as e: print(f"网络请求失败: {e}") return None # 使用BeautifulSoup解析HTML内容,指定lxml解析器 soup = BeautifulSoup(response.content, 'lxml') # 假设数据在id为‘league-table’的table中 # 你需要根据实际网页结构修改这里的选择器 table = soup.find('table', {'id': 'league-table'}) if not table: print("未找到积分榜表格,请检查网页结构或选择器。") return None # 使用pandas直接读取HTML中的表格,这通常是最快的方法 # 注意:read_html返回的是一个DataFrame列表 df_list = pd.read_html(str(table)) if df_list: df = df_list[0] # 假设第一个表格就是我们需要的 print("数据爬取成功!") print(df.head()) # 预览前5行数据 return df else: print("未能从表格中解析出数据。") return None def save_data(df, filename='premier_league_0809_raw.csv'): """将DataFrame保存为CSV文件到data/raw目录""" raw_data_path = os.path.join('data', 'raw') os.makedirs(raw_data_path, exist_ok=True) # 如果目录不存在则创建 file_path = os.path.join(raw_data_path, filename) df.to_csv(file_path, index=False, encoding='utf-8-sig') # utf-8-sig避免中文乱码 print(f"原始数据已保存至: {file_path}") if __name__ == '__main__': # 执行爬虫 data_df = scrape_premier_league_0809() if data_df is not None: # 保存数据 save_data(data_df) print("爬虫任务完成!") else: print("爬虫任务失败,未获取到数据。")关键点解释与注意事项:
- 请求头(User-Agent):许多网站会屏蔽没有User-Agent的请求(被认为是爬虫)。添加一个常见的浏览器UA可以绕过简单的反爬。
- 异常处理:网络请求可能失败(超时、404等),使用
try...except包裹并给出友好提示是必须的。 - 解析器选择:
lxml解析器需要单独安装(已在requirements中),它比Python内置的html.parser更快更强大。 - 选择器:
soup.find(‘table‘, {‘id‘: ‘league-table‘})是定位元素的关键。你需要根据实际网页的HTML结构来调整id或class的值。这是爬虫编写中最需要耐心调试的部分。 pd.read_html:Pandas的这个函数非常强大,可以直接将HTML中的表格转换为DataFrame。但它可能无法完美解析所有复杂表格,必要时仍需手动遍历table的tr和td标签。- 延迟与礼貌:在循环爬取多个页面时,务必在请求之间添加
time.sleep(2)等延迟,避免对目标服务器造成过大负担。
运行这个脚本后,你将在data/raw/目录下得到一个premier_league_0809_raw.csv文件,里面就是爬取到的原始积分榜数据。
4. 核心步骤二:数据清洗与预处理
爬取到的原始数据往往很“脏”,可能存在缺失值、重复行、格式不一致、无关字符等问题。数据清洗是数据分析中至关重要且耗时的一步,直接决定了后续分析的质量。
让我们在src/cleaner.py中创建一个数据清洗函数。
# src/cleaner.py import pandas as pd import numpy as np import os def load_raw_data(filename='premier_league_0809_raw.csv'): """从data/raw目录加载原始CSV数据""" file_path = os.path.join('data', 'raw', filename) if not os.path.exists(file_path): raise FileNotFoundError(f"原始数据文件未找到: {file_path}") df = pd.read_csv(file_path, encoding='utf-8-sig') print(f"成功加载数据,形状: {df.shape}") return df def clean_league_data(df): """ 清洗联赛积分榜数据。 这是一个通用流程,具体清洗步骤需视原始数据情况而定。 """ # 1. 创建副本,避免修改原数据 df_clean = df.copy() print("=== 开始数据清洗 ===") # 2. 查看基本信息 print("1. 数据前5行:") print(df_clean.head()) print("\n2. 数据信息:") print(df_clean.info()) print("\n3. 数值列统计描述:") print(df_clean.describe()) # 3. 处理列名:去除空格、换行符,统一为小写 df_clean.columns = df_clean.columns.str.strip().str.lower().str.replace(' ', '_') print(f"\n4. 标准化后的列名: {list(df_clean.columns)}") # 4. 处理缺失值 print(f"\n5. 缺失值统计:\n{df_clean.isnull().sum()}") # 策略:数值列用中位数填充,球队名列等关键列若有缺失则删除整行 for col in df_clean.columns: if df_clean[col].dtype in ['int64', 'float64']: # 数值列,用中位数填充 if df_clean[col].isnull().any(): median_val = df_clean[col].median() df_clean[col].fillna(median_val, inplace=True) print(f" - 数值列 '{col}' 的缺失值已用中位数 {median_val} 填充。") else: # 非数值列(如球队名),删除缺失行 null_count = df_clean[col].isnull().sum() if null_count > 0: df_clean.dropna(subset=[col], inplace=True) print(f" - 因列 '{col}' 有 {null_count} 个缺失值,已删除对应行。") # 5. 处理重复行 duplicate_count = df_clean.duplicated().sum() if duplicate_count > 0: df_clean.drop_duplicates(inplace=True) print(f"\n6. 发现并删除了 {duplicate_count} 个完全重复的行。") else: print(f"\n6. 未发现完全重复的行。") # 6. 数据格式转换与创建新特征 # 假设原始数据有‘matches_played‘(比赛场次), ‘points‘(积分)等列 # 我们可以计算‘points_per_match‘(场均积分)这个新特征 if all(col in df_clean.columns for col in ['points', 'matches_played']): df_clean['points_per_match'] = df_clean['points'] / df_clean['matches_played'] df_clean['points_per_match'] = df_clean['points_per_match'].round(2) print(f" - 已创建新特征 ‘points_per_match‘ (场均积分)。") # 7. 重置索引 df_clean.reset_index(drop=True, inplace=True) print(f"\n=== 清洗完成 ===") print(f"清洗后数据形状: {df_clean.shape}") return df_clean def save_clean_data(df, filename='premier_league_0809_clean.csv'): """将清洗后的DataFrame保存为CSV文件到data/processed目录""" processed_data_path = os.path.join('data', 'processed') os.makedirs(processed_data_path, exist_ok=True) file_path = os.path.join(processed_data_path, filename) df.to_csv(file_path, index=False, encoding='utf-8-sig') print(f"清洗后的数据已保存至: {file_path}") if __name__ == '__main__': # 加载原始数据 try: raw_df = load_raw_data() except FileNotFoundError as e: print(e) exit(1) # 清洗数据 clean_df = clean_league_data(raw_df) # 保存清洗后的数据 save_clean_data(clean_df)清洗逻辑详解:
- 探索性查看:使用
head(),info(),describe()快速了解数据全貌,这是制定清洗策略的基础。 - 列名标准化:混乱的列名是后续分析的噩梦。统一为小写并用下划线连接,能极大提升代码可读性。
- 缺失值处理:这是核心步骤。没有放之四海而皆准的方法,需要根据业务逻辑判断。
- 数值列(积分、进球):用中位数填充比用均值更稳健,不易受极端值影响。
- 关键标识列(球队名):如果缺失,这行数据很可能无效,直接删除。
- 重复值处理:使用
duplicated()和drop_duplicates()轻松处理。 - 特征工程:在清洗阶段就可以开始创造新的、更有意义的特征。例如“场均积分”比单纯的“总积分”更能反映球队在整个赛季的稳定表现。
- 保存结果:将清洗后的干净数据单独保存,与原始数据隔离。这是数据管道中良好的实践。
运行清洗脚本后,你将在data/processed/目录下得到一份干净、规整的数据集,可以放心地用于后续分析。
5. 核心步骤三:数据分析与洞察挖掘
有了干净的数据,我们就可以开始提出具体问题并用数据来回答了。我们创建一个src/analyzer.py文件来进行分析。
假设我们的清洗后数据包含以下列:team,matches_played,wins,draws,losses,goals_for,goals_against,points,points_per_match。
# src/analyzer.py import pandas as pd import numpy as np import os import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体和图表样式(可选,如需显示中文) # plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 # plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 sns.set_style("whitegrid") # 设置seaborn绘图风格 def load_clean_data(filename='premier_league_0809_clean.csv'): """加载清洗后的数据""" file_path = os.path.join('data', 'processed', filename) df = pd.read_csv(file_path, encoding='utf-8-sig') print(f"分析数据加载成功,共 {df.shape[0]} 支球队,{df.shape[1]} 个特征。") return df def basic_analysis(df): """基础统计分析""" print("="*50) print("基础统计分析") print("="*50) # 1. 冠军、降级区 champion = df.loc[df['points'].idxmax()] print(f"冠军球队: {champion['team']},积分: {champion['points']}") # 假设最后三名降级 (真实情况需查规则,如英超当季是最后三名降级) relegated = df.nsmallest(3, 'points') print(f"\n降级球队 (积分最低的三支):") for _, row in relegated.iterrows(): print(f" - {row['team']}: {row['points']} 分") # 2. 进攻与防守排行榜 print(f"\n进攻最强 (进球最多) 前三名:") top_attack = df.nlargest(3, 'goals_for') for _, row in top_attack.iterrows(): print(f" - {row['team']}: {row['goals_for']} 球") print(f"\n防守最佳 (失球最少) 前三名:") top_defense = df.nsmallest(3, 'goals_against') for _, row in top_defense.iterrows(): print(f" - {row['team']}: {row['goals_against']} 球") # 3. 净胜球分布 df['goal_difference'] = df['goals_for'] - df['goals_against'] print(f"\n净胜球最高的球队: {df.loc[df['goal_difference'].idxmax(), 'team']} ({df['goal_difference'].max()} 球)") print(f"净胜球最低的球队: {df.loc[df['goal_difference'].idxmin(), 'team']} ({df['goal_difference'].min()} 球)") return df # 返回添加了新列‘goal_difference‘的DataFrame def advanced_metrics(df): """计算高级指标""" print("\n" + "="*50) print("高级指标计算") print("="*50) # 1. 胜率、平率、负率 df['win_rate'] = (df['wins'] / df['matches_played']).round(3) df['draw_rate'] = (df['draws'] / df['matches_played']).round(3) df['loss_rate'] = (df['losses'] / df['matches_played']).round(3) # 2. 场均进球、场均失球 df['goals_for_per_match'] = (df['goals_for'] / df['matches_played']).round(2) df['goals_against_per_match'] = (df['goals_against'] / df['matches_played']).round(2) # 3. 积分效率 (实际积分/最大可能积分)。最大可能积分 = 比赛场次 * 3 max_possible_points = df['matches_played'] * 3 df['points_efficiency'] = (df['points'] / max_possible_points).round(3) print("已计算胜率、场均进球、积分效率等高级指标。") print(df[['team', 'win_rate', 'goals_for_per_match', 'points_efficiency']].head()) return df def correlation_analysis(df): """相关性分析:哪些因素与积分最相关?""" print("\n" + "="*50) print("特征相关性分析") print("="*50) # 选择数值型特征 numeric_cols = ['points', 'wins', 'draws', 'losses', 'goals_for', 'goals_against', 'goal_difference', 'win_rate', 'goals_for_per_match', 'points_efficiency'] # 确保这些列都存在 numeric_cols = [col for col in numeric_cols if col in df.columns] correlation_matrix = df[numeric_cols].corr() # 找出与‘points‘相关性最高的特征 points_corr = correlation_matrix['points'].sort_values(ascending=False) print("特征与‘积分‘的相关性排序 (从高到低):") for feat, corr_val in points_corr.items(): print(f" {feat:25s}: {corr_val:.3f}") # 可视化相关性热力图 plt.figure(figsize=(10, 8)) sns.heatmap(correlation_matrix, annot=True, fmt='.2f', cmap='coolwarm', center=0, square=True, linewidths=.5, cbar_kws={"shrink": .8}) plt.title('08-09赛季英超球队数据相关性热力图') plt.tight_layout() # 保存图表 output_dir = 'output' os.makedirs(output_dir, exist_ok=True) plt.savefig(os.path.join(output_dir, 'correlation_heatmap.png'), dpi=300) print(f"\n相关性热力图已保存至: {output_dir}/correlation_heatmap.png") # plt.show() # 如果在Jupyter中运行,可以显示 return points_corr if __name__ == '__main__': # 加载数据 df = load_clean_data() # 执行基础分析 df = basic_analysis(df) # 计算高级指标 df = advanced_metrics(df) # 进行相关性分析 corr_result = correlation_analysis(df) # (可选) 将增强后的分析数据保存下来 analysis_output_path = os.path.join('data', 'processed', 'premier_league_0809_analyzed.csv') df.to_csv(analysis_output_path, index=False, encoding='utf-8-sig') print(f"\n完整的分析数据已保存至: {analysis_output_path}")分析思路解读:
- 基础分析:回答最直接的问题——谁赢了?谁输了?谁进攻强?谁防守好?这利用了Pandas的排序和索引功能。
- 高级指标:原始数据是总计,但不同球队比赛场次相同,我们可以将其“标准化”,计算比率类指标,如胜率、场均进球等。这能让我们进行更公平的跨球队比较。“积分效率”是一个有趣的指标,它衡量了球队获取理论最大积分的比例,反映了稳定性和抢分能力。
- 相关性分析:这是挖掘数据背后故事的关键。我们计算所有数值特征之间的相关系数(皮尔逊相关系数)。例如,我们发现“胜场数”与“积分”的相关性必然极高(因为赢球得3分)。但更有趣的是,“净胜球”与“积分”的相关性可能也非常高,这说明了攻守平衡的重要性。通过热力图,我们可以直观地看到所有变量之间的关系。
运行这个分析脚本,你不仅会在控制台看到各种文字结论,还会在output/文件夹下生成一张精美的相关性热力图。
6. 核心步骤四:数据可视化与故事呈现
数字是冰冷的,图表却能讲故事。让我们用更多的图表来全方位展示08-09赛季英超的故事。我们在src/visualizer.py中创建可视化函数。
# src/visualizer.py import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import os # 设置全局样式 plt.style.use('seaborn-v0_8-whitegrid') sns.set_palette("husl") def create_league_table_chart(df): """创建积分榜可视化:条形图展示前6名球队的积分""" top_n = 6 df_sorted = df.sort_values('points', ascending=False).head(top_n) fig, ax = plt.subplots(figsize=(10, 6)) bars = ax.barh(df_sorted['team'], df_sorted['points'], color=sns.color_palette('Blues_r', top_n)) ax.set_xlabel('积分', fontsize=12) ax.set_title(f'08-09赛季英超积分榜 (前{top_n}名)', fontsize=14, fontweight='bold') ax.invert_yaxis() # 让第一名在最上面 # 在条形末端显示积分值 for bar in bars: width = bar.get_width() ax.text(width + 0.5, bar.get_y() + bar.get_height()/2, f'{int(width)}', ha='left', va='center', fontsize=10) plt.tight_layout() save_path = os.path.join('output', 'league_table_top6.png') plt.savefig(save_path, dpi=300) plt.close(fig) # 关闭图形,避免在非交互环境下重叠 print(f"积分榜条形图已保存: {save_path}") def create_goals_scatter(df): """创建进球-失球散点图,观察球队攻防风格""" fig, ax = plt.subplots(figsize=(11, 7)) scatter = ax.scatter(df['goals_for'], df['goals_against'], s=df['points']*5, # 用点的大小表示积分多少 alpha=0.7, c=df['goal_difference'], # 用颜色表示净胜球 cmap='RdYlGn') # 红-黄-绿色彩映射,净胜球高为绿,低为红 ax.set_xlabel('总进球数', fontsize=12) ax.set_ylabel('总失球数', fontsize=12) ax.set_title('08-09赛季英超球队攻防分布图 (点大小=积分,颜色=净胜球)', fontsize=13, fontweight='bold') # 添加球队标签,避免重叠 for i, row in df.iterrows(): # 只为积分较高或特征明显的球队添加标签 if row['points'] > 50 or row['goals_for'] > 65 or row['goals_against'] > 65: ax.annotate(row['team'], (row['goals_for'], row['goals_against']), xytext=(5, 5), textcoords='offset points', fontsize=9) # 添加参考线:平均进球、平均失球 avg_goals_for = df['goals_for'].mean() avg_goals_against = df['goals_against'].mean() ax.axvline(avg_goals_for, color='grey', linestyle='--', alpha=0.5, label=f'平均进球 ({avg_goals_for:.1f})') ax.axhline(avg_goals_against, color='black', linestyle='--', alpha=0.5, label=f'平均失球 ({avg_goals_against:.1f})') ax.legend() plt.colorbar(scatter, label='净胜球') plt.tight_layout() save_path = os.path.join('output', 'goals_scatter.png') plt.savefig(save_path, dpi=300) plt.close(fig) print(f"攻防散点图已保存: {save_path}") def create_radar_chart_for_top_teams(df): """为前三名球队创建雷达图,对比多项关键指标""" # 选择要对比的指标 stats_columns = ['win_rate', 'goals_for_per_match', 'goals_against_per_match', 'points_per_match', 'points_efficiency'] stats_labels = ['胜率', '场均进球', '场均失球', '场均积分', '积分效率'] top_teams = df.nlargest(3, 'points') team_names = top_teams['team'].tolist() # 准备数据:每个指标需要归一化到 [0, 1] 区间,以便在雷达图上比较 from math import pi plot_data = [] for col in stats_columns: max_val = top_teams[col].max() min_val = top_teams[col].min() # 避免除零 if max_val == min_val: normalized = [0.5 for _ in range(len(top_teams))] else: normalized = [(x - min_val) / (max_val - min_val) for x in top_teams[col]] plot_data.append(normalized) # 雷达图需要将第一个点重复一次以闭合图形 plot_data.append(plot_data[0]) stats_labels.append(stats_labels[0]) angles = [n / float(len(stats_labels)-1) * 2 * pi for n in range(len(stats_labels))] angles += angles[:1] # 闭合 fig, ax = plt.subplots(figsize=(9, 9), subplot_kw=dict(projection='polar')) colors = ['#1f77b4', '#ff7f0e', '#2ca02c'] # 为三支球队指定颜色 for idx, team in enumerate(team_names): values = [row[idx] for row in plot_data] ax.plot(angles, values, 'o-', linewidth=2, label=team, color=colors[idx]) ax.fill(angles, values, alpha=0.1, color=colors[idx]) ax.set_xticks(angles[:-1]) ax.set_xticklabels(stats_labels[:-1], fontsize=11) ax.set_ylim(0, 1) ax.set_title('08-09赛季英超前三名球队关键指标雷达图对比', size=14, fontweight='bold', pad=20) ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.0)) plt.tight_layout() save_path = os.path.join('output', 'top_teams_radar.png') plt.savefig(save_path, dpi=300, bbox_inches='tight') plt.close(fig) print(f"球队对比雷达图已保存: {save_path}") if __name__ == '__main__': # 确保输出目录存在 os.makedirs('output', exist_ok=True) # 加载分析后的数据 data_path = os.path.join('data', 'processed', 'premier_league_0809_analyzed.csv') if not os.path.exists(data_path): print(f"分析数据文件不存在,请先运行 analyzer.py。") exit(1) df = pd.read_csv(data_path, encoding='utf-8-sig') print(f"已加载数据,准备生成可视化图表...") # 生成各类图表 create_league_table_chart(df) create_goals_scatter(df) create_radar_chart_for_top_teams(df) print("所有可视化图表已生成完毕,请查看 ‘output‘ 文件夹。")可视化设计思路:
- 积分榜条形图:采用横向条形图,并让积分最高的球队排在最上方,符合阅读习惯。用颜色深浅和直接标注数值来强化信息。
- 攻防散点图:这是分析球队风格的利器。X轴是进球,Y轴是失球。
- 右下区域:进球多、失球少,是攻守俱佳的冠军相球队。
- 右上区域:进球多、失球也多,是激情四射但防守不稳的球队。
- 左下区域:进球少、失球少,是防守稳健但进攻乏力的球队。
- 左上区域:进球少、失球多,是表现不佳的球队。
- 用点的大小表示积分,颜色表示净胜球,一张图里融合了四个维度的信息。
- 雷达图:用于多维度对比顶级球队的综合实力。我们将胜率、场均进球等不同量纲的指标归一化后放在同一个雷达图上,可以清晰看出各队的优势与短板。例如,冠军球队可能在所有维度都领先,而亚军球队可能在“场均进球”上突出,但“积分效率”稍弱。
运行可视化脚本后,打开output/文件夹,你就能看到三张从不同角度讲述08-09赛季英超故事的图表。
7. 常见问题与排查思路
在实践这个项目时,你可能会遇到一些典型问题。下面是一个快速排查指南。
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 运行爬虫脚本时连接超时或拒绝 | 1. 目标网站URL错误或失效。 2. 网站有反爬机制(如IP限制)。 3. 网络问题。 | 1. 检查URL是否正确,用浏览器手动访问确认。 2. 添加更完整的请求头(如 Referer,Accept),并显著增加请求间隔(如time.sleep(5))。3. 检查本地网络,尝试使用代理(需合法合规)。 |
BeautifulSoup找不到表格 (table为None) | 1. 网页结构发生变化,HTML标签或属性已更改。 2. 表格数据是通过JavaScript动态加载的,初始HTML中没有。 | 1. 重新用开发者工具检查网页,更新find函数中的选择器(可能不是table,或是class不同)。2. 考虑使用 Selenium或Playwright等工具来模拟浏览器渲染动态内容。 |
pd.read_html解析出的数据是空的或错乱 | 1. 表格结构复杂,含有合并单元格等,read_html无法完美解析。2. 表格可能嵌套在多个标签内。 | 1. 放弃read_html,改用BeautifulSoup手动遍历tr和td标签,逐行提取数据。2. 将找到的 table的HTML字符串保存到文件,仔细检查其结构。 |
数据分析时出现KeyError | 1. 代码中引用的列名在DataFrame中不存在。 2. 列名在清洗前后不一致(如大小写、空格)。 | 1. 使用df.columns打印所有列名,确保引用正确。2. 在数据加载后和清洗后都打印列名,确保清洗步骤没有意外修改或删除列。 |
| 图表中文显示为方框 | 系统中没有合适的中文字体,或matplotlib未正确配置。 | 1. (推荐)在可视化代码中指定中文字体路径,如plt.rcParams[‘font.sans-serif‘] = [‘SimHei‘]。2. 或者,避免在图表中使用中文,改用英文标签。 |
| 生成的图表图片是空白 | 1. 在非交互式环境(如脚本)中未正确保存或显示图形。 2. 图形被后续的绘图覆盖。 | 1. 确保在plt.savefig()之后调用plt.close(fig)关闭当前图形对象。2. 检查保存路径的文件夹权限,确保有写入权限。 |
8. 最佳实践与项目扩展建议
完成基础分析后,你可以遵循以下最佳实践来完善项目,并尝试更有挑战性的扩展。
8.1 项目结构与代码优化
- 模块化:我们已经将爬虫、清洗、分析、可视化分离到不同文件,这是良好的开端。可以进一步将通用函数(如数据保存、日志记录)提取到
utils.py中。 - 配置文件:将URL、数据库连接字符串、API密钥等配置信息写入
config.yaml或config.ini文件,使代码更易维护和安全。 - 日志记录:使用Python的
logging模块替代print语句,可以更好地记录程序运行状态和错误信息,方便调试。 - 错误处理:在关键步骤(如网络请求、文件读写、数据库操作)增加更细致的
try-except块,并提供有意义的错误提示和恢复逻辑。
8.2 数据源扩展与深度分析
- 多赛季对比:爬取07-08,09-10等相邻赛季的数据,分析联赛格局的演变趋势。
- 球员数据:寻找球员级别的数据(射手榜、助攻榜、传球成功率等),分析金靴奖得主的特点,或挖掘被低估的球员。
- 比赛事件数据:如果可能,获取更细粒度的数据,如每场比赛的射门位置、传球路线、控球时段。这可以用于计算“预期进球(xG)”等现代足球分析指标。
- 集成第三方API:使用像
Football-Data.org、API-Football等提供的付费或免费API(注意调用限制和条款),获取更规范、稳定的数据。
8.3 技术栈进阶
- 数据库存储:将SQLite升级为PostgreSQL或MySQL,以处理更大规模的数据。使用
SQLAlchemy的ORM可以平滑过渡。 - 自动化与调度:使用
Apache Airflow或Prefect构建数据管道,定期自动运行爬虫、清洗和分析任务。 - 交互式可视化:使用
Plotly或Dash库创建交互式网页仪表盘,让用户能够自主筛选赛季、球队,动态查看图表。 - 机器学习初探:尝试使用
scikit-learn构建简单的预测模型。例如,利用球队上半赛季的数据,预测其最终联赛排名。
8.4 分析维度深化
- 主场/客场表现:将数据按主客场拆分,分析“主场龙”和“客场虫”现象。
- 赛季阶段分析:将38轮联赛分为开局、中期、收官等阶段,分析球队在不同阶段的抢分能力和状态稳定性。
- 对阵关系网络:使用
NetworkX绘制球队之间的对阵关系图,用边的粗细表示胜负关系或进球数,可视化联赛内的“食物链”。
通过这个项目,你收获的不仅仅是对08-09赛季英超的数据解读,更是一套完整的、可复用的数据分析工程能力。从数据获取到洞察呈现,每一步都充满了挑战和乐趣。试着用这套方法,去分析你喜爱的其他赛季、其他联赛,或者完全不同的体育项目吧。数据的世界,等你探索。