三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python实战:从数据获取到可视化,复现C罗欧冠经典战役分析

Python实战:从数据获取到可视化,复现C罗欧冠经典战役分析

在足球史上,欧冠联赛的经典对决总能点燃球迷的激情,而将顶级球星与豪门球队的数据进行技术化分析,则是现代足球战术研究的重要一环。本文将以2016-17赛季欧冠四分之一决赛中,克里斯蒂亚诺·罗纳尔多(C罗)对阵拜仁慕尼黑的两回合史诗级表现为核心,完整拆解如何利用Python进行足球赛事数据的获取、清洗、分析与可视化。无论你是体育数据分析的初学者,还是希望将数据技能应用于新领域的开发者,都能通过本文掌握从零搭建一个足球比赛数据分析项目的能力,并得到一套可直接复用的代码。

1. 背景与核心概念:足球数据分析的价值与技术栈

足球数据分析早已超越了简单的“数进球、看助攻”阶段。它通过量化球员的每一次触球、跑动、射门,为战术制定、球员评估、比赛复盘提供科学依据。对于C罗这样的历史级球员,其在高强度淘汰赛中的表现,更是分析“关键球员决定性作用”的绝佳样本。

2016-17赛季欧冠四分之一决赛,皇家马德里对阵拜仁慕尼黑。C罗在两回合比赛中打入5球,尤其是在次回合加时赛完成帽子戏法,几乎以一己之力将球队送入四强。分析这场比赛,我们关注的不仅仅是5个进球,还包括他的射门分布、进攻参与度、在对方禁区内的威胁等维度。

要实现这样的分析,我们需要一个清晰的技术路径:

  1. 数据获取:从公开的足球数据网站或API获取结构化的比赛事件数据。
  2. 数据清洗与处理:将原始数据转换为适合分析的格式,处理缺失值,计算衍生指标。
  3. 数据分析与可视化:运用统计方法和绘图库,直观展示球员的热点图、射门图、传球网络等。
  4. 报告生成:将分析结果整合成可读性强的报告或仪表盘。

本文将主要使用Python生态中的pandasnumpy进行数据处理,使用matplotlibseaborn进行可视化,并使用requests库模拟数据获取过程。

2. 环境准备与版本说明

在开始编写代码前,请确保你的开发环境已就绪。本文将使用Python 3.8+版本进行演示,重点在于分析流程的完整性。部分在线数据源可能需要网络访问,请确保你的开发环境具备相应的条件。

核心工具与库:

  • Python: 3.8 或更高版本。
  • Jupyter Notebook / Jupyter Lab: 推荐使用,便于交互式分析和展示。当然,标准的.py脚本文件也同样有效。
  • 关键Python库
    • pandas(>=1.3.0): 数据分析的基石。
    • numpy(>=1.21.0): 数值计算。
    • matplotlib(>=3.5.0): 基础绘图库。
    • seaborn(>=0.11.0): 基于matplotlib的统计图表库,样式更美观。
    • requests(>=2.27.0): 用于HTTP请求,获取网络数据。
    • json: Python标准库,用于解析JSON格式数据。

安装命令:你可以使用pip一次性安装所有必需的库。建议在虚拟环境中操作。

pip install pandas numpy matplotlib seaborn requests

项目结构建议:创建一个清晰的项目文件夹,有助于管理代码和数据。

c罗_vs_拜仁_分析/ ├── data/ # 存放原始和清洗后的数据文件 │ ├── raw/ # 原始数据(如从API下载的JSON) │ └── processed/ # 清洗处理后的数据(如CSV文件) ├── notebooks/ # Jupyter Notebook文件 │ └── analysis.ipynb # 主分析笔记本 ├── scripts/ # 可重用的Python脚本 │ ├── data_fetcher.py # 数据获取脚本 │ └── visualizer.py # 可视化函数脚本 ├── output/ # 生成的图表和报告 │ └── figures/ # 图片输出目录 └── README.md # 项目说明

