ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python美妆销售数据分析实战:从清洗到可视化

Python美妆销售数据分析实战:从清洗到可视化

简介:这份Python示例源码面向数据分析初学者与高校学生,以天猫双十一美妆销售数据为对象,演示从数据清洗、转换到可视化呈现的完整分析流程,适合用作课程大作业或数据分析入门练手项目。压缩包共17个文件,约5.08MB,包含py主程序、ipynb与html分析笔记、csv与xlsx原始及清洗后数据、png图表、xml配置与url快捷方式等,覆盖代码、数据与文档多种类型,便于直接运行与对照学习。目前已有149人学习下载。源码约400行,重点运用pandas完成分组、排序、筛选与聚合,并借助matplotlib绘制直方图、折线图、散点图等,展示品牌、类别、价格区间等维度的销售变化,同时涉及数据预处理与自动化分析思路,可帮助读者掌握电商数据清洗、可视化与结果解读的完整方法。

1. 一份约400行的美妆销售数据分析源码,到底能跑出什么结论

每年大促结束,后台都会堆出一份几十万行的订单明细。字段看着齐全:下单时间、商品名、品类、单价、销量、省份、渠道。可真要回答“哪个品类贡献了最多销售额”“哪个价格带卖得最好”“复购集中在哪几天”,很多人第一反应还是把表丢进表格软件,拉几个透视表,然后对着屏幕发呆。这份约400行的 Python 示例源码,解决的正是这个尴尬:它用 matplotlib 加基础数据分析,把一份电商美妆销售明细,从清洗一路做到可视化结论。它适合两类人——正在做大作业、需要一份能跑通、能改、能讲清楚逻辑的学生;以及刚接手电商数据、想用最短路径搭出一套分析脚本的初级数据从业者。核心不是代码多高级,而是把“数据进、结论出”这条链路走完整。

2. 先想清楚分析目标:美妆销售数据要回答哪几个问题

2.1 从业务问题倒推字段需求

拿到一份销售明细,不要急着写read_csv。先问自己:这份数据要回答什么。美妆电商大促场景下,最常被追问的无非四类问题。第一类是品类结构,哪个品类卖得好,是护肤、彩妆还是香水。第二类是价格带分布,销量集中在哪个价位,是百元以下还是三百以上。第三类是时间节奏,大促当天和预热期的销量曲线长什么样。第四类是地域差异,哪些省份贡献了主要销售额。

这四类问题决定了你至少需要这些字段:订单编号、下单时间、商品名称、品类、单价、销量、省份、渠道。如果原始表里没有品类,就得从商品名称里做关键词映射,比如含“精华”“面霜”的归到护肤,含“口红”“粉底”的归到彩妆。这一步是后面所有分析的根基,字段缺了,图再漂亮也是空中楼阁。

我一般会先写一个字段盘点的小脚本,把列名、缺失率、唯一值数量打印出来,确认哪些字段能用、哪些要补。这个习惯能省掉后面大量返工。

2.2 数据清洗的三个必做动作

清洗不是可选项。美妆销售数据常见的脏法有三种:单价带货币符号、销量是字符串、时间格式不统一。下面这段代码是我常用的清洗骨架,约400行的源码里也是类似结构。

import pandas as pd import numpy as np # 读取原始销售明细,注意编码,电商导出常见 utf-8-sig 或 gbk df = pd.read_csv("sales_raw.csv", encoding="utf-8-sig") # 1. 列名统一:去掉空格,转小写,避免后续调用时找不到列 df.columns = [c.strip().lower().replace(" ", "_") for c in df.columns] # 2. 单价清洗:去掉 ¥、$ 等符号,转成浮点数 df["price"] = ( df["price"] .astype(str) .str.replace(r"[^\d.]", "", regex=True) .replace("", np.nan) .astype(float) ) # 3. 销量清洗:转数值,无法转换的置为 NaN df["qty"] = pd.to_numeric(df["qty"], errors="coerce") # 4. 时间统一:兼容多种格式,统一转 datetime df["order_time"] = pd.to_datetime(df["order_time"], errors="coerce") # 5. 丢弃关键字段缺失的行,避免后续聚合被污染 df = df.dropna(subset=["price", "qty", "order_time"]) # 6. 新增销售额字段,后续所有金额分析都基于它 df["amount"] = df["price"] * df["qty"] print(df.shape) print(df[["price", "qty", "amount"]].describe())

