阅读我们的文章

class="lazyload

商品组合优化是零售业中的关键环节,旨在在兼顾诸多物流限制的前提下,精心策划最理想的产品组合以满足消费者需求。零售商必须确保在恰当的时间提供恰当的产品,并保持恰当的库存量。通过利用数据和消费者洞察,零售商能够基于客户偏好、季节性趋势和销售模式,就应采购哪些商品、如何管理库存以及优先采购哪些产品做出明智决策。

对于零售企业而言,商品组合优化对于在多样性效率之间取得平衡至关重要。选择过少可能会导致顾客流失,而选择过多则可能引发顾客困惑、库存积压以及利润率下降。通过优化商品组合,企业既能确保畅销商品有货,又能淘汰那些占用宝贵货架空间却表现不佳的产品,从而提升顾客满意度。

选择建模是优化商品组合的一种有效方法,因为它提供了一个数据驱动的框架,有助于理解顾客偏好并预测他们在不同产品之间的选择倾向。通过分析价格敏感度、产品特性及品牌忠诚度等各种因素,选择建模能帮助零售商确定哪些产品最有可能满足顾客需求。

归根结底,选择建模能够帮助零售商提供恰当的产品组合,根据特定客户群定制商品种类,还能优化货架空间以提升盈利能力,甚至影响商品定价。

如果您从未听说过选择建模,可以阅读我们那篇通过实例介绍其核心概念的文章。本文将主要探讨如何利用离散选择模型来优化产品组合。我们提供了基于choice-learn库的代码示例,该库旨在帮助数据科学家处理此类应用场景。

提供的代码使用了 choice-learn Python 包,可在笔记本中找到 此处

环境搭建:安装 Python 和 Choice-Learn

在本文中,我们提供了与说明配套的代码片段。该代码使用了Choice-Learn库,该库为选择建模及多种应用(例如商品组合优化或定价)提供了高效的工具。Choice-Learn 可通过 PyPI 获取,您只需执行以下命令即可安装:


pip install choice-learn

数据集:销售收据

我们将使用TaFeng杂货数据集。您可以从Kaggle下载该数据集,并在Python环境中使用choice-learn将其打开:

from choice_learn.datasets import load_tafeng
tafeng_df = load_tafeng(as_frame=True)
print(tafeng_df.head())
class="lazyload

该数据集包含一家中国杂货店超过80万笔的单笔购买记录。每笔购买记录都提供了详细信息,包括所购商品(PRODUCT_ID)、销售价格(SALES_PRICE)以及顾客的年龄段(AGE_GROUP)。

您可以发现,虽然提供了许多不同的商品,但其中有些很少有人购买。为了优化物流,零售商可能会选择减少所售商品的种类。在此情况下,目标是确定最优的商品子集进行销售。

为了实现这一目标,我们重点关注畅销商品,因为这些商品再次被购买的可能性更高,且在构建更高效、更盈利的商品组合方面将发挥关键作用。请注意,我们这样做主要是为了简化示例,实际上所有商品都可以保留。

# 仅保留销量前20的产品
tafeng_df = tafeng_df.loc[
tafeng_df.PRODUCT_ID.isin(tafeng_df.PRODUCT_ID.value_counts().index[:20])
].reset_index(drop=True)
# 移除 NaN 值
tafeng_df = tafeng_df.loc[
tafeng_df.AGE_GROUP.isin([“25-29”,“40-44”,“45-49”,“>65”,“30-34”,“35-39”,“50-54”,“55-59”,“60-64”] )
].reset_index(drop=True)
print(tafeng_df.head())

我们还用“one-hot”编码将年龄分组,每十年为一个类别:

# 对年龄组进行编码
tafeng_df[“twenties”] = tafeng_df.apply(lambdarow:1 ifrow[“AGE_GROUP”] ==“25-29” else 0,axis=1)
tafeng_df[“三十多岁”] = tafeng_df.apply(
lambdarow:1 ifrow[“AGE_GROUP”]in([“30-34”,“35-39”])else 0,axis=1
)
tafeng_df[“forties”] = tafeng_df.apply(
lambdarow:1 ifrow[“AGE_GROUP”]in([“40-44”,“45-49”])else 0,axis=1
)
tafeng_df[“fifties”] = tafeng_df.apply(
lambdarow:1 ifrow[“AGE_GROUP”]in([“50-54”,“55-59”])else 0,axis=1
)
tafeng_df[“sixties_and_above”] = tafeng_df.apply(
lambdarow:1 ifrow[“AGE_GROUP”]in([“60-64”,“>65”])else 0,axis=1
)

既然数据已经准备就绪,我们需要创建一个ChoiceDataset,choice-learn 中的数据处理对象。这需要指定描述购买行为发生背景的特征:

  • 客户特征(共同特征):年龄段
  • 产品特性(商品详情):商品价格

选择模型的一个关键点在于,我们需要获取购买时所有可用商品的特征,而不仅仅是最终选中的那件。这使我们能够分析不同产品的价格如何影响消费者的决策。由于数据集中并未直接提供这些信息,因此我们假设:对于每次购买,其他商品的价格均与前一次销售时保持一致。

