☰
Python数据分析实战:CSV/JSON清洗与动态柱状图制作
2026/10/22 2:59:09 网站建设 项目流程

前些天有人拿这个"综合案例实现1"来问我,需求看着非常简单:把csv、json数据转换成动态柱状图,横坐标是日期,纵坐标是每日销售总额。这类练习基本已经成为Python数据分析入门的标配作业了,我一开始也觉得无非是pandas读文件、pyecharts画个图,三四行代码就能搞定。真正上手才发现,需求里的每个词都藏着东西:"csv和json两种数据格式要怎么合并""动态到底指Timeline翻页还是柱子生长""日期字段格式不统一怎么处理",任何一个环节没想清楚,最后要么图出不来,要么数据对不上。

这篇文章我就按自己跑通整个案例的顺序来写,从需求拆解、数据清洗、动态图实现,到调试过程中踩过的坑,全部摊开讲。文章面向正在完成Python数据分析作业、或者想自己做一个"按日期播报销售额动态图"的读者。你可以直接照着代码套,更重要的是理解每一步为什么这么设计,这样下次换成商品销售Top榜、各区域销量对比,你也知道怎么改。

1. 先把"动态柱状图"这个词拆清楚,否则后面全白做

1.1 两种完全不同的"动态"理解,决定了两套代码方案

"动态柱状图"在中文教程里其实有歧义。一种是时间轴轮播式:每个日期对应一个独立画面,播放时像翻页一样,一天一天切换,每天的画面里通常只有当天一根柱子;另一种是同一坐标系内增长式:横坐标始终是全部日期,动画播放时柱子从0开始一根根长高,后面出现的日期把前面的柱子顶到左边去。

需求里写了"横坐标是日期,纵坐标为每日销售总额",严格按字面理解,两种都说得通。但教学案例默认的"动态"绝大多数是第一种,也就是pyecharts的Timeline组件搭配Bar来实现,交互性最好,还能在浏览器里手动拖动时间轴。第二种我用matplotlib.animation也实现过,适合导出GIF放进PPT里,后面第4节单独讲。建议你先确认手头的需求希望的是哪种效果,再做决定——我见过有人辛辛苦苦做成了增长式,结果验收标准是轮播式,白白返工。

1.2 数据结构的"长相"比数据格式本身更重要

很多人一看到"csv、json"就紧张,其实CSV和JSON只是两种装数据的容器,真正决定代码怎么写的,是里面的字段和粒度。我处理这个案例时,先建了一个模拟场景:某零售店铺1月1日到1月7日的销售记录。

CSV文件是订单明细,一行一笔交易,长这样:

日期,商品名称,销售额 2024-01-01,笔记本,3200.00 2024-01-01,中性笔,1500.00 2024-01-02,文件夹,800.00

JSON文件则是按天汇总好的数据,是一个对象数组:

[ {"date": "2024/01/02", "total_sales": 800.00}, {"date": "2024/01/03", "total_sales": 2600.00} ]

看出问题了吧?两个文件字段名不一样:一个叫"销售额",一个叫"total_sales";日期格式也不一样:一个是2024-01-01,一个是2024/01/02;最要命的是粒度不同:CSV是明细,JSON已经是汇总值。如果直接两个文件拼起来算当日总额,某些日期会重复计数,某些日期又缺数据。

所以拿到任何csv、json练习数据,第一件事不是开编辑器,而是先回答三个问题:两个文件的字段分别是什么?键名是否一致?数据粒度是明细还是汇总?这三个问题想清楚,"字段对齐"就成功了一半。

2. 数据读入与清洗:把CSV和JSON统一成一张"日期-金额"二维表

2.1 读CSV:先破编码关,再破列名关

读CSV没什么技巧,但有两件事很烦:

第一是编码。Windows下导出的CSV经常是gbk编码,直接用pd.read_csv('sales.csv')会报错或者中文乱码。我习惯先试utf-8,乱码就换gbk。想省事可以写个小函数自动尝试编码,但案例阶段没必要,最直接的做法是:

import pandas as pd # 先试 utf-8,乱码改 gbk df_csv = pd.read_csv('sales.csv', encoding='utf-8') print(df_csv.head()) # 打印前5行,确认列名和内容 print(df_csv.columns.tolist()) # 看列名列表,检查有没有隐藏空格