这段代码的逻辑很直白:先把列名规整,避免大小写和空格导致的 KeyError;再用正则把单价里的非数字字符清掉,这一步能救回大量因为货币符号而变成 object 类型的列;销量用to_numeric加coerce,把脏值变成 NaN 而不是直接报错;时间用to_datetime统一,后面才能按天、按小时聚合。最后新增amount字段,这是所有销售额分析的唯一口径。

参数上,encoding要根据实际文件调整,中文环境常见utf-8-sig和gbk,读出来乱码就换。errors="coerce"是清洗时的后悔药,宁可先变 NaN,也不要让一行脏数据把整个脚本打断。dropna的 subset 只锁关键字段,不要全列 drop,否则容易把有效行误删。

2.3 品类映射:没有品类字段时怎么补

很多电商导出表只有商品名称,没有品类。这时候需要做关键词映射。常见做法是维护一个字典,把商品名里的关键词映射到品类。

# 品类关键词映射,实际项目里可以放到单独的 json 或 csv 里维护 category_map = { "精华": "护肤", "面霜": "护肤", "面膜": "护肤", "口红": "彩妆", "粉底": "彩妆", "眼影": "彩妆", "香水": "香水", "香氛": "香水", } def map_category(name): if not isinstance(name, str): return "其他" for kw, cat in category_map.items(): if kw in name: return cat return "其他" df["category"] = df["product_name"].apply(map_category) print(df["category"].value_counts())

映射逻辑是逐关键词匹配,命中即返回。这里有个坑:关键词顺序会影响结果,比如“香水口红”这种组合名,先命中哪个取决于字典顺序。实际使用时,建议把更具体的关键词放前面。映射完一定要value_counts看一眼,“其他”占比过高说明字典覆盖不够,需要补词。

3. 用 matplotlib 把结论画出来:四张图讲清销售结构

3.1 品类销售额柱状图与占比饼图

清洗完数据,第一张图我一般画品类销售额。柱状图看绝对值,饼图看占比,两张配合能快速判断结构。

import matplotlib.pyplot as plt # 设置中文字体,Windows 常用 SimHei,Mac 常用 Arial Unicode MS plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False # 按品类聚合销售额 cat_amount = df.groupby("category")["amount"].sum().sort_values(ascending=False) fig, axes = plt.subplots(1, 2, figsize=(12, 5)) # 左图:柱状图 axes[0].bar(cat_amount.index, cat_amount.values, color="#4C72B0") axes[0].set_title("各品类销售额") axes[0].set_ylabel("销售额") for i, v in enumerate(cat_amount.values): axes[0].text(i, v, f"{v:.0f}", ha="center", va="bottom", fontsize=9) # 右图:饼图 axes[1].pie(cat_amount.values, labels=cat_amount.index, autopct="%1.1f%%", startangle=90) axes[1].set_title("品类销售额占比") plt.tight_layout() plt.savefig("category_amount.png", dpi=150) plt.show()

groupby加sum是最基础的聚合,sort_values保证图里顺序清晰。柱状图上加数值标签,是为了让读者不用猜高度。饼图的autopct控制百分比格式,startangle=90让第一块从正上方开始,视觉上更稳。dpi=150是保存图片的清晰度,大作业里够用,再高文件会变大。

中文字体是 matplotlib 最常见的翻车点。如果图里中文变成方框,先确认系统里有没有对应字体,再检查font.sans-serif是否设置正确。Linux 环境下可能需要额外指定字体路径。

3.2 价格带分布:用分箱看销量集中区

价格带分析能把“卖得好”拆成“哪个价位卖得好”。常见做法是用pd.cut分箱。

# 定义价格带,边界根据实际数据调整 bins = [0, 50, 100, 200, 300, 500, 1000, np.inf] labels = ["0-50", "50-100", "100-200", "200-300", "300-500", "500-1000", "1000+"] df["price_band"] = pd.cut(df["price"], bins=bins, labels=labels, right=False) # 按价格带聚合销量和销售额 band_stat = df.groupby("price_band", observed=True).agg( qty_sum=("qty", "sum"), amount_sum=("amount", "sum") ).reset_index() fig, ax1 = plt.subplots(figsize=(10, 5)) ax1.bar(band_stat["price_band"], band_stat["qty_sum"], color="#55A868", label="销量") ax1.set_xlabel("价格带") ax1.set_ylabel("销量") ax1.set_title("各价格带销量与销售额") ax2 = ax1.twinx() ax2.plot(band_stat["price_band"], band_stat["amount_sum"], color="#C44E52", marker="o", label="销售额") ax2.set_ylabel("销售额") fig.legend(loc="upper right") plt.tight_layout() plt.savefig("price_band.png", dpi=150) plt.show()