3. 核心步骤拆解:从数据获取到洞察

3.1 数据获取策略与模拟

公开的足球数据源有很多,例如StatsBomb、FBref等,它们通常提供免费和付费的API。由于直接调用真实API涉及注册、密钥和配额限制,为了教程的通用性和可复现性,我们将模拟这一过程。

思路:我们将创建一个模拟数据集,其结构模仿真实足球事件数据,包含C罗在该两场比赛中的关键事件。然后,我们会编写一个模拟从“API”获取数据的函数。

模拟数据字段说明: 一个典型的事件数据行可能包含:

  • match_id: 比赛唯一标识。
  • team: 球队名称。
  • player: 球员姓名。
  • minute: 事件发生分钟。
  • second: 事件发生秒数。
  • type: 事件类型(如ShotPassDribble等)。
  • subtype: 事件子类型(如GoalSavedKey Pass等)。
  • x,y: 事件发生的球场坐标(通常标准化为0-100)。
  • end_x,end_y: 事件的结束坐标(如传球的落点)。

3.2 创建模拟数据集

我们将直接在Python中构造一个DataFrame来代表C罗在这两场比赛中的事件数据。

# 文件:scripts/data_simulator.py 或直接在notebook中运行 import pandas as pd import numpy as np def create_cr7_vs_bayern_data(): """ 模拟创建C罗在16-17欧冠对阵拜仁两回合比赛的事件数据。 数据基于公开的比赛报告和集锦进行合理虚构,用于演示分析流程。 """ # 定义比赛ID:假设第一回合ID为1,第二回合ID为2 match_ids = [1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2] # 对应下面11个事件 minutes = [25, 47, 77, 84, 76, 78, 105, 109, 112, 115, 118] # 事件发生分钟 # C罗在次回合加时赛打入3球,这里模拟其相关事件 players = ['Cristiano Ronaldo'] * 11 teams = ['Real Madrid'] * 11 # 事件类型和子类型 types = ['Shot', 'Shot', 'Pass', 'Shot', 'Shot', 'Pass', 'Shot', 'Shot', 'Shot', 'Dribble', 'Pass'] subtypes = ['Goal', 'Saved', 'Key Pass', 'Blocked', 'Goal', 'Assist', 'Goal', 'Goal', 'Saved', 'Successful', 'Key Pass'] # 模拟球场坐标 (x: 0-100, 从左向右进攻; y: 0-100, 从下到上) # 射门和事件起始点 x_start = [88, 85, 40, 90, 87, 30, 92, 89, 85, 70, 50] y_start = [50, 48, 60, 52, 50, 40, 48, 52, 55, 50, 65] # 事件结束点(对于传球和射门) x_end = [100, 94, 85, 92, 100, 88, 100, 100, 90, 75, 80] # 射门x_end通常>100表示出界或进球 y_end = [50, 50, 52, 50, 50, 48, 48, 52, 50, 55, 50] # 创建DataFrame data = pd.DataFrame({ 'match_id': match_ids, 'minute': minutes, 'player': players, 'team': teams, 'type': types, 'subtype': subtypes, 'x': x_start, 'y': y_start, 'end_x': x_end, 'end_y': y_end }) # 添加一个复合事件列,便于阅读 data['event'] = data['type'] + ' - ' + data['subtype'] # 添加结果列,简化射门结果 def map_result(row): if row['type'] == 'Shot': if row['subtype'] == 'Goal': return 'Goal' elif row['subtype'] == 'Saved': return 'Saved' else: return 'Blocked/Off Target' else: return 'Non-Shot' data['shot_result'] = data.apply(map_result, axis=1) return data # 生成数据 cr7_data = create_cr7_vs_bayern_data() print("模拟数据预览:") print(cr7_data[['match_id', 'minute', 'event', 'x', 'y', 'shot_result']].head()) print(f"\n数据形状:{cr7_data.shape}")

