ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python数据分析实战:构建足球明星生涯数据平台

2026/8/6 3:05:03 拓冰建站 浏览量
Python数据分析实战:构建足球明星生涯数据平台

最近在整理足球历史资料时,我反复思考一个问题:如何用技术手段去量化、分析乃至“复现”那些深深烙印在球迷记忆中的经典瞬间与漫长叙事?比如C罗两段曼联生涯的辉煌、挣扎与传奇色彩。这不仅仅是情怀,更是数据、时间线与复杂情感交织的系统工程。本文将以“曼联C罗”这个极具代表性的主题为案例,手把手带你构建一个技术分析框架,涵盖数据爬取、存储、可视化与情感分析全链路。无论你是想深入学习Python数据分析,还是希望为自己支持的球队或偶像建立一套数字档案,这套方法都能直接复用。

1. 背景与核心概念:当足球传奇遇见数据科学

我们谈论的“曼联C罗”,是一个跨越时间的复合体。它包含:

  • 实体:克里斯蒂亚诺·罗纳尔多(C罗)作为球员。
  • 关系:他与曼联足球俱乐部两次的雇佣关系(2003-2009, 2021-2022)。
  • 事件:在这两个时期内的比赛、进球、荣誉、公众言论、更衣室动态等。
  • 情感:球迷、媒体在事件不同阶段所投射的期待、赞美、争议与怀念。

传统的文章或视频回顾,依赖于主观的文字描述和影像剪辑。而技术分析的目标,是尝试将这部分主观叙事进行结构化、数据化的拆解。这涉及到几个核心概念:

  1. 结构化数据:能够用固定字段记录的信息,如比赛日期、对手、进球数、助攻数、出场时间。这类数据易于分析和统计。
  2. 非结构化数据:文本(新闻、社交媒体评论)、图片、视频。这类数据蕴含大量情感和上下文,需要借助自然语言处理(NLP)和图像分析来提取信息。
  3. 时间序列分析:所有事件都沿着时间轴发生。分析数据随时间的变化趋势(如进球率、媒体情感倾向)是理解故事脉络的关键。
  4. 情感分析:通过算法判断一段文本(如赛后新闻标题或推特话题)所表达的情感是正面、负面还是中性。这可以帮助我们量化“风评”的变化。

本文的实战项目,就是将上述概念落地,打造一个“曼联C罗生涯数据分析平台”。我们将从互联网上采集相关数据,存入数据库,进行分析,并最终通过图表和仪表板呈现出一个数据驱动的“故事”。

2. 环境准备与版本说明

本项目主要使用Python生态下的工具链,以下是推荐的环境配置。请注意,版本号以当前主流稳定版为例,实际操作中可根据情况微调。

操作系统:Windows 10/11, macOS 或 Linux (如Ubuntu 20.04+) 均可。编程语言:Python 3.8 或以上版本。IDE/编辑器:Visual Studio Code (推荐) 或 PyCharm。

核心Python库及用途

  • 数据获取requests(发送HTTP请求),selenium(模拟浏览器处理动态页面)。
  • 数据解析beautifulsoup4(解析HTML),lxml(作为BS4的解析引擎,速度更快)。
  • 数据处理与分析pandas(数据分析核心),numpy(数值计算)。
  • 情感分析snownlp(中文情感分析),textblob(英文情感分析, 需要额外安装textblob库并下载语料)。
  • 数据可视化matplotlib,seaborn(静态图表),plotly(交互式图表, 更适合网页仪表板)。
  • 数据库sqlite3(Python内置, 轻量), 或pymysql/sqlalchemy(连接MySQL等)。
  • Web框架(可选,用于展示)flask(轻量级) 或streamlit(快速构建数据应用)。

项目结构预览: 在开始编码前,建议先创建如下目录结构,保持代码清晰。

