目录
关联规则和序列模式
本文基于《Web数据挖掘》(Bing Liu著)一书,是对其中重点内容的摘抄和总结。
本人不学无术,只学习了考试要考的部分,更详细和丰富的内容请参见上述书籍。
0. 概述
关联规则挖掘的目标是在数据项目中找出所有的并发关系,即关联。
关联规则挖掘的经典应用是购物篮分析,目的是找出顾客在商场所选购商品之间的关联(如啤酒和牛奶)。
关联规则挖掘并没有考虑客户在购买商品时的顺序,而这正是序列模式挖掘所要考虑的问题。
1. 关联规则
1.1 关联规则的基本概念
Ⅰ = { i1, i2, …, im }是项目(Item)集合,m是Ⅰ中项目的数目,
T = { t1, t2, …, tn }是(数据库)事务(Transaction)集合,n是T中事务的数目,
其中每个事务 ti 是一个项目集合,并满足 ti ⊆ Ⅰ 。
1.1.1 关联规则的表示
一个关联规则是一个如下形式的蕴涵关系:
X→Y,其中X⊂Ⅰ,Y⊂Ⅰ 且 X∩Y=∅
X(或Y)是项目的集合,称为项集(Itemset)。
例1: 试分析在一个商场里出售的商品间的关联。
Ⅰ表示商场中出售的所有商品。
一个事务 ti 可以简化为一位客户一次购买的商品集合。例如 {牛肉,鸡肉,奶酪},表示一位客户一次购买了牛肉、鸡肉和奶酪这三件商品。
而一条关联规则是:牛肉,鸡肉→奶酪
其中{牛肉,鸡肉}就是X,{奶酪}就是Y。
为了简单起见,在表示事务和规则时,两边的大括号常常略去不写。
1.1.2 关联规则强度的衡量
X在T中的支持计数(Support Count)(表示为X.count)是 T中包含X的事务的数目。
指标:支持度(Support),置信度(Confidence)
(1) 支持度:
T中包含X∪Y的事务的占比(也可以看作是概率 Pr(X∪Y) 的估计)。
规则的支持度表示规则在事务集合T中出现的频繁程度。
支持度 = (X∪Y).count / n
如果支持度太低,则表明相应的规则很可能只是偶然发生的。
在一个商业环境中,一个覆盖了太少案例(或事务)的规则很可能没有任何价值(由于不会盈利,所以不值得为此而采取任何商业措施)。
(2) 置信度:
既包含了X又包含了Y的事务占所有包含了X的事务的百分比(可以看作是概率 Pr(Y| X) 的估计)。
置信度决定了规则的可预测度(也就是 可信度、可靠程度我自己说的)。
置信度 = (X∪Y).count / X.count
如果一条规则的置信度太低,那么从X就很难可靠地推断出Y。
置信度太低的规则在实际应用中也不会有多大用处。
1.1.3 关联规则挖掘
目标:根据给定的事务集合T,找出T中所有满足支持度和置信度分别高于一个用户指定的最小支持度(minsup)和最小置信度(minconf)的关联规则。
key feature:1.所有规则(完整性);2.没有目标项;3.数据量大。
例2:下图是一个包含7个事务的事务集合。每个事务 ti 表示一位客户在商店一次购买的商品集合。集合Ⅰ是商店中出售的所有商品的集合。
给定一个用户指定的最小支持度minsup=30%和最小置信度minconf=80%,则下面这条关联规则是符合要求的(sup是支持度,conf是置信度): 鸡肉,衣服→牛奶[sup=3/7,conf=3/3]
因为它的支持度是42.86%(>30%),置信度是100%(>80%)。
很明显,从以上例子中还可以挖掘出更多的规则。
1.2 关联规则挖掘算法
已经有众多文献提出了大量的关联规则挖掘算法。尽管它们的效率各不相同,但是在同样的关联规则定义下,它们的输出结果都应该是一样的。
换句话说,给定一个事务数据集合T,一个最小支持度阀值和一个最小置信度阀值,则T中存在的关联规则集合是唯一确定的。任何算法,不管它们的计算效率和空间需求的差别有多大,都应该找出同一个规则集合。
在众多算法中,最著名的是Apriori算法。
1.2.1 Apriori算法
Apriori算法分两步进行:
(1)生成所有频繁项集:一个频繁项集(Frequent Itemset)是一个支持度高于minsup的项集。
(2)从频繁项集中生成所有可信关联规则:一个可信关联规则(Confident Association Rule)是置信度大于minconf的规则。
我们称一个项集中项目的个数为该项集的基数,称一个基数为k的项集为k-项集,相应的,称一个基数为k的频繁项集为k-频繁项集。
回顾例2中,{鸡肉,衣服,牛奶}是一个3-频繁项集,因为它的支持度是3/7(minsup=30%)。
更进一步,我们可以从这个频繁项集中生成下述三个关联规则(minconf=80%):
规则1:鸡肉,衣服→牛奶[sup=3/7,conf=3/3]
规则2:衣服,牛奶→鸡肉[sup=3/7,conf=3/3]
规则3:衣服→牛奶,鸡肉[sup=3/7,conf=3/3]
下面逐一介绍此算法的两个步骤。
(1) 生成频繁项集:
——只与支持度有关!!
Apriori算法基于演绎(Apriori)原理(或称为向下封闭属性)来高效地产生所有频繁项集。
向下封闭属性(Downward Closure Property):如果一个项集满足某个最小支持度要求,那么这个项集的任何非空子集必然都满足这个最小支持度。
这个属性是显而易见的,因为如果一个事务包含一个项目集合X,那么它必然包含X的任何非空子集。
根据这个属性和最小支持度阀值就可以将大量的不可能是频繁项集的项集丢弃。
为了确保频繁项集生成的高效性,Apriori算法假定Ⅰ中的项目都采用字典序(Lexicographic Order)排列。在算法中涉及的每个项集也都假定始终保持这个顺序。
我们采用符号 {w[1],w[2],…,w[k]} 来表示一个包含w[1],w[2],…,w[k]这k个项目的k-项集w,并且满足w[1]<w[2]<…<w[k](字典序)。
下图给出了Apriori算法的用于频繁项集生成部分的算法,这个算法基于逐级搜索(Level-wise Search)思想。
它采用多轮搜索的方法,每一轮搜索扫描一遍整个数据集,并最终生成所有的频繁项集。
在第一轮搜索中,算法计算出所有只包含一个项目的项集在事务集合中的支持度,然后根据支持度得到初始的单项目频繁项集的集合,即F1。
随后的每一轮搜索都分为三步(不妨设接下来进行第k轮搜索):
(1)将算法第(k-1)轮搜索生成的频繁项集集合Fk-1作为种子集合产生候选项集集合Ck,Ck中的候选项集都是可能的频繁项集。这个过程由candidate-gen()函数完成。
(2)对整个事务数据库进行扫描,计算Ck中每个候选项集c的支持度。
值得一提的是,在整个计算过程中,我们并不需要将整个数据集加载入内存。事实上,在任何时候我们都只需要在内存中保留一条事务记录,这是Apriori算法的一大特点。由于这个特点,Apriori算法可以用于处理非常巨大的数据集。
(3)在本轮搜索的最后,算法根据支持度将符合最小支持度要求的候选项集(即k-频繁项集)加入Fk中。
算法最终输出所有频繁项集的集合F(算法第13行)。
下面我们进一步介绍candidate-gen()(候选项集集合Ck的生成)函数。
Candidate-gen函数可以分为两步:合并和剪枝。
合并(第2~ 6行):将两个(k-1)-频繁项集合并来产生一个可能的 k-候选项集c(第6行)。其中两个频繁项集f1和f2的前k-2个项目都是相同的,只有最后一个项目不同。随后c被加入到候选项集集合Ck中。
剪枝(第8 ~ 11行):合并得到的候选项集集合并不是最终的Ck。有一部分候选项集可以被剪枝剔除。这一步判断c的所有(k-1)-子集(总共k个)是否都在Fk-1中。如果其中任何一个子集不在Fk-1中,则根据向下封闭原理,c必然不可能是频繁项集,于是可将c从候选集Ck中删除。
注意生成的Ck中只是可能的频繁项集,是否真是频繁项集需要计算支持度。
例4:下面给出在如图事务数据上跑一遍Apriori算法的整个过程,其中假设minsup=30%。
注意:每个频繁项集后的数字表示这个频繁项集的支持计数,也即在事务数据集中包含这个频繁项集的事务的数量。在这个例子中我们将最小支持计数设为3是足够的,因为3/7大于30%,其中7是事务的总数。
F1:{{牛肉}:4,{奶酪}:4,{鸡肉}:5,{衣服}:3,{牛奶}:4}
C2:{{牛肉,奶酪},{牛肉, 鸡肉},{牛肉,衣服},{牛肉,牛奶},{奶酪,鸡肉},{奶酪,衣服},{奶酪,牛奶},{鸡肉,衣服},{鸡肉,牛奶},{衣服,牛奶}
F2:{{牛肉,鸡肉}:3,{牛肉,奶酪}:3,{鸡肉,衣服}:3,{鸡肉,牛奶}:4,{衣服,牛奶}:3}
C3:{{鸡肉,衣服,牛奶}}
F3:{{鸡肉,衣服,牛奶}:3}
(2) 生成关联规则:
——只与置信度有关!!
在许多实际应用中,频繁项集已经足够满足需求了,因此没必要生成关联规则,但也不乏某些应用需要利用关联规则。事实上关联规则只需要基于频繁项集就能生成。和频繁项集的生成过程相比,关联规则的生成要简单得多。
从频繁项集f中提取所有关联规则需要用到f的所有非空子集。
一个看似可行的算法为:设α为f的任一非空子集,则可生成如下的一条关联规则:
(f-α)→α,如果满足 置信度= f.count/(f-α).count ≥minconf
简单来说就是,把f拆为互不相交的两个集合,看是否满足置信度要求(我自己说的)
Apriori算法实现(python)
import pandas as pd
import itertools
def concat_func(row):
"""定义拼接函数,对购物篮中商品进行去重"""
return pd.Series({
'商品': ','.join(map(str, row['产品名称'].unique())),
})
def count_nums(data, C):
"""对所有事务扫描一遍,统计x.count
:param data: 事务集T
:param C: 待统计的候选集Ck,其中商品名称用,连接
"""
# 通过字符串进行匹配:将c中商品名称split,看是否在一项事务中完整出现
for i, items in data.iterrows():
items = items[0]
for key in C:
names = key.split(',')
n = len(names)
for j in range(n):
if items.find(names[j]) == -1:
break
if j == n-1:
C[key] = C[key] + 1
def get_F(C, support):
"""根据Ck得到Fk
:param C: Ck
:param support: minsup
:return: Fk,其中F是商品列表,F_是字典{x:x.count}
"""
F = []
F_ = {}
for item in C:
if C[item] >= support:
F.append(item)
F_[item] = C[item]
return F, F_
def candidate_gen(F):
"""生成候选项集集合
:param F: F(k-1)
:return: Ck
"""
C = {}
nums = len(F)
# (1)合并
# 只有最后一项不同:最后一个,前的所有内容相同
for i in range(nums):
f1 = F[i]
for j in range(i+1, nums):
f2 = F[j]
index = f2.rfind(',') + 1
f2_front = f2[:index]
if f1.startswith(f2_front):
c = f1 + ',' + f2[index:]
# (2)剪枝
count = c.count(',')
begin2 = -1
for k in range(count):
begin1 = c.find(',', begin2+1)
if begin1 == -1:
name = c[:begin2]
else:
begin2 = c.find(',', begin1 + 1)
if begin2 == -1:
name = c[:begin1]
else:
name = c[:begin1] + c[begin2:]
if not (name in F):
break
if k == count-1:
C[c] = 0
return C
def apriori(data, support, confidence):
# 转化为次数,方便
n = len(data)
support = n * support
# 1.生成所有频繁项集
F = {}
print("正在生成频繁项集……")
# 第一轮搜索
C1 = {} # key是商品名称x,value是x.count
for i, items in data.iterrows():
items = items[0].split(',')
for item in items:
if item in C1:
C1[item] = C1[item] + 1
else:
C1[item] = 1
F1, F1_ = get_F(C1, support) # 方便操作,二者的区别是有无x.count
F1.sort() # 不是按照常规顺序(拼音)排的,但没关系,只要有序就行(我自己说的)
F1_ = dict(sorted(F1_.items(), key=lambda x: x[0]))
F.update(F1_)
Fk_last = F1
# 后续搜索
while len(Fk_last) > 1:
Ck = candidate_gen(Fk_last)
count_nums(data, Ck)
Fk, Fk_ = get_F(Ck, support)
Fk.sort()
Fk_ = dict(sorted(Fk_.items(), key=lambda x: x[0]))
F.update(Fk_)
Fk_last = Fk
print("正在生成关联规则……")
# 2.生成所有关联规则
rules = []
for f in F:
nums = f.count(',')
if nums == 0:
continue
# 划分f为两部分,并计算置信度
maxsize = nums // 2 + 1
names = f.split(',')
# x(即f-a)中所含的item数
for i in range(1, maxsize+1):
# 利用组合方法生成
xs = list(itertools.combinations(names, i))
for x in xs:
x = ','.join(x)
conf = F[f] / F[x]
# 存储结果
if conf >= confidence:
# 生成Y
x_name = x.split(',')
y = f
for m in range(len(x_name)):
y = y.replace(x_name[m]+",", "")
y = y.replace(","+x_name[m], "")
# 生成规则
rule = [x, y, F[f]/n, conf]
rules.append(rule)
# 3.输出结果
result = pd.DataFrame(rules, columns=['X', 'Y', 'support', 'confidence'])
return result
if __name__ == "__main__":
inputFile = 'data/data.xlsx'
outputFile = 'result/apriori.xlsx'
df = pd.DataFrame(pd.read_excel(inputFile))
# 设置参数
minsup = 0.3
minconf = 0.8
apriori(df, minsup, minconf).to_excel(outputFile)
print("done")
输入上述例2事务集得到:
2. 序列模式
关联规则挖掘不考虑事务间的顺序,然而在很多应用中这样的顺序是很重要的。比如在购物篮分析中,了解顾客购买商品时的顺序是很有意义的。比如一般顾客会先买床,过一阵子之后再买床单。在Web数据挖掘中,从用户浏览网页的顺序中挖掘网站的浏览模式是很有用的。在文本挖掘中,根据词在句子中的顺序挖掘语言模式也是非常重要的。对于这些应用来讲,关联规则不适用,这时候就需要序列模式挖掘。
2.1 序列模式的基本概念
2.1.1 序列模式的表示
Ⅰ={i1, i2, …, im}是项目的集合。
一个序列(Sequence)是一个排过序的项集列表(回顾一下之前的定义,项集X是一个非空的项目集合X⊆Ⅰ)。一个序列 s 表示为<a1a2a3>,其中ai是一个项集,也称为 s 的一个元素。不失一般性,我们假设序列元素中的项目都是以字典序排列的。
一个项目在一个序列的某个元素中只能出现一次,但可以出现在多个不同的元素中。
一个序列中元素(或项集)的个数称为该序列的基数。
一个序列中项目的个数称为该序列的长度,长度为k的序列称为k-序列。如果某个项目在一个序列的多个元素中出现,其每次出现都累加到k上。
对于两个序列s1=a1a2…ar>和S=<b1b2…bv>,如果存在整数1≤j1<j2<…<jr-1<jr≤v 使得a1⊆bj1, a2⊆bj2, …, ar⊆bjr, 则称s1为s2的子序列,反过来s2是s1的超序列,或者称s2包含s1。
就是s2中有不连续的一段,其中每个元素都是s1中元素的超集,且要按顺序。我自己说的
例5:设Ⅰ={1,2,3,4,5,6,7,8,9}
序列<{6}{3,7){9}{4,5,8}{3,8}>包含<{3}{4,5}{8}>。因为{3}⊆{3,7},{4,5)⊆{4,5,8},{8}⊆{3,8}。
<{3}{8}>和<{3,8}>并不相互包含。
序列<{3}{4,5}{8}>的基数为3,长度为4。
2.1.2 序列模式挖掘
目标:给定一个输入数据序列(Data Sequences)集合S(或称序列数据库,Sequence Database),序列模式挖掘问题是指找出所有满足用户指定的最小支持度的序列。
每个这样的序列称为一个频繁序列(Frequent Sequence),或者一个序列模式(Sequential Pattern)。
一个序列的支持度是指s中包含有该序列的数据占总数据的比重。
2.2 序列模式挖掘算法——GSP算法
GSP的算法思想基本和Apriori算法一样。算法最大的不同在于候选生成函数candidate-gen-SPM()。
我们仍然采用Fk来表示所有k-频繁序列的集合,采用Ck来表示所有候选k-序列的集合。