运行上述代码,你将得到一个包含C罗11个关键事件的DataFrame。在实际项目中,你会从API或CSV文件中读取规模大得多的真实数据集。

3.3 数据清洗与初步探索

拿到数据后,第一步永远是理解和清洗它。

# 文件:notebooks/analysis.ipynb 中的单元格 import pandas as pd import matplotlib.pyplot as plt import seaborn as sns # 设置中文显示和图表样式(可选) plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 sns.set_style("whitegrid") # 假设cr7_data已经通过上面的函数创建 # 1. 查看数据基本信息 print("数据基本信息:") print(cr7_data.info()) print("\n数据描述性统计:") print(cr7_data.describe()) # 2. 检查缺失值 print("\n各列缺失值数量:") print(cr7_data.isnull().sum()) # 3. 查看事件类型分布 print("\n事件类型分布:") event_dist = cr7_data['type'].value_counts() print(event_dist) # 4. 射门数据分析:筛选出所有射门 shots_data = cr7_data[cr7_data['type'] == 'Shot'].copy() print(f"\nC罗在两场比赛中共有{len(shots_data)}次射门。") print("射门结果分布:") print(shots_data['shot_result'].value_counts()) # 计算射门转化率 goals = (shots_data['shot_result'] == 'Goal').sum() total_shots = len(shots_data) conversion_rate = goals / total_shots * 100 print(f"\n射门转化率:{goals}/{total_shots} = {conversion_rate:.1f}%")

通过简单的清洗和统计,我们快速得到了C罗的射门次数和转化率。这是数据分析最基础的步骤。

4. 完整实战案例:可视化分析与深度洞察

4.1 创建射门点位图(Shot Map)

射门图是分析前锋表现最直观的工具之一。它能展示射门位置、结果和威胁程度。

# 继续在 notebook 中 def plot_shot_map(shots_df, pitch_length=100, pitch_width=100): """ 绘制射门点位图。 参数: shots_df: 包含射门事件的DataFrame,必须有'x', 'y', 'shot_result'列。 pitch_length, pitch_width: 球场标准化尺寸,默认为100。 """ fig, ax = plt.subplots(figsize=(12, 8)) # 绘制一个简单的半场球场轮廓(假设从左向右进攻) # 底线和边线 ax.plot([0, pitch_length], [0, 0], color='black', linewidth=2) # 底边线 ax.plot([0, pitch_length], [pitch_width, pitch_width], color='black', linewidth=2) # 顶边线 ax.plot([0, 0], [0, pitch_width], color='black', linewidth=2) # 左边线 ax.plot([pitch_length, pitch_length], [0, pitch_width], color='black', linewidth=2) # 右边线(中线位置) # 球门区(简化) goal_width = 10 # 假设球门宽度在y轴占10个单位 goal_y_start = (pitch_width - goal_width) / 2 ax.plot([pitch_length, pitch_length], [goal_y_start, goal_y_start + goal_width], color='red', linewidth=3) # 右侧球门线 # 禁区(简化矩形) box_length = 16 box_width = 40 box_x_start = pitch_length - box_length box_y_start = (pitch_width - box_width) / 2 rect = plt.Rectangle((box_x_start, box_y_start), box_length, box_width, linewidth=1, edgecolor='gray', facecolor='none', linestyle='--') ax.add_patch(rect) # 根据射门结果绘制不同颜色和大小的点 result_colors = {'Goal': 'green', 'Saved': 'blue', 'Blocked/Off Target': 'red'} result_sizes = {'Goal': 200, 'Saved': 120, 'Blocked/Off Target': 80} for result, color in result_colors.items(): subset = shots_df[shots_df['shot_result'] == result] if not subset.empty: ax.scatter(subset['x'], subset['y'], c=color, s=result_sizes[result], label=f'{result} ({len(subset)})', edgecolors='black', alpha=0.8, zorder=5) # 添加从射门点到球门的连线(仅对非进球射门,增强视觉) for _, shot in shots_df[shots_df['shot_result'] != 'Goal'].iterrows(): ax.plot([shot['x'], shot['end_x']], [shot['y'], shot['end_y']], color='gray', linestyle=':', alpha=0.5, linewidth=1) ax.set_xlim(-5, pitch_length + 5) ax.set_ylim(-5, pitch_width + 5) ax.set_aspect('equal') ax.set_title("C罗 vs 拜仁慕尼黑 (16-17欧冠) - 射门点位图", fontsize=16, fontweight='bold') ax.set_xlabel("球场长度方向 (0 -> 本方底线, 100 -> 对方底线)") ax.set_ylabel("球场宽度方向") ax.legend(title="射门结果", loc='upper left', bbox_to_anchor=(1.05, 1)) ax.invert_yaxis() # 反转y轴,使坐标原点在左上角(符合足球数据常见习惯) plt.tight_layout() # 保存图片 output_path = '../output/figures/cr7_shot_map_vs_bayern.png' plt.savefig(output_path, dpi=300, bbox_inches='tight') print(f"射门图已保存至:{output_path}") plt.show() # 调用函数绘制射门图 plot_shot_map(shots_data)