cronaldo_analysis/ │ ├── data/ # 存放原始和清洗后的数据文件 │ ├── raw/ # 原始爬取数据 │ └── processed/ # 清洗后的数据 │ ├── src/ # 源代码 │ ├── crawler/ # 爬虫模块 │ │ ├── __init__.py │ │ ├── stats_crawler.py # 爬取比赛数据 │ │ └── news_crawler.py # 爬取新闻数据 │ ├── database/ # 数据库操作模块 │ │ ├── __init__.py │ │ └── db_handler.py │ ├── analysis/ # 数据分析模块 │ │ ├── __init__.py │ │ ├── career_stats.py # 生涯统计 │ │ └── sentiment_analysis.py # 情感分析 │ ├── visualization/ # 可视化模块 │ │ ├── __init__.py │ │ └── plot_dashboard.py │ └── main.py # 主程序入口 │ ├── config/ # 配置文件 │ └── settings.py │ ├── requirements.txt # 项目依赖列表 ├── README.md └── .gitignore

你可以通过以下命令快速安装主要依赖(在项目根目录下):

# 创建并激活虚拟环境(以conda为例) conda create -n cr7_analysis python=3.9 conda activate cr7_analysis # 安装核心库 pip install requests beautifulsoup4 lxml pandas numpy matplotlib seaborn plotly snownlp textblob selenium flask # 对于textblob,还需要下载语料库(在Python交互环境中运行) # python -m textblob.download_corpora

3. 核心模块拆解与关键技术

3.1 数据爬取:多渠道信息聚合

数据是分析的基石。我们需要从多个维度获取数据。

1. 结构化比赛数据来源

  • 目标网站:像https://www.transfermarkt.com(需注意反爬),https://fbref.com等专业足球数据网站提供了极其详细的球员比赛数据。
  • 爬取策略
    • 检查Robots协议:首先访问目标网站/robots.txt,尊重网站的爬虫规则。
    • 使用Requests+BS4:对于静态页面,直接获取HTML并解析。
    • 添加请求头:模拟真实浏览器访问,避免被简单屏蔽。
    • 控制频率:在请求间添加随机延时(如time.sleep(random.uniform(1, 3))),避免对服务器造成压力。

示例:爬取FBref上C罗曼联时期数据概览(概念代码)

# src/crawler/stats_crawler.py import requests from bs4 import BeautifulSoup import pandas as pd import time import random def scrape_fbref_stats(player_url): """ 从FBref爬取球员赛季数据 :param player_url: 球员数据页面的URL :return: 包含赛季数据的DataFrame """ 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: response = requests.get(player_url, headers=headers) response.raise_for_status() # 检查请求是否成功 soup = BeautifulSoup(response.content, 'lxml') # FBref的数据通常在<table>标签中,id包含'stats' # 这里需要具体分析网页结构,以下为示例定位 table = soup.find('table', id='stats_standard_9') # 示例ID,需根据实际情况调整 if table: # 使用pandas直接读取HTML表格是最快的方式 df_list = pd.read_html(str(table)) if df_list: df = df_list[0] print(f"成功爬取数据,形状:{df.shape}") # 这里需要进行大量的数据清洗,如处理多级列名、删除无效行等 # cleaned_df = clean_fbref_data(df) return df except requests.RequestException as e: print(f"网络请求出错:{e}") except Exception as e: print(f"解析数据出错:{e}") return pd.DataFrame() # 返回空DataFrame # 使用示例 if __name__ == '__main__': # C罗在FBref的页面(示例URL,可能需要更新) url = 'https://fbref.com/en/players/dea698d9/Cristiano-Ronaldo' stats_df = scrape_fbref_stats(url) if not stats_df.empty: # 筛选曼联时期的数据(需要根据DataFrame的列进行过滤) # 假设有一列‘Season’和‘Squad’ man_utd_stats = stats_df[stats_df['Squad'].str.contains('Manchester Utd', na=False)] print(man_utd_stats[['Season', 'Comp', 'Matches', 'Goals', 'Assists']].head()) # 保存数据 man_utd_stats.to_csv('../data/raw/man_utd_cr7_stats.csv', index=False) time.sleep(random.uniform(2, 5)) # 礼貌性延时

2. 非结构化新闻/社交媒体数据

  • 目标:获取关键时间点(如回归官宣、帽子戏法、采访风波、离队)的新闻报道和社交舆论。
  • 策略
    • 使用特定关键词(“Ronaldo Manchester United return”, “C罗 曼联 采访”)和日期范围在新闻网站或社交媒体API(如Twitter API v2, 需申请)进行搜索。
    • 对于动态加载的网站,可能需要SeleniumPlaywright来模拟浏览器操作。
    • 重要:严格遵守数据使用条款,仅用于个人学习研究,不得大规模商用或侵犯隐私。

