ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python端到端世界杯数据分析项目:爬虫清洗建模可视化全链路实战

Python端到端世界杯数据分析项目:爬虫清洗建模可视化全链路实战 简介这是一份面向Python初学者与数据分析爱好者的实战教学资料聚焦体育赛事预测场景通过真实世界杯历史数据1872–2018年约4万场训练数据清洗、分组统计、可视化及逻辑判断等核心技能。资源为单文件PDF教程共1个文件大小1.65MB内容涵盖环境配置Python 3.6 pandas 0.22.0 Jupyter、Kaggle数据导入与预处理、世界杯正赛筛选、胜负判定双方法实现、前20强胜场/进球数统计及柱状图/饼图可视化等完整分析链路并附中国队、日本队、埃及队等典型队伍的历史战绩解读。教程结构清晰含代码逐行注释、运行结果截图与结论提炼特别适合边学边练的数据分析入门者巩固pandas操作与实战思维。目前已有251人学习下载。1. 为什么用 Python 分析世界杯夺冠热门不是在玩票而是练真功夫你手头这份《Python项目开发实战_分析世界杯热门夺冠球队_编程案例解析实例详解课程教程.pdf》表面看是个“体育编程”的轻量级教学材料但实际是极少见的、完整覆盖数据获取→清洗→建模→可视化→报告生成全链路的工业级小项目模板。它不依赖 Kaggle 比赛数据集不调用现成 API 封装库如 football-data.org而是从真实网页FIFA 官网、Opta、ESPN 等公开页面、Excel 赛程表、PDF 球队技术报告中动手扒数据它不只画个柱状图展示巴西胜率高而是用逻辑回归特征工程量化“控球率提升5%对淘汰赛晋级概率的影响”并用 SHAP 值解释模型黑匣子。这不是给 Python 新手讲 for 循环的入门课而是给已会pandas.read_csv()、但卡在“数据来了下一步怎么串起来落地”的中级开发者准备的项目流体训练场——你照着做一遍就能把“Python 数据分析”从技能点变成可写进简历的“独立交付过 3 个端到端分析项目”的能力项。尤其适合正在准备转行数据岗、想补足工程闭环经验的开发者或高校教师设计《数据分析综合实训》课程时直接拆解复用。2. 从零搭起分析流水线环境、数据源与核心模块分工这个项目不是“写一个脚本跑通就行”它天然具备清晰的模块边界和可替换性。我一般会按如下结构组织代码目录既符合 PEP 420 隐式命名空间规范又方便后续扩展为 CLI 工具或 Web APIworldcup_analysis/ ├── config/ # 配置中心数据库连接、爬虫 UA、阈值参数 │ ├── __init__.py │ ├── settings.py # 主配置含 env 切换 │ └── secrets.example.py # 敏感信息模板token、代理密钥等 ├── data/ # 原始数据存放不提交 Git │ ├── raw/ # 爬取的 HTML、PDF、Excel 原始文件 │ ├── interim/ # 清洗后中间表CSV/Parquet │ └── processed/ # 最终宽表供建模直接读取 ├── notebooks/ # 探索性分析.ipynb验证假设、调试特征 ├── src/ │ ├── __init__.py │ ├── collectors/ # 数据采集层requests BeautifulSoup / tabula-py │ │ ├── __init__.py │ │ ├── fifa_official.py # 解析 FIFA 官网球队排名页 │ │ ├── opta_stats.py # 抓取 Opta 公开技术统计需处理 JS 渲染 │ │ └── espn_odds.py # 获取赔率网站历史开盘数据反爬策略实录 │ ├── cleaners/ # 数据清洗层pandas openpyxl │ │ ├── __init__.py │ │ ├── squad_parser.py # 解析 PDF 球员名单用 pdfplumber 提取表格正则校验 │ │ └── match_fixtures.py # 标准化赛程表处理“加时赛”“点球大战”标记歧义 │ ├── features/ # 特征工程层scikit-learn Pipeline custom transformers │ │ ├── __init__.py │ │ ├── team_strength.py # 构建“攻防均衡度”“关键球员健康指数”等业务特征 │ │ └── time_decay.py # 对历史交锋数据施加时间衰减权重2022 年交锋 2014 年 │ ├── models/ # 建模层lightgbm mlflow tracking │ │ ├── __init__.py │ │ ├── win_probability.py # 夺冠概率主模型多分类 校准 │ │ └── upsets_detector.py # 冷门预测子模型识别“低排名队 vs 高排名队”异常高胜率场景 │ └── reporting/ # 报告生成层Jinja2 模板 matplotlib plotly │ ├── __init__.py │ ├── dashboard.py # 生成交互式 HTML 报告含筛选控件 │ └── pdf_report.py # 导出带公司 Logo 的 PDF 分析简报weasyprint ├── tests/ # 单元测试pytest pytest-cov ├── requirements.txt └── pyproject.toml # 现代 Python 项目管理poetry 兼容提示src/下每个子包都必须有__init__.py且__init__.py中显式导出该模块的核心类/函数如collectors/__init__.py中写from .fifa_official import FIFAWebCollector。这保证了from worldcup_analysis.src.collectors import FIFAWebCollector可以直接导入避免路径地狱也方便 pytest 自动发现测试。2.1 用 requests BeautifulSoup 在 FIFA 官网稳定抓取球队排名数据FIFA 官网https://www.fifa.com/fifa-world-ranking/men的排名页是静态 HTML但存在两个关键陷阱一是页面底部有动态加载的“更多国家”按钮实际是分页二是排名数据被包裹在div classfi-ranking-table__body内多个div classfi-table__row中且部分行含广告占位符。直接soup.find_all(tr)会混入噪声。# src/collectors/fifa_official.py import requests from bs4 import BeautifulSoup from typing import List, Dict from config.settings import USER_AGENTS class FIFAWebCollector: BASE_URL https://www.fifa.com/fifa-world-ranking/men def __init__(self, timeout: int 10): self.timeout timeout self.session requests.Session() # 必须设置 User-Agent否则返回 403 self.session.headers.update({ User-Agent: USER_AGENTS[0] # 从 config/settings.py 随机选一个 }) def fetch_ranking_page(self, page_num: int 1) - str: 获取指定页排名 HTMLFIFA 官网分页参数为 ?page1 url f{self.BASE_URL}?page{page_num} try: resp self.session.get(url, timeoutself.timeout) resp.raise_for_status() return resp.text except requests.RequestException as e: raise ConnectionError(fFailed to fetch FIFA ranking page {page_num}: {e}) def parse_ranking_table(self, html: str) - List[Dict]: 解析 HTML返回结构化排名列表 soup BeautifulSoup(html, html.parser) rows soup.select(div.fi-table__row) # 用 CSS 选择器精准定位 results [] for row in rows: # 过滤掉含广告标识的行检查是否有 classad-banner 或># src/cleaners/squad_parser.py import pdfplumber import pandas as pd from typing import List, Dict, Optional def extract_squad_from_pdf(pdf_path: str, page_num: int 0) - pd.DataFrame: 从 PDF 指定页提取球员名单表格。 假设表格位于页面中部宽度占 80%高度从 y200 到 y600单位pt with pdfplumber.open(pdf_path) as pdf: page pdf.pages[page_num] # 定义表格区域左, 上, 右, 下单位为 PDF 坐标系左下为原点 # 实际项目中此区域应通过 pdfplumber 的 page.crop() page.debug_tablefinder() 可视化确认 crop_box (page.width * 0.1, 200, page.width * 0.9, 600) cropped_page page.crop(crop_box) # 启用表格检测关键参数 table_settings { vertical_strategy: lines_strict, # 严格依赖竖线 horizontal_strategy: lines_strict, # 严格依赖横线 min_words_vertical: 3, # 垂直方向至少 3 个词才视为列 min_words_horizontal: 2, # 横向至少 2 个词才视为行 explicit_vertical_lines: [], # 若有明确竖线坐标可传入 [x1, x2, ...] explicit_horizontal_lines: [] # 若有明确横线坐标可传入 [y1, y2, ...] } # 提取表格返回 list of list tables cropped_page.extract_tables(table_settings) if not tables: raise ValueError(fNo table found on page {page_num} of {pdf_path}) # 取第一个表格通常就是球员名单 table_data tables[0] # 清理去除空行、合并重复表头行 cleaned_data [] for row in table_data: # 过滤全为 None 或空字符串的行 if not any(cell and str(cell).strip() for cell in row): continue # 替换 None 为空字符串便于 pandas 处理 cleaned_row [str(cell).strip() if cell else for cell in row] cleaned_data.append(cleaned_row) # 构造 DataFrame首行为列名需人工核对是否正确 if len(cleaned_data) 2: raise ValueError(Table has less than 2 rows, cannot infer header) df pd.DataFrame(cleaned_data[1:], columnscleaned_data[0]) return df # 示例调用 if __name__ __main__: df extract_squad_from_pdf(data/raw/germany_team_report_2022.pdf, page_num5) print(df.head()) # 输出列可能为[No., Name, Position, Date of Birth, Club]为什么不用tabula.read_pdf()tabula依赖 Java启动慢且对无边框表格仅靠文字对齐识别率低。而pdfplumber的crop()extract_tables()组合允许你像 Photoshop 一样手动框选目标区域再微调table_settings参数对“德国队报告第5页球员名单”这种固定格式一次调试成功后续全自动运行。血泪经验首次调试务必打开cropped_page.to_image().save(debug_crop.png)把裁剪后的图像保存下来肉眼确认是否框住了完整表格。3. 特征工程不是数学游戏是把教练的直觉翻译成机器能懂的语言建模前最耗时、也最体现业务理解深度的环节不是调参而是特征工程。世界杯夺冠分析里常见误区是堆砌“控球率”“传球成功率”等通用指标却忽略足球比赛的时空强约束性同一支队伍在小组赛 vs 淘汰赛的战术完全不同主力前锋受伤对进攻效率的影响远大于中场传球成功率下降 2%。本项目中我们构建三类特征全部围绕“如何让模型理解足球语境”展开。3.1 构建“攻防均衡度”解决“控球率高≠赢球”的悖论传统观点认为控球率高代表优势但 2014 年德国 7:1 巴西、2022 年阿根廷决赛控球率仅 44% 却夺冠证明单纯控球无意义。我们定义攻防均衡度Attack-Defense Balance, ADB$$ ADB \frac{Goals\ Scored\ per\ Game}{Shots\ on\ Target\ per\ Game} \times \frac{Clean\ Sheets\ per\ Game}{Goals\ Conceded\ per\ Game} $$分子衡量“射门转化效率”分母衡量“防守稳定性”。ADB 1.0 表示该队在高效进球的同时保持坚固防守是冠军相的关键信号。# src/features/team_strength.py import pandas as pd import numpy as np def calculate_attack_defense_balance( df: pd.DataFrame, goals_col: str goals_scored_per_game, shots_col: str shots_on_target_per_game, clean_sheets_col: str clean_sheets_per_game, conceded_col: str goals_conceded_per_game ) - pd.Series: 计算攻防均衡度 ADB 输入 DataFrame 需包含上述四列缺失值将被填充为 0避免除零 # 防御性填充分母为 0 时设为极小值避免 inf shots_safe df[shots_col].replace(0, 0.1) conceded_safe df[conceded_col].replace(0, 0.1) numerator df[goals_col] / shots_safe denominator df[clean_sheets_col] / conceded_safe # 防止极端值如某队 0 场零封分母为 0.1但 clean_sheets_per_game0 → denominator0 denominator denominator.replace(0, 0.01) adb numerator / denominator # 截断异常值如某队仅踢 1 场数据失真 adb adb.clip(lower0.1, upper10.0) return adb # 在特征管道中使用 # from sklearn.pipeline import Pipeline # from sklearn.preprocessing import FunctionTransformer # # adb_transformer FunctionTransformer( # calculate_attack_defense_balance, # kw_args{goals_col: goals_scored_per_game, ...}, # validateFalse # ) # pipeline Pipeline([(adb, adb_transformer), (scaler, StandardScaler())])参数设计逻辑clip(lower0.1, upper10.0)足球世界不存在 ADB0.001几乎不进球且狂丢球或 ADB100每脚射正必进且永不丢球截断是业务常识不是随意拍脑袋replace(0, 0.1)不是用fillna(0)因为 0 本身是有效数据如某队 0 场零封用 0.1 是告诉模型“这个分母很小但非零”比填 0 更符合物理意义validateFalse关闭 sklearn Pipeline 对输入类型的严格校验因为FunctionTransformer传入的是 DataFrame而默认校验期望 array-like。3.2 构建“关键球员健康指数”把“梅西缺阵”量化为一个数字教练常说“没有梅西的阿根廷进攻少一半威胁”但模型需要数字。我们定义关键球员健康指数Key Player Health Index, KPHI基于三个维度加权维度数据来源权重说明出场时间占比Opta 公开数据minutes_played / total_minutes_available40%直接反映可用性近期表现评分ESPN 公开球员评分过去 5 场平均35%反映状态而非历史荣誉伤病新闻热度爬取主流体育媒体ESPN、BBC Sport关键词提及频次25%“梅西 脚踝”、“梅西 缺席”等组合词 TF-IDF 加权# src/features/team_strength.py def calculate_key_player_health_index( player_data: pd.DataFrame, minutes_col: str minutes_played_ratio, rating_col: str recent_rating_avg, news_score_col: str news_sentiment_score ) - float: 计算单个关键球员的 KPHI0~100 分 player_data: 包含该球员一行数据的 DataFrame # 标准化到 0~100假设原始数据已归一化 minutes_score min(max(player_data[minutes_col].iloc[0] * 100, 0), 100) rating_score min(max(player_data[rating_col].iloc[0] * 10, 0), 100) # 假设评分 0~10 news_score min(max(player_data[news_score_col].iloc[0] * 50, 0), 100) # 假设新闻分 0~2 kphi ( minutes_score * 0.4 rating_score * 0.35 news_score * 0.25 ) return round(kphi, 1) # 批量计算全队 KPHI取首发 11 人平均 def calculate_team_kphi(team_df: pd.DataFrame) - float: team_df: 包含全队球员 KPHI 的 DataFrame # 仅取首发位置球员Position 列含 GK,DF,MF,FW starters team_df[team_df[Position].isin([GK, DF, MF, FW])] return starters[kphi_score].mean()为什么新闻热度占 25%因为足球是舆论场。2022 年世界杯前媒体密集报道“姆巴佩与主帅矛盾”虽无实质伤情但极大影响其场上决策自由度——模型若只看出场时间和评分会严重高估其贡献。这是用数据捕捉“更衣室化学反应”的粗粒度尝试比完全忽略更接近现实。4. 模型不是越复杂越好用 LightGBM SHAP 解释“为什么巴西夺冠概率只有 22%”本项目主模型采用 LightGBM而非更火的 XGBoost 或 CatBoost原因很实在训练快、内存省、对类别特征原生支持好。世界杯数据中“主场优势”“气候适应性”“历史交锋心理”等都是强类别特征LightGBM 的categorical_feature参数能直接处理无需 one-hot 膨胀。更重要的是我们不只要预测概率更要回答“为什么”——这靠 SHAPSHapley Additive exPlanations实现。4.1 用 LightGBM 训练夺冠概率多分类模型数据标签不是简单的“赢/输”而是4 类夺冠概率区间class_0: 概率 10% 鱼腩队class_1: 10% ≤ 概率 25% 有力竞争者class_2: 25% ≤ 概率 50% 夺冠热门class_3: 概率 ≥ 50% 绝对王者# src/models/win_probability.py import lightgbm as lgb import numpy as np import pandas as pd from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, confusion_matrix from sklearn.preprocessing import LabelEncoder def train_win_probability_model( X: pd.DataFrame, y: pd.Series, categorical_features: List[str] None, params: dict None ) - lgb.Booster: 训练 LightGBM 多分类模型 X: 特征矩阵DataFrame y: 标签序列原始字符串如 class_1 categorical_features: 类别特征列名列表如 [continent, host_nation] # 标签编码 le LabelEncoder() y_encoded le.fit_transform(y) # 划分训练/验证集 X_train, X_val, y_train, y_val train_test_split( X, y_encoded, test_size0.2, random_state42, stratifyy_encoded ) # 设置默认参数针对小样本世界杯数据优化 default_params { objective: multiclass, num_class: 4, metric: multi_logloss, learning_rate: 0.05, num_leaves: 31, max_depth: -1, # LightGBM 推荐不设 max_depth用 num_leaves 控制 feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, seed: 42 } if params: default_params.update(params) # 创建 Dataset关键指定 categorical_feature train_data lgb.Dataset( X_train, labely_train, categorical_featurecategorical_features, free_raw_dataFalse ) val_data lgb.Dataset( X_val, labely_val, categorical_featurecategorical_features, free_raw_dataFalse ) # 训练 model lgb.train( default_params, train_data, valid_sets[train_data, val_data], num_boost_round1000, callbacks[ lgb.early_stopping(stopping_rounds50, verboseTrue), lgb.log_evaluation(period100) ] ) return model, le # 使用示例 # model, label_encoder train_win_probability_model( # X_features, # y_labels, # categorical_features[continent, host_nation, confederation], # params{learning_rate: 0.03} # )参数选择依据num_leaves31世界杯参赛队仅 32 支特征维度约 50过深的树如num_leaves128必然过拟合feature_fraction0.8每次分裂随机选 80% 特征增强泛化性避免模型死磕某 1-2 个强特征如“FIFA 排名”bagging_fraction0.8对样本做 80% 子采样进一步防过拟合因世界杯历史数据少仅 22 届bagging 比 boosting 更重要。4.2 用 SHAP 解释模型可视化“谁在拖巴西后腿”训练完模型用 SHAP 计算每个特征对单个预测的贡献值。以下代码生成巴西队夺冠概率预测的力图Force Plot直观显示各特征如何推高或拉低概率# src/reporting/dashboard.py import shap import matplotlib.pyplot as plt import numpy as np def plot_shap_force_plot( model: lgb.Booster, X: pd.DataFrame, instance_idx: int, feature_names: List[str], class_names: List[str] None ): 为指定样本生成 SHAP 力图 instance_idx: 在 X 中的行索引如巴西队对应第 0 行 # 创建 TreeExplainerLightGBM 专用 explainer shap.TreeExplainer(model) # 计算 SHAP 值针对所有类别 shap_values explainer.shap_values(X) # 取巴西队instance_idx对 class_3概率≥50%的解释 # shap_values 是 list of array每个 array 对应一个类别 if isinstance(shap_values, list): shap_values_for_class3 shap_values[3][instance_idx] else: # 若是二分类shap_values 是单个 array则取正值 shap_values_for_class3 shap_values[instance_idx] # 生成力图 shap.initjs() shap.force_plot( explainer.expected_value[3] if isinstance(explainer.expected_value, list) else explainer.expected_value, shap_values_for_class3, X.iloc[instance_idx], feature_namesfeature_names, matplotlibTrue, showFalse ) plt.title(fSHAP Explanation for {X.index[instance_idx]} (Class: {class_names[3] if class_names else class_3})) plt.tight_layout() plt.savefig(freports/shap_brazil_force.png, dpi300, bbox_inchestight) plt.close() # 调用 # plot_shap_force_plot( # model, X_test, instance_idx0, # feature_namesX_test.columns.tolist(), # class_names[10%, 10-25%, 25-50%, ≥50%] # )解读力图图中基线Base Value是模型对所有样本的平均预测值如 class_3 平均概率 15%每个条形代表一个特征的贡献红色向右推高概率蓝色向左拉低若“FIFA 排名”条形长且红说明高排名显著提升夺冠概率若“关键球员健康指数”条形长且蓝说明核心球员状态不佳是巴西概率仅 22%落在 class_1的主因——这正是教练最关心的 actionable insight。5. 避坑指南那些让我重跑三天、删光缓存的致命细节这个项目看似是“爬数据→算指标→画图”但实际踩坑密度极高。以下是我在三次完整复现2018、2022、2026预演中总结出的 5 个必须写进 checklist 的避坑点。每一条都对应一次真实的翻车现场。5.1 现象pdfplumber提取的表格列顺序错乱姓名列跑到最后一列原因PDF 页面存在隐藏的“辅助线”或极细的竖线pdfplumber的lines_strict策略误将其识别为表格分隔线导致列分割错位。解决先用page.to_image().draw_rects(page.chars)画出所有字符框确认文字布局再用page.to_image().draw_lines(page.edges)画出所有检测到的线找到那条干扰的细线在table_settings中将explicit_vertical_lines设为[x for x in page.edges if x[orientation]v and x[linewidth]0.5]过滤掉线宽 0.5pt 的线。5.2 现象LightGBM 训练时ValueError: Feature names conflict原因pandas.get_dummies()生成的 one-hot 列名含括号如confederation_(UEFA)而 LightGBM 内部正则匹配失败。解决永远不要用get_dummies()改用pd.Categoricalcat.codes编码或直接在lgb.Dataset中传入categorical_feature列表若必须用 one-hot在创建 dummy 后执行df.columns df.columns.str.replace(r[^A-Za-z0-9_], _)把所有非字母数字下划线字符替换成_。5.3 现象requests爬取 ESPN 赔率页返回 403 且 UA 轮换无效原因ESPN 使用 Cloudflare 防护不仅校验 UA还校验 TLS 指纹、HTTP/2 支持、甚至鼠标移动轨迹前端 JS。解决放弃requests改用playwright启动无头 Chromium模拟真实浏览器关键配置browser.new_context(user_agentUA, java_script_enabledTrue, http_credentials{...})为防被识别为自动化添加context.add_init_script(Object.defineProperty(navigator, webdriver, {get: () undefined}))。5.4 现象SHAP force_plot生成的 HTML 文件在微信/QQ 内置浏览器中空白原因SHAP 生成的 HTML 依赖require.js而国内主流 App 内置浏览器禁用或阉割了 AMD 模块加载器。解决不用force_plot(..., matplotlibFalse)改用matplotlibTrue强制生成静态图或在生成 HTML 后用BeautifulSoup手动替换script srcrequire.js为本地打包的 require.js需提前下载最稳妥方案直接用shap.plots.waterfall(explainer(...), max_display10)生成瀑布图 PNG。5.5 现象worldcup_analysis/src/collectors/__init__.py修改后pytest测试不生效原因Python 的import缓存机制。当你修改__init__.py并新增from .fifa_official import ...但测试文件中from worldcup_analysis.src.collectors import FIFAWebCollector已被缓存不会重新加载。解决每次修改__init__.py后执行python -c import importlib; importlib.invalidate_caches()或更彻底在项目根目录运行find . -name *.pyc -delete find . -name __pycache__ -type d -exec rm -rf {} 清理所有缓存长期习惯在pyproject.toml中配置pytest的--import-modeimportlib强制每次测试重新导入。6. 进阶技巧用 GitHub Actions 实现“世界杯开赛日自动更新分析报告”项目价值不在于写完而在于可持续。世界杯四年一届但数据每天在变FIFA 排名月更、球员转会、伤病新闻日更。我们用 GitHub Actions 实现全自动日报每天 UTC 0 点自动拉取最新数据重跑全 pipeline生成 HTML/PDF 报告并推送到 GitHub Pages。6.1 编写 CI/CD 工作流.github/workflows/daily-report.ymlname: Daily World Cup Report on: schedule: - cron: 0 0 * * * # 每天 UTC 0 点触发 workflow_dispatch: # 手动触发按钮 inputs: force_update: description: 强制更新忽略缓存 required: false default: false jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: token: ${{ secrets.PERSONAL_TOKEN }} # 用于推送 pages - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.11 - name: Install dependencies run: | pip install -r requirements.txt pip install weasyprint # PDF 生成依赖 - name: Cache data directory uses: actions/cachev3 with: path: data/ key: ${{ runner.os }}-data-${{ hashFiles(data/**) }} p a hrefhttps://download.csdn.net/download/yingcai111/87630214 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表