这段代码会生成一张射门点位图,绿色点代表进球,蓝色代表被扑救,红色代表被封堵或偏出。通过此图,我们可以直观看到C罗的射门大多集中在禁区内外右侧(他的惯用脚区域),且进球点分布显示了他作为“禁区之王”的终结效率。

4.2 比赛事件时间线分析

分析球员在比赛不同时间段的活动频率,能反映其体能分配和关键时刻的表现。

# 创建比赛分钟事件分布图 def plot_event_timeline(event_df, player_name): """ 绘制指定球员在比赛中的事件时间线。 """ fig, ax = plt.subplots(figsize=(14, 6)) # 为不同事件类型分配颜色 event_types = event_df['type'].unique() colors = plt.cm.tab10(np.linspace(0, 1, len(event_types))) color_map = dict(zip(event_types, colors)) # 按比赛ID分组绘制 matches = event_df['match_id'].unique() for i, match_id in enumerate(matches): match_data = event_df[event_df['match_id'] == match_id] y_pos = i * 1.5 # 为每场比赛分配一个垂直位置 for _, event in match_data.iterrows(): event_color = color_map[event['type']] # 绘制事件点 ax.scatter(event['minute'], y_pos, color=event_color, s=150, edgecolors='black', zorder=5, label=event['type'] if f"{event['type']}_{match_id}" not in [l.get_label() for l in ax.collections] else "") # 添加事件文本(简写) ax.text(event['minute'], y_pos+0.15, event['subtype'][:3], ha='center', va='bottom', fontsize=9, fontweight='bold') # 美化图表 ax.set_yticks([i * 1.5 for i in range(len(matches))]) ax.set_yticklabels([f'比赛 {mid}' for mid in matches]) ax.set_xlabel('比赛分钟', fontsize=12) ax.set_title(f'{player_name} 对阵拜仁 - 事件时间线', fontsize=16, fontweight='bold') ax.grid(True, axis='x', linestyle='--', alpha=0.7) # 创建图例(去重) handles, labels = [], [] for handle, label in zip(*ax.get_legend_handles_labels()): if label not in labels: handles.append(handle) labels.append(label) ax.legend(handles, labels, title='事件类型', loc='upper right') # 标记加时赛开始(第二回合) ax.axvline(x=90, color='red', linestyle=':', alpha=0.7, label='常规时间结束') ax.axvline(x=105, color='orange', linestyle=':', alpha=0.7, label='加时赛上半场结束') plt.tight_layout() output_path = f'../output/figures/{player_name.replace(" ", "_")}_event_timeline.png' plt.savefig(output_path, dpi=300, bbox_inches='tight') print(f"事件时间线图已保存至:{output_path}") plt.show() plot_event_timeline(cr7_data, 'Cristiano Ronaldo')

