26  某国际电商平台销售数据分析

26.1 引言电商数据的多维度分析

电商平台关键问题:

  • 折扣策略: 多大折扣最优?
  • 利润分析: 折扣与利润关系
  • 品类表现: 哪些品类利润高?
  • 定价策略: 如何平衡销量与利润

26.2 本章学习目标

本章围绕”折扣打到几折最划算”这一电商定价问题,练习在聚合口径上做文章。通过本章学习,你将掌握:

  1. 电商交易数据的读取与冗余字段清理
  2. nunique 检验”每天是否统一折扣”“各品类折扣是否一致”这类业务前提
  3. groupby + agg 多指标聚合,并用连除构造”单位时间、单位商品”的平均量指标
  4. query 过滤小样本折扣档位,控制结论的代表性
  5. 用多指标折线图定位折扣-利润关系的拐点与最优折扣区间

先修内容:第 章节 14 章(Excel文件读取)与第 章节 13 章(分组聚合);折线图细节回看第 章节 19 章。

26.3 数据准备

任务要求:读取平台内置的 superstore.xlsx 并删除 Postal Code 列;先检验”同一天折扣是否统一”“各品类折扣是否一致”两个前提;再按折扣分组聚合销量、销售额、利润、天数与商品数,构造”单位时间、单位商品”的平均利润、平均销售额、平均销量三个指标;过滤商品种类过少的折扣档位后,绘制三条折线对比不同折扣下的平均表现。

平台任务(平台原始代码)

以下代码与教学平台任务要求完全一致:

列表 26.1: 平台原始代码
# 注:superstore.xlsx 数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 导入必要的包
import pandas as pd # 用于数据处理和分析
import matplotlib.pyplot as plt # 用于绘图
import seaborn as sns # 用于高级绘图
from datetime import datetime # 用于处理日期时间数据

# 读取Excel文件
data = pd.read_excel('superstore.xlsx')  

# 删除Postal Code字段
del data['Postal Code']  
print(data.head())  # 输出前几行数据

# 折扣与销售利润关系探究

# 判断每一天是不是同一个折扣
huigui01 = data.groupby('Order Date').Discount.nunique()  
print(huigui01.head())  # 输出前几行数据

# 判断每个品类的折扣情况
huigui03 = data.groupby(['Sub-Category', 'Discount']).agg({
  'Profit': 'sum', # 计算每个子类别和折扣组合的总利润
  'Order Date': 'nunique', # 计算每个子类别和折扣组合的唯一订单日期数量
})  # 数据结构定义结束
huigui03['Profit_pd'] = huigui03['Profit'] / huigui03['Order Date'] # 计算单位时间内的平均利润
print(huigui03.head(15)) # 打印前15行结果,检查每个子类别和折扣组合的利润情况

# 通过前面的判断,可以发现:
# 1)同一天不同商品会采取不同的折扣;
# 2)不同品类的商品,折扣力度也并不相同;
# 为了简化问题,我们将所有销售数据按照折扣分组,然后对销量、销售额、利润等数据进行汇总聚合;
# 同时,考虑到折扣促销的本质,就是为了通过降价提高销量,相应单个产品的利润会有所下降,但是单个品类、单位时间的利润总和会得到提升;
# 因此我们在探究折扣与利润之间关系时候,需要剔除品类数量、折扣时间的影响。

# 按折扣分组,计算汇总指标
profit_8discount = data.groupby('Discount').agg({
  'Quantity': 'sum', # 计算每个折扣下的总销售数量
  'Sales': 'sum', # 计算每个折扣下的总销售额
  'Profit': 'sum', # 计算每个折扣下的总利润
  'Order Date': 'nunique', # 计算每个折扣下的唯一订单日期数量
  'Product Name': 'nunique', # 计算每个折扣下的唯一商品名称数量
})  # 数据结构定义结束

# 重命名列
profit_8discount.columns = ['Quantity_sum', 'Sales_sum', 'Profit_sum', 'Dates_sum', 'Products_sum']

