Python+Pandas+matplotlib:双十一美妆销售数据分析实战 📅 发布时间:2026/9/13 12:04:08 👁 浏览次数: 简介基于Python的天猫双十一美妆销售数据分析实战资源聚焦数据清洗、探索分析与matplotlib可视化适合希望快速上手Python数据分析的学生、运营及电商从业者。压缩包共含6个文件包括2个Jupyter Notebook交互式分析文档、原始CSV数据、清洗后的Excel表格、可复现的Python脚本及一份HTML结果报告整体大小约4.93MBNotebook便于分步运行脚本适合直接复用HTML方便展示结论足以支撑从数据读取到图表输出的完整演示。已有835人学习下载是入门数据可视化的高频参考案例。通过约400行代码可完整走通销售数据的处理链路包括缺失值处理、字段探索、品牌与品类销售排行以及用折线图、柱状图呈现双十一销售趋势同时能理解从数据清洗到结果呈现的完整思考路径便于模仿搭建自己的分析项目。整套流程清晰适合作为毕业设计或课堂练习的参考蓝本。1. 双十一美妆数据拆解销售额最高那天不是当天把天猫双十一美妆销售数据拉出来跑一遍最先看到的可能和你预期相反销量最高的单品、销售额最大的品牌、客单价最高的人群往往不是集中在双十一当天而是分布在预售期和返场期。这背后是促销节奏、价格锚点和用户决策周期的综合结果也正是这份约 400 行代码项目最有价值的地方——它把 csv 和 xlsx 两套数据源、清洗流程、matplotlib 可视化和 Pandas 聚合分析串成了一条完整可复现的链路。项目里同时包含.ipynb、.py和.html三种形态既能在 Jupyter 里逐格调试也能直接跑脚本出报告。对想通过真实业务数据练手 Python 数据分析、又不想只用鸢尾花和房价数据集的人来说这是一个贴近电商场景、能直接改参数复用的实战样本。2. 数据读入与清洗CSV 和 XLSX 双源头的字段统一2.1 为什么需要同时处理两种数据格式电商数据分析的常态是数据散落在多个系统里运营导出的 csv、财务提供的 Excel、甚至还有手工维护的副本。这个项目里恰好同时出现了双十一淘宝美妆数据.csv和clean_beautymakeup.xlsx一个偏原始记录、一个是清洗后的版本。直接把两个文件读进来做对比分析能发现很多隐藏问题比如同一品牌在不同文件里的名称写法不一致、价格字段一个有单位一个没有、日期格式一个带时分秒一个只有年月日。常见做法是先各自读入再统一列名和类型最后按业务主键合并。我一般会先打印两个文件的列名和dtypes确认差异再动手避免后面聚合时踩类型不一致的坑。import pandas as pd df_raw pd.read_csv(双十一淘宝美妆数据.csv, encodingutf-8) df_clean pd.read_excel(clean_beautymakeup.xlsx) print(df_raw.columns.tolist()) print(df_clean.columns.tolist()) print(df_raw.dtypes) print(df_clean.dtypes)这段代码的逻辑分三步先分别读取原始 csv 和清洗后的 Excel然后打印列名确认字段命名差异——原始文件里可能是中文列名、清洗文件里可能是英文列名最后打印数据类型重点看日期、价格、销量这三类字段是否已经是数值型或时间型。encodingutf-8是处理 csv 时的必填项如果源文件是用 Excel 另存为的经常会遇到gbk编码这时就要改成encodinggbk否则读进来就是乱码。2.2 缺失值和异常值的处理策略这类销售数据里最常见的脏数据有三类价格字段为 0 或负数、销量为空、日期格式异常。价格为 0 的极端情况多数是缺货标记、赠品记录或者数据录入错误直接参与聚合会严重拉低客单价销量为空的行在按品牌或品类求和时会丢数据需要先决定是删除还是填充。# 统一列名映射方便后续操作 df_raw.columns [date, brand, category, price, sales_volume] # 删除价格无效的记录0 和负数 df_raw df_raw[df_raw[price] 0] # 删除销量缺失的行 df_raw df_raw.dropna(subset[sales_volume]) # 时间字段统一为 datetime 类型并只保留日期部分 df_raw[date] pd.to_datetime(df_raw[date]).dt.normalize() # 新增销售额计算列 df_raw[sales_amount] df_raw[price] * df_raw[sales_volume] print(df_raw.shape) print(df_raw.isnull().sum())逻辑说明先重命名列名让后续代码好写然后用布尔索引把price 0的行过滤掉——这里用的是df_raw[df_raw[price] 0]而不是dropna因为价格这类数值字段里的异常值和缺失值的处理逻辑不同。dropna(subset[sales_volume])代表只关注销量列是否有缺失其他列的缺失暂时不影响本阶段分析。dt.normalize()是把2024-11-11 08:30:00归一化成2024-11-11这样按天聚合时不会把同一天的数据拆成多个桶。最后新增的sales_amount是后续所有销售额分析的基础字段。参数的调整空间在于如果业务上需要保留价格异常的行做单独研究可以把过滤条件改成df_raw[price] 0并在聚合前用中位数填充销量缺失时也可以用fillna(df_raw[sales_volume].median())替代直接删除具体取舍看分析目标是趋势还是精确值。这一步结束后应该用df_raw[brand].nunique()看一下唯一品牌数量如果比预期少很多大概率是品牌名里有空格或全半角符号差异。3. matplotlib 可视化从折线图到多子图组合3.1 中文乱码和负号显示的根治方案matplotlib 默认字体不含中文字符直接plt.title(销售额趋势)画出来就是一堆方块。这是每个用 matplotlib 做中文可视化的人都会遇到的第一道坎。根治的办法是显式指定系统中文字体而不是靠rcParams默认值瞎猜。Windows 下用SimHei或Microsoft YaHeimacOS 下用PingFang SC或Heiti TCLinux 服务器上要看有没有安装wqy-microhei或Noto Sans CJK SC。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei, PingFang SC] plt.rcParams[axes.unicode_minus] False # 验证是否生效 fig, ax plt.subplots(figsize(6, 3)) ax.set_title(中文标题测试) fig.savefig(test_cn.png, dpi100)参数说明font.sans-serif填的是一个字体列表matplotlib 会从左到右逐个查找系统里存在的字体这比只写一个字体名更保险因为不同环境下同一字体名称可能不可用。axes.unicode_minus必须设为False否则坐标轴上的负号会显示成方块。确认字体生效后后续所有图表函数都应该复用同一套rcParams配置不要在每个绘图函数里重复设置。3.2 销售额趋势折线图时间序列在多日促销中的应用双十一的销售不是单日爆发而是从预售期就开始爬坡到当天冲顶再进入返场期回落。用折线图看全周期的日销售额曲线能明显看到几个关键拐点预售定金支付日的小高峰、11 月 10 日晚的预热冲刺、11 月 11 日 0 点后的脉冲式增长。import pandas as pd import matplotlib.pyplot as plt daily df_raw.groupby(date)[sales_amount].sum().reset_index() fig, ax plt.subplots(figsize(12, 5)) ax.plot(daily[date], daily[sales_amount], color#c0392b, linewidth2, markero, markersize4) ax.set_title(双十一周期美妆日销售额趋势, fontsize14) ax.set_xlabel(日期) ax.set_ylabel(销售额元) ax.grid(True, linestyle--, alpha0.6) fig.autofmt_xdate(rotation30) plt.tight_layout() plt.show()这里先用groupby(date)按天聚合销售额总和生成一个两列的 DataFrame然后绘图。linewidth2控制线条粗细太细在多日跨度下不明显markero在每个数据点上画圆点方便精确读数linestyle--的网格线是为了不对主线条造成视觉干扰alpha0.6进一步弱化网格存在感。fig.autofmt_xdate(rotation30)是时间序列绘图的常用处理——日期标签如果横着排很容易重叠旋转 30 度后既能看清又不挤。3.3 品牌销量 TOP10 柱状图横向还是竖向的关键取舍品牌销量排名用柱状图表达最直观但当品牌名超过 6 个字符时竖向柱状图的 x 轴标签必然倾斜甚至重叠。电商数据里品牌名基本都是中英文组合比如「巴黎欧莱雅」「完美日记」横向柱状图是更好的选择。brand_sales df_raw.groupby(brand)[sales_amount].sum().nlargest(10) fig, ax plt.subplots(figsize(10, 6)) bars ax.barh(brand_sales.index[::-1], brand_sales.values[::-1], color#2980b9, alpha0.85) ax.set_title(销售额 TOP10 品牌, fontsize14) ax.set_xlabel(总销售额元) # 在柱子右侧标注具体数值 for bar in bars: width bar.get_width() ax.text(width * 1.02, bar.get_y() bar.get_height() / 2, f{width:,.0f}, vacenter, fontsize9) plt.tight_layout() plt.show()逻辑说明nlargest(10)从聚合结果里取出销售额最高的 10 个品牌[::-1]是 Python 的负步长切片把顺序反转让最大的品牌排在图表顶部这是横向柱状图的通用阅读习惯。ax.barh是横向柱状图接口参数顺序和bar不一样第一个参数是 y 轴坐标、第二个是长度。遍历bars用bar.get_width()获取每个柱子的数值然后通过ax.text在柱子末端 1.02 倍的位置标注带千分位格式的文本——f{width:,.0f}里的,是千分位分隔符0f表示不保留小数。表格柱状图常用参数速查参数取值示例作用使用场景alpha0.8柱子整体透明度多系列重叠时需要透视edgecolorwhite柱子描边颜色深色填充时让边界清晰bottom0.5y 轴起始偏移对数坐标或突出非零基线width0.7柱子宽度占比品牌数多时收窄避免粘连3.4 多子图组合一张图看完四个维度的布局技巧项目中约 400 行代码里最值得复用的部分是plt.subplots的多子图组合。用 2x2 的网格同时展示日销售额趋势、品牌 TOP10、品类分布和价格带散点可以在单屏内完成数据概览。fig, axes plt.subplots(2, 2, figsize(16, 10)) fig.suptitle(天猫双十一美妆销售数据总览, fontsize16, y0.98) # 左上日销售额折线图 axes[0, 0].plot(daily[date], daily[sales_amount], color#c0392b) axes[0, 0].set_title(日销售额趋势) axes[0, 0].tick_params(axisx, rotation30) # 右上品牌 TOP10 横向柱状图 axes[0, 1].barh(brand_sales.index, brand_sales.values, color#2980b9) # 左下品类销量占比 category_counts df_raw[category].value_counts() axes[1, 0].pie(category_counts.values, labelscategory_counts.index, autopct%.1f%%, startangle90) # 右下价格与销量散点图 axes[1, 1].scatter(df_raw[price], df_raw[sales_volume], alpha0.4, s8, color#27ae60) axes[1, 1].set_xscale(log) axes[1, 1].set_title(价格 vs 销量) plt.tight_layout(rect[0, 0, 1, 0.95]) plt.show()这里的核心是axes二维数组的索引方式axes[0, 0]是左上、axes[0, 1]是右上、axes[1, 0]是左下、axes[1, 1]是右下。fig.suptitle是整个大图的全局标题y0.98把标题位置稍微抬高避免和子图标题重叠。左下角用饼图展示品类占比autopct%.1f%%控制饼图上的百分比显示格式startangle90让第一个扇区从 12 点方向开始排。右下角散点的s8是散点面积参数数据点较多时调小避免糊成一团set_xscale(log)是因为美妆价格跨度从 9.9 到 1000 以上线性坐标会把低价区数据完全压扁。4. 深入分析品牌结构、品类关联与价格带分布4.1 用 Pandas 交叉表分析品牌与品类的销售结构可视化之前需要先把分析指标算出来。品牌维度的聚合只能回答「谁卖得好」但要进一步看「卖得好是因为品类布局广还是单品爆款强」需要用pivot_table或crosstab做品牌 x 品类的透视矩阵。这个矩阵既能看每个品牌在各个品类的铺货情况也能横向比较不同品牌的品类集中度。pivot pd.pivot_table( df_raw, indexbrand, columnscategory, valuessales_amount, aggfuncsum, fill_value0 ) # 计算每个品牌的品类集中度最大品类销售额 / 品牌总销售额 brand_total pivot.sum(axis1) brand_max_cat pivot.max(axis1) pivot[concentration] (brand_max_cat / brand_total).round(3) # 取出销售额 TOP5 品牌查看品类结构 top5 pivot.sort_values(total_sales, ascendingFalse).head(5) print(top5[[category_A, category_B, category_C, concentration]])参数说明pivot_table的index是行索引维度columns是列维度values是需要聚合的指标aggfuncsum决定汇总方式fill_value0把没有记录的交叉单元格填成 0否则会出现大量 NaN后续计算集中度时会被 Pandas 跳过。concentration集中度是一个 0 到 1 之间的指标越接近 1 说明品牌越依赖某个单一品类越接近 0 说明品类结构越均衡。sort_values(total_sales)前需要先把各品牌的合计列加入 DataFrame否则排序没有依据。提示pivot_table的aggfunc也可以传多个函数比如[sum, mean, count]同时看总销售额、平均客单价和成交笔数三项指标。品牌太多时先按总销售额排序再取前 20 个做热力图否则矩阵过宽无法阅读。4.2 价格带分桶与销量分布离散化揭示的真实需求区间电商数据分析里价格不是连续变量直接看而是要先分桶看区间效应。美妆品类尤其明显50 元以下往往是眉笔、唇釉等小物件冲动消费为主100 到 300 元是护肤品的主力价格带500 元以上则是高端精华和礼盒的天下。用pd.cut把价格离散化成 5 个区间再按区间聚合销量和销售额能直接看出哪个价格带的转化效率最高。bins [0, 50, 150, 300, 500, float(inf)] labels [0-50, 50-150, 150-300, 300-500, 500] df_raw[price_band] pd.cut(df_raw[price], binsbins, labelslabels, rightFalse) band_stats df_raw.groupby(price_band, observedTrue).agg( total_sales( sales_amount, sum), total_volume(sales_volume, sum), order_count(sales_volume, count) ) band_stats[avg_price] band_stats[total_sales] / band_stats[total_volume] print(band_stats)pd.cut的三个核心参数bins是区间边界列表rightFalse意味着区间是左闭右开即[0, 50)包含价格等于 0 的记录但不包含等于 50 的这样避免区间重叠。labels给每个区间起一个可读性强的名称。groupby(... , observedTrue)是 Pandas 2.x 里的要求当分组列是 Categorical 类型时observedTrue只统计实际出现的类别否则会输出空类别导致结果里出现一堆 0 行。agg里用named aggregation语法每个指标以新列名(原列名, 聚合函数)的形式传给agg比字典方式更清晰也支持对同一列做不同聚合。从输出结果看行order_count在这里用的是count它统计的是每个区间内的记录条数代表成交笔数如果sales_volume本身就是含多件购买的累计数值成交笔数应该单独用订单号列去重统计而不能用count代替。4.3 相关系数矩阵找出销售数据中的隐藏关联价格、销量、销售额三个原始指标之间必然高度相关但引入「单价 x 折扣力度」「销量 x 评价数」这类衍生字段后相关矩阵会揭示更有意思的关系。美妆品类常见的结论是价格与销量呈中度负相关但个别高端品牌因为品牌溢价反而出现正相关——这正是异常值和分层分析的价值所在。import numpy as np df_raw[is_high_end] (df_raw[price] 300).astype(int) corr_df df_raw[[price, sales_volume, sales_amount, is_high_end]].corr() fig, ax plt.subplots(figsize(6, 5)) im ax.imshow(corr_df, cmapcoolwarm, vmin-1, vmax1) ax.set_xticks(range(len(corr_df.columns))) ax.set_yticks(range(len(corr_df.columns))) ax.set_xticklabels(corr_df.columns, rotation45, haright) ax.set_yticklabels(corr_df.columns) for i in range(len(corr_df.columns)): for j in range(len(corr_df.columns)): ax.text(j, i, f{corr_df.iloc[i, j]:.2f}, hacenter, vacenter, fontsize10, colorblack) plt.colorbar(im, shrink0.8) plt.title(核心指标相关系数矩阵) plt.tight_layout() plt.show()逻辑说明is_high_end是价格是否大于等于 300 的哑变量把它放进相关矩阵是为了验证「高端产品是否真的卖得少」这个假设。corr()默认计算皮尔逊相关系数对线性关系敏感。热力图的颜色映射用coolwarm蓝色代表负相关、红色代表正相关vmin-1, vmax1把色彩范围固定住不然系数都在 0.9 左右时颜色会糊成一片。双重for循环在每个单元格中心标注具体数值f{corr_df.iloc[i, j]:.2f}保留两位小数。如果只想看某个品牌的数据在计算相关矩阵前先加一行df_brand df_raw[df_raw[brand] 完美日记]对比整体和单品牌的相关性差异这是分群分析的起点。5. 用函数封装把可视化流程变成可复用模块5.1 从脚本到函数消除重复代码的通用绘图模板400 行代码如果全部平铺在.ipynb里换一个数据文件重跑时改动量会非常大。把数据清洗和可视化拆成独立函数是把这个项目从「跑通一次」升级为「换个数据也能用」的关键一步。下面是一个通用的销售趋势绘图函数参数留好了日期列、金额列和数据源的接口。def plot_sales_trend(df, date_col, value_col, title销售趋势, figsize(12, 5)): 按指定时间列聚合数值列并绘制折线图 Args: df: 含日期和数值列的 DataFrame date_col: 日期列名需为 datetime 类型 value_col: 需要聚合的数值列名 title: 图表标题 figsize: 画布尺寸 daily df.groupby(date_col)[value_col].sum().reset_index() fig, ax plt.subplots(figsizefigsize) ax.plot(daily[date_col], daily[value_col], color#c0392b, linewidth2, markero, markersize4) ax.set_title(title) ax.set_xlabel(date_col) ax.set_ylabel(value_col) ax.grid(True, linestyle--, alpha0.6) fig.autofmt_xdate(rotation30) plt.tight_layout() return fig, ax函数设计的关键在参数抽象date_col和value_col分开传换一个数据集时不需要改函数体只需要改调用时的列名。返回值是(fig, ax)而不是直接plt.show()这样调用方可以决定是保存图片、叠加更多图层还是嵌入报告。默认参数figsize(12, 5)满足大多数场景高频使用的颜色、线宽也写成默认值而不是每处硬编码。调用方式fig, ax plot_sales_trend(df_raw, date, sales_amount) fig.savefig(daily_sales.png, dpi200, bbox_inchestight)bbox_inchestight会把多余的空白边裁掉避免图表周围出现大片白边。dpi200是适合 PPT 或公众号配图的分辨率如果只是屏幕看 100 dpi 就够了文件体积也更小。5.2 主题风格统一切换与商业图表样式matplotlib 默认样式偏学术直接用在业务汇报里显得朴素。好的做法是在程序入口一次性加载自定义样式而不是在每个绘图函数里逐项修改rcParams。先创建mystyle.mplstyle文件axes.spines.top: False axes.spines.right: False axes.grid: True grid.linestyle: -- grid.alpha: 0.5 axes.titlesize: 16 axes.labelsize: 12 legend.frameon: False figure.dpi: 120 savefig.dpi: 200然后在主程序里用一行代码加载import matplotlib as mpl mpl.rc_file(mystyle.mplstyle) # 局部覆盖样式 with mpl.rc_context({lines.linewidth: 1.5}): fig, ax plot_sales_trend(df_raw, date, sales_amount)rc_file的好处是样式集中在单独的配置文件里业务逻辑和图表样式彻底分离。rc_context用于局部覆盖比如某个图表需要突出粗线条时在with代码块里临时修改lines.linewidth退出代码块后自动恢复。5.3 用 DataFrame.style 快速生成简化版 HTML 报告项目里包含.html报告文件除了用nbconvert转换 Jupyter NotebookPandas 自带的to_html也能快速把分析结果输出为可分享的网页。加上style模块的格式化后表格的可读性能达到基础汇报标准。brand_summary df_raw.groupby(brand).agg( total_sales(sales_amount, sum), total_volume(sales_volume, sum), avg_price(sales_amount, lambda x: x.sum() / df_raw.loc[x.index, sales_volume].sum()) ).sort_values(total_sales, ascendingFalse).head(10) html_out (brand_summary.style .format({total_sales: ¥{:,.0f}, total_volume: {:,.0f}, avg_price: ¥{:,.1f}}) .background_gradient(subset[total_sales], cmapYlOrRd) .set_table_attributes(classtable table-striped) .to_html()) with open(brand_report.html, w, encodingutf-8) as f: f.write(fmeta charsetutf-8{html_out})这里用lambda计算加权平均价格因为直接用mean会把高价低量品牌抬得太高sales_amount 总和 / sales_volume 总和才是真实的加权客单价。background_gradient按销售额值从浅黄到深红渐变填充单元格方便快速定位头部品牌。set_table_attributes写入 Bootstrap 的表格样式类名生成的 HTML 放进任何带 Bootstrap 的页面里就能直接渲染出美化后的表格。写入文件时手动加meta charsetutf-8是防止浏览器用错误编码解析中文内容不写的话 Chrome 可能默认按Windows-1252解码品牌名全部乱码。本文还有配套的精品资源点击获取