pd.cut的bins和labels要一一对应,right=False表示左闭右开,避免边界值重复计数。双轴图里,左轴画销量柱状,右轴画销售额折线,能同时看量和额。observed=True在 pandas 新版本里能避免空类别报错。

价格带边界不是拍脑袋定的。美妆品类常见价格带在 50 到 300 之间,但具体要看数据分布。我一般先画一个价格直方图,看数据集中在哪,再定分箱边界。分箱太粗看不出差异,太细又会让图变得零碎。

3.3 时间趋势:按天聚合看大促节奏

大促数据的时间维度最有价值。按天聚合销售额,能看出预热、爆发、返场三个阶段。

# 按天聚合 df["order_date"] = df["order_time"].dt.date daily = df.groupby("order_date")["amount"].sum().reset_index() daily["order_date"] = pd.to_datetime(daily["order_date"]) fig, ax = plt.subplots(figsize=(12, 5)) ax.plot(daily["order_date"], daily["amount"], marker="o", color="#4C72B0") ax.set_title("每日销售额趋势") ax.set_xlabel("日期") ax.set_ylabel("销售额") ax.tick_params(axis="x", rotation=45) # 标注峰值 peak_idx = daily["amount"].idxmax() peak_date = daily.loc[peak_idx, "order_date"] peak_val = daily.loc[peak_idx, "amount"] ax.annotate(f"峰值 {peak_val:.0f}", xy=(peak_date, peak_val), xytext=(peak_date, peak_val * 1.1), arrowprops=dict(arrowstyle="->")) plt.tight_layout() plt.savefig("daily_trend.png", dpi=150) plt.show()

dt.date把时间戳截断到天,groupby后得到每日汇总。折线图加marker让每个数据点可见。annotate标注峰值,是大作业里很讨巧的一笔,能让结论一眼可见。rotation=45防止日期标签重叠。

如果数据量很大,按天聚合前可以先确认时间范围,避免把非大促期间的数据混进来。常见做法是用df = df[(df["order_time"] >= start) & (df["order_time"] <= end)]先截取活动周期。

3.4 省份销售额:横向条形图更适合长标签

省份名称较长,横向条形图比纵向柱状图更易读。

# 按省份聚合,取前 10 province_amount = df.groupby("province")["amount"].sum().sort_values(ascending=True).tail(10) fig, ax = plt.subplots(figsize=(10, 6)) ax.barh(province_amount.index, province_amount.values, color="#8172B3") ax.set_title("销售额前 10 省份") ax.set_xlabel("销售额") for i, v in enumerate(province_amount.values): ax.text(v, i, f"{v:.0f}", va="center", fontsize=9) plt.tight_layout() plt.savefig("province_amount.png", dpi=150) plt.show()

barh画横向条,sort_values(ascending=True)配合tail(10)让最大值在最上面。数值标签用ax.text加在条末端,va="center"保证垂直居中。省份字段如果有“省”“市”后缀不统一,清洗时可以用str.replace去掉,避免同一个省被拆成两条。

4. 避坑与排查:这类分析脚本最容易翻车的五个地方

4.1 中文显示成方框

现象:图里标题、标签全是方框,英文正常。原因:matplotlib 默认字体不含中文。解决:设置plt.rcParams["font.sans-serif"]为系统里存在的中文字体,Windows 用 SimHei,Mac 用 Arial Unicode MS,Linux 需要先确认字体已安装。设置完记得加axes.unicode_minus=False,否则负号也会出问题。

4.2 金额算出来偏大或偏小

现象:销售额和后台对不上。原因:单价字段里混了“元”“¥”等符号,或者销量字段里有“件”“个”导致转数值失败被置 NaN,聚合时被忽略。解决:清洗后先describe看 price 和 qty 的 min、max 是否合理,再抽查几行原始数据比对。amount字段一定要在清洗完成后统一计算,不要在原始表上直接乘。

4.3 时间聚合出现重复日期

现象:按天聚合后,同一天出现两条记录。原因:order_time里混了不同时区或格式,to_datetime解析后有的带时间有的不带,dt.date截断后仍可能因原始格式差异产生重复。解决:统一用pd.to_datetime(..., errors="coerce")后,再dt.normalize()或dt.floor("D")统一到天,然后groupby时用as_index=False并检查唯一值。

4.4 饼图类别太多挤成一团