# 计算单位时间内每个商品的平均利润、平均销售额和平均销售数量
profit_8discount['Profit_p_p'] = profit_8discount['Profit_sum'] / profit_8discount['Products_sum'] / profit_8discount['Dates_sum'] # 平均利润
profit_8discount['Sales_p_p'] = profit_8discount['Sales_sum'] / profit_8discount['Products_sum'] / profit_8discount['Dates_sum'] # 平均销售额
profit_8discount['Quantity_p_p'] = profit_8discount['Quantity_sum'] / profit_8discount['Products_sum'] / profit_8discount['Dates_sum'] # 平均销售数量

# 重置索引,方便后续操作
profit_8discount = profit_8discount.reset_index()
print(profit_8discount)  # 输出利润数据

# 选取折扣涉及商品种类大于(商品种类总数)*0.005的情形;
profit_8discount22 = profit_8discount.query('Products_sum > 19') 
print(profit_8discount22)  # 输出利润数据

# 绘制折扣与利润关系的折线图
sns.set(style="darkgrid") # 设置绘图风格为'darkgrid'
plt.figure(figsize=(10, 6)) # 设置画布大小

# 遍历需要绘制的列(平均利润、平均销售额、平均销售数量)
for i in profit_8discount22.columns[6:-2]:
  ax = sns.lineplot(x='Discount', y=i, label=i, data=profit_8discount22)  # 绘制折线图

# 添加图例和标签
plt.legend() # 显示图例
plt.xlabel('Discount Range') # 设置x轴标签
plt.ylabel('Sales') # 设置y轴标签
plt.savefig("10.png") # 保存图表为PNG文件
plt.show() # 显示图表

# 通过分析发现:
# 1)销售折扣20%(折扣为0.2表示打8折(即降价20%))是销售利润正负的拐点,当折扣继续增加,销售利润基本为负值。
# 2)当折扣为0.002和0.07时,单个品类单位时间的总利润有显著的提升,也就是营销中低折扣的“小刀”策略,可以有效刺激消费者购买欲望,进而提升销量和利润。
# 3)当折扣为0.85、0.55、0.57的时候,利润会出现一些极端的负数值,通过查看对应的商品种类和该折扣销售天数,
# 可以发现此种情况是特殊个例,属于基于特定商品、网红商品的引流活动,对整体商品的定价策略不具备代表性。

预期输出:数据文件平台内置,本地无数据,具体数值以平台运行结果为准。判读要点:

  1. 文本输出:每日折扣种类数的前几行;子品类×折扣组合的利润、天数表前15行;按折扣聚合的指标总表及过滤后的表——重点比较 Profit_p_p 列随 Discount 增大的变化方向与符号。
  2. 图形输出(10.png):横轴为折扣、纵轴为平均量的三条折线。判读要点:平均利润曲线由正转负的折扣位置(拐点)、低折扣区间的抬升幅度、高折扣端是否出现深谷;三条曲线量纲不同,读形态与拐点,不比较绝对高低。
  3. 平台原始代码的块尾注释保留了作者的三条判读(20%为利润正负拐点、低折扣”小刀”策略、极端折扣属引流个例),可作读图参照,最终以平台运行结果为准。

下方提供一个本地演练版:由于 superstore.xlsx 仅平台内置,本地以一份 12 行的内联合成迷你表代替(保留平台代码实际用到的字段,取值为演示流程而设的模拟值),并复现本章核心口径的一个变体——统计利润为负的折扣订单占比,并按折扣档位分解:

列表 26.2: 本地演练版(合成数据)
# 本地演练说明:平台内置的superstore.xlsx本地没有,以下用同结构的内联合成迷你数据代替
import pandas as pd  # 导入Pandas数据分析库
mini = pd.DataFrame({  # 构造12行迷你表,列取平台代码实际用到的字段
    'Order Date': ['2023-01-03', '2023-01-03', '2023-01-10', '2023-01-15', '2023-01-15', '2023-02-01',
                   '2023-02-07', '2023-02-07', '2023-02-14', '2023-02-21', '2023-03-01', '2023-03-08'],  # 订单日期
    'Sub-Category': ['Chairs', 'Tables', 'Phones', 'Chairs', 'Bookcases', 'Tables', 'Phones', 'Chairs', 'Bookcases', 'Tables', 'Phones', 'Chairs'],  # 子品类
    'Product Name': ['产品A', '产品B', '产品C', '产品A', '产品D', '产品B', '产品E', '产品A', '产品D', '产品F', '产品C', '产品G'],  # 商品名称
    'Sales': [520.0, 980.0, 340.0, 460.0, 810.0, 1250.0, 390.0, 430.0, 760.0, 1120.0, 410.0, 380.0],  # 销售额
    'Quantity': [3, 5, 2, 3, 4, 6, 2, 3, 4, 5, 2, 2],  # 销量
    'Discount': [0.0, 0.2, 0.0, 0.1, 0.3, 0.2, 0.0, 0.5, 0.7, 0.2, 0.0, 0.8],  # 折扣(0.2表示降价20%)
    'Profit': [180.2, -95.4, 62.1, 120.5, -210.0, -88.0, 75.3, -260.5, -540.2, -102.6, 68.9, -318.4]  # 利润
})
disc = mini[mini['Discount'] > 0]  # 只看有折扣的订单,与平台任务"折扣与利润关系"的研究对象一致
neg_share = (disc['Profit'] < 0).mean()  # 利润为负的折扣订单占比:负利润订单数/折扣订单数
by_discount = disc.groupby('Discount')['Profit'].agg(订单数='size', 负利润订单数=lambda s: (s < 0).sum())  # 按折扣档位汇总
by_discount['负利润订单占比'] = by_discount['负利润订单数'] / by_discount['订单数']  # 各档位内部占比
print('利润为负的折扣订单占比:', round(neg_share, 4))  # 总体占比
print('按折扣档位汇总:')
print(by_discount)

预期输出(本机实际运行结果):

利润为负的折扣订单占比: 0.875
按折扣档位汇总:
          订单数  负利润订单数  负利润订单占比
Discount                      
0.1         1       0      0.0
0.2         3       3      1.0
0.3         1       1      1.0
0.5         1       1      1.0
0.7         1       1      1.0
0.8         1       1      1.0

判读要点与平台任务同型:迷你表中折扣订单共 8 笔、其中 7 笔利润为负,总占比 0.875;分档位看,折扣 0.1 的订单利润仍为正,折扣升至 0.2 及以上后全部转负——对应平台任务”20% 折扣是利润正负拐点”的判读。真实数据中各档位订单数远不止 1—3 笔,占比不会如此整齐,这正是平台任务先做 query('Products_sum > 19') 样本量过滤的原因。

列表 26.3: 电商平台数据预处理(讲解块)
# 注:本块是平台任务的分步讲解版(重述读取与清理部分,非新增分析)
# 注:superstore.xlsx 数据文件本地没有,但平台已经内置
# =============================================================================
# 题目:国际电商平台销售数据预处理与清洗
# =============================================================================
# 本代码块实现电商销售数据的读取、清洗和初步探索
# 重点处理缺失值和异常值,为后续分析奠定基础

# ==================== 导入数据分析库 ====================
import pandas as pd  # 导入Pandas库,用于数据表操作
import matplotlib.pyplot as plt  # 导入Matplotlib库,用于绘图
import seaborn as sns  # 导入Seaborn库,用于高级统计绘图
from datetime import datetime  # 导入datetime模块,用于日期时间处理

# ==================== 读取电商平台销售数据 ====================
data = pd.read_excel('superstore.xlsx')  # 读取Excel格式的销售数据文件

# ==================== 数据清洗:删除缺失值过多的字段 ====================
del data['Postal Code']  # 删除邮政编码字段,因为该字段缺失值较多,对分析贡献有限

print('数据形状:', data.shape)  # 输出数据集的行数和列数,了解数据规模
print('\n数据预览:')  # 输出标题
print(data.head())  # 显示数据的前5行,初步了解数据结构

26.4 折扣与销售利润关系探究

26.4.1 问题1每天是否采用统一折扣?

列表 26.4: 每日折扣唯一性分析
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# ==================== 统计每天的折扣种类数量 ====================
daily_discounts = data.groupby('Order Date').Discount.nunique()
# 按订单日期分组,统计每天有多少种不同的折扣力度
# nunique()计算唯一值的数量,用于评估折扣策略的复杂性