3.2 数据存储:SQLite与MySQL的选择

爬取的数据需要持久化存储。

  • SQLite:轻量,单文件,无需服务器,适合本地开发和中小型数据集。Python内置支持。
  • MySQL/PostgreSQL:功能强大,支持多用户并发,适合数据量大或需要远程访问的场景。

示例:使用SQLite存储比赛数据

# src/database/db_handler.py import sqlite3 import pandas as pd from datetime import datetime class FootballDB: def __init__(self, db_path='../data/football.db'): self.conn = sqlite3.connect(db_path) self.create_tables() def create_tables(self): """创建数据表""" cursor = self.conn.cursor() # 球员信息表 cursor.execute(''' CREATE TABLE IF NOT EXISTS players ( player_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, full_name TEXT, birth_date TEXT, nationality TEXT ) ''') # 比赛数据表(简化版) cursor.execute(''' CREATE TABLE IF NOT EXISTS match_stats ( stat_id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER, season TEXT, competition TEXT, matches_played INTEGER, goals INTEGER, assists INTEGER, minutes_played INTEGER, FOREIGN KEY (player_id) REFERENCES players (player_id) ) ''') # 新闻/事件表 cursor.execute(''' CREATE TABLE IF NOT EXISTS news_events ( event_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, content TEXT, source TEXT, publish_date TEXT, sentiment_score REAL, -- 情感分析得分 keywords TEXT, -- 关键词,逗号分隔 related_player_id INTEGER, FOREIGN KEY (related_player_id) REFERENCES players (player_id) ) ''') self.conn.commit() print("数据表创建/检查完成。") def insert_match_stats(self, player_name, stats_df): """插入或更新比赛数据""" # 1. 确保球员存在 cursor = self.conn.cursor() cursor.execute("SELECT player_id FROM players WHERE name = ?", (player_name,)) player = cursor.fetchone() if not player: cursor.execute("INSERT INTO players (name) VALUES (?)", (player_name,)) player_id = cursor.lastrowid self.conn.commit() else: player_id = player[0] # 2. 插入比赛数据(这里假设stats_df已处理好) for _, row in stats_df.iterrows(): # 避免重复插入,使用 season+competition 作为唯一标识 cursor.execute(''' INSERT OR REPLACE INTO match_stats (player_id, season, competition, matches_played, goals, assists, minutes_played) VALUES (?, ?, ?, ?, ?, ?, ?) ''', (player_id, row['Season'], row['Comp'], row['Matches'], row['Goals'], row['Assists'], row['Minutes'])) self.conn.commit() print(f"已为球员 {player_name} 插入/更新 {len(stats_df)} 条比赛记录。") def close(self): self.conn.close() # 使用示例 if __name__ == '__main__': db = FootballDB() # 假设我们已经有一个清洗好的DataFrame `cleaned_stats` # db.insert_match_stats('Cristiano Ronaldo', cleaned_stats) db.close()

3.3 情感分析:量化舆论风向

情感分析能让我们看到“梦啼妆泪红阑干”这种复杂情感在数据上的投射。我们以英文新闻标题为例,使用TextBlob

# src/analysis/sentiment_analysis.py from textblob import TextBlob import pandas as pd def analyze_sentiment(text): """ 分析单条文本的情感极性。 :param text: 英文文本 :return: polarity (-1到1, 负向到正向), subjectivity (0到1, 客观到主观) """ blob = TextBlob(text) return blob.sentiment.polarity, blob.sentiment.subjectivity def batch_sentiment_analysis(news_df, title_col='title'): """ 批量分析新闻标题情感。 :param news_df: 包含新闻标题的DataFrame :param title_col: 标题列名 :return: 添加了情感得分列的DataFrame """ sentiments = news_df[title_col].apply(lambda x: analyze_sentiment(str(x))) news_df['sentiment_polarity'] = [s[0] for s in sentiments] news_df['sentiment_subjectivity'] = [s[1] for s in sentiments] # 简单分类 news_df['sentiment_label'] = news_df['sentiment_polarity'].apply( lambda x: '正面' if x > 0.1 else ('负面' if x < -0.1 else '中性') ) return news_df # 示例数据 sample_news = pd.DataFrame({ 'title': [ 'Ronaldo returns to Manchester United as a hero', 'Frustrating night for Ronaldo as United lose derby', 'Ronaldo scores a hat-trick in thrilling comeback', 'Reports of tension between Ronaldo and the manager surface', 'United confirm Ronaldo departure by mutual agreement' ], 'date': ['2021-08-27', '2022-03-06', '2022-04-16', '2022-10-20', '2022-11-22'] }) # 执行分析 result_df = batch_sentiment_analysis(sample_news) print(result_df[['title', 'sentiment_polarity', 'sentiment_label']])