时间线图清晰展示了C罗在两场比赛中的事件分布。可以明显看到,在第二场比赛的加时赛时段(90分钟以后),事件点(尤其是射门Shot)非常密集,直观印证了他在关键时刻的爆发力和决定性作用。

4.3 综合数据报告生成

最后,我们可以将关键指标汇总成一个简单的文本报告,并保存所有分析结果。

# 生成文本分析报告 def generate_match_report(player_data, shots_data): """ 根据分析数据生成一份简明的文本报告。 """ total_events = len(player_data) total_shots = len(shots_data) total_goals = (shots_data['shot_result'] == 'Goal').sum() conversion_rate = total_goals / total_shots * 100 if total_shots > 0 else 0 # 计算平均射门位置 avg_shot_x = shots_data['x'].mean() avg_shot_y = shots_data['y'].mean() # 按比赛拆分 match1_data = player_data[player_data['match_id'] == 1] match2_data = player_data[player_data['match_id'] == 2] match1_shots = match1_data[match1_data['type'] == 'Shot'] match2_shots = match2_data[match2_data['type'] == 'Shot'] match1_goals = (match1_shots['shot_result'] == 'Goal').sum() match2_goals = (match2_shots['shot_result'] == 'Goal').sum() report_lines = [] report_lines.append("="*60) report_lines.append(" C罗 vs 拜仁慕尼黑 (16-17欧冠) 数据分析报告") report_lines.append("="*60) report_lines.append("") report_lines.append(f"分析球员: Cristiano Ronaldo") report_lines.append(f"分析场次: 欧冠1/4决赛 两回合") report_lines.append("") report_lines.append("【整体数据概览】") report_lines.append(f" 总记录事件数: {total_events}") report_lines.append(f" 总射门次数: {total_shots}") report_lines.append(f" 总进球数: {total_goals}") report_lines.append(f" 射门转化率: {conversion_rate:.1f}%") report_lines.append(f" 平均射门位置 (x, y): ({avg_shot_x:.1f}, {avg_shot_y:.1f})") report_lines.append("") report_lines.append("【分场比赛数据】") report_lines.append(f" 第一回合:") report_lines.append(f" 事件数: {len(match1_data)}") report_lines.append(f" 射门/进球: {len(match1_shots)} / {match1_goals}") report_lines.append(f" 第二回合:") report_lines.append(f" 事件数: {len(match2_data)}") report_lines.append(f" 射门/进球: {len(match2_shots)} / {match2_goals}") report_lines.append("") report_lines.append("【关键洞察】") report_lines.append(" 1. 极高的射门转化率是决定比赛的关键。") report_lines.append(" 2. 第二回合加时赛阶段活动显著增加,体现了顶级的体能和决胜心态。") report_lines.append(" 3. 射门多集中于禁区内外偏右区域,符合其左脚球员的进攻习惯。") report_lines.append("") report_lines.append("="*60) report_text = "\n".join(report_lines) return report_text # 生成并打印报告 report = generate_match_report(cr7_data, shots_data) print(report) # 将报告保存为文本文件 report_path = '../output/cr7_vs_bayern_analysis_report.txt' with open(report_path, 'w', encoding='utf-8') as f: f.write(report) print(f"\n分析报告已保存至:{report_path}") # 将清洗后的数据保存为CSV,便于后续使用 cr7_data.to_csv('../data/processed/cr7_events_processed.csv', index=False, encoding='utf-8-sig') shots_data.to_csv('../data/processed/cr7_shots_processed.csv', index=False, encoding='utf-8-sig') print("处理后的数据已保存至 data/processed/ 目录。")

5. 常见问题与排查思路

在实际进行足球数据分析时,你可能会遇到以下问题:

