如果你平时喜欢看乒乓球比赛,应该会留意到,赛后球迷最想看的除了冠军归属,还有选手一路晋级的轨迹:从八强到四强,从半决赛到决赛,每一场的比分、用时、对阵关系都值得回味。近期横滨冠军赛男单决赛中,张本智和在自己的主场以 4:1 击败吴晙诚,拿下男单冠军。作为技术开发者,除了关注赛果,我们还可以用 Python 把这些比赛记录整理成结构化数据,再做一次完整的统计分析,输出图表和报表。
这篇文章不打算停留在“看比分”的层面,而是把这场赛事当作一个实战场景,带大家完成一个可以运行的乒乓球赛事数据采集与分析小项目。你会掌握:如何设计比赛数据字段、用 pandas 清洗数据、统计选手胜率与净胜局、绘制横向对比柱状图与选手间胜率热力图,最后把分析结果导出为 CSV 报表。文章中的示例数据以可复现为主,技术流程可以复用到足球、篮球、羽毛球等各类体育赛事分析中。
1. 背景与核心概念
1.1 从一场乒乓球决赛说起
横滨冠军赛男单决赛,张本智和在家门口作战,最终以大比分 4:1 击败吴晙诚。对普通观众来说,这是一场精彩的比赛;对数据分析从业者来说,这就像一个微型的“赛事指标集”:赢了几局、输了几局、打了多久、在哪个场馆比赛,这些信息汇总在一起,就是最基础的体育数据。
我们可以进一步思考:如果只记录“冠军”和“亚军”,信息量是远远不够的。数据分析的一个重要习惯,是把事件拆成可量化的字段。比如一场比赛至少包含对阵双方、赛段、总比分、比赛时长、比赛场地。有了这些字段,我们才能继续回答“主场胜率是否真的更高”“进入决赛前哪名选手消耗更大”“张本智和与吴晙诚的直接交手记录如何”这类问题。
1.2 乒乓球赛事数据能分析什么
乒乓球比赛的记录方式简洁,很适合入门数据分析。每场比赛的比分、局数、赛程阶段都可以快速整理成表格。常见分析维度包括:
- 胜场数与胜率:衡量一名选手在一段时间内的整体表现。
- 净胜局:总得分局与总失分局的差值,能反映比赛统治力。
- 主客场胜率:区分在“横滨体育馆”这样的主场场馆,与其他场馆的胜率差异。
- 对阵关系矩阵:统计 A 选手面对 B 选手的胜率,寻找“克星”关系。
- 比赛用时:不同阶段消耗的体能差异。
这些指标在真实的职业赛事分析中很常见。本文会用模拟数据把整套流程跑通。
1.3 本文的技术目标
本文的核心技术目标是:
- 设计比赛数据模型,构造可复现的示例数据集。
- 使用 pandas 完成数据加载、清洗、类型转换。
- 按选手统计胜场、胜率、净胜局、平均用时。
- 使用 matplotlib 输出柱状图与胜率热力图。
- 将统计结果导出为 CSV 报表。
整个过程只需要 Python 环境和两个依赖库,没有复杂的外部服务,适合初学者照着敲一遍。
2. 准备工作:环境与项目结构
2.1 运行环境与版本说明
本文示例代码依赖以下环境:
- 操作系统:Windows 10/11、macOS、Linux 均可。
- Python 版本:建议 3.9 或以上。
- 依赖库:pandas、matplotlib。
- 可选依赖:openpyxl,仅在你需要导出 Excel 时安装。
版本不需要刻意追求最新。pandas 2.x 和 matplotlib 3.x 都能正常运行本文代码。如果你的环境是 Python 3.7 或更低,建议先升级 Python 版本,否则部分语法和数据操作方式可能不兼容。
2.2 安装 Python 依赖
推荐使用虚拟环境,避免污染系统 Python。打开终端,执行:
mkdir pingpong_data_analysis cd pingpong_data_analysis python -m venv venvWindows 下激活虚拟环境:
venv\Scripts\activatemacOS 或 Linux 下激活虚拟环境:
source venv/bin/activate然后安装依赖:
pip install pandas matplotlib如果你想导出 Excel 报表,可以再安装:
pip install openpyxl安装完成后,可以用下面的命令确认版本:
python -c "import pandas as pd; import matplotlib; print(pd.__version__, matplotlib.__version__)"只要命令能正常输出版本号,环境就准备好了。
2.3 项目目录结构
为了让代码清晰,建议按下面的结构组织文件:
pingpong_data_analysis/ ├── make_sample_data.py ├── analyze.py ├── match_records.csv ├── player_stats.csv ├── win_chart.png └── head_to_head_matrix.png其中:
make_sample_data.py:负责构造示例比赛数据,并写入match_records.csv。analyze.py:主分析脚本,读取 CSV、清洗、统计、画图、导出报表。match_records.csv:比赛数据源文件,由脚本生成。player_stats.csv:选手统计报表。win_chart.png:胜场数柱状图。head_to_head_matrix.png:选手间胜率热力图。
后面我们逐文件编写。
3. 数据获取与数据合规
3.1 数据从哪里来
真实乒乓球赛事数据可以来自官方网站、赛事新闻、体育数据平台。常见字段包括选手名称、比赛日期、赛段、比分、场馆等。
在把数据交给程序分析之前,我们通常要完成两步:
- 人工或脚本采集原始数据。
- 把原始数据整理成统一结构的表格。
如果你的数据来源是网页,可以用 requests 抓取 HTML,再用 BeautifulSoup 解析页面。但这篇文章不直接写爬虫逻辑,因为不同网站的反爬策略和页面结构差异很大,写死某一家网站的解析规则反而容易过时。
3.2 爬取公开数据时的边界
公开网页数据不等于可以随意大批量抓取。开发者在做数据采集时,至少要关注几点:
- 查看网站的
robots.txt,了解服务端是否允许爬取。 - 遵循网站的服务条款,不用于商业用途。
- 控制请求频率,避免对目标服务器造成压力。
- 尊重版权,尤其是转述和引用比赛数据时要注明来源。
如果只是自己做学习研究,建议用少量数据做技术验证,不要在公网上高频请求。
3.3 为什么本文使用模拟数据
很多初学者卡在“找不到数据”这一步。其实对数据分析项目来说,第一步是打通代码流程,而不是纠结数据是否绝对真实。本文构造一份模拟数据集,字段结构尽量接近真实比赛数据,这样你可以完整跑通“读取-清洗-统计-可视化-导出”的全流程。等代码验证完毕,再把数据源替换成真实的官方赛程数据即可。
需要特别说明的是:本文数据集中出现的“张本智和”“吴晙诚”仅用于关联赛事背景,所有比分、日期、场馆记录均为演示用途,不代表该赛事真实统计结果。
4. 数据建模与示例数据集
4.1 字段设计
建表之前,先想清楚需要哪些字段。本文设计一个match_records.csv,字段如下:
| 字段名 | 类型 | 含义 |
|---|---|---|
| date | str | 比赛日期,格式为 YYYY-MM-DD |
| tournament | str | 赛事名称 |
| stage | str | 赛段,例如 1/4决赛、半决赛、决赛 |
| player1 | str | 对阵选手一 |
| player2 | str | 对阵选手二 |
| score1 | int | 选手一赢得的局数 |
| score2 | int | 选手二赢得的局数 |
| duration_min | float | 比赛时长,单位分钟 |
| venue | str | 比赛场馆 |
为什么字段这样设计?原因在于,后续统计需要区分胜方和负方,而“选手一”“选手二”这种对称结构可以保留原始对阵信息,不会在采集阶段就丢失顺序。总比分用两个独立的数字字段记录,后续计算净胜局时非常方便。
4.2 构造示例数据集
新建make_sample_data.py,写入以下代码:
# 文件路径:make_sample_data.py import pandas as pd def build_demo_data(): columns = [ "date", "tournament", "stage", "player1", "player2", "score1", "score2", "duration_min", "venue", ] rows = [ ["2025-04-01", "横滨冠军赛", "1/4决赛", "张本智和", "选手A", 3, 0, 25, "横滨体育馆"], ["2025-04-02", "横滨冠军赛", "半决赛", "张本智和", "选手B", 3, 2, 48, "横滨体育馆"], ["2025-04-03", "横滨冠军赛", "决赛", "张本智和", "吴晙诚", 4, 1, 42, "横滨体育馆"], ["2025-03-15", "新加坡大满贯", "1/4决赛", "吴晙诚", "选手C", 3, 1, 36, "新加坡室内体育馆"], ["2025-03-16", "新加坡大满贯", "半决赛", "吴晙诚", "选手D", 1, 3, 39, "新加坡室内体育馆"], ] df = pd.DataFrame(rows, columns=columns) return df if __name__ == "__main__": df = build_demo_data() df.to_csv("match_records.csv", index=False, encoding="utf-8-sig") print("示例数据已生成到 match_records.csv") print(df.to_string(index=False))这段代码使用 pandas 的DataFrame构造表格数据。注意to_csv中的encoding="utf-8-sig"参数,它会在文件开头写入 BOM 头,让 Excel 打开 CSV 时中文不乱码。如果你在命令行里运行脚本,可以看到表格内容打印在终端上。
4.3 运行数据生成脚本
在项目目录下执行:
python make_sample_data.py预期输出中,你会看到一张包含 5 场比赛记录的表。同时项目目录下多出match_records.csv文件。
有一点需要提醒:示例数据只有 5 条记录,真实项目里数据量会大得多。不过本文重点是演示流程,数据量小反而更容易排查问题。
5. 核心代码实现:赛事数据分析
接下来编写主分析脚本analyze.py。为了讲解清晰,我按功能拆成几个代码块,最终合并在同一个脚本中。
5.1 读取与清洗数据
数据分析第一步永远是“清洗”。原始数据可能存在空值、字符串空格、错误类型等问题。下面这段代码负责读取 CSV,并做基础清洗:
# 文件路径:analyze.py(片段一:加载与清洗) import pandas as pd def load_and_clean(path="match_records.csv"): df = pd.read_csv(path, dtype={"date": str}) # 删除对阵双方或比分为空的行 df = df.dropna(subset=["player1", "player2", "score1", "score2"]) # 比分必须转成整数,否则后续计算会报错 df["score1"] = df["score1"].astype(int) df["score2"] = df["score2"].astype(int) # 比赛时长如果是空值,暂时转为 NaN df["duration_min"] = pd.to_numeric(df["duration_min"], errors="coerce") # 去掉选手名两侧空格 df["player1"] = df["player1"].str.strip() df["player2"] = df["player2"].str.strip() return df这里有几个关键点:
dtype={"date": str}设置日期字段读入时按字符串处理,避免被 pandas 自动解析成时间格式。dropna会删除关键字段为空的行,防止统计时出现错误。astype(int)把比分强制转为整数,因为 CSV 里的3可能被读成字符串。pd.to_numeric(..., errors="coerce")是常见的容错写法,遇到无法转成数字的值时记为NaN,而不是让程序崩溃。
5.2 添加派生字段
清洗完成后,我们需要在表中增加一些方便统计的派生字段,比如净胜局、总比分、胜方标记:
# 文件路径:analyze.py(片段二:派生字段) def add_result_columns(df): df["score_gap"] = (df["score1"] - df["score2"]).abs() df["total_score"] = df["score1"] + df["score2"] df["is_player1_win"] = df["score1"] > df["score2"] return dfscore_gap表示两名选手赢得的局数差距。比如张本智和 4:1 击败吴晙诚,净胜局为 3。total_score是双方总局数,这项指标可以粗略反映比赛激烈程度。is_player1_win是一个布尔值,后续判断胜负时可以直接使用。
5.3 按选手统计核心指标
现在实现一个通用的选手统计函数。它能在“这名选手出现在 player1 还是 player2”两种情况下,正确统计出场次数、胜场数、胜率、净胜局和平均用时:
# 文件路径:analyze.py(片段三:选手统计) def player_stats(df, player): sub = df[(df["player1"] == player) | (df["player2"] == player)].copy() sub["win"] = sub.apply( lambda r: (r["player1"] == player and r["score1"] > r["score2"]) or (r["player2"] == player and r["score2"] > r["score1"]), axis=1, ) games = len(sub) wins = int(sub["win"].sum()) win_rate = wins / games if games else 0 total_gap = 0 for _, r in sub.iterrows(): if r["player1"] == player: total_gap += r["score1"] - r["score2"] else: total_gap += r["score2"] - r["score1"] avg_duration = sub["duration_min"].mean() return { "player": player, "games": games, "wins": wins, "win_rate": round(win_rate, 3), "total_score_gap": total_gap, "avg_duration": round(avg_duration, 1) if pd.notna(avg_duration) else None, }这段代码的思路是:先用布尔条件筛选出包含该选手的所有比赛,再判断每一场该选手是否获胜。apply接收一个匿名函数,逐行处理,逻辑直观。计算总净胜局时,如果选手在player1列,就用score1 - score2;如果在player2列,就用score2 - score1。
这里提醒一点:用apply和iterrows处理几千行数据没问题,但如果数据量达到十几万行,性能会明显下降。真实项目中建议用groupby或numpy做向量化计算。本文示例数据量小,先用最易读的方式。
5.4 主客场胜率统计
张本智和是横滨的主场选手,那么“主场胜率是否真的更高”是很有意思的问题。下面函数判断选手是否在横滨体育馆比赛,并分别统计主客场战绩:
# 文件路径:analyze.py(片段四:主客场统计) def venue_stats(df, player): sub = df[(df["player1"] == player) | (df["player2"] == player)].copy() sub["is_home"] = sub["venue"] == "横滨体育馆" result = {} for home_flag, label in [(True, "主场"), (False, "客场")]: part = sub[sub["is_home"] == home_flag] games = len(part) if games == 0: result[label] = {"games": 0, "wins": 0, "win_rate": None} continue wins = 0 for _, r in part.iterrows(): if (r["player1"] == player and r["score1"] > r["score2"]) or ( r["player2"] == player and r["score2"] > r["score1"] ): wins += 1 result[label] = { "games": games, "wins": wins, "win_rate": round(wins / games, 3), } return result在实际项目中,“主场”不一定只看固定场馆名,有时需要维护一个“主场场馆列表”。示例代码直接把“横滨体育馆”判定为主场,逻辑简单,一目了然。
5.5 绘制胜场柱状图
接下来使用 matplotlib 绘制各选手胜场数柱状图:
# 文件路径:analyze.py(片段五:柱状图) import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC", "Arial Unicode MS"] plt.rcParams["axes.unicode_minus"] = False def draw_win_chart(stats_df): df = stats_df.sort_values("wins", ascending=True) plt.figure(figsize=(8, 4)) plt.bar(df["player"], df["wins"], color="#4C72B0") plt.xlabel("选手") plt.ylabel("胜场数") plt.title("模拟赛事选手胜场统计") for i, v in enumerate(df["wins"]): plt.text(i, v + 0.05, str(v), ha="center") plt.tight_layout() plt.savefig("win_chart.png", dpi=120) plt.show()sort_values("wins", ascending=True)让胜场最少的选手排在左侧,最多的在右侧,柱子按递增顺序排列,视觉效果更好。plt.text用于在柱子上方标注具体数值。
中文字体是 Windows 和 macOS 开发者经常会遇到的问题。示例代码提供了几种常见中文字体,具体使用哪一种取决于系统中是否安装。如果画出来的图是方框字符,可以按第 7 章的排查方法处理。
5.6 绘制选手间胜率热力图
最后一类可视化是胜率热力图。它用来回答“张本智和面对吴晙诚的胜率是多少”“吴晙诚面对其他选手的胜率是多少”这类问题:
# 文件路径:analyze.py(片段六:胜率矩阵与热力图) def draw_matrix(df, players): matrix = pd.DataFrame(index=players, columns=players, dtype=float) for p1 in players: for p2 in players: if p1 == p2: matrix.loc[p1, p2] = None continue sub = df[ ((df["player1"] == p1) & (df["player2"] == p2)) | ((df["player1"] == p2) & (df["player2"] == p1)) ] if len(sub) == 0: matrix.loc[p1, p2] = None else: wins = 0 for _, r in sub.iterrows(): if (r["player1"] == p1 and r["score1"] > r["score2"]) or ( r["player2"] == p1 and r["score2"] > r["score1"] ): wins += 1 matrix.loc[p1, p2] = wins / len(sub) return matrix def draw_heatmap(matrix): fig, ax = plt.subplots(figsize=(7, 6)) im = ax.imshow(matrix.values, cmap="YlOrRd") ax.set_xticks(range(len(matrix.columns))) ax.set_xticklabels(matrix.columns) ax.set_yticks(range(len(matrix.index))) ax.set_yticklabels(matrix.index) for i in range(len(matrix.index)): for j in range(len(matrix.columns)): v = matrix.iloc[i, j] if pd.notna(v): ax.text(j, i, f"{v:.2f}", ha="center", va="center") fig.colorbar(im, ax=ax) ax.set_title("选手间胜率矩阵(行选手面对列选手)") plt.tight_layout() plt.savefig("head_to_head_matrix.png", dpi=120) plt.show()胜率矩阵的每个格子,表示“行选手”面对“列选手”的胜率。对角线表示选手与自己比赛,没有意义,因此设置为空。
这里要明确说明:矩阵里只有两位真实选手的示例记录,其余多为占位符选手。真实项目中,矩阵会比较大,颜色深浅能直观反映选手之间的“克制关系”。
5.7 导出统计报表
统计完成后,把各选手的核心指标导出为 CSV:
# 文件路径:analyze.py(片段七:导出报表) def export_report(stats_df, path="player_stats.csv"): stats_df.to_csv(path, index=False, encoding="utf-8-sig")index=False表示不把行号写进文件,encoding="utf-8-sig"继续解决中文乱码问题。
5.8 组装完整脚本
把上面的函数组合进main函数,形成完整的analyze.py文件:
# 文件路径:analyze.py(完整版) import pandas as pd import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC", "Arial Unicode MS"] plt.rcParams["axes.unicode_minus"] = False def load_and_clean(path="match_records.csv"): df = pd.read_csv(path, dtype={"date": str}) df = df.dropna(subset=["player1", "player2", "score1", "score2"]) df["score1"] = df["score1"].astype(int) df["score2"] = df["score2"].astype(int) df["duration_min"] = pd.to_numeric(df["duration_min"], errors="coerce") df["player1"] = df["player1"].str.strip() df["player2"] = df["player2"].str.strip() return df def add_result_columns(df): df["score_gap"] = (df["score1"] - df["score2"]).abs() df["total_score"] = df["score1"] + df["score2"] df["is_player1_win"] = df["score1"] > df["score2"] return df def player_stats(df, player): sub = df[(df["player1"] == player) | (df["player2"] == player)].copy() sub["win"] = sub.apply( lambda r: (r["player1"] == player and r["score1"] > r["score2"]) or (r["player2"] == player and r["score2"] > r["score1"]), axis=1, ) games = len(sub) wins = int(sub["win"].sum()) win_rate = wins / games if games else 0 total_gap = 0 for _, r in sub.iterrows(): if r["player1"] == player: total_gap += r["score1"] - r["score2"] else: total_gap += r["score2"] - r["score1"] avg_duration = sub["duration_min"].mean() return { "player": player, "games": games, "wins": wins, "win_rate": round(win_rate, 3), "total_score_gap": total_gap, "avg_duration": round(avg_duration, 1) if pd.notna(avg_duration) else None, } def venue_stats(df, player): sub = df[(df["player1"] == player) | (df["player2"] == player)].copy() sub["is_home"] = sub["venue"] == "横滨体育馆" result = {} for home_flag, label in [(True, "主场"), (False, "客场")]: part = sub[sub["is_home"] == home_flag] games = len(part) if games == 0: result[label] = {"games": 0, "wins": 0, "win_rate": None} continue wins = 0 for _, r in part.iterrows(): if (r["player1"] == player and r["score1"] > r["score2"]) or ( r["player2"] == player and r["score2"] > r["score1"] ): wins += 1 result[label] = { "games": games, "wins": wins, "win_rate": round(wins / games, 3), } return result def draw_win_chart(stats_df): df = stats_df.sort_values("wins", ascending=True) plt.figure(figsize=(8, 4)) plt.bar(df["player"], df["wins"], color="#4C72B0") plt.xlabel("选手") plt.ylabel("胜场数") plt.title("模拟赛事选手胜场统计") for i, v in enumerate(df["wins"]): plt.text(i, v + 0.05, str(v), ha="center") plt.tight_layout() plt.savefig("win_chart.png", dpi=120) plt.show() def draw_matrix(df, players): matrix = pd.DataFrame(index=players, columns=players, dtype=float) for p1 in players: for p2 in players: if p1 == p2: matrix.loc[p1, p2] = None continue sub = df[ ((df["player1"] == p1) & (df["player2"] == p2)) | ((df["player1"] == p2) & (df["player2"] == p1)) ] if len(sub) == 0: matrix.loc[p1, p2] = None else: wins = 0 for _, r in sub.iterrows(): if (r["player1"] == p1 and r["score1"] > r["score2"]) or ( r["player2"] == p1 and r["score2"] > r["score1"] ): wins += 1 matrix.loc[p1, p2] = wins / len(sub) return matrix def draw_heatmap(matrix): fig, ax = plt.subplots(figsize=(7, 6)) im = ax.imshow(matrix.values, cmap="YlOrRd") ax.set_xticks(range(len(matrix.columns))) ax.set_xticklabels(matrix.columns) ax.set_yticks(range(len(matrix.index))) ax.set_yticklabels(matrix.index) for i in range(len(matrix.index)): for j in range(len(matrix.columns)): v = matrix.iloc[i, j] if pd.notna(v): ax.text(j, i, f"{v:.2f}", ha="center", va="center") fig.colorbar(im, ax=ax) ax.set_title("选手间胜率矩阵(行选手面对列选手)") plt.tight_layout() plt.savefig("head_to_head_matrix.png", dpi=120) plt.show() def export_report(stats_df, path="player_stats.csv"): stats_df.to_csv(path, index=False, encoding="utf-8-sig") def main(): df = load_and_clean() df = add_result_columns(df) players = ["张本智和", "吴晙诚", "选手A", "选手B", "选手C", "选手D"] stats_rows = [player_stats(df, p) for p in players] stats_df = pd.DataFrame(stats_rows) print("=== 选手统计 ===") print(stats_df.to_string(index=False)) export_report(stats_df) draw_win_chart(stats_df) matrix = draw_matrix(df, players) draw_heatmap(matrix) print("\n=== 张本智和主客场表现 ===") print(venue_stats(df, "张本智和")) print("\n=== 张本智和 vs 吴晙诚 ===") head_df = df[ ((df["player1"] == "张本智和") & (df["player2"] == "吴晙诚")) | ((df["player1"] == "吴晙诚") & (df["player2"] == "张本智和")) ] print(player_stats(head_df, "张本智和")) if __name__ == "__main__": main()6. 运行与结果说明
6.1 运行方式
确保项目目录下已经有match_records.csv,然后在虚拟环境中执行:
python analyze.py程序会依次完成数据加载、清洗、统计和画图。执行过程中,终端会弹出两个 matplotlib 窗口,一个显示胜场柱状图,一个显示选手间胜率热力图。如果没有显示,说明当前环境缺少图形界面,你可以把plt.show()注释掉,只保留plt.savefig(),图片会保存到本地文件。
6.2 预期输出
终端中会显示类似下面的选手统计结果:
=== 选手统计 === player games wins win_rate total_score_gap avg_duration 张本智和 3 3 1.000 6 38.3 吴晙诚 2 1 0.500 0 37.5 选手A 1 0 0.000 -3 25.0 选手B 1 0 0.000 -2 48.0 选手C 1 0 0.000 -2 36.0 选手D 1 1 1.000 2 39.0从模拟数据可以看到,张本智和 3 场比赛全部获胜,胜率 1.000,净胜局为 6,说明他在示例数据中的统治力较强。吴晙诚出战 2 场,1 胜 1 负,胜率 0.500。
“张本智和 vs 吴晙诚”的单独统计会输出张本智和对阵吴晙诚 1 场、胜率 1.000 的结果,与标题中的 4:1 赛果一致。
同时,项目目录下会生成两个图片文件和一个 CSV 报表:
win_chart.png:各选手胜场柱状图。head_to_head_matrix.png:选手间胜率热力图。player_stats.csv:选手统计报表。
7. 常见问题与排查思路
运行数据分析脚本时,最常遇到的并不是分析逻辑问题,而是环境、编码、中文字体这些细节。下面整理一份排查表:
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
运行pip install报错 | 网络源不稳定或权限不足 | 使用国内镜像源,或加--user参数 |
| 读取 CSV 后中文变成乱码 | CSV 编码不一致 | 写入时使用utf-8-sig,读取时检查encoding |
| matplotlib 图片中的中文显示为方框 | 系统中没有匹配的中文字体 | 修改plt.rcParams["font.sans-serif"]为已安装字体 |
| 图表负号显示异常 | 未关闭 Unicode 负号 | 设置plt.rcParams["axes.unicode_minus"] = False |
astype(int)报错 | CSV 中存在空值或文本比分 | 先dropna,再使用pd.to_numeric |
调用plt.show()没有窗口弹出 | 服务器或无图形界面环境 | 改为只savefig保存图片 |
| 数据量大时程序运行缓慢 | 使用大量apply和iterrows | 改用groupby、merge等向量化操作 |
从工程角度说,遇到报