print('每天折扣种类数统计:')  # 输出标题
print(daily_discounts.head(10))  # 显示前10天的折扣种类数

print(f'\n平均每天折扣种类数: {daily_discounts.mean():.2f}')  # 输出平均每天有几种折扣,保留2位小数
print(f'最大折扣种类数: {daily_discounts.max()}')  # 输出折扣种类数最多的天有几种折扣

结论: 同一天不同商品会采取不同的折扣

26.4.2 问题2各品类折扣情况

列表 26.5: 各品类折扣情况分析
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# ==================== 按子品类和折扣分组统计 ====================
category_discount = data.groupby(['Sub-Category', 'Discount']).agg({
    'Profit': 'sum',  # 对利润求和,计算该品类在该折扣力度下的总利润
    'Order Date': 'nunique'  # 统计不重复的日期数,计算该折扣持续的天数
})

# ==================== 计算单位时间平均利润 ====================
category_discount['Profit_per_day'] = category_discount['Profit'] / category_discount['Order Date']
# 计算每个品类在每个折扣水平下的日均利润
# 这个指标可以帮助判断哪种折扣力度更有效率

print('子品类折扣分析:')  # 输出标题
print(category_discount.head(15))  # 显示前15行结果

结论: 不同品类的商品,折扣力度不相同

26.4.3 问题3折扣与利润的总体关系

问题1、问题2确认了”同一天折扣不同、不同品类折扣不同”,平台代码因此把全部订单按折扣档位重新聚合,并用 query('Products_sum > 19') 剔除参与商品过少的档位(阈值19≈商品总数×0.005,具体以平台数据为准)。本节不再逐行重述这段聚合代码,改做一个变式实验:把过滤阈值这一个参数在”平台口径19、放宽到5、收紧到100”三档之间切换,观察保留的折扣档位与利润形态如何变化。实验在一张合成的”按折扣聚合”表上运行(列名与平台 profit_8discount 完全一致,数值量级参照superstore真实量级),请重点观察:三档阈值各保留几个档位、Profit_p_p 首次转负的位置是否移动、以及放宽阈值后极端负值有多深。

列表 26.6: 变式实验:放宽与收紧样本量过滤阈值
# 本地演练说明:superstore.xlsx仅平台内置,以下按平台profit_8discount的同构列名构造合成聚合表
import pandas as pd  # 导入Pandas数据分析库
profit_tbl = pd.DataFrame({  # 合成的"按折扣聚合"表,列名与平台profit_8discount完全一致,量级参照superstore真实量级
    'Discount': [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],  # 折扣档位(0.2表示降价20%,即打8折)
    'Quantity_sum': [20000, 9000, 7000, 3500, 600, 350, 40, 25, 30],  # 各档位总销量
    'Sales_sum': [600000, 220000, 180000, 90000, 15000, 9000, 1200, 800, 900],  # 各档位总销售额
    'Profit_sum': [200000, 60000, -10000, -40000, -25000, -30000, -12000, -9000, -8000],  # 各档位总利润
    'Dates_sum': [400, 350, 330, 300, 200, 150, 30, 12, 10],  # 各档位出现的天数
    'Products_sum': [1500, 800, 700, 400, 90, 60, 8, 5, 6],  # 各档位参与的商品种类数
})
profit_tbl['Profit_p_p'] = profit_tbl['Profit_sum'] / profit_tbl['Products_sum'] / profit_tbl['Dates_sum']  # 与平台同口径:总利润/商品数/天数
print('商品总数合计:', profit_tbl['Products_sum'].sum(), ';平台阈值19 ≈ 商品总数×0.005')  # 说明阈值19的由来
for thr in [19, 5, 100]:  # 三档阈值:平台口径19、放宽到5、收紧到100
    kept = profit_tbl.query('Products_sum > @thr')[['Discount', 'Products_sum', 'Profit_p_p']]  # 该阈值下保留的折扣档位
    flip = kept.loc[kept['Profit_p_p'] < 0, 'Discount'].min()  # 平均利润首次转负的折扣档位
    print('=== 阈值 Products_sum > %d:保留 %d 个折扣档位,平均利润首次转负于 %.1f ===' % (thr, len(kept), flip))  # 概览行
    print(kept.to_string(index=False))  # 明细:观察哪些档位被留下、极端负值有多深