问题现象可能原因解决思路
无法获取真实数据(API错误、限制)1. API密钥无效或过期。
2. 请求频率超限。
3. 数据源结构已更新。
1. 检查并更新API密钥。
2. 添加请求延迟(如time.sleep)避免限流。
3. 查阅数据源最新文档,调整解析逻辑。
坐标数据与预期球场图不匹配1. 坐标系标准不同(如Opta vs StatsBomb)。
2. 数据未进行标准化处理。
1.务必确认数据源的坐标系说明。StatsBomb使用0-120和0-80,而Opta常用0-100。
2. 编写转换函数,将坐标统一映射到自己的绘图标准(如0-100)。
pandas操作报错KeyError1. 列名拼写错误或不存在。
2. 数据清洗后列被误删除。
1. 使用df.columns打印所有列名进行核对。
2. 在关键操作前使用df = df.copy()避免链式赋值警告和错误。
可视化图形中文显示为方框matplotlib默认字体不包含中文。在绘图前设置中文字体:
plt.rcParams['font.sans-serif'] = ['SimHei', ...]
plt.rcParams['axes.unicode_minus'] = False
分析结果与公开统计有出入1. 数据源本身统计口径不同。
2. 模拟数据与真实数据存在偏差。
3. 过滤条件(如事件类型定义)不一致。
1. 明确分析所采用的数据定义(如“射门”是否包含被封堵)。
2.强调分析的报告是基于特定数据源。对于本文,重点是演示流程,数据为模拟。

6. 最佳实践与工程建议

将兴趣分析与工程化结合,能让你的项目更专业、更可持续。

  1. 数据获取层封装

    • 将API请求逻辑(包括错误重试、速率限制)封装成独立的类或函数(如DataFetcher)。
    • 将原始响应数据持久化存储(如保存为JSON文件),避免频繁请求同一数据。
    # scripts/data_fetcher.py 示例片段 import requests import time import json class FootballDataFetcher: def __init__(self, api_key, base_url): self.api_key = api_key self.base_url = base_url self.session = requests.Session() self.session.headers.update({'Authorization': f'Bearer {api_key}'}) def fetch_match_events(self, match_id, save_raw=True): """获取指定比赛的事件数据""" url = f"{self.base_url}/matches/{match_id}/events" try: response = self.session.get(url, timeout=10) response.raise_for_status() data = response.json() if save_raw: with open(f'../data/raw/match_{match_id}_events.json', 'w') as f: json.dump(data, f, indent=2) return data except requests.exceptions.RequestException as e: print(f"请求失败: {e}") return None
  2. 配置与秘钥管理

    • 绝对不要将API密钥等敏感信息硬编码在脚本中。
    • 使用环境变量或配置文件(如.env文件)管理,并通过python-dotenv等库读取。
  3. 分析代码模块化

    • 将数据清洗、可视化绘图、指标计算等功能拆分为独立的函数或模块。
    • 这样不仅提高代码可读性,也便于单元测试和复用。本文中的plot_shot_mapgenerate_match_report就是很好的例子。
  4. 版本控制与文档

    • 使用Git进行版本控制,特别是当分析逻辑迭代或数据更新时。
    • 在项目根目录维护一个清晰的README.md,说明项目目的、数据来源、环境配置和运行方法。
  5. 从模拟走向真实

    • 本文使用模拟数据是为了保证教程的通用性和可复现性。
    • 当你掌握了整个流程后,可以尝试接入真实的免费数据源,例如:
      • StatsBomb Open Data: 提供丰富的免费比赛事件数据,但需要学习其特定的数据模型。
      • FBref: 可以通过爬虫(遵守robots.txt)或第三方包装库获取聚合统计数据。
      • 接入真实数据时,数据清洗和转换的步骤会变得至关重要。

通过以上步骤,你不仅完成了一次对经典比赛的技术复盘,更构建了一个可扩展的足球数据分析项目框架。你可以将此框架应用于分析其他球员、其他比赛或整个赛季的趋势,将感性的足球欣赏转化为理性的数据洞察。

← 返回列表