目的:对于现烤烘焙类产品进行门店各天的商品选品推荐,组合推荐
算法选择:序列挖掘算法 (关联规则Apriori)
模型思路及代码API:
模型思路类似GSP算法:
首先按照星期来划分数据集,周一至周日共7条线,或者按照工作日与非工作日来区分数据集属性。若使用一年的数据,那每个星期的数据源可以有365/7 =52条(考虑近期时间越准确的因素)。
对于现烤类产品,时间点的预测把商品组合把控度较严格,所以按照全天的售卖时间线来做各点可用商品组合选取。
基于Aprior算法上最改进及更符合实际应用需求,GSP算法是Aprior算法的一种,相较于Aprior算法可以有效减少需扫描的候选序列,减少无用模式的产生。原始Aprior算法是全遍历形式,耗时很高吃内存。Aprior算法简单的运行遍历图如下:
算法输入表分两部分,引入了销量和及销售频次两个指标来进行选品筛选。如下销售表,若销售数量>0则定义为有销售 1,否则为0。
商品组合的SKU数在算法中可理解为SKU序列个数为k时对应的support值情况,该序列产品集最易覆盖销售需求面。Support频次概率指标数是基于商品集最易覆盖销售面的考量;而salesum销售和指标;门店实际业务设立k值范围15-20sku数,仅打印算法该部分商品组合结果做后续时间线接替。
API 函数下面。
第一部分: 结果构建每个时间点下最易售卖及销量和较高的产品组合集:
第二部分:确定时间序列下的商品组合最优接替选项(采用全匹配):
确定接替选项选取指标值:p1(组合销量和) p2(组合销售频次) p3(组合间接替性)
假设时间点A的组合选项为A1:[a,b,c,d,e] A2:[a,b,d,f] A3:[c,d,e,f]
时间点B的组合选项为B1:[a,b,e,f] B2:[a,c,d,e,f,g] B3:[b,c,d]
时间点C……
即指标值表现为:
(p3为商品组合交集个数*2/组合并集总数 代表商品组合间的接替性)
各自进行标准化后加和作为综合指标值。
按综合指标值来确定最优时间线上的商品组合接替排序。还可以引入现烤产品的烤制时间,烤制批数等因素考虑。
思路总结:规则算法对于业务细节的抠点和关键点需要紧密斜街和设立相应规则指标来符合现实业务情况,业务跟进及理解并用于模型构建要求度高;规则的按规设立表现在算法运行层面上会导致更改复杂及运行时间复杂度高,很多规则需要全条件匹配思考。目前并没有对运行时间上进行优化,对于整理算法的主要是基于业务转化规则的构思,而对于传统机器理论并没有较多涉及。
git:https://github.com/lpvote/application_exploration.git
主要API:
def generate_new_combinations(old_combinations):
items_types_in_previous_step = np.unique(old_combinations.flatten())
for old_combination in old_combinations:
max_combination = max(old_combination)
for item in items_types_in_previous_step:
if item > max_combination:
res = tuple(old_combination) + (item,)
yield res
def apriori_addsum(df, df_sum, min_support=0.5, use_colnames=False, min_len=1, max_len=None, n_jobs=1):
'''
#将销量指标引入进来
#销量表:df_sum 有无销售表:df--用于计算关联规则范围 两张表索引一致
#不使用销量和做排序选取,后续取单品成本利润 做利润和 排序选取 先实现算法
support:销售频次概率 低于min_support 的单品id不需要
ary_col_idx: 单品id
support_salesum: 单品销量和 --后续替换利润和
'''
# df_sum = A_weekhour_sum
# df = A_weekhour
# min_support=0.03
# use_colnames=True
# max_len=3
# n_jobs=1
allowed_val = {0, 1, True, False}
unique_val = np.unique(df.values.ravel()) #去除重复值
for val in unique_val:
if val not in allowed_val:
s = ('The allowed values for a DataFrame'
' are True, False, 0, 1. Found value %s' % (val))
raise ValueError(s)
is_sparse = hasattr(df, "to_coo")
if is_sparse: #False
X = df.to_coo().tocsc()
support = np.array(np.sum(X, axis=0) / float(X.shape[0])).reshape(-1)
else:
X = df.values
sale_sum = df_sum.values
support = (np.sum(X, axis=0) / float(X.shape[0])) #support:每个单品的有销售天数/总天数 = 销售频次概率
support_salesum = np.sum(sale_sum, axis=0) #每个单品的总销量
ary_col_idx = np.arange(X.shape[1]) #单品id
support_dict = {1: support[support >= min_support]} #support:销售频次概率
itemset_dict = {1: ary_col_idx[support >= min_support].reshape(-1, 1)}
salesum_dict = {1: support_salesum[support >= min_support]}
max_itemset = 1
rows_count = float(X.shape[0])
if max_len is None:
max_len = float('inf')
while max_itemset and max_itemset < max_len: #从1 至max值 循环;每次循环+1
next_max_itemset = max_itemset + 1
combin = generate_new_combinations(itemset_dict[max_itemset]) #构建全单品id关联-索引结构
frequent_items = []
frequent_items_support = []
frequent_items_salesum = []
if is_sparse: #False
all_ones = np.ones((X.shape[0], next_max_itemset))
for c in combin: #exp:(27, 29, 32) 单品id关联
if is_sparse: #False
together = np.all(X[:, c] == all_ones, axis=1)
together_sum = np.all(X[:, c] == all_ones, axis=1)
else:
together = X[:, c].all(axis=1) #exp:(27, 29, 32) 索引至X→df表值 ;判断是否全部为1即全部有销售返回Ture否则返回F
together_sum = sale_sum[:, c].sum(axis=1)
support = together.sum() / rows_count #全部有销售的商品组合 频次概率值
support_salesum = together_sum.sum() / rows_count #全部有销售的商品组合 销量总和日均
if support >= min_support:
frequent_items.append(c)
frequent_items_support.append(support)
frequent_items_salesum.append(support_salesum)
if frequent_items: #结果构建为dict;k=1数值不合理 是不要的
itemset_dict[next_max_itemset] = np.array(frequent_items)
support_dict[next_max_itemset] = np.array(frequent_items_support)
salesum_dict[next_max_itemset] = np.array(frequent_items_salesum)
max_itemset = next_max_itemset
else:
max_itemset = 0
#字典拆开填充表内
all_res = []
for k in sorted(itemset_dict):
#单品的sku种类数为大于min_len个时关联规则可利用做打印 min_len=15
if k >= min_len:
support = pd.Series(support_dict[k])
itemsets = pd.Series([frozenset(i) for i in itemset_dict[k]])
salesumsets = pd.Series(salesum_dict[k])
res = pd.concat((support, itemsets, salesumsets), axis=1)
if res.shape[0] >= 5:
res_sumsort = res.sort_values(by=[2],ascending=False).head() #按销量和排序取前5的商品组合
res_countsort = res.sort_values(by=[0],ascending=False).head() #按销售频次排序取前5的商品组合
else :
res_sumsort = res.sort_values(by=[2],ascending=False)
res_countsort = res.sort_values(by=[0],ascending=False)
all_res.append(res_sumsort)
all_res.append(res_countsort)
#k>=15的关联规则列表,k筛选范围,组合过多了后续选择根本无法串联匹配
res_df = pd.concat(all_res)
res_df.columns = ['support', 'itemsets', 'salesumsets']
res_df = res_df.drop_duplicates()
if use_colnames: #True
mapping = {idx: item for idx, item in enumerate(df.columns)}
# global atest,btest,ctest,dtest,etest
# atest = mapping
# btest = res_df
# ctest = itemset_dict
# dtest = support_dict
# etest = all_res
res_df['itemsets'] = res_df['itemsets'].apply(lambda x: [mapping[i] for i in x])
res_df = res_df.reset_index(drop=True)
return res_df