预期输出(本机实际运行结果):

商品总数合计: 3569 ;平台阈值19 ≈ 商品总数×0.005
=== 阈值 Products_sum > 19:保留 6 个折扣档位,平均利润首次转负于 0.2 ===
 Discount  Products_sum  Profit_p_p
      0.0          1500    0.333333
      0.1           800    0.214286
      0.2           700   -0.043290
      0.3           400   -0.333333
      0.4            90   -1.388889
      0.5            60   -3.333333
=== 阈值 Products_sum > 5:保留 8 个折扣档位,平均利润首次转负于 0.2 ===
 Discount  Products_sum  Profit_p_p
      0.0          1500    0.333333
      0.1           800    0.214286
      0.2           700   -0.043290
      0.3           400   -0.333333
      0.4            90   -1.388889
      0.5            60   -3.333333
      0.6             8  -50.000000
      0.8             6 -133.333333
=== 阈值 Products_sum > 100:保留 4 个折扣档位,平均利润首次转负于 0.2 ===
 Discount  Products_sum  Profit_p_p
      0.0          1500    0.333333
      0.1           800    0.214286
      0.2           700   -0.043290
      0.3           400   -0.333333

三档阈值的读数可概括为”拐点稳、量级变”:无论阈值取19、5还是100,平均利润首次转负的折扣档位都停在0.2,平台任务”20%折扣是利润正负拐点”的判读在这份合成数据上对过滤参数不敏感——这类”换一个分析参数、检查结论是否随之改变”的检验,方法上叫敏感性分析。但另外两个读数对阈值高度敏感:放宽到 > 5 后,仅有8种、6种商品参与的0.6与0.8档被放进结果,其 Profit_p_p 深达-50与-133,照此绘图会把整个纵轴拉向极端值,常规折扣区间的形态反而看不清;收紧到 > 100 则只剩4个档位,代表性更强,但0.4及以上折扣的信息全部丢失。可见过滤阈值本身就是口径的一部分:报告”最优折扣区间”时,应同时报告阈值及其依据,而不是只报一条曲线。

26.4.4 可视化分析

列表 26.7
# 注:本块是平台任务对应部分的分步讲解版(重述平台代码,非新增分析)。该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# ==================== 设置绘图风格 ====================
sns.set(style='darkgrid')  # 使用Seaborn的深色网格风格,使图表更专业
plt.figure(figsize=(10, 6))  # 创建10x6英寸的画布

# ==================== 绘制多条折线对比不同指标 ====================
metrics = ['Profit_p_p', 'Sales_p_p', 'Quantity_p_p']  # 定义要绘制的三个指标
labels = ['平均利润', '平均销售额', '平均销售数量']  # 定义每个指标的中文标签

for metric, label in zip(metrics, labels):  # 循环绘制三条曲线
    sns.lineplot(
        x='Discount',  # x轴为折扣力度
        y=metric,  # y轴为对应的业务指标
        label=label,  # 设置图例标签
        data=profit_8discount22  # 使用上方平台任务代码块过滤后的数据(变量沿用平台任务)
    )

plt.legend()  # 显示图例,区分不同指标
plt.xlabel('折扣', fontsize=12)  # 设置x轴标签
plt.ylabel('数值', fontsize=12)  # 设置y轴标签
plt.title('折扣与业务指标关系', fontsize=14)  # 设置图表标题
plt.grid(True, alpha=0.3)  # 显示网格线,透明度0.3,辅助读图
plt.tight_layout()  # 自动调整布局
plt.show()  # 显示图表

26.5 分析结论