输出示例

title sentiment_polarity sentiment_label 0 Ronaldo returns to Manchester United as a hero 0.500000 正面 1 Frustrating night for Ronaldo as United lose ... -0.300000 负面 2 Ronaldo scores a hat-trick in thrilling comeback 0.316667 正面 3 Reports of tension between Ronaldo and the ma... -0.050000 中性 4 United confirm Ronaldo departure by mutual ag... 0.000000 中性

可以看到,算法成功识别了“hero”(英雄)、“frustrating”(沮丧)、“thrilling”(激动人心)等词汇的情感倾向。

4. 完整实战案例:构建曼联C罗生涯仪表板

现在,我们将上述模块串联,创建一个完整的分析流程,并生成可视化仪表板。我们将使用Streamlit,因为它能快速将数据脚本转化为交互式Web应用。

4.1 项目初始化与数据准备

确保已安装streamlit。在项目根目录创建app.py

首先,我们准备一些模拟和清洗好的数据(在实际项目中,这部分数据来自之前的爬虫和数据库)。

# app.py import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from datetime import datetime # 设置页面 st.set_page_config(page_title="曼联C罗生涯数据分析", layout="wide") st.title("⚽ 曼联C罗生涯数据分析平台") st.markdown("---") # 1. 加载数据(这里用模拟数据,实际应从数据库或CSV读取) @st.cache_data def load_career_stats(): """模拟加载C罗曼联生涯赛季数据""" data = { '赛季': ['2003-04', '2004-05', '2005-06', '2006-07', '2007-08', '2008-09', '2021-22'], '赛事': ['英超', '英超', '英超', '英超', '英超', '英超', '英超'], '出场': [29, 33, 33, 34, 34, 33, 30], '进球': [4, 5, 9, 17, 31, 18, 18], '助攻': [4, 4, 6, 7, 6, 6, 3], '分钟': [2163, 2924, 2889, 3101, 2929, 2845, 2511] } df = pd.DataFrame(data) df['进球效率(每90分钟)'] = df['进球'] / (df['分钟'] / 90) df['时期'] = ['一期'] * 6 + ['二期'] # 标记两个时期 return df @st.cache_data def load_sentiment_timeline(): """模拟加载情感分析时间线数据""" dates = pd.date_range(start='2021-08-01', end='2022-12-01', freq='MS') # 按月 import numpy as np np.random.seed(42) # 模拟情感得分:回归时极高,随后波动,离队时偏低 polarity = [0.8, 0.6, 0.5, 0.4, 0.2, -0.1, -0.3, 0.1, 0.0, -0.2, -0.4, -0.1, 0.0] events = [ '官宣回归', '', '欧冠绝杀', '', '帽子戏法', '', '采访风波', '', '离队传闻', '', '正式离队', '', '' ] df = pd.DataFrame({'日期': dates, '情感极性': polarity[:len(dates)], '关键事件': events[:len(dates)]}) return df career_df = load_career_stats() sentiment_df = load_sentiment_timeline()

4.2 构建交互式可视化

使用Plotly创建可交互图表,并嵌入到Streamlit中。

