本文是个人对分析商品间关联关系的一篇总结。
不同于找相似商品,关联关系想要找到商品间有潜在购买关系,比如啤酒尿布,香烟和打火机,炒菜锅和炒勺等等。
首先从Apriori开始讲起:
关联规则简述
此处大部分是对一篇英文博客的理解,原地址找不到了…
Association rules analysis is a technique to uncover how items are associated to each other. There are three common ways to measure association.
关联规则分析是一种揭示item如何相互关联的技术。 度量关联性有三种常用方法。
Measure 1: Support支持度. This says how popular an itemset is, as measured by the proportion of transactions in which an itemset appears. In Table 1 below, the support of {apple} is 4 out of 8, or 50%. Itemsets can also contain multiple items. For instance, the support of {apple, beer, rice} is 2 out of 8, or 25%.
这表示项目集的受欢迎程度,在所有交易记录中出现越多,值越大。 在下面的表1中,{apple}的支持是8个中的4个,或50%。 项目集还可以包含多个项目。 例如,{apple,beer,rice}的支持是8个中的2个,或25%。
通俗解释:简单地说,X=>Y的支持度就是在所有交易记录中,物品集X和物品集Y同时出现的概率。每一条Transaction 就是一条交易或浏览记录
Measure 2: 置信度. This says how likely item Y is purchased when item X is purchased, expressed as {X -> Y}. This is measured by the proportion of transactions with item X, in which item Y also appears. In Table 1, the confidence of {apple -> beer} is 3 out of 4, or 75%.
这表示购买商品X时购买商品Y的可能性,表示为{X - > Y}。 在表1中,{apple - > beer}的置信度是4中的3或75%。
通俗解释:简单地说,置信度就是指在购买了X的交易记录中,物品集Y也同时出现的概率有多大。
缺陷:没有考虑Y的受欢迎程度。如果Y物品也很受欢迎,那么含有X的记录中,也大概率会出现Y,从而提高了confidence的值。
Measure 3: Lift. This says how likely item Y is purchased when item X is purchased, while controlling for how popular item Y is. In Table 1, the lift of {apple -> beer} is 1,which implies no association between items. A lift value greater than 1 means that item Y is likely to be bought if item X is bought, while a value less than 1 means that item Y is unlikely to be bought if item X is bought.
这表示购买商品X时购买商品Y的可能性,同时控制商品Y的受欢迎程度。 在表1中,{apple - > beer}的lift值为1,这意味着项目之间没有关联。 lift大于1意味着如果购买物品X则可能购买物品Y,而小于1的值意味着如果购买物品X则不太可能购买物品Y.
在实际的应用中,通常使用Lift作为关联关系的度量准则:
Lift(apple->beer)= confidence(apple->beer)/support(beer) <=> P(AB)/[P(A)*P(B)]
关联规则挖掘任务分解为如下两个主要的子任务
- 频繁项集产生:目标是发现满足最小支持度阈值的所有项集,这些项集称作频繁项集(frequent itemset)。
- 关联规则的产生:目标是从上一步发现的频繁项集中提取所有高置信度的规则,这些规则称作强规则(strong rule)。
通常,频繁项集产生所需的计算开销远大于产生规则所需的计算开销。
经典Apriori算法的两个非常重要的结论
Apriori定律2:如果一个集合不是频繁项集,则它的所有超集都不是频繁项集。
Apriori定律1:如果一个集合是频繁项集(frequent set,满足最小支持度阈值的所有项集),则它的所有子集都是频繁项集。
FPGrowth
极大地提升了经典算法的效率。上学时候不懂效率的重要性,工作后,动辄上亿的数据量,效率为王。
原理参考博客:https://blog.csdn.net/yutao03081/article/details/77127500
大概工作流程如下:
首先构建FP树 ,然后利用它来挖掘频繁项集。为构建FP树 ,需要对原始数据集扫描两遍。第一遍对所有元素项(单个项目)的出现次数进行计数。根据Apriori原理,即如果某元素是不频繁的,那么包含该元素的超集也是不频繁的,所以就不需要考虑这些超集。数据库的第一遍扫描用来统计出现的频率,而第二遍扫描中只考虑那些频繁元素。
业务分析:
为了分析用户购买行为模式,找到购买商品之间潜在的联系。在本次测试中,只取一个月的浏览,加购记录。每一个用户一天的行为作为一条Trans。如果后期有要求,可以将行为更细分到一定时间之内,比如10min,一小时等。
在FPGrowth中,比较重要的参数是miniSupport。这个参数在网上找了好久,很少有人说怎么设置,只有一个建议是说根据业务需要,考虑售卖多少商品会对利润带来影响,由此设置。我在此处分析商品在一个月内被浏览,加购,收藏次数的百分位数(如果有更好的方法还请大神在下面讨论),过滤一些冷门商品,确定一个minisupport。
业务上只关心2项频繁项集,即A->B的这种形式,让算法递归完成整棵树是非常耗时和浪费内存的,因此考虑更改源码。
先逐步分析源码(基于scala):
def run[Item: ClassTag](data: RDD[Array[Item]]): FPGrowthModel[Item] = {
if (data.getStorageLevel == StorageLevel.NONE) {
logWarning("Input data is not cached.")
}
val count = data.count()
val minCount = math.ceil(minSupport * count).toLong
val numParts = if (numPartitions > 0) numPartitions else data.partitions.length
val partitioner = new HashPartitioner(numParts)
val freqItems = genFreqItems(data, minCount, partitioner)
val freqItemsets = genFreqItemsets(data, minCount, freqItems, partitioner)
new FPGrowthModel(freqItemsets)
}
spark MLlib FPGrowth要求输入数据data的形式是RDD[Array[Item]],每一个Array表示一条Trans,一个Item表示一件商品。genFreqItems用于过滤输入数据集,得到满足最小支持度的Item,并按照降序排列,得到频繁项。genFreqItemsets用于计算频繁项集,这一步最为耗时,也是需要修改的地方。
/**
* Generate frequent itemsets by building FP-Trees, the extraction is done on each partition.
* @param data transactions
* @param minCount minimum count for frequent itemsets
* @param freqItems frequent items
* @param partitioner partitioner used to distribute transactions
* @return an RDD of (frequent itemset, count)
*/
private def genFreqItemsets[Item: ClassTag](
data: RDD[Array[Item]],
minCount: Long,
freqItems: Array[Item],
partitioner: Partitioner): RDD[FreqItemset[Item]] = {
val itemToRank = freqItems.zipWithIndex.toMap
data.flatMap { transaction =>
genCondTransactions(transaction, itemToRank, partitioner)
}.aggregateByKey(new FPTree[Int], partitioner.numPartitions)(
(tree, transaction) => tree.add(transaction, 1L),
(tree1, tree2) => tree1.merge(tree2))
.flatMap { case (part, tree) =>
tree.extract(minCount, x => partitioner.getPartition(x) == part)
}.map { case (ranks, count) =>
new FreqItemset(ranks.map(i => freqItems(i)).toArray, count)
}
}
其中extract是核心的迭代方法,用于提取以某个频繁项集为后缀(suffix)的频繁项集。在业务中只取2项频繁项集,要知道每一次迭代都是向频繁后缀(valid suffix)前加入一个item,这里需要加入一个限制条件,限制迭代只进行两次,或者限制只取后缀长度为1的情况再迭代。我这里选择第一种情况,加入一个level,表示迭代终止条件,这样就只会产生A->B的情况,不考虑{A,C}->B等情况。
def extract(minCount: Long,
level: Int = 0,
validateSuffix: T => Boolean = _ => true): Iterator[(List[T], Long)] = {
summaries.iterator.flatMap { case (item, summary) =>
if (validateSuffix(item) && summary.count >= minCount && level <= 1) {
Iterator.single((item :: Nil, summary.count)) ++
project(item).extract(minCount, level + 1).map { case (t, c) =>
(item :: t, c)
}
} else {
Iterator.empty
}
}
}
这样,在有7千万条trans的情况下, 大概能跑40mins,效果可以接受。原生代码没有设置lift的地方,稍微写下即可得到每个关联规则的lift,这样对于每个商品,可以根据lift排序,选择一个召回商品池。根据实际效果,可以取top10得到最近似的商品(大多数是同品牌不同型号),以及后5得到有关联的商品(大多是相关产品)。
val lift = confidenceA2B / supportB
总结完毕。