关键发现:

  1. 利润拐点: 折扣20%是利润正负的拐点
    • 折扣<20%: 利润为正
    • 折扣>20%: 利润为负
  2. 最优折扣: 0.2%和7%折扣时,单位时间利润显著提升
    • 低折扣”小刀”策略刺激购买
    • 提升销量和利润
  3. 极端折扣: 85%, 55%, 57%出现极端负利润
    • 特殊商品的引流活动
    • 不具备代表性

26.6 商业建议

  1. 常规定价: 控制折扣在20%以内
  2. 促销策略: 小幅折扣(0.2%-7%)效果最佳
  3. 避免亏损: 谨慎使用高折扣促销
  4. 差异化定价: 不同品类采用不同折扣策略

26.7 本章小结

本章新增的技能要点(分析结论见上文,此处只列方法):

  1. 先验证业务前提再聚合:用 nunique 检验”每天/每品类折扣是否统一”,其结果决定后续”按折扣直接聚合”是否需要口径修正。
  2. 双重平均口径的设计:总利润 / 商品数 / 天数 同时剔除参与商品数量与持续时间的影响,是本章最核心的指标构造。
  3. 样本量过滤:query('Products_sum > 19') 剔除参与商品过少的折扣档位,防止小样本均值把结论带偏。
  4. 多指标同图对比:三条曲线共用横轴折扣,用于找拐点与形态对照,而非比较绝对数值。
  5. 聚合前的列名重整:agg 之后立即重命名列并在 reset_index 后再绘图,是保证后续代码可读的顺序习惯。

易错点:

  • Discount=0.2 读成”打到2折”:本章口径是降价20%(即打8折),误读会把”折扣控制在20%以内”错放宽成”80%以内”,0.55、0.57、0.85 也会被误当成正常促销区间
  • 直接比较各折扣档位的利润总和:总额混入体量因素,得到的是”主流档位总额最高”的规模结论;定价比较须用 Profit_p_p 的”总利润/商品数/天数”口径
  • 拿 0.55、0.57、0.85 档的极端负利润当一般规律:这些是参与商品极少的引流个例,正是 query('Products_sum > 19') 要过滤掉的小样本
  • 过滤阈值随手取、不随结论交代:阈值决定哪些档位入图,放宽后小样本档的极端负值会把纵轴拉深、掩盖常规折扣区间的形态
  • 三条曲线共用一个纵轴就比较绝对高低:三指标量纲不同,只能读形态与拐点,不能比较曲线之间的数值大小
  • agg 之后跳过列名重整:沿用 Order Date 这类原列名,query('Products_sum > 19') 会因列不存在报错,按列位置切片也会错位

26.8 动手与思考