第二是列名里的空格。有些文件是从Excel导出的,列名可能带着看不见的前后空格,比如" 日期 "。这会导致你用df_csv['日期']取列时报KeyError。处理方式是一行代码清掉:

df_csv.columns = df_csv.columns.str.strip()

顺带检查一下金额列的类型。如果销售额是"12,345.6"这种带千分位的字符串,读进来会变成object类型,后面求和会出问题。需要先去掉逗号再转float:

df_csv['销售额'] = df_csv['销售额'].astype(str).str.replace(',', '').astype(float)

2.2 读JSON:常见的简单结构和嵌套结构都要会处理

JSON读起来比CSV更随意,因为它可以嵌套。最简单的就是上面那种对象数组,pd.read_json('sales.json')直接就能读成DataFrame。但很多练习数据会把数据包在一个顶层键里,比如:

{ "data": [ {"date": "2024-01-03", "total_sales": 2600.0} ] }

这时候直接读出来的DataFrame里只有一行data列,你需要把里面的数组展开。步骤是先用json模块读进来,再看结构决定怎么拍平:

import json with open('sales.json', encoding='utf-8') as f: raw = json.load(f) # 如果 raw 是 {"data": [...]} 这样的嵌套结构 records = raw['data'] if isinstance(raw, dict) else raw df_json = pd.json_normalize(records) print(df_json.head())

pd.json_normalize能把嵌套字典里的字段自动拍平成多列,比手动循环取字典要稳得多。读完以后,把JSON里的字段名统一成和CSV一致的:

df_json = df_json.rename(columns={ 'date': '日期', 'total_sales': '销售额' })

2.3 日期归一化:各种奇奇怪怪的格式先转成"日期类型"

两个文件的日期格式可能完全不同,2024-01-01、2024/01/02、20240103、2024年1月4日都有可能。统一的办法是用pd.to_datetime,它能自动识别绝大多数常见格式:

df_csv['日期'] = pd.to_datetime(df_csv['日期']) df_json['日期'] = pd.to_datetime(df_json['日期'])

如果某个日期格式太怪,转换后会得到NaT(Not a Time),用下面这段检查一下到底哪些行出了问题:

print(df_csv[df_csv['日期'].isna()]) # 找到异常行后,单独修正格式,再转一次

还有一个细节:pd.to_datetime转出来的时间戳带时分秒,比如2024-01-01 00:00:00。如果原始数据里有时间戳比如2024-01-01 14:30:00,直接按"日期"分组会把同一天分到两个桶里。所以归一化之后最好截断到日期:

df_csv['日期'] = df_csv['日期'].dt.normalize() # 或者 .dt.date df_json['日期'] = df_json['日期'].dt.normalize()

到这里,两个文件已经可以在列结构上合并了。

2.4 合并并对每日销售总额做聚合,注意单位必须一致

把两个表纵向拼接:

df = pd.concat([df_csv, df_json], ignore_index=True) daily = df.groupby('日期')['销售额'].sum().sort_index() print(daily)

看起来很简单,但这里藏着这个案例最大的陷阱:单位。我遇到过JSON里按"万元"汇总、CSV里按"元"记录的情况,直接合并后某个日期的总额会错得天翻地覆。所以合并前一定确认两边金额单位是否一致,不一致先换算:

# 假设 JSON 里是万元,换算成元 df_json['销售额'] = df_json['销售额'] * 10000

然后记得把daily转成DataFrame,后面画图更方便:

daily_df = daily.reset_index() daily_df.rename(columns={'日期': 'date', '销售额': 'daily_sales'}, inplace=True)

2.5 动手画图之前,先做三层数据校验

我见过很多人清洗完数据就开始画图,图画完才发现数据是错的。不管数据简单还是复杂,我都建议先花两分钟做三个校验:

第一,行数校验。合并后的总行数应该等于两个文件原始行数之和(除非你本来就打算去重)。如果明显变小,说明读取时丢了行。

第二,总额校验。daily.sum()应该等于CSV销售额总和加上JSON销售额总和。如果对不上,多半是重复或者单位错了。这一步在图表上直接体现为"某天销售额突然翻倍",非常典型。