现象:饼图标签重叠,看不清。原因:品类或省份唯一值太多,每个扇形太小。解决:先聚合取前 N 个,剩下的归为“其他”。饼图适合 5 到 7 个类别,超过就换柱状图或横向条形图。不要为了“全”而牺牲可读性。

4.5 保存图片空白

现象:savefig出来的图是空白或只有一部分。原因:savefig在show之后调用,或者tight_layout没生效导致内容被裁掉。解决:先savefig再show,保存时加bbox_inches="tight"自动裁剪边距。如果用了subplots,确认tight_layout在保存前调用。

5. 进阶技巧:让这份分析脚本从作业变成可复用工具

5.1 把配置抽出来,换数据不用改代码

约400行的脚本如果所有参数都写死在代码里,换一份数据就要改十几处。我的习惯是把路径、价格带边界、品类字典、图表尺寸都抽到一个配置字典或单独的 json 文件里。

import json config = { "data_path": "sales_raw.csv", "encoding": "utf-8-sig", "price_bins": [0, 50, 100, 200, 300, 500, 1000, 999999], "price_labels": ["0-50", "50-100", "100-200", "200-300", "300-500", "500-1000", "1000+"], "top_n_province": 10, "figsize": [12, 5], "dpi": 150 } with open("config.json", "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2)

读取时用json.load加载,后续所有函数从 config 取值。这样换数据只需改 json,不用动分析逻辑。大作业里这么做,答辩时也能讲出“配置与逻辑分离”的设计意识。

5.2 用函数封装每张图,方便单独调用

把每张图的绘制逻辑封装成函数,输入是 DataFrame 和配置,输出是图片路径。这样主流程只剩读取、清洗、调用几个函数,结构清晰,也方便单独调试某一张图。

def plot_category_amount(df, config): cat_amount = df.groupby("category")["amount"].sum().sort_values(ascending=False) fig, axes = plt.subplots(1, 2, figsize=config["figsize"]) axes[0].bar(cat_amount.index, cat_amount.values) axes[0].set_title("各品类销售额") axes[1].pie(cat_amount.values, labels=cat_amount.index, autopct="%1.1f%%") axes[1].set_title("品类销售额占比") plt.tight_layout() path = "category_amount.png" plt.savefig(path, dpi=config["dpi"], bbox_inches="tight") plt.close() return path

plt.close()很重要,批量画图时不关闭会内存泄漏。返回路径方便后续汇总报告。函数只做一件事,参数从 config 取,这样每个函数都能独立测试。

5.3 验证分析结果是否可信的三个检查

图跑出来不等于结论可信。我一般做三个检查。第一,总额核对:df["amount"].sum()和后台总销售额比,差异超过 5% 就要查清洗逻辑。第二,抽样核对:随机抽 5 行,手工算price * qty是否等于amount。第三,分布检查:describe看 price 和 qty 的 max 是否离谱,比如单价出现 999999,多半是脏数据没清干净。

这三个检查花不了几分钟,但能挡住大部分“图很漂亮、结论全错”的情况。血泪经验是,曾经有一次单价字段里混了“1,299”这种带千分位的值,正则没处理逗号,结果 1299 变成 129,销售额直接少了一个数量级。从那以后,我每次清洗完都会先看 describe 的 max。

5.4 从作业到工具:加一个命令行入口

如果想让脚本能直接命令行调用,加一个argparse入口,传入数据路径和输出目录。

import argparse def main(): parser = argparse.ArgumentParser() parser.add_argument("--data", required=True, help="原始销售数据路径") parser.add_argument("--out", default="output", help="图片输出目录") args = parser.parse_args() df = load_and_clean(args.data) plot_category_amount(df, config) plot_price_band(df, config) plot_daily_trend(df, config) plot_province_amount(df, config) print(f"分析完成,图片已保存到 {args.out}") if __name__ == "__main__": main()

这样换数据只需python analyze.py --data new_sales.csv --out result,不用改代码。大作业里加上这个,演示时能直接跑,观感会好很多。参数required=True强制传数据路径,避免跑空。

我自己的习惯是,任何分析脚本只要超过 200 行,就一定要抽配置、封函数、留入口。不然过两周回头看,自己都记不清哪段代码对应哪张图。这份约400行的美妆销售分析源码,价值不在于代码量,而在于它把清洗、聚合、可视化、排查这条链路完整走了一遍。你把它跑通,改一改配置,换一份自己的数据,就能得到一套能讲、能复现、能继续扩展的分析流程。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进