以下练习每题附参考答案(默认折叠);概念题与变式题可对照自查,商业判断题不设唯一答案,判断依据与口径比结论更重要。

  1. 概念辨析:直接比较各折扣档位的利润总和,与比较 Profit_p_p(单位时间、单位商品的平均利润),结论可能为何不同?本章为什么选择后者?

    参考答案(点开前请先独立完成)

    解题思路:利润总和混入了“体量”因素——无折扣档参与的普通商品多、持续天数长,利润总额天然偏大;深折扣档可能只有少数商品、短期活动,总额自然小。直接比较总额,得到的往往是“主流档位总额最高”这种由体量决定的结论,回答不了“定价打到几折更划算”。Profit_p_p 用“总利润/商品数/天数”的连除(列表 26.1 中平台代码的构造方式),把参与商品数量与持续时间的影响同时剔除,度量的是“同样多的商品、同样长的时间下,哪个折扣档位平均更赚钱”。本章的业务问题是折扣档位之间的定价比较,不是档位之间的规模比较,所以必须用单位化口径;总额口径并非无用,它适合回答“哪个档位贡献了最多利润绝对额”。

    回扣本章:对应“本章新增的技能要点”第2条(双重平均口径的设计:总利润/商品数/天数)。

  2. 概念辨析:按本章口径,Discount=0.2 表示”降价20%(打8折)“。若有人误读为”打到2折”,会在业务决策上造成什么后果?请结合拐点位置说明。

    参考答案(点开前请先独立完成)

    解题思路:本章口径下利润正负的拐点在 Discount=0.2,即降价一旦超过两成、单位平均利润基本转负(列表 26.1 块尾注释的判读)。若把 0.2 误读成“打到2折”(降价80%),数值与含义整个翻转:其一,会以为拐点意味着“降价80%才危险”,从而把“折扣控制在20%以内”的结论错放宽成“控制在80%以内”,等于把安全边界放大了四倍;其二,会把 0.55、0.57、0.85 这些在本章口径下已是深度亏损区的档位,误读成“降价四成多、不算深”的正常促销区间,直接对标它们做活动会带来持续亏损;其三,低折扣“小刀”策略(0.002、0.07 这类小幅降价)的刺激作用也会被低估。折扣字段一个符号的口径没对齐,定价决策的方向就可能完全相反,这正是“先验证业务前提再聚合”的第一步。

    回扣本章:对应“本章新增的技能要点”第1条(先验证业务前提再聚合,字段含义是最大的前提)。

  3. 变式任务:把过滤阈值 Products_sum > 19 分别放宽和收紧一档,检验拐点结论是否稳健——这类检验在方法上叫什么、为什么必要?

    参考答案(点开前请先独立完成)

    改造思路:这类检验在方法上叫敏感性分析(sensitivity analysis)——换一个分析参数(此处是样本量过滤阈值),检查结论是否随之改变。它必要是因为过滤阈值本身就是口径的一部分:阈值决定哪些折扣档位有资格进入图表,若拐点位置对阈值高度敏感,“最优折扣区间”的结论就只在特定阈值下成立。正文已经做过这一实验(列表 26.6,在合成聚合表上把阈值在19、5、100三档切换),结论可概括为“拐点稳、量级变”:三档阈值下平均利润首次转负的折扣档位都停在0.2,但保留的档位数与极端负值的深度对阈值高度敏感。变式代码把阈值参数化后循环:

    # 变式代码:阈值敏感性检验——聚合口径不变,仅切换query中的阈值参数
    profit_8discount = data.groupby('Discount').agg({  # 与平台任务一致的聚合
        'Quantity': 'sum', 'Sales': 'sum', 'Profit': 'sum',  # 三个总量指标
        'Order Date': 'nunique', 'Product Name': 'nunique'  # 天数与商品种类数
    })
    profit_8discount.columns = ['Quantity_sum', 'Sales_sum', 'Profit_sum', 'Dates_sum', 'Products_sum']  # 与平台任务一致的列名重整
    profit_8discount['Profit_p_p'] = profit_8discount['Profit_sum'] / profit_8discount['Products_sum'] / profit_8discount['Dates_sum']  # 与平台任务一致的双重平均口径
    profit_8discount = profit_8discount.reset_index()  # 与平台任务一致
    for threshold in [19, 5, 100]:  # ← 改动点:平台口径19、放宽到5、收紧到100三档切换
        kept = profit_8discount.query('Products_sum > @threshold')  # ← 改动点:阈值由常量19换成循环变量
        flip = kept.loc[kept['Profit_p_p'] < 0, 'Discount'].min()  # 平均利润首次转负的折扣档位
        print('阈值>%d:保留%d个档位,Profit_p_p首次转负于%s' % (threshold, len(kept), flip))  # 三档读数并排输出

    结构性判读:输出由三行概览组成,每行报告一个阈值下保留的档位数与首次转负位置。判读要点:若三行的“首次转负位置”相同,拐点结论对该阈值稳健;若保留档位数或极端负值深度随阈值大幅变化,报告“最优折扣区间”时必须同时报告阈值及其依据,而不是只报一条曲线。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 26.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第3条(样本量过滤防止小样本均值带偏结论,阈值怎么取同样是口径)。

  4. 变式任务:若要按品类(Sub-Category)分别绘制”折扣-平均利润”曲线,代码结构需要怎样扩展?预计会遇到的读图难点是什么?

    参考答案(点开前请先独立完成)

    改造思路:平台代码其实已经算出了“子品类×折扣”的聚合(列表 26.1 中的 huigui03,含各组合的总利润与天数),扩展分三步:在该表上补一列单位时间平均利润;reset_index() 把子品类与折扣从索引还原为普通列(这是能按列绘图的前提);再按子品类循环绘制折线、每品类一条曲线用图例区分(品类多时可改用 sns.relplot(col='Sub-Category', col_wrap=4) 分面成小图矩阵)。

    # 变式代码:按子品类分别绘制“折扣-平均利润”曲线(在平台已有的子品类聚合上扩展)
    category_discount = data.groupby(['Sub-Category', 'Discount']).agg({'Profit': 'sum', 'Order Date': 'nunique'})  # 与平台任务huigui03同型
    category_discount['Profit_pd'] = category_discount['Profit'] / category_discount['Order Date']  # 与平台任务一致:单位时间平均利润
    category_discount = category_discount.reset_index()  # ← 新增:把子品类与折扣从索引还原为普通列,便于按列绘图
    sns.set(style='darkgrid')  # 与平台任务一致
    for sub_category, sub_table in category_discount.groupby('Sub-Category'):  # ← 新增:按子品类逐一循环
        sns.lineplot(x='Discount', y='Profit_pd', data=sub_table, label=sub_category)  # ← 新增:每个子品类一条曲线
    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')  # ← 新增:图例外置,防止长品类名遮挡曲线
    plt.xlabel('Discount Range')  # 与平台任务一致
    plt.ylabel('Profit per day')  # ← 改动点:轴标签换成本变式的指标名

    结构性判读:图形由“一张图三条平均量曲线”变为“一张图多条品类曲线(或多幅分面小图)”,横轴仍是折扣。读图难点有三:其一,子品类数量多,同图曲线容易拥挤,色系与图例要精心安排;其二,各品类体量悬殊,单位时间利润的量纲不可比,曲线高低不能直接当“谁更赚钱”读,只能读各自随折扣的形态与拐点;其三,小样本品类的曲线锯齿大,个别深谷可能是少数订单造成的,应对照该品类的样本量再下判断(与平台任务过滤小样本档位是同一个道理)。

    预期输出:数据文件由教学平台内置,本机无法复现,具体数值以平台运行结果为准。

    注意:以上为本题变式的独立代码;列表 26.1 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应“本章新增的技能要点”第5条(agg 之后重整列名、reset_index() 之后再绘图,是保证代码可读的顺序习惯)。

  5. 商业判断:运营团队计划对标平台结果中极端折扣档位(0.55、0.57、0.85,以平台运行结果为准)对个别网红商品做引流活动。依据本章口径,你支持还是反对?决策前还应补充哪些数据?

    参考答案(点开前请先独立完成)

    参考作答框架:本题不设唯一结论,优秀作答应覆盖以下维度。

    • 口径先行:平台结果显示的这几个档位处于极端负利润,但注意它们正是被平台代码 query('Products_sum > 19') 过滤掉的小样本档位(列表 26.1 块尾注释也判断其为引流个例、不具代表性);且 Profit_p_p 是“单位时间、单位商品”的口径,不是活动整体损益的口径。
    • 分析维度:引流活动的目标函数不是该商品自身的利润,而是流量、新客与连带销售——判断依据应是“毛利牺牲+活动成本”对比“新客获取成本节省+连带客单价提升+复购生命周期价值”,而不是单看该档位的平均利润符号。
    • 风险考量:深度折扣的价格锚定效应(拉低消费者对该商品乃至品类的心理价位)、羊毛党套利、亏损常态化(一次活动被要求无限续期)、平台补贴与合规规则变动。
    • 模型边界:聚合口径看不出单客行为,历史相关性不等于因果;0.55、0.85 档位的负利润来自极少数商品,换一个网红商品未必复现。
    • 还需补充的数据:引流商品订单的连带购买率与整体客单价变化、活动带来新客的复购率与留存曲线、活动前后毛利与退货率对比、竞品同款价格、该商品的历史价格弹性。
    • 常见错误作答形态:一票否决——“平均利润为负所以不能做”(口径错配);或直接照搬0.55、0.85的折扣深度当成“平台验证过的成功经验”;只算该商品的账不算连带与复购的账。

    回扣本章:对应“本章新增的技能要点”第3条(小样本档位的均值不具代表性,引流个例正是被过滤的那类样本)。