# 将产品 ID 映射到索引
id_to_index =
fori, product_idin enumerate(np.sort(tafeng_df.PRODUCT_ID.unique())):
id_to_index[product_id] = i
# 初始化商品价格
prices = [[0]for_in range(len(id_to_index))] fork, vinid_to_index.items():
prices[v][0] = tafeng_df.loc[tafeng_df.PRODUCT_ID == k].SALES_PRICE.to_numpy()[0] # 创建构成 ChoiceDataset 的数组
shared_features = [] items_features = [] choices = [] # 对于每个已购买的商品,我们保存:
# – 客户的年龄表示(one-hot编码)
# – 所有售出商品的价格
fori, rowintafeng_df.iterrows():
item_index = id_to_index[row.PRODUCT_ID] prices[item_index][0] = row.SALES_PRICE
shared_features.append(
row[["twenties","thirties","forties","fifties","sixties_and_above"]].to_numpy()
)
items_features.append(prices)
choices.append(item_index)

既然我们已经有了所有信息,就可以创建 ChoiceDataset 了:

fromchoice_learn.dataimportChoiceDataset
dataset = ChoiceDataset(
shared_features_by_choice=shared_features,
shared_features_by_choice_names=[‘twenties’,‘thirties’,‘forties’,‘fifties’,‘sixties_and_above’],
items_features_by_choice=items_features,
items_features_by_choice_names=[“SALES_PRICE”],
choices=choices
)

定义和估计选择模型

我们将构建并拟合一个选择模型,用于预测客户从一整套相似产品中选择特定商品的概率。基于现有数据集,针对客户j考虑的商品i,我们定义如下效用函数

class="lazyload

该函数表示顾客选择某件商品所获得的效用(或满意度),其受顾客年龄和商品价格的双重影响。

有关我们如何构建效用函数的更多细节,请参阅我们的第一篇文章。需要注意的是,另一种合乎逻辑的模型(但为了简化起见未在此呈现)是针对每个年龄段估算一个价格敏感度。

以下是使用 choice-learn 估计此类模型的代码:

fromchoice_learn.modelsimportConditionalLogit
model = ConditionalLogit(optimizer="Adam",batch_size=1024,epochs=300,lr=0.002)
forage_categoryin[“二十多岁”,“三十多岁”,“四十多岁”,“五十多岁”,“六十岁及以上”]:
model.add_coefficients(
coefficient_name=age_category, feature_name=age_category,items_indexes=list(range(20))
)
model.add_shared_coefficient(
coefficient_name="price", feature_name="SALES_PRICE",items_indexes=list(range(20))
)
hist = model.fit(dataset)

你可以验证该模型是否与数据集吻合良好:

importmatplotlib.pyplotasplt
plt.plot(hist[“train_loss”])
plt.xlabel(“Epoch”)
plt.ylabel(“Negative Log Likelihood”)
plt.show(
class="lazyload

确定最佳产品组合

有了这些购买概率,我们现在可以使用以下公式来估算商品组合A的平均每客单价:

class="lazyload

为了找出能最大化收益的商品组合,我们可以评估所有可能的组合,并选择平均收益最高的那个。不过,更有效的方法是使用线性规划(LP)。本文将重点介绍如何使用choice-learn实现的商品组合优化器。

必须区分“最大化收入”与“最大化利润率”。虽然收入很重要,但利润率会将每件产品相关的成本纳入考量。根据您的目标,您可能更倾向于优化利润而非单纯追求收入。

为了优化产品组合,我们需要提供以下几个关键信息:

  • 我们希望赋予每个年龄段的权重,就按其客户占比来定吧
  • 各年龄段中每项指标的效用值(由我们的选择模型计算得出)
  • 每个项目的优化目标(此处指收入)
  • 套装的数量(例如,12件)

以下是使用choice-learn 的操作方法:


fromchoice_learn.toolbox.assortment_optimizerimportLatentClassAssortmentOptimizer
# 每件商品的价格
future_prices = np.stack([items_features[-1]]*5,axis=0)
age_category = np.eye(5).astype("float32")
# 根据商品价格和各年龄段计算每件商品的效用值
predicted_utilities = model.compute_batch_utility(shared_features_by_choice=age_category,
                                                  items_features_by_choice=future_prices,
                                                  available_items_by_choice=np.ones((5,20)),
                                                 choices=None
                                                  )
age_category_weights = np.sum(shared_features,axis=0) /len(shared_features)
opt = LatentClassAssortmentOptimizer(
solver="or-tools",# 使用的求解器,可选 "or-tools" 或 "gurobi" (如持有许可证)
class_weights=age_category_weights,# 各年龄段的权重
class_utilities=np.exp(predicted_utilities),# 效用值,形状为 (n_classes, n_items)
itemwise_values=future_prices[0][:,0],# 每个商品需优化的值,此处为用于计算周转率的售价
assortment_size=12)# 所需的商品组合规模
assortment, opt_obj = opt.solve()

运行代码后,你应该会看到类似以下内容:


class="lazyload

向量中数值为1的索引所对应的商品组合,是能够实现收入最大化的最优方案。理论上,该商品组合可带来人均134元的平均收入。您可以尝试其他组合,但它们带来的平均收入都会低于此方案。

另一个目标可能是最大限度地提高销售量。在这种情况下,所有商品的优化目标值均设为1,从而得出不同的最优商品组合。

当引入额外约束条件时,该方法的效率便显而易见。例如,您可能需要考虑店铺货架空间的限制。在这种情况下,您可以优化商品组合,确保其总尺寸不超过可用货架空间。本文将演示这一额外约束条件,以及定价策略等其他约束条件。


结论

如果你正在从事商品组合优化或定价工作,选择建模是一个非常有用的工具,一定要了解一下。Choice-Learn 在其GitHub 上提供了许多精彩的示例。去看看吧,如果觉得有用,别忘了点个星!