第三,日期范围校验。daily.index.min()和daily.index.max()应该和业务预期一致。有时候CSV里有1月8日的数据但JSON只到1月7日,范围不对后面动态图播放就会莫名其妙多跳一天。

这三层校验我每次都会做,不是因为不信任代码,而是因为csv、json这种练习数据往往来源不同,隐含脏数据概率非常高。

3. 用pyecharts Timeline实现"一天一页"的轮播动态柱状图

3.1 为什么选pyecharts而不是纯matplotlib

做动态柱状图,pyecharts的Timeline是最省心的方案。它渲染成HTML,浏览器打开后自带播放/暂停/拖动按钮,观众可以自己控制节奏;而且pyecharts的图对中文支持好,不需要像matplotlib那样折腾中文字体。之前我用纯matplotlib做动画,光解决中文乱码和GIF导出就折腾了很久,效果还不一定有HTML交互版直观。

pyecharts需要先安装:

pip install pyecharts

版本目前都是2.x,接口稳定。如果你是在Jupyter Notebook里用,可以先from pyecharts.globals import CurrentConfig, NotebookType之类的配置,但案例阶段我建议直接render()生成HTML文件,浏览器打开看效果,和平台无关,也好分享。

3.2 Timeline组装的核心逻辑:先画好"单日Bar",再挂到时间轴

Timeline的用法说穿了很简单:先把每个日期画成一个独立的Bar,然后用Timeline.add(bar, time_point=日期)按时间顺序挂上去。初学者最容易犯的错是试图一上来就画一整张图再让它动起来,其实不是,Timeline是"多张图的纵向堆叠",播放时把每一张按顺序放给你看。

最贴合"横坐标是日期、纵坐标是当日销售总额"的做法,是每帧只放当天一根柱子:

from pyecharts.charts import Bar, Timeline from pyecharts import options as opts timeline = Timeline() for day, value in daily.items(): bar = Bar() bar.add_xaxis([str(day)]) # 横坐标:当前日期 bar.add_yaxis( "每日销售总额", [round(float(value), 2)], label_opts=opts.LabelOpts(position="top", formatter="{c}") ) bar.set_global_opts( title_opts=opts.TitleOpts(title=f"{day} 销售总额"), yaxis_opts=opts.AxisOpts(name="每日销售总额"), ) timeline.add(bar, time_point=str(day)) timeline.add_schema( play_interval=500, # 播放间隔,单位毫秒 is_auto_play=True, # 打开页面自动播放 is_loop_play=False, # 播完停在最后一天 ) timeline.render("daily_sales.html")

这段代码跑完,浏览器打开daily_sales.html,就能看到一个按日期轮播的动态柱状图。每帧的横坐标就是当天的日期,柱子高度就是当日销售额。

不过说实话,这种"每帧一根柱子"的视觉冲击力有点弱。如果想让动态图更有"增长感",我建议改成累计模式:每一帧的横坐标都包含全部日期,已经过去的日期正常显示柱子,未来的日期补0。这样播放时,左边的柱子越来越高、越来越多,非常直观地呈现"销售额随时间累积"的趋势。改动也很小:

all_dates = [str(d) for d in daily.index] for i, day in enumerate(daily.index): values = [] for j, date in enumerate(all_dates): if j <= i: values.append(round(float(daily.iloc[j]), 2)) else: values.append(0) bar = Bar() bar.add_xaxis(all_dates) bar.add_yaxis("每日销售总额", values) bar.set_global_opts( title_opts=opts.TitleOpts(title=f"截止 {day} 每日销售总额"), ) timeline.add(bar, time_point=str(day))

注意,累计模式里必须把all_dates固定下来,不能每一帧重新传一遍不同长度的日期列表,否则播放时横坐标会跳来跳去,看得人眼晕。

3.3 时间轴节奏和柱子入场动画,是"动态感"的关键

很多人第一次跑出Timeline会觉得"怎么只有翻页没有动画",其实是因为没有调动画参数。柱状图的"生长"效果由Bar的animation_opts控制:

bar.set_series_opts( animation_opts=opts.AnimationOpts( animation_duration=400, animation_easing="cubicOut" ) )

