简介:一套完整的Python演唱会数据分析与可视化大作业源码,面向高校学生、课程设计者及数据分析入门者,解决从数据获取到业务展示的全流程实践需求。项目以演唱会数据为对象,先用爬虫自动化抓取网页信息,再用pandas完成缺失值处理、字段转换等清洗工作;分析阶段引入Apache Spark进行分布式统计与趋势挖掘,最后通过Flask构建后端服务、ECharts渲染交互图表,在前端大屏上直观呈现票房、热度等多维指标。资源包共65个文件,涵盖Python与Scala源码、HTML/JS前端页面、CSV数据文件、JSON及图片资源等,包体约11.86MB,目录按爬虫、清洗、分析、展示模块划分,便于局部替换或二次开发。目前已有79人学习下载,适合希望快速参考完整项目链路、提升大数据可视化能力的学习者。
1. 平台演唱会数据分析:这个 Python 大作业到底在写什么
如果你正在为 Python 大作业发愁,打算选数据分析与可视化的方向,平台演唱会数据集是一个很聪明的选题:它有艺人、城市、场馆、日期、票价、观众数多个维度,数据贴近生活,可视化做出来也有话题度。这个项目的本质是——从票务平台抓取或整理演唱会原始数据,用 pandas 清洗成结构化表,再围绕时间、城市、票价做统计分析,最后用 matplotlib、seaborn、pyecharts 把结论变成图表甚至可视化大屏,交出一份能跑的源码加一份能讲的报告。适合一周内需要完成课程设计或结课大作业的同学,也适合刚学完 pandas、想用真实场景练手的人。它不需要机器学习,但能把数据处理能力和图表表达练得很扎实,这两点正是本科大作业最看重的。
2. 先搭数据地基:演唱会数据从哪来、怎么整理成 DataFrame
2.1 数据来源的三种常见做法:爬虫、公开数据集、模拟数据
做数据分析项目,数据来源直接决定后面要踩多少坑。平台演唱会数据的获取,常见做法有三种。
第一种是爬虫。用 requests 加 BeautifulSoup 抓取票务平台或音乐平台的演唱会列表页,能拿到演唱会名称、艺人、城市、场馆、日期、票价区间等公开字段。这个方案真实感强,但要处理反爬、分页、页面结构变化,写起来会比较费时间。我一般只建议在大作业中抓少量页面,比如几百条就够,频率控制在每秒一次以内,避免给对方服务器造成压力,也避免把自己搞进黑名单。纯技术验证可以用网上的公开页面或官方提供的数据接口,不要碰需要登录和验证码的部分。
第二种是公开数据集。GitHub、Kaggle 上有不少演唱会或演出历史数据,下载成 CSV 或 Excel 后直接读取。这个方案最省事,但字段经常不齐,有的没有票价,有的没有场馆容量,需要花时间补全。而且公开数据集的地区、时间分布不一定符合你想讲的故事,最后图表和结论可能不太搭。
第三种是模拟数据,这也是我推荐给赶时间同学的做法。按真实分布用 numpy 构造数据,比如票价集中在 180 到 1500 元之间,一线城市场次多于二三线城市,暑期和年末场次偏多。下面这段代码生成 600 条演唱会记录,字段覆盖整个大作业需要的基本维度:
import pandas as pd import numpy as np rng = np.random.default_rng(2024) # 固定随机种子,保证结果可复现 cities = ["北京", "上海", "广州", "深圳", "成都", "杭州", "武汉", "南京", "西安", "长沙"] # 每座城市配几个候选场馆,模拟数据时保证城市和场馆不会错配 venues = { "北京": ["国家体育场", "凯迪拉克中心", "北京工人体育场"], "上海": ["梅赛德斯-奔驰文化中心", "上海体育场"], "广州": ["广州体育馆", "宝能国际体育演艺中心"], "深圳": ["深圳湾体育中心", "华润深圳湾体育中心"], "成都": ["成都金融城演艺中心"], "杭州": ["杭州奥体中心体育馆"], "武汉": ["武汉体育中心体育馆"], "南京": ["南京奥体中心体育馆"], "西安": ["西安奥体中心体育馆"], "长沙": ["湖南国际会展中心"] } singers = ["周杰伦", "林俊杰", "陈奕迅", "Taylor Swift", "薛之谦", "五月天", "邓紫棋", "张杰"] rows = [] for i in range(600): city = rng.choice(cities) singer = rng.choice(singers) year = int(rng.integers(2023, 2025)) month = int(rng.integers(1, 13)) day = int(rng.integers(1, 29)) low = int(rng.integers(180, 680)) # 最低票价 high = low + int(rng.integers(200, 1200)) # 最高票价=最低+区间长度 rows.append([i, singer, city, rng.choice(venues[city]), f"{year}-{month:02d}-{day:02d}", f"{low}-{high}", int(rng.integers(8000, 50000))]) df = pd.DataFrame(rows, columns=["演唱会ID", "艺人", "城市", "场馆", "日期", "票价区间", "观众数"]) df.head()代码里的 rng = np.random.default_rng(2024) 是固定随机种子,每次运行都生成同一份数据,交作业时图表、数字、结论都能对上,这是大作业的一个隐藏加分项。场馆列表放在字典里,城市和场馆天然绑定,后面做城市分布时不会出现“上海的国家体育场”这种怪数据。日期先用字符串,后面统一转成 pandas 的 datetime 类型。票价区间写成"低-高"字符串,是为了模拟爬虫抓到的原始形态,下一步正好演示清洗。
如果你确实要写爬虫,核心框架大致是这样:
import requests from bs4 import BeautifulSoup import time def crawl_concert_page(url): # 仅用于学习研究,注意遵守目标网站的 robots 协议 resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=10) soup = BeautifulSoup(resp.text, "html.parser") # 先打印页面结构,再写具体的选择器 items = soup.select(".concert-item") for item in items: title = item.select_one(".title").text.strip() city = item.select_one(".city").text.strip() date = item.select_one(".date").text.strip() price = item.select_one(".price").text.strip() # 这里可以追加到列表,最后转成 DataFrame return items这段代码是个框架,真正的选择器要按你抓的站点的 HTML 结构来写。爬下来的字段通常是字符串,比如"2024-05-18 周六 19:30"和"¥380起",后面要写正则或 split 提取核心信息。我的建议是:爬虫只为演示数据获取能力,后续清洗、分析、可视化才是大作业的评分大头,别在爬虫上耗太多时间。
2.2 用 pandas 清洗:日期、票价、城市从字符串里抠出来
拿到原始 DataFrame 后的第一件事是清洗。爬虫和模拟数据最大的区别就是脏数据多:日期可能带星期几,票价可能写"380-1280"也可能写"380起",城市名可能带后缀。下面这两个函数是清洗的核心:
def parse_date(raw): # 兼容 "2024-05-18"、 "2024-05-18 周六"、 "2024/5/18" 等常见格式 s = str(raw).split(" ")[0].replace("/", "-") return pd.to_datetime(s, format="%Y-%m-%d", errors="coerce") def parse_price(raw): # 输入 "380-1280",返回最低价、最高价、均价;输入 "380起" 时返回单一价格 s = str(raw).replace("¥", "").replace("起", "").replace("元", "") try: parts = s.split("-") low = float(parts[0]) high = float(parts[1]) if len(parts) > 1 else low except Exception: return None, None, None return low, high, (low + high) / 2 df["日期DT"] = df["日期"].apply(parse_date) df["最低价"] = df["票价区间"].apply(lambda x: parse_price(x)[0]) df["最高价"] = df["票价区间"].apply(lambda x: parse_price(x)[1]) df["票价均值"] = df["票价区间"].apply(lambda x: parse_price(x)[2])parse_date 先把字符串按空格切段,取第一段,再把斜杠替换成横杠,最后用 pd.to_datetime 严格按格式解析。errors="coerce" 是关键参数:解析不了的值会变成 NaT(缺失时间)而不是抛异常中断整个脚本,这给我们留下了排查的机会。parse_price 先清掉"¥"、"起"、"元"这些干扰词,再按横杠切分;如果没有横杠,说明是单一票价,最高价取最低价同一值。
这样一次 apply 会调用三次 parse_price,数据量小无所谓,但更干净的做法是一次调用返回三列:
df[["最低价", "最高价", "票价均值"]] = df["票价区间"].apply(lambda x: pd.Series(parse_price(x)))清洗完要做一次缺失值检查:
print(df.isnull().sum()) print(df[df["日期DT"].isna()].head())如果日期解析失败很多,大概率是格式里有"待定"、"延期"这类词,这时需要先把这些记录筛掉或标注状态,而不是直接丢进分析。票价解析失败则多是"价格待定"这类值,可以先置为 NaN,分析时用 dropna 排除。我见过不少大作业翻车在数据清洗这步,原始 DataFrame 里一堆字符串直接拿去 groupby,结果中文排序乱、日期按字典序排,整个报告都不成立。
2.3 多表关联的常规做法:演唱会表、场馆表合并
真实数据很少是一张表,往往有演唱会信息表、场馆容量表、艺人信息表。大作业如果只分析单表,维度不够丰富;合并一张场馆表,就能多出"上座率"这个很有说服力的分析指标。
venue_info = pd.DataFrame([ ["国家体育场", "北京", 91000], ["凯迪拉克中心", "北京", 18000], ["梅赛德斯-奔驰文化中心", "上海", 18000], ["上海体育场", "上海", 56000], ["广州体育馆", "广州", 14000], ["深圳湾体育中心", "深圳", 20000], ["成都金融城演艺中心", "成都", 12000], ["杭州奥体中心体育馆", "杭州", 18000], ["武汉体育中心体育馆", "武汉", 30000], ["南京奥体中心体育馆", "南京", 21000], ["西安奥体中心体育馆", "西安", 18000], ["湖南国际会展中心", "长沙", 15000] ], columns=["场馆", "城市", "容纳人数"]) df_merged = df.merge(venue_info, on="场馆", how="left") df_merged["上座率"] = df_merged["观众数"] / df_merged["容纳人数"].clip(lower=1) print(df_merged["上座率"].describe())merge 用 on="场馆" 关联,两张表里字段同名才写这个参数,如果一侧叫"场馆"另一侧叫"场馆名称",要写成 on="场馆", left_on="场馆", right_on="场馆名称",并且注意合并后列名。how="left" 保证左表 600 条演唱会记录全部保留,右表没有匹配到的场馆,上座率自然是 NaN。clip(lower=1) 是防止场馆容量数据为 0 时除零,容量 0 会置成 1,上座率虽然有值但会很离谱,所以后面分析前最好再过滤一次。
到这里,DataFrame 里已经有了日期、城市、票价、观众数、场馆容量、上座率,接下来就可以做真正的分析了。
3. 把数据变成洞察:演唱会的核心分析维度与统计口径
3.1 时间维度:用 resample 看场次与票房的月度趋势
演唱会数据最直观的分析是时间趋势。把日期列设为索引,用 resample 按月聚合,就能回答"哪个月开唱最多""旺季是什么时候"这类问题。
df = df_merged.copy() df["日期DT"] = pd.to_datetime(df["日期DT"]) monthly = df.set_index("日期DT").resample("M").agg( 场次数=("演唱会ID", "count"), 观众总数=("观众数", "sum"), 平均票价=("票价均值", "mean") ).dropna() print(monthly.head())set_index 之后,日期成为行索引,resample("M") 表示按月(Month end)分组。agg 里用 named aggregation,新列名写在左侧,读起来一目了然。dropna 是为了去掉还没有整月数据的最后一个月,比如模拟数据只到 2024 年 8 月,8 月场次数只有半个月的量,画折线时尾部会掉得很明显。
如果想看季节规律,把月份字段提取出来再做分组:
df["月份"] = df["日期DT"].dt.month season = df.groupby("月份").agg( 场次数=("演唱会ID", "count"), 平均票价=("票价均值", "mean") ).sort_index() print(season)这里要留心统计口径:按场次统计和按观众总数统计,峰值可能不一样。比如 5 月场次多但都是小型 livehouse,观众总数反而不如 8 月几场大型体育场演唱会。写报告时要把口径写清楚,不要只说"演唱会旺季在几月",而要说"按场次统计,旺季在 X 月和 Y 月;按观众总量统计,旺季是 Z 月"。这种细节在答辩时非常加分。
时间维度另一个常见分析是跨年对比。如果数据覆盖两年,先按年份分两组再比较同月份场次,可以看到市场整体增长。还有按星期几统计,演唱会通常集中在周五和周六,用 df["日期DT"].dt.dayofweek 就能做出来,这个结论贴近大众认知,放在报告里很容易引发共鸣。
3.2 城市与场馆:头部效应、上座率与区域分布
演唱会分布从来不是均匀的,一线城市占据大量场次,这是数据分析报告里最好讲的故事。用 groupby 按城市聚合:
city_stats = df.groupby("城市").agg( 场次数=("演唱会ID", "count"), 观众总数=("观众数", "sum"), 平均票价=("票价均值", "mean"), 平均上座率=("上座率", "mean") ).sort_values("场次数", ascending=False) city_stats["场次占比"] = (city_stats["场次数"] / city_stats["场次数"].sum()).round(4) print(city_stats)sort_values("场次数", ascending=False) 把热门城市排在前面,一眼看到头部效应。场次占比用总和做分母,算出北京、上海加起来占多少。如果北京、上海、广州三个城市占了 50% 以上,报告里就可以写"演唱会市场呈现明显头部聚集效应"。
上座率的均值这里用的是算术平均,严格来说应该按观众总数除以场馆容量总和做加权平均:
city_stats_adj = df.groupby("城市").agg( 观众总数=("观众数", "sum"), 总容量=("容纳人数", "sum") ) city_stats_adj["加权上座率"] = city_stats_adj["观众总数"] / city_stats_adj["总容量"]两种口径算出来的上座率会有差异,特别是当一座城市既有十万人的超级体育场,也有几千人的小型场馆时,算术平均会把小场馆的上座率放大。建议正文用加权口径,附录说明两种口径区别,这能体现你对数据严谨性的理解。
场馆维度可以做最热门场馆 Top10,用 value_counts:
top_venues = df["场馆"].value_counts().head(10) print(top_venues)结合上座率还能发现一个有意思的现象:有些热门场馆场次多,但上座率不一定高,因为大场馆容量大,票房压力也大。这种交叉分析比单列排行榜更有深度。
3.3 票价结构:区间票价如何拆成档位和分布
票价是演唱会的核心经济指标。我们把连续型的票价均值切分成几个档位,便于做饼图和柱状图:
df["票价档位"] = pd.cut( df["票价均值"], bins=[0, 300, 600, 1000, 2000], labels=["大众", "中档", "高档", "VIP"] ) price_dist = df.groupby("票价档位", observed=True).agg( 场次数=("演唱会ID", "count"), 平均票价=("票价均值", "mean"), 观众总数=("观众数", "sum") ) print(price_dist)bins 的边界不是随便写的。我一般先看 describe:
print(df["票价均值"].describe())如果四分之一分位数是 400,中位数是 650,那用 300 和 600 做切分是合理的,说明大众价位和主流价位能分开。如果最低价都到 500,可以用 [0, 600, 1000, 2000] 两档切。pd.cut 的 labels 要和 bins 数量匹配,多一个都会报错。observed=True 用于取消分类变量的警告,pandas 2.0 之后不写也没事,但写上更稳。
票价档位和艺人交叉分析更有意思:
artist_price = df.groupby(["艺人", "票价档位"], observed=True).size().unstack(fill_value=0) print(artist_price)比如 Taylor Swift 的 VIP 档场次占比远高于本土歌手,说明头部国际艺人的票价天花板更高。这个发现可以直接做成堆叠柱状图,是报告的核心论点之一。
票价数据还能计算价格集中度。用累计占比看,比如票价最高的 20% 场次贡献了多少观众。把票价均值排序,按累加观众数算占比,这一步可以用 cumsum 实现。计算逻辑不复杂,但放在报告中非常能体现分析深度。
4. 可视化输出:从 Matplotlib 基础图到可交互大屏
4.1 Matplotlib + Seaborn 静态图:折线、箱线、热力图
分析做完,下一步就是把结果画出来。最容易出错的是中文显示,先配好全局参数:
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "PingFang SC"] plt.rcParams["axes.unicode_minus"] = False sns.set_theme(style="whitegrid", font="SimHei")rcParams 设置 sans-serif 字体列表,系统会按顺序找第一个可用的中文字体,Windows 上用 SimHei,macOS 上用 PingFang SC,这样换电脑也不容易乱码。axes.unicode_minus 必须设成 False,不然坐标轴负号显示成方块。
月度趋势用折线图:
fig, ax = plt.subplots(figsize=(10, 4)) sns.lineplot(data=monthly, x=monthly.index, y="场次数", marker="o", ax=ax) ax.set_title("演唱会月度场次趋势") fig.savefig("output/月度趋势.png", dpi=200, bbox_inches="tight")linplot 的 x 传 DatetimeIndex 会正确显示时间刻度。dpi=200 是印刷质量,bbox_inches="tight" 避免标题被截掉。大作业图片导出用这两参数基本不会翻车。
票价分布用箱线图对比城市差异:
fig, ax = plt.subplots(figsize=(12, 5)) sns.boxplot(data=df, x="城市", y="票价均值", order=city_stats.index[:10], ax=ax) ax.set_xticklabels(ax.get_xticklabels(), rotation=45) fig.tight_layout() fig.savefig("output/城市票价箱线图.png", dpi=200, bbox_inches="tight")order 参数可以指定城市显示顺序,不然 seaborn 默认按中文拼音排,北京未必在第一个,看起来没有逻辑。中文城市名在坐标轴上旋转 45 度,避免重叠。tight_layout 自动调整留白。
城市与月份交叉的热力图:
city_month = df.pivot_table( index="城市", columns="月份", values="演唱会ID", aggfunc="count", fill_value=0 ) # 只保留 Top8 城市 city_month = city_month.loc[city_stats.index[:8]] fig, ax = plt.subplots(figsize=(12, 6)) sns.heatmap(city_month, cmap="Reds", annot=True, fmt="d", ax=ax) ax.set_title("城市×月份 演唱会场次热力图") fig.savefig("output/城市月份热力图.png", dpi=200, bbox_inches="tight")pivot_table 把行变成城市、列变成月份,值是场次数。loc 只取 Top8 城市,避免 10 个城市挤在一张图里看不清。annot=True 显示数字,fmt="d" 表示用整数格式。热力图是可视化大屏之外最有视觉冲击力的静态图,建议放在报告第一个分析位置。
4.2 用 Pyecharts 生成可交互大屏:柱状图、地图、词云
静态图适合写报告,但如果大作业要求做出"可视化大屏",或者你想在答辩现场展示动态效果,pyecharts 是最顺手的工具。它生成 html 文件,浏览器打开就能交互,不需要启动本地服务。
城市 Top10 柱状图:
from pyecharts.charts import Bar, Map, WordCloud, Page from pyecharts import options as opts top_cities = city_stats.head(10) bar = ( Bar(init_opts=opts.InitOpts(width="900px", height="500px")) .add_xaxis(top_cities.index.tolist()) .add_yaxis("场次数", top_cities["场次数"].astype(int).tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="演唱会城市 TOP10"), yaxis_opts=opts.AxisOpts(name="场次") ) ) bar.render("output/city_bar.html")pyecharts 的链式调用很直观,add_xaxis 加类目轴,add_yaxis 加系列。数值要转成 int 列表,避免 numpy int64 类型在 json 序列化时出问题。render 直接生成 html,双击就能打开。
中国地图用场次数填色:
city_layer = list(zip(city_stats.index.tolist(), city_stats["场次数"].astype(int).tolist())) map_chart = ( Map() .add("演唱会场次", city_layer, maptype="china") .set_global_opts( title_opts=opts.TitleOpts(title="演唱会场次地区分布"), visualmap_opts=opts.VisualMapOpts(max_=100) ) ) map_chart.render("output/city_map.html")这里最容易踩的坑是城市名称格式。maptype="china" 的地图数据要求省份或直辖市全名带"市"或"省",比如"北京市"、"四川省",如果你的数据只有"北京",地图上会缺色。解决办法是在构造 city_layer 时做一层映射:
province_map = {"北京": "北京市", "上海": "上海市", "广东": "广东省"} city_layer = [[province_map.get(c, c), v] for c, v in city_layer]WordCloud 词云展示艺人出现频次:
word_data = df["艺人"].value_counts().head(30) wordcloud = ( WordCloud() .add("", list(word_data.items()), word_size_range=[16, 80], shape="circle") .set_global_opts(title_opts=opts.TitleOpts(title="艺人演唱会频次词云")) ) wordcloud.render("output/artist_wordcloud.html")词云适合放在大屏右下角或首页,用来回答"谁最爱开演唱会"。shape 参数可以改成 "diamond"、"triangle",但 circle 最稳。
最后用 Page 把多个图合成一张大屏:
page = Page(page_title="演唱会数据分析大屏") page.add(bar, map_chart, wordcloud) page.render("output/dashboard.html")Page 会把所有图按顺序纵向排列,打开 html 后可以滚动查看。如果需要更复杂的布局,用 Grid 组合,每个 Grid 控制宽高和位置。大屏的标题、图表数量不要贪多,三到四个图足够撑起一场答辩,放太多反而显得堆砌。
4.3 一键跑通:一个函数保存所有分析和图表
大作业源码最忌讳东一榔头西一棒子,写一个入口函数,运行时自动生成所有图表和 CSV,是让老师眼前一亮的结构。我通常这样组织:
from pathlib import Path def run_analysis(df, output_dir="output"): out = Path(output_dir) out.mkdir(exist_ok=True) # 数据清洗 df["日期DT"] = df["日期"].apply(parse_date) df[["最低价", "最高价", "票价均值"]] = df["票价区间"].apply(lambda x: pd.Series(parse_price(x))) df = df.dropna(subset=["日期DT", "票价均值"]) # 分析结果导出 monthly.to_csv(out / "月度趋势.csv", encoding="utf-8-sig") city_stats.to_csv(out / "城市统计.csv", encoding="utf-8-sig") price_dist.to_csv(out / "票价档位分布.csv", encoding="utf-8-sig") # 绘图... fig.savefig(out / "月度趋势.png", dpi=200, bbox_inches="tight") bar.render(str(out / "city_bar.html")) map_chart.render(str(out / "city_map.html")) # ... return out用 pathlib.Path 处理路径,避免字符串拼接时反斜杠出问题。CSV 导出用 utf-8-sig 编码,这样用 Excel 打开不会乱码,这是一个很多人不知道的细节。入口函数返回输出目录,主程序里只需要调用 run_analysis(df),代码结构就清晰了。
5. 大作业避坑/常见问题/排查:从数据到答辩的 5 个典型坑
5.1 中文乱码:图里全是方块,坐标轴文字不显示
现象:用 matplotlib 画图,标题、坐标轴标签全是方块,或者报错"Glyph 缺失"。
原因:matplotlib 默认字体是 DejaVu Sans,不支持中文字符。很多教程只写一两行字,但你换了平台就踩坑。
解决:绘图前必须设置中文字体。
plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows plt.rcParams["axes.unicode_minus"] = False如果你在 macOS 上跑,SimHei 不存在,可以改成:
plt.rcParams["font.sans-serif"] = ["PingFang SC"]更稳的办法是使用 seaborn 的 set_theme 里指定 font 参数。sns.set_theme(style="whitegrid", font="SimHei") 一次设置全局字体。检查当前系统可用中文字体可以执行下面的命令:
import matplotlib.font_manager as fm fonts = [f.name for f in fm.fontManager.ttflist if "Hei" in f.name] print(fonts)这条命令能列出所有含 Hei 的字体名,找不到就说明系统没装中文字体,去系统字体目录安装。这个坑几乎每个人都踩,早发现早解决。
5.2 日期解析失败:to_datetime 抛异常或全是 NaT
现象:pd.to_datetime 对"2024-05-18 周六"这种字符串解析报错,或者返回一堆 NaT,导致后面 resample 没了数据。
原因:日期字符串里混了星期、时间、"延期"等额外字符,且格式不统一;也有的数据是数字格式,比如20240518,会被当成整数。
解决:清洗函数里先切分再指定格式。
def parse_date(raw): s = str(raw).split(" ")[0].replace("/", "-") return pd.to_datetime(s, format="%Y-%m-%d", errors="coerce")对"20240518"这种纯数字,要先转成标准格式:
def parse_date(raw): s = str(raw).strip() if s.isdigit() and len(s) == 8: s = f"{s[:4]}-{s[4:6]}-{s[6:]}" return pd.to_datetime(s, errors="coerce")还有一点,如果数据里有"待定"、"时间另行通知",to_datetime 会识别的很难看,建议清洗前先过滤:
df = df[~df["日期"].str.contains("待定|另行通知", na=False)]排查时不要只看缺失值数量,把 isna() 为 True 的记录打印出来,看原始字符串长什么样,这样才能对症下药。
5.3 merge 之后行数翻倍或出现重复记录
现象:两个 DataFrame merge 后,行数从 600 变成 1200 或更多,分析结果明显不对。
原因:关联字段在右表中不唯一。比如场馆表里"国家体育场"出现了两行,可能是不同年份容量不同,merge 是笛卡尔积展开,每条左表记录会匹配到多条右表记录。
解决:先检查关联字段是否唯一:
print(venue_info["场馆"].duplicated().sum()) venue_info_dedup = venue_info.drop_duplicates(subset="场馆", keep="first")如果确实需要多行信息,比如不同年份的容量,那就把年份也加进 merge 条件:
df_merged = df.merge(venue_info, on=["场馆", "年份"], how="left")merge 完再核对行数:
assert len(df_merged) == len(df), "合并后行数不一致,检查关联字段的唯一性"这个 assert 放在 merge 之后,一旦出错立刻暴露,是防止数据翻倍的好习惯。大作业里数据量小,行数翻倍可能看不出来,但算上座率时分子分母对不上,图表数值就会很奇怪。
5.4 Pyecharts 地图白屏或地区不显示颜色
现象:pyecharts 的 Map 页面打开后,地图轮廓正常,但某些区域没有颜色,或者提示"数据不合法"。
原因:城市名称和地图包内置名称不匹配。maptype="china" 用的是中国省级行政区划名,比如"北京市"、"广东省",而你的数据里是"北京"、"广东"。
解决:统一做名称映射。
city_name_map = { "北京": "北京市", "上海": "上海市", "天津": "天津市", "重庆": "重庆市", "广东": "广东省", "四川": "四川省", "浙江": "浙江省" } mapped_data = [[city_name_map.get(city, city), value] for city, value in city_data]如果地图上某个省份一直显示灰色,先检查该省份名是否在映射字典里。还有一个容易忽略的问题:pyecharts 需要联网加载地图文件,如果答辩教室断网,地图会白屏。稳妥的做法是提前把 html 在本地打开验证一次,或者把地图也截图保存一份 PNG 放在报告里备用。担心版本问题的话,map 的 add 方法里 maptype 参数不要写错,验证可用的组合再全量跑。
5.5 源码里写死绝对路径,换电脑跑就崩
现象:在自己电脑上运行正常,拷贝到实验室或答辩机器上,报错 FileNotFoundError,读取不到数据文件。
原因:代码里写了 C:\Users\你的名字\Desktop\演唱会.csv 这种绝对路径。换电脑后目录结构不同,路径当然失效。
解决:所有文件操作都基于脚本所在目录。
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent DATA_DIR = BASE_DIR / "data" OUTPUT_DIR = BASE_DIR / "output" df = pd.read_csv(DATA_DIR / "concerts.csv")用 Path(file).resolve().parent 拿到代码文件所在目录,再拼数据文件名,这样整个项目打包成一个文件夹后,无论在哪个机器上运行都能找到资源。还有一个细节:读取中文 CSV 时要指定 encoding="utf-8-sig",否则用 pandas 读出来中文乱码。答辩前最后检查一遍,把项目文件夹干净打包,删掉 pycache 和临时文件,老师体验会好很多。
6. 把源码变成过答辩的作品:验证方法、报告结构与三个加分技巧
6.1 结果验证:怎么确认你没有算错
提交前最怕数据分析结果有硬伤。我习惯做三步验证:第一,随机抽样核对。
sample = df.sample(10, random_state=7) print(sample[["日期DT", "城市", "票价均值"]])抽出来的记录和原始表格逐条对照,日期格式、城市名、票价区间确认无误。第二,计算口径复核。用 groupby 算出的总场次数,自己用 value_counts 再算一遍,两数对得上才放心。第三,做边界测试。把某个极端值去掉,比如删掉最高票价那条,看均值和图表变化是否符合预期,如果变化异常剧烈,说明数据里有离群点需要说明。
6.2 报告结构:让老师 10 分钟看懂你在做什么
答辩报告建议按这个结构走,每一部分都有明确落点:
| 报告章节 | 重点内容 | 时长 |
|---|---|---|
| 项目背景与目标 | 说明数据来源、分析维度 | 1 分钟 |
| 数据清洗过程 | 展示原始数据到 DataFrame 的处理逻辑 | 2 分钟 |
| 分析结果 | 时间趋势、城市分布、票价结构 | 4 分钟 |
| 可视化展示 | 静态图与交互大屏 | 2 分钟 |
| 结论与反思 | 三个核心结论,说清统计口径 | 1 分钟 |
核心结论不要超过三个,多了记不住。比如"演唱会集中在头部城市""暑期和年末是办演出的旺季""国际艺人票价天花板明显更高",每个结论配一张图,逻辑闭环。
6.3 三个加分技巧
第一个技巧:在报告中放一张"数据清洗前后对比表",左边是原始脏数据,右边是清洗后的标准格式,这让老师一眼看到你的处理能力。第二个技巧:用 pyecharts 的 Timeline 做一个按年份切换的柱状图,交互式展示场次变化,比静态多图更有记忆点。第三个技巧:把源码结构整理清楚,src 目录放脚本,data 放数据,output 放生成图表,README 写运行步骤,做到"双击一个命令就能跑完全部流程"。
我自己做类似课题时,最深刻的一条教训是永远不要把生成图表的代码和数据分析混在一长段脚本里,越到后面越难改。把清洗、分析、绘图拆成三个函数,出问题能快速定位。这次整理的内容就是一个可以直接参照的模板,你照着搭出项目骨架,把数据换一换,图表参数调一调,就能得到一份完整的 Python 大作业作品。希望帮到你。
本文还有配套的精品资源,点击获取