# 2. 侧边栏 - 数据筛选 st.sidebar.header("🔍 数据筛选") selected_season = st.sidebar.multiselect( "选择赛季", options=career_df['赛季'].unique(), default=career_df['赛季'].tolist() ) selected_period = st.sidebar.multiselect( "选择时期", options=career_df['时期'].unique(), default=career_df['时期'].unique() ) # 应用筛选 filtered_df = career_df[ (career_df['赛季'].isin(selected_season)) & (career_df['时期'].isin(selected_period)) ] # 3. 主界面 - 关键指标 st.header("📊 生涯关键数据概览") col1, col2, col3, col4 = st.columns(4) with col1: total_goals = filtered_df['进球'].sum() st.metric("总进球数", total_goals) with col2: total_assists = filtered_df['助攻'].sum() st.metric("总助攻数", total_assists) with col3: avg_goals_per_90 = filtered_df['进球效率(每90分钟)'].mean() st.metric("平均每90分钟进球", f"{avg_goals_per_90:.2f}") with col4: peak_season = filtered_df.loc[filtered_df['进球'].idxmax()] st.metric("单赛季最高进球", f"{peak_season['进球']}球 ({peak_season['赛季']})") # 4. 可视化图表 st.markdown("---") col_left, col_right = st.columns(2) with col_left: st.subheader("📈 赛季进球与助攻趋势") # 使用Plotly创建双Y轴图 fig1 = go.Figure() fig1.add_trace(go.Scatter( x=filtered_df['赛季'], y=filtered_df['进球'], mode='lines+markers', name='进球数', line=dict(color='firebrick', width=3) )) fig1.add_trace(go.Scatter( x=filtered_df['赛季'], y=filtered_df['助攻'], mode='lines+markers', name='助攻数', yaxis='y2', line=dict(color='royalblue', width=3, dash='dash') )) fig1.update_layout( xaxis_title="赛季", yaxis=dict(title="进球数", titlefont=dict(color='firebrick')), yaxis2=dict(title="助攻数", titlefont=dict(color='royalblue'), overlaying='y', side='right'), hovermode='x unified', legend=dict(x=0.02, y=0.98) ) st.plotly_chart(fig1, use_container_width=True) with col_right: st.subheader("🎭 媒体情感分析时间线") fig2 = px.line(sentiment_df, x='日期', y='情感极性', title='基于新闻标题的情感极性变化', markers=True) # 添加关键事件标注 for idx, row in sentiment_df[sentiment_df['关键事件'] != ''].iterrows(): fig2.add_annotation(x=row['日期'], y=row['情感极性'], text=row['关键事件'], showarrow=True, arrowhead=1, ax=0, ay=-40) fig2.update_layout(hovermode='x unified') fig2.add_hline(y=0, line_dash="dash", line_color="grey", opacity=0.5) st.plotly_chart(fig2, use_container_width=True) # 5. 数据表格展示 st.markdown("---") st.subheader("📋 详细数据表") st.dataframe(filtered_df.style.highlight_max(subset=['进球', '助攻'], color='lightgreen'), use_container_width=True) # 6. 分析结论板块 st.markdown("---") st.subheader("💡 数据洞察") with st.expander("点击查看分析总结"): st.markdown(""" **从数据层面看C罗的两次曼联生涯:** 1. **第一次曼联时期(2003-2009)**: * **成长轨迹明显**:进球数从个位数飙升至31球(07-08赛季金球奖赛季),呈现指数级增长。 * **效率巅峰**:07-08赛季的进球效率(每90分钟约0.95球)是生涯最高点之一,体现了从边锋到射门员的完美转型。 * **数据贡献全面**:助攻数稳定在4-7个,说明其作为进攻核心的全面性。 2. **第二次曼联时期(2021-2022)**: * **即战力依旧顶级**:37岁高龄回归英超,单赛季仍打入18球,是队内最佳射手,进球效率(约0.64球/90分钟)远超同龄前锋。 * **环境与角色变化**:助攻数下降,球队整体竞争力与战术体系已非当年,个人数据与团队成绩的割裂感增强。 * **舆论情感波动**:从回归时的“英雄归来”(情感极性极高),到赛季中后期的争议与挫败感(情感极性转负),最终在离队时趋于平静,完整映射了一次充满戏剧性的“归来”叙事。 **技术复盘价值**:本项目演示了如何将体育、人物等软性叙事,通过**数据采集、清洗、存储、分析、可视化**的完整技术管线进行解构。这套方法可迁移至任何你想深度分析的领域人物或团队。 """)