这样每次时间轴切到新一页,柱子都会从底部"蹿"上来,而不是生硬地直接变成新高度。实际演示时,这个生长效果比翻页本身更能让人觉得"这是动态柱状图"。

add_schema里还有几个参数值得调:

  • play_interval:单位是毫秒,500就是0.5秒切换一帧。如果日期有几十天,建议200~300,否则整个播放太拖沓;
  • is_auto_play=False:关闭自动播放,让用户自己点播放按钮,适合嵌入网页时用;
  • is_timeline_show=False:隐藏时间轴控件,只看柱子变化,视觉更干净。

3.4 渲染得到的是HTML文件,不是图片,别用错地方

render()生成的是HTML文件,双击用浏览器打开,可以交互缩放、悬浮看数值。这既是优点也是限制:如果你想把成品放到PPT里、或者发到只支持图片的平台上,HTML反而不好用。我通常在项目汇报里直接用浏览器全屏演示,效果最好;要出图的话,打开HTML然后截图,或者干脆用下一节的matplotlib方案做GIF。

日期特别多的时候,Timeline播放几十上百帧体验很差。我的经验是数据量大时先把日期聚合到"周"或"月",按周/月播放,每个时间点展示该周期的每日平均或累计总额。宁可少几帧,也别让观众等着无聊。

4. 备选方案:用matplotlib逐帧动画导出GIF

4.1 什么场景下必须换成matplotlib

pyecharts的HTML交互体验再爽,也有两个硬伤:一是无法直接产出GIF/MP4,二是内网环境下装包偶尔会遇上麻烦。如果你需要把动态效果放进文档、公众号或者录好的视频里,matplotlib.animation才是更合适的选择。

matplotlib做动态图的思路和Timeline完全不同:它是在一个坐标系里不断"更新画布内容",用FuncAnimation逐帧调用更新函数。它的优点是输出文件是常见视频/GIF格式,缺点是要手动处理中文字体、坐标轴范围等细节。

4.2 FuncAnimation逐帧更新:先画好空柱子,再不断set_height

刚开始我按直觉写了每帧clear()重画的版本,结果动画疯狂闪烁,坐标轴也跟着跳。后来改成"一次性初始化图形,每帧只更新柱子高度",效果立刻稳定了。核心代码如下:

import matplotlib.pyplot as plt import matplotlib.animation as animation # 准备数据 daily_sorted = daily.sort_index() # 确保按时间顺序 dates = [str(d) for d in daily_sorted.index] values = daily_sorted.values.astype(float) n_days = len(dates) # 初始化画布和柱状图 fig, ax = plt.subplots(figsize=(10, 6)) x = range(n_days) bars = ax.bar(x, [0] * n_days, color='#5B9BD5') ax.set_xticks(x) ax.set_xticklabels(dates, rotation=45) ax.set_ylabel('每日销售总额') ax.set_title('每日销售总额动态变化') # 逐帧更新函数 def update(frame): bars[frame].set_height(values[frame]) return bars ani = animation.FuncAnimation( fig, update, frames=n_days, interval=300, repeat=False ) ani.save("daily_sales.gif", writer="pillow", dpi=100)

interval=300表示每300毫秒更新一帧,repeat=False表示播完不再循环。个人建议就算想循环,也先关掉,看清楚整体效果再改。

4.3 中文字体与负号显示,是matplotlib最常见的两道坎

matplotlib默认字体不支持中文,画出来全是方框。代码里必须显式设置:

plt.rcParams['font.sans-serif'] = ['SimHei'] # Windows # mac 上可以写 ['Arial Unicode MS'] plt.rcParams['axes.unicode_minus'] = False # 让负号正常显示

这几行必须在画图代码之前执行。另外y轴金额如果比较大,matplotlib默认显示科学计数法,观感很怪。可以用FuncFormatter把数值格式化成带千分位的字符串:

from matplotlib.ticker import FuncFormatter def fmt(x, pos): if x >= 10000: return f"{x/10000:.1f}万" return f"{x:.0f}" ax.yaxis.set_major_formatter(FuncFormatter(fmt))

4.4 导出GIF的细节:writer选择与清晰度

