ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

数据清洗到可视化:用Pandas完成销售数据分析全流程实操

数据清洗到可视化:用Pandas完成销售数据分析全流程实操 1. 先说清楚这次作业是什么老规矩先把背景交代明白。这是我参加的数据分析训练营里老师布置的第二次作业题目给得很简单拿到一份公开的销售数据自己完成一次从数据清洗到可视化分析的全流程最后输出一份能讲清楚卖得怎么样、哪里卖得好、什么品类拖后腿的简短报告。第一次作业还是单纯练 pandas 的语法题第二次就变成了一个需要独立完成的迷你项目。很多人在这一步会突然卡住不是因为代码难而是因为不知道从哪下手。这份数据集大约五千行字段包含订单编号、成交时间、商品分类、销售额、成本、客户地区、支付方式等信息。看起来字段不多但真实数据的问题一个不少有缺失值有重复记录有金额格式里混着人民币符号和千分位逗号有日期格式五花八门甚至还有销售额为负数的脏数据。这篇文章就是我当时从拿到数据到最后交作业的完整实操记录每一步都写了思路、代码和当时踩过的坑。如果你也正处于刚学完 pandas 基础语法、要做第一个完整小项目的阶段这篇内容可以直接当参考模板。重点不在于那份销售数据本身而在于处理这类半脏数据时一个人该按什么顺序想问题、该在哪些地方保护自己、又在哪些地方敢于做取舍。2. 数据清洗作业里最容易翻车的环节2.1 先别急着清洗把数据看明白我见过太多人拿到数据后第一件事就是写dropna()这其实是一种偷懒也是一种风险。数据清洗的第一步不是处理而是观察。先搞清楚每一列是什么类型、有多少非空值、取值范围是否合理再决定怎么动手。我当时是这么做的import pandas as pd df pd.read_csv(sales_data.csv, parse_dates[成交时间]) print(df.shape) print(df.info()) print(df.head()) print(df.describe())info()会告诉你每一列的非空数量哪列缺得厉害一目了然。describe()能看出销售额、成本这类数值列的大致分布比如最小值为负数、最大值离谱这种信号都会在这里暴露出来。有一个细节特别容易忽略parse_dates[成交时间]这个参数让 pandas 在读取时就把日期列转成 datetime 类型而不是等后面再统一处理。这个习惯帮你省下很多后续的格式转换问题。看完之后我记录下几个关键情况客户地区有少量缺失大约占总量的百分之二成本列缺失比较严重接近百分之十五销售额里出现了负值和一些明显异常的小数位订单编号存在完全重复的记录。拿到这个清单之后才算真正进入清洗环节。2.2 缺失值、重复值、格式错误分步处理清洗要按顺序来我习惯的优先级是先处理重复值再处理格式最后处理缺失值和异常值。因为如果你先删了缺失行再发现订单编号有重复那删重复时就拿不到完整的上下文反过来如果先把格式统一好再执行缺失值操作一些因为格式问题产生的假空值也能自动解决。首先是直接能看到的问题# 查看重复记录数量 print(df.duplicated().sum()) # 删除完全重复的记录保留第一条 df df.drop_duplicates()这一步没什么技术含量真正要注意的是判断重复的粒度。完全重复删除没问题但如果只是订单编号相同、其他字段不同那可能是多笔子订单不能一删了之。所以我在作业里只处理了整行完全重复的情况而不是单独对订单编号去重。然后是格式清洗。销售额那一列读进来时是字符串长这样¥1,299.00。这种格式如果不处理直接astype(float)肯定会报错而且很容易让新手误以为是数据本身的问题其实是自己没有先做字符串清理。df[销售额] ( df[销售额] .astype(str) .str.replace(¥, , regexFalse) .str.replace(,, , regexFalse) .astype(float) )str.replace(¥, )把人民币符号去掉str.replace(,, )把千分位逗号去掉最后再转 float。这里我特意使用regexFalse因为这两个字符本身没有正则含义避免 pandas 去解析特殊符号性能也更好。如果你要处理的是美元或者欧元符号原理完全相同。日期格式也要统一。原始数据里同一个日期列同时存在2023/1/5和2023-01-05两种写法这在实际数据里很常见。用pd.to_datetime()统一即可df[成交时间] pd.to_datetime(df[成交时间])这一步做完后你就可以通过df[成交时间].dt.month等方式访问月份、星期等信息为后面的月度趋势分析打基础。分类字段也得清理常见脏数据。比如商品分类里有的叫 数码产品 带前后空格有的叫数码产品还有的叫数码电器。这些不统一会让后面的groupby统计结果裂开明明是同一类产品却被分成两个类目导致图表出现几乎一样但分属两类的柱子。处理方式也很简单先去掉首尾空格再做一次人工映射df[商品分类] df[商品分类].str.strip() category_map { 数码电器: 数码产品, 数码: 数码产品, 家用电器: 家电, 家电产品: 家电, } df[商品分类] df[商品分类].replace(category_map)这种字段值归并的活没有标准答案完全取决于你对业务的理解。但是清洗环节里这类问题必须处理否则后面分析出来的结论会被一些杂音带偏。建议先把唯一值列出来检查一遍不管字段多长都值得扫一眼。缺失值处理是重头戏。先看缺失数量print(df.isna().sum())订单编号和客户地区的缺失占比很小我直接删掉了对应的行。判断依据是缺失比例低于百分之三而且这些字段没有业务上的填充逻辑强行用众数或前后值填充反而制造虚假信息。成本列的缺失不能用删除处理因为它占比百分之十五删掉会损失太多样本。我的补全思路是用同类商品毛利率的中位数反推出缺失的成本。先算出已有记录的单笔毛利率再按分类汇总得到各类目毛利率中位数最后对缺失成本的行执行成本 销售额 * (1 - 同类毛利率中位数)。# 先算已有成本记录的毛利率 valid df.dropna(subset[成本]) df.loc[valid.index, 毛利率] 1 - valid[成本] / valid[销售额] # 按分类取中位数 ratio_by_cat df.groupby(商品分类)[毛利率].median() # 合并回原表填充缺失成本 df df.merge(ratio_by_cat.rename(ratio_median), on商品分类, howleft) mask df[成本].isna() df.loc[mask, 成本] df.loc[mask, 销售额] * (1 - df.loc[mask, ratio_median]) df.drop(columns[ratio_median], inplaceTrue)这里有一个很容易被忽略的坑因为成本缺失的那批记录本身计算不出毛利率所以计算分类毛利率中位数时必须先把缺失成本的记录剔除出去否则中位数会是 NaN反推成本时全部落空。我在第一次跑的时候没注意结果填充后的成本列依然全是空回头检查才发现问题出在groupby时把缺失值也带了进去。2.3 异常值要讲道理不能乱删异常值处理是整个清洗环节里最考验判断力的部分也是作业展示成熟度的地方。我看到销售额有负数时第一反应不是直接删除而是先看一下这些负数的出现场景订单状态、时间分布、数值大小。查看后发现负销售额集中在某个特定时间段更像是测试数据或者退款记录混进来了。这种条件下删除是合理的。但如果只是因为想当然觉得销售额不该为负就删除遇到真实业务里的退款场景反而会误伤数据。所以我在代码里加了一个简单的业务假设只删销售额小于零的记录并保留注释说明原因。成本大于销售额这种记录也要处理因为毛利率为负虽然可能存在但负到离谱的就值得怀疑。我设置了一个阈值毛利率低于负百分之五十的记录视为异常不再参与后续分析。这里没有用复杂的统计方法只是给了一个业务上说得通的边界确保结论不会被极端值影响。df df[df[销售额] 0] df[毛利率] 1 - df[成本] / df[销售额] df df[df[毛利率] -0.5]做完这一步清洗阶段就算收尾了。我重新跑了一次df.info()和df.describe()确认没有新的空值、数值范围合理、字段类型全部正确才开始做分析。整个过程看起来不难但实际执行时每一步都可能出小岔子尤其是缺失值补全和格式转换的先后顺序多踩两次坑自然就有感觉了。3. 探索性分析让数据自己开口说话3.1 从三个问题出发做分析清洗完成后面对一张五千行的表直接开始画图是没有章法的。我先问了自己三个业务问题整体销售趋势在全年是如何变化的哪些地区贡献了主要收入不同品类的销售额和毛利结构有什么差异这三个问题分别对应时间维度、地域维度、品类维度基本覆盖了这家店卖得怎么样的核心判断。分析代码其实很简单重点是你要有明确的问题意识否则就会陷入为了画图而画图。比如画二十张图表每张都好看但串不出一条完整的业务结论作业效果反而打折扣。按问题拆解后我做了三个关键统计# 月度销售额和订单量趋势 monthly df.resample(M, on成交时间).agg( 销售额(销售额, sum), 订单量(订单编号, count) ) # 地区销售额 Top10 region_top10 df.groupby(客户地区)[销售额].sum().nlargest(10) # 品类销售额与毛利率 category_stats df.groupby(商品分类).agg( 销售额(销售额, sum), 订单量(订单编号, count), 平均毛利率(毛利率, mean) ).sort_values(销售额, ascendingFalse)resample(M, on成交时间)是月度聚合的便捷写法它会自动按月份重新采样。这个函数对新手来说比较陌生但比to_period(M).groupby()更直观推荐直接使用。注意聚合时要明确每个字段对应的计算方式别让 pandas 自动推断否则遇到混合类型会得到意料之外的字段。3.2 用表格和分组统计验证想法分析结果出来后我会习惯性地先看表格再画图。因为图表适合做展示但表格更适合做判断。比如地区销售 Top10在表格里你能看到第一名和第十名的具体差额、每个地区的占比这些信息在柱状图里虽然能看出来但不如表格精确。当时我发现一个有意思的现象华东地区销售额最高但毛利率排名靠后。这说明该地区销量大但折扣力度也大或者说该地区的热门品类本身就是低毛利产品。这个结论不能只靠销售额排行看出来必须把销售额和毛利率放在一起对照。品类透视表也很有价值pivot pd.pivot_table( df, index商品分类, values[销售额, 成本, 毛利率], aggfunc{ 销售额: sum, 成本: sum, 毛利率: mean } ) pivot[毛利额] pivot[销售额] - pivot[成本] pivot pivot.sort_values(毛利额, ascendingFalse)为什么用pivot_table而不是groupby因为这里需要同时用三种不同的聚合函数pivot_table通过传字典的方式表达更清晰不容易在agg参数上写错。实际跑下来手机配件类销售额排第一但毛利额排第三因为平均毛利率不到两成相反生活电器类销售额中等但毛利额更高。这种销售额高不等于赚得多的结论就是分析报告里最有信息量的部分。探索性分析的目的不是得出一个完美结论而是形成一条逻辑链数据经过清洗后按业务问题拆解用分组统计找出值得深挖的方向再用可视化展示结果。我当时在作业里写出了这样的句子华东地区贡献了约百分之三十的销售额但该地区主打的商品品类集中在手机配件拉低了整体毛利率。这一句话背后得有数据支撑这就是分析和拍脑袋的区别。4. 可视化让图表替你汇报4.1 中文乱码与图表美化的几个坑分析做到这一步图表展示是重头戏。但我必须先把最容易劝退新手的中文乱码问题说清楚因为这一步不解决后面所有图都白画。直接使用默认配置图里的中文会变成一个个方框这在 matplotlib 里是老问题。解决办法是设置中文字体我通常这样写import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei] plt.rcParams[axes.unicode_minus] False第一行指定使用微软雅黑或黑体第二行解决负号显示为方框的问题。注意axes.unicode_minus这个配置也很重要当坐标轴出现负号时如果没有关闭 Unicode 减号会出现一个小方块很多新手在这卡半天。还有另一个坑如果你在 Jupyter Notebook 里画图要先执行%matplotlib inline否则图不会显示。这个细节太基础了但真的有人忽略。如果图表只显示一行带Figure字样的对象而没有图形十有八九就是漏了它。关于图表的美化我的经验是不要为了花哨而堆颜色一张图的字体大小、图例位置、坐标轴标签这些基本功要优先保证。作业图的尺寸可以统一为figsize(10, 6)保存时用bbox_inchestight防止标签被截断。4.2 四张核心图表的实现代码我最终提交了四张图月度销售额趋势折线图、地区销售额 Top10 柱状图、品类销售额占比饼图、销售额分布箱线图。每张图对应一个分析问题不重复不堆砌。折线图用来看趋势fig, ax plt.subplots(figsize(10, 6)) ax.plot(monthly.index, monthly[销售额], markero, linewidth2) ax.set_title(月度销售额趋势) ax.set_xlabel(月份) ax.set_ylabel(销售额元) ax.grid(alpha0.3) fig.autofmt_xdate() plt.tight_layout() plt.savefig(../output/trend.png, dpi200, bbox_inchestight)fig.autofmt_xdate()会自动旋转日期标签避免月份挤在一起。markero给每个数据点加上圆点标记这样实际的月度数值点一眼就能看出来而不是只有一条光秃秃的线。柱状图看地区差异fig, ax plt.subplots(figsize(10, 6)) bars ax.bar(region_top10.index, region_top10.values, color#4C72B0) ax.bar_label(bars, fmt%.0f) ax.set_title(地区销售额 Top10) ax.tick_params(axisx, rotation45) plt.tight_layout() plt.savefig(../output/region_top10.png, dpi200, bbox_inchestight)ax.bar_label()是 matplotlib 3.4 之后新增的参数可以在柱子上直接标注数值省去了手动写循环加标签的麻烦。如果你用的版本比较老就得用plt.text()循环处理这一点要注意。饼图看结构占比category_sum category_stats[销售额].head(6) others category_stats[销售额].iloc[6:].sum() merged pd.concat([category_sum, pd.Series({其他: others})]) fig, ax plt.subplots(figsize(7, 7)) ax.pie(merged, labelsmerged.index, autopct%.1f%%, startangle90) ax.set_title(品类销售额占比) plt.tight_layout() plt.savefig(../output/category_pie.png, dpi200, bbox_inchestight)这里做了一个常规的合并处理品类太多时只突出前六名其余统一归为其他避免饼图密密麻麻看不清。startangle90让第一块从正上方开始视觉上舒服一点。箱线图看销售额分布fig, ax plt.subplots(figsize(10, 6)) top_categories category_stats.head(5).index plot_data [df.loc[df[商品分类] c, 销售额] for c in top_categories] ax.boxplot(plot_data, labelstop_categories, showfliersTrue) ax.set_title(Top5 品类销售额分布) ax.set_ylabel(销售额元) ax.tick_params(axisx, rotation30) plt.tight_layout() plt.savefig(../output/category_box.png, dpi200, bbox_inchestight)箱线图是我比较喜欢加进去的一张图因为它能揭示平均值掩盖的信息。比如手机配件销售额总量高但中位数可能很低说明少数大单拉高了总额大部分订单仍是小金额。这种观察用柱状图很难体现。四张图全部保存到output目录后我还做了一件事把图片的文件名改得更直白一些比如sales_trend_by_month.png而不是figure1.png。这样报告里引用图片时文件名本身就是表意的一部分后面翻盘对照也方便。5. 从交作业到像样的小项目5.1 目录结构与脚本分层很多人的第二次作业就是一个 Jupyter Notebook 从头跑到尾我当时也差点这么交。后来想一想既然以后要持续写代码不如直接从这次作业开始按一个最小项目的标准来组织文件。我当时的目录结构长这样second_assignment/ ├── data/ │ └── sales_data.csv ├── scripts/ │ ├── 01_load_data.py │ ├── 02_clean_data.py │ ├── 03_analysis.py │ └── 04_visualization.py ├── output/ │ ├── sales_trend_by_month.png │ ├── region_top10.png │ ├── category_pie.png │ └── category_box.png └── requirements.txt把不同环节拆成独立脚本不是为了让代码变多而是让每一步都有明确的边界。01_load_data.py只负责读取原始数据02_clean_data.py只负责输出清洗后的数据表03_analysis.py负责计算04_visualization.py负责出图。这样如果可视化出问题你只需要改第四个文件而不用在一千行的 Notebook 里找是哪一段代码出了问题。数据文件放在data/目录图片输出到output/目录脚本不直接访问绝对路径而是使用相对路径。如果你把整个目录拷贝到另一台电脑依然能跑起来。这种目录组织的习惯比多学十个函数都值钱。5.2 函数封装和留痕习惯脚本拆分之后我顺手把每个环节的核心逻辑包装成了函数。目的是让代码具备复用性如果下一次老师换了一份数据只需要修改主函数里的文件路径就能复用整套清洗和分析流程。简单示例# 02_clean_data.py def load_and_clean(path): df pd.read_csv(path, parse_dates[成交时间]) # 清洗逻辑... df.to_parquet(../data/sales_clean.parquet, indexFalse) return df这里用to_parquet保存清洗后的数据而不是to_csv。Parquet 格式有几个明显优势压缩率高、保留数据类型、读写速度快。如果你不熟悉 parquet继续用to_csv也可以但要记得把清洗后的列类型保持一致否则下次读取又得重新处理一遍。这类数据留痕的习惯看着不起眼但对复盘和排查非常有帮助。另一个我踩过的坑是直接覆盖原始数据。一开始我在脚本里执行df.to_csv(sales_data.csv)把清洗结果直接覆盖了原文件。后来想重跑一次清洗发现原始数据已经没了只好重新下载。从那以后我再也不会让任何处理步骤修改原始文件。更稳妥的做法是清洗前先执行df_clean df.copy()后续所有操作都在这个副本上进行。requirements.txt也该在这次作业里建立起来。只需要一行命令生成pip freeze requirements.txt这样别人拿到你的项目后执行pip install -r requirements.txt就能复现环境。交作业评阅时也会方便很多。6. 现场实录踩过的坑和排查方法6.1 典型问题速查表这部分我把当时实际遇到的几个典型问题整理成了一张表方便你遇到相似报错时快速对照。每一个问题都是我真实踩过的不是从文档里抄来的。现象原因处理办法matplotlib 图中中文变方框未配置中文字体plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei]坐标轴负号显示成小方块未关闭 Unicode 减号plt.rcParams[axes.unicode_minus] Falsepd.to_datetime报错无法解析原数据包含混合格式或脏字符先查看df[成交时间].unique()定位异常格式必要时加errorscoerce再做删除月份聚合后日期列变成索引画图时横轴错位没有意识到resample后索引是 DatetimeIndex画图直接用索引或先reset_index()分组聚合结果始终不对groupby的 key 列里有前后空格先执行.str.strip()再分组一张图显示两行图例重叠图例位置没设置合理加plt.legend(locbest)或手动指定locupper left读取 CSV 后中文变成乱码文件编码不是 UTF-8文件是 GBK 编码时用encodinggbk保存时建议用utf-8-sigpd.to_datetime报错是我那次作业里印象最深的问题因为数据里除了2023/1/5和2023-01-05还有几条记录类似2023.1.5。errorscoerce会把解析失败的值变成 NaT但如果你不检查就继续往下做后面聚合时这些日期全部消失最终结果会悄悄少一段数据。所以我的建议是转换日期后立刻执行print(df[成交时间].isna().sum())确认没有新的空值。6.2 那次让我印象最深的报错说一个让我改了半个小时的错误。在计算毛利率时我本来是这样写的df[毛利率] 1 - df[成本] / df[销售额]结果发现新生成的列全是 NaN而且没有任何报错。当时的第一反应是数据有问题后来仔细检查才发现问题出在成本列已经被我填充过但填充后的数据里部分行同时存在未填充的脏值。听起来简单实际排查时却花了很多时间因为没有报错往往比有报错更可怕。后来我养成了一个习惯在做任何涉及将列相除、相乘的操作前先检查参与运算的列是否全为数值类型、是否还有空值。用一行代码就能查清楚print(df[[成本, 销售额]].dtypes) print(df[[成本, 销售额]].isna().sum())这种提前检查看起来多了一步实际上能省下一大段排查时间。第二个教训是当计算结果的 NaN 数量异常时不要急着看业务逻辑先检查数据类型和空值这两个原因占了大概率。还遇到过一次让我哭笑不得的问题因为把清洗后的结果保存成了 CSV再读取时日期列又变回了字符串后续按月份聚合全乱套。后来我学乖了中间结果用 parquet 保存或者干脆在同一个进程里完成清洗和分析避免序列化时类型信息丢失。7. 复盘第二次作业真正教会我的三件事如果让我总结这次作业最大的收获不是学会了groupby、resample或者matplotlib而是三个做数据项目时容易忽略的习惯。第一原始数据永远不动。不管清洗逻辑多完善、代码写得多自信都不要在原文件上直接覆盖。最安全的办法是清洗前创建副本或者把清洗结果输出到独立路径。这个习惯在真实工作中价值更大因为一份数据往往要服务多个分析方向谁也不想为了一个临时需求毁掉唯一的原始数据源。第二每个结论都要能用数据指认。当时我在报告里写华东地区销售额最高但毛利率偏低评阅老师追问了一句这个结论是哪张表、哪一行数据支持的这个提问让我意识到好的分析不是观点鲜明而是每一个观点背后都有一串可以追溯的数字和图表。数据分析里的沟通本质上是在教人拿证据说话。第三代码要能一键重跑。把清洗、分析、可视化拆成模块化脚本后我每次修改完只需要执行一次python 02_clean_data.py就能确认清洗结果没有变化不会因为手动执行了部分单元格就漏掉某个依赖。这种重现性平时不起眼但当你需要反复调整图表配色、或者换一份新数据时它的价值一下就体现出来了。第二次作业给我的真实体验是它能清楚地划分出会写 pandas和能用数据解决问题这两件事之间的差距。前者只需要记住函数和参数后者考验的是面对一团乱麻时你能不能按一个稳妥的顺序理出头绪。这份数据清洗脚本和分析思路后来很多次都用上了我不觉得这是一个一次性作业更像是一个后续所有数据项目的起手式。
返回列表