4.3 运行与验证

在终端中,进入项目根目录,运行:

streamlit run app.py

Streamlit 会自动在浏览器中打开一个本地地址(通常是http://localhost:8501)。你将看到一个交互式的仪表板,可以通过侧边栏筛选赛季,查看动态更新的图表和数据。

5. 常见问题与排查思路

在实现上述流程时,你可能会遇到以下典型问题:

问题现象常见原因解决思路
爬虫无法获取数据,返回403或空内容1. 网站有反爬机制(如验证User-Agent)。
2. 页面数据由JavaScript动态加载。
1. 检查并完善请求头,模拟真实浏览器。
2. 使用SeleniumPlaywright等工具渲染动态页面。
3. 添加请求延时,避免访问过快。
pandas.read_html找不到表格表格结构复杂(如多层表头、合并单元格),或表格在<iframe>内。1. 使用BeautifulSoup精细定位表格HTML。
2. 查看网页源代码,确认表格的正确idclass
3. 考虑直接解析JSON数据(如果网站提供)。
情感分析结果不准确1. 文本包含大量足球专业术语、人名、俱乐部名(中性词)。
2. 讽刺或双重否定句。
1. 对文本进行预处理:去除停用词、特殊字符,只保留情感相关词汇。
2. 使用领域特定的情感词典(需自己构建或寻找)。
3. 考虑使用更先进的模型,如基于Transformer的Hugging Face情感分析管道。
Streamlit 图表不显示或报错1.Plotly图表对象创建有误。
2. 数据格式不对(如日期不是datetime类型)。
3. Streamlit版本与库不兼容。
1. 使用st.write(fig)st.plotly_chart(fig)前,先用print(type(fig))确认对象类型。
2. 确保用于坐标轴的数据是图表库支持的格式。
3. 检查库版本,更新至稳定版。
数据库操作慢或锁死1. 频繁建立和关闭连接。
2. 未使用事务,或单条插入数据量巨大。
1. 使用连接池或保持长连接(注意适时关闭)。
2. 对于批量插入,使用executemany()pandas.to_sql()
3. 对SQLite,在批量操作前执行PRAGMA journal_mode = WAL;提升并发性。

6. 最佳实践与工程建议

将个人兴趣项目工程化,不仅能提升代码质量,也是重要的学习过程。

  1. 配置与密钥管理

    • 绝对不要将API密钥、数据库密码等敏感信息硬编码在代码中。
    • 使用配置文件(如config/settings.py.env文件),并通过环境变量读取。
    • .env文件添加到.gitignore中。
  2. 错误处理与日志记录

    • 爬虫模块必须有完善的异常处理(try...except),记录失败URL,便于重试。
    • 使用Python内置的logging模块,为不同模块设置不同日志级别,输出到文件,方便排查问题。
    import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[logging.FileHandler('crawler.log'), logging.StreamHandler()]) logger = logging.getLogger(__name__)
  3. 数据清洗的健壮性

    • 假设外部数据是“脏”的,清洗函数要能处理缺失值(NaN)、异常值、格式不一致(如日期格式)等问题。
    • 清洗后,将原始数据和清洗后数据分开保存,便于追溯和回滚。
  4. 代码可维护性

    • 遵循函数单一职责原则。一个函数只做一件事(如爬取、解析、清洗、存储)。
    • 使用类型提示(Type Hints),提高代码可读性和IDE支持。
    • 编写清晰的文档字符串(Docstring),说明函数用途、参数和返回值。
  5. 项目扩展方向

    • 数据源扩展:加入社交媒体数据(Twitter Reddit)、视频数据(YouTube评论)、更详细的比赛事件数据(射门、传球、抢断)。
    • 分析深度扩展:进行对手分析、与同期其他球星的对比分析、基于机器学习的赛季表现预测。
    • 技术栈升级:用FastAPI构建后端API,用React/Vue构建更复杂的前端仪表板,将应用部署到云服务器(如AWS EC2, Heroku)。

通过这个项目,你不仅完成了一次对特定主题的数据叙事,更实践了一套完整的数据分析 pipeline。技术是工具,而你想讲述的故事,才是灵魂。