ani.save("daily_sales.gif", writer="pillow")里有意思的是writer参数。pillow是Python图像库,导出GIF稳定,不依赖额外软件,但压缩率一般,文件会偏大;想导出更流畅质量更高的GIF或MP4,可以用imagemagick或ffmpeg,但那属于额外的系统级依赖,配上有时会比较折腾。

dpi参数也值得注意:dpi=100导出的GIF在屏幕上够用;要放大放PPT,dpi=150更清晰,但文件会大不少。日期多的时候,x轴标签会自动重叠,记得plt.tight_layout(),再不行就把横轴标签旋转45度:

plt.setp(ax.get_xticklabels(), rotation=45, ha='right')

5. 调试过程中最容易翻车的四个细节

5.1 日期排序:看起来是日期,其实可能是字符串在作怪

动态图播放顺序错了是最尴尬的事。最常见原因是daily.index不是时间顺序,而是字符串顺序。比如原始数据里有2024-1-2和2024-01-10,字符串排序会把2024-1-2排在后面,播放顺序就乱了。解决方法是在聚合之前就做pd.to_datetime,让它变成真正的日期类型,groupby之后再sort_index()。如果你为了显示美观又把日期格式化成字符串,请确保格式是YYYY-MM-DD这种补零格式,字符串排序才不会出错。

5.2 重复日期导致某天销售额翻倍:先搞清楚两个文件的"血缘关系"

我做这个案例时踩过一个大坑:把CSV和JSON合并后,某一天销售额直接翻倍了。排查半天发现,这两个文件其实是同一批业务数据的不同导出——CSV是明细,JSON是汇总后的结果。我把明细加总和JSON直接加总,等于同一天算了两次。

这个问题的本质不是代码写错,而是没有先理解数据关系。排查方法很简单:对daily按销售额排序,看有没有异常高的日期,再用df[df['日期'] == '2024-01-02']把该日的明细打印出来,和JSON汇总值对比,很快能发现。

如果确认两个文件表达的信息有重叠,我通常会选择以明细文件为准,汇总文件只用来做交叉验证,而不是直接合并求和。

5.3 缺失日期:补0要谨慎,别把"没记录"伪装成"没销售额"

店铺周日可能不上班,或者某天数据文件里就是没这一行。Timeline按日期播放时,缺了某一天,画面会直接从周一跳到周三,视觉上像断档。如果日期是连续的、业务上也应该有销售额,可以用resample补0:

daily_full = daily.resample('D').sum().fillna(0)

但注意,resample('D').sum()会把缺失日期变成0,而有些缺失其实代表"该时间段本无数据",比如一个只工作日的门店,周末补0就会造假。所以补0之前务必想清楚业务含义,否则图表好看但结论是错的。

5.4 金额显示:动态图的信息量比静态图更大,别让观众去数0

动态柱状图播放时停留时间很短,观众没功夫仔细看y轴刻度。我建议至少做两件事:一是把数值直接标在柱子上方,pyecharts里用label_opts=opts.LabelOpts(position="top", formatter="{c}");二是金额大的时候用"万"做单位,或者图表标题直接说明"单位:元"。否则播放时观众注意力全在"这个柱子多高",根本顾不上换算数字。

另外,如果一个画面里同时塞几十根柱子,动画再流畅也白搭,因为根本看不清谁高谁低。数据点多的时候,我强烈建议先按月/周汇总再播放,或者只播放TopN日期。

结尾

做完这个案例,我最深的感受是:动态图最难的部分往往不是画图那一步,而是把csv、json两种异构数据规整到同一张"日期-金额"二维表的过程。数据读入、字段对齐、日期归一化、单位统一、去重校验,每一步都比最后的Timeline代码更能决定成品质量。这也是为什么我愿意在这篇文章里花大篇幅讲清洗,而不是直接甩一份pyecharts代码给你。

最后分享一个我自己的习惯:无论手头数据多简单,我都会在画图前打印一次聚合结果,再打印一次原始数据每列的dtype,确认没有隐藏问题。这个习惯救了我很多次。掌握了csv/json -> 清洗 -> 聚合 -> 动态轮播图这条链路之后,你会发现换个场景、换成别的字段,本质都是一样的,无非是改改列名、换换坐标轴。希望这篇能把你的"综合案例实现"真正跑通,也帮你把背后的通用能力带走。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询