假设你是商场的一名推销员,正与一位刚在商店买了面包的顾客交谈。你应该向她推荐什么产品?你应该想知道你的顾客在购买了面包之后频繁的购买的哪些物品,这些信息是非常有用的。在这种情况下,频繁模式和关联规则正是你想要挖掘的知识。
频繁模式(frequent pattern)是指频繁地出现在数据集中的模式,例如频繁的同时出现在交易数据集中的商品(比如牛奶和面包)集合是频繁项集。
如果我们想象全域是商店中商品的集合,则每种商品可以代表一个布尔变量,表示该商品是否被购买过。那么可以用一个“商品A=>商品B”这种形式的布尔向量来表示商品A与商品B的关联程度。如何去衡量这个关联程度呢?我们引入两个测距:支持度与置信度。
支持度(support)代表该关联规则的重要性,定义为集合中商品A出现且商品B出现的次数除以总的商品个数,即support(A=>B) = P(A U B)。
置信度(confidence)代表该关联规则的可靠性,定义为集合中商品A出现且商品B出现的次数除以商品A出现的次数,即confidence(A=>B) = P(B | A)。
因为我们认为满足最小支持度阈值(min_sup)和最小置信度阈值(min_conf)的规则是强规则。这两个最小阈值是由相关领域的专家人为给定的。
项的集合称为项集。包含k个项的项集称为k项集。比如之前提到的面包和牛奶这些商品就可以理解为项,某次购买中购买了商品的集合{牛奶,面包,坚果}就可以称为项集,在本例中也就是3项集。若该项集的支持度与置信度均大于给定的最小值,那么就认为该项集是频繁项集,记作Lk。
那么如何找到这样强关联规则呢?由置信度的定义可以得到
confidence(A=>B) = P(B | A) = support(A U B) / support(A) = support_count(A U B) / support_count(A)
也就是说,如果获得了AUB、A的支持度计数,也就是出现的次数,那么就可以很容易的获得规则A=>B的置信度。因此,关联规则的挖掘就可以演变为 频繁项集的挖掘。
那么如何挖掘频繁项集呢?首先可以想到如下算法:
1.输入事务集D,每一个事务都是一个项集(比如代表某一次购买的商品集合),以及总的项集合T、结果集C。
2.对于T的每一个子集t
遍历一遍D,统计t在D中出现的次数cnt
如果cnt大于等于给定的阈值
那么将t加入结果集C中
否则就继续
假设共有n项,那么T的子集就有2^n个。上述算法的复杂度就是O(2^n*size(D))。显然指数级的复杂度是难以接受的。因此就轮到本文的主角登场了,Apriori算法(方便称为 爱普弱算法,虽然好像不是这么发音的:D)
首先介绍一下Apriori性质:
1.若A是一个频繁项集,则A的每一个子集都是一个频繁项集。
该性质具有反单调性,于是我们得到:
2.若A不是一个频繁项集,那么它的所有超集也一定不是频繁的。(超集的意思就是说集合Y的子集为A,且Y不等于A)。
该算法的主要思想是利用 逐层搜索迭代的方法,即用k项集来探索(k+1)项集,我们用Lk来代表频繁k项集,Ck来代表长度为k的候选项集,那么该算法的流程可以概括如下:
L1->C2->L2->C3->L3->…->一直到某一集合为空
那么该算法显然可以概括为如下两个步骤:
Step1.自连接,即如何由Lk产生Ck+1
Step2.剪枝,按如下规则剪枝:
1.根据性质1和2,如果Ck中的某一k-1子集不在Lk-1中,那么其一定不是频繁项集,可以直接将其剪去。
2.再1的基础上,遍历D对每个候选进行计数,得到的结果再与最小支持度比较来进行剪枝。
对于Step1,首先假定事务或项集中的项按字典序排序。对于Lk中的每两个项集,规定若其前k-1项都相同,且前者的第k项小于后者的第k项,那么就把两者求个并,得到一个k+1项的候选项集。
这么做的主要方法是为了避免重复。
下面给出Apriori算法的伪代码,然后给出C++和Python的实现。
算法1 Apriori。使用逐层迭代方法基于候选产生找出频繁项集
输入:
1.D: 事务数据库
2.min_sup: 最小支持度阈值
输出: L, D中的频繁项集。
方法:
(1) L1 = find_frequent_itemsets(D) //首先找出频繁1项集
(2) for (k=2; Lk-1 != 空集; k++) {
(3) Ck = apriori_gen(Lk-1);
(4) for each 事务t∈D { //扫描D,进行计数
(5) Ct = subset(Ck, t);
(6) for each 候选c∈Ct
(7) c.count++;
(8) }
(9) Lk = {c(Ck|c.count >= min_sup)}
(10) }
(11) return L = 所有Lk的集合
procedure apriori_gen(Lk-1: frequent(k-1) itemset)
(1) for each 项集l1∈Lk-1
(2) for each 项集l2∈Lk-1
(3) if (l1[1]==l2[1]) && (l1[2]==l2[2]) && ... &&
(4) (l1[k-2] == l2[k-2]) && (l1[k-1] < l2[k-1]) {
(5) c = l1 link l2;
(6) if has_infrequent_subset(c, Lk-1) then
(7) delete c;
(8) else add c to Ck;
(9)return Ck;
producer has_infrequent_subset(c: candidate k itemset; Lk-1: frequent(k-1 itemset))
(1) for each(k-1) subset sof c
(2) if s 不属于 Lk-1 then
(3) return TRUE;
(4) return FALSE;
测试代码是自己写的,只是为了实现测试功能,具体细节优化没有考虑:
#include
#include
#include
#include
using namespace std;
//N代表总共的项数+1,这里假设有5项
#define N 6
#define MIN_SUPPORT 2
//L[k]就代表频繁k项集,C[k]代表长度为k的候选项集
vector<vector<int>> L[N];
vector<vector<int>> C[N];
//用到的函数声明
vector<vector<int>> find_frequent_1_itemsets(vector<vector<int>> D, int min_support);
vector<vector<int>> aproiri_gen(vector<vector<int>> L);
bool isValid(vector<int> l1, vector<int> l2);
bool has_infrequent_subset(vector<int> c, vector<vector<int>> L);
bool isexisted(vector<int> itemset, vector<int> candi);
void Aproiri(vector<vector<int>> D, int min_support) {
if (D.size() == 0) return;
//首先找出频繁1项集
L[1] = find_frequent_1_itemsets(D, min_support);
for (int k = 2; L[k - 1].size() != 0; k++) {
C[k] = aproiri_gen(L[k - 1]);
//输出C[k]测试
printf("aproiri_gen产生的k候选集C[%d]:\n", k);
for (int i = 0; i < C[k].size(); i++) {
for (int j = 0; j < C[k][i].size(); j++) {
if (j) printf(" ");
printf("%d", C[k][i][j]);
}
printf("\n");
}
//扫描D进行计数,进行第二次剪枝
map<vector<int>, int> cnt;
for (auto candi : C[k]) {
for (auto itemset : D) {
if (isexisted(itemset, candi)) {
cnt[candi]++;
}
}
if (cnt[candi] >= min_support) {
L[k].push_back(candi);
}
}
}
}
vector<vector<int>> find_frequent_1_itemsets(vector<vector<int>> D, int min_support) {
vector<vector<int>> res;
vector<int> cnt(N, 0);
for (auto vec : D) {
for (auto num : vec) {
cnt[num]++;
}
}
for (int i = 1; i < N; i++) {
if (cnt[i] >= min_support) {
res.push_back(vector<int>{i});
}
}
return res;
}
vector<vector<int>> aproiri_gen(vector<vector<int>> L) {
vector<vector<int>> res;
for (int i = 0; i < L.size(); i++) {
for (int j = 0; j < L.size(); j++) {
if (i == j) continue;
vector<int> candidate = L[i];
if (isValid(L[i], L[j])) {
candidate.push_back(L[j].back());
}
if (!has_infrequent_subset(candidate, L)) {
res.push_back(candidate);
}
}
}
return res;
}
//函数isValid,判断两个项集是否可以进行自连接产生新的项集
bool isValid(vector<int> l1, vector<int> l2) {
if (l1.size() != l2.size()) return false;
int len = l1.size();
for (int i = 0; i < len - 1; i++) {
if (l1[i] != l2[i]) return false;
}
return l1[len - 1] < l2[len - 1];
}
bool has_infrequent_subset(vector<int> c, vector<vector<int>> L) {
for (auto ite = c.begin(); ite != c.end(); ite++) {
vector<int> subset = c;
auto site = subset.begin() + (ite - c.begin());
subset.erase(site);
if (find(L.begin(), L.end(), subset) == L.end()) {
return true;
}
}
return false;
}
bool isexisted(vector<int> itemset, vector<int> candi) {
int len = candi.size();
for (int i = 0; i < len; i++) {
if (find(itemset.begin(), itemset.end(), candi[i]) == itemset.end())
return false;
}
return true;
}
int main() {
//假设数据集为{1,2,5}, {2,4}, {2,3}, {1,2,4}, {1,3},
// {2,3},{1,3},{1,2,3,5}, {1,2,3}
vector<vector<int>> D{ { 1, 2, 5 }, { 2, 4 }, { 2, 3 }, { 1, 2, 4 }, { 1, 3 }, { 2, 3 }, { 1, 3 }, { 1, 2, 3, 5 }, { 1, 2, 3 } };
Aproiri(D, MIN_SUPPORT);
printf("最终挖掘结果:\n");
for (int i = 1; i < N; i++) {
if (L[i].size() == 0) break;
printf("频繁%d项集:\n", i);
for (int j = 0; j < L[i].size(); j++) {
for (int k = 0; k < L[i][j].size(); k++) {
if (k) printf(" ");
printf("%d", L[i][j][k]);
}
printf("\n");
}
}
return 0;
}
我这里用的是Windows下的Anaconda,直接是傻瓜式安装,Ipython,Jupyter Notebook直接一步到位,用起来很舒服。贴个安装教程:
http://blog.csdn.net/zr459927180/article/details/51253087
由于自己对Python不是很熟练,所以决定用Python来实现一下该算法。
照着C++的代码就能写出来说,不得不说python真是巨tm好使。。
def loadData():
return [[1,2,5], [2,4], [2,3], [1,2,4], [1,3], [2,3], [1,3], [1,2,3,5], [1,2,3]]
def find_frequent_1_itemsets(D, minsupport):
L1 = []
C1 = []
cnt = {}
for transcation in D:
for item in transcation:
if not [item] in C1:
C1.append([item])
cnt[item] = 1
else:
cnt[item] += 1
for transcation in C1:
for item in transcation:
if cnt[item] >= minsupport:
L1.append(transcation)
L1.sort()
return L1
def aproiri_gen(L, k):
res = []
lenL = len(L)
for i in range(lenL):
for j in range(i+1,lenL):
l1 = L[i][:-1]
l2 = L[j][:-1]
if l1 == l2 and L[i][-1] < L[j][-1]:
candidate = list(set(L[i]).union(set(L[j])))
if not has_infrequent_subset(candidate, L):
res.append(candidate)
return res
def has_infrequent_subset(candidate, L):
for i in range(len(candidate)):
subset = candidate.copy()
subset.remove(candidate[i])
if subset not in L:
return True
return False
def compareList(l1, l2):
for item in l1:
if item not in l2:
return False
return True
def Aproiri(D, minsupport):
L = []
L1 = find_frequent_1_itemsets(D, minsupport)
L.append([])
L.append(L1)
for k in range(2, 5):
Lk = []
if len(L[k-1]) == 0:
break
Ck = aproiri_gen(L[k-1], k-1)
print("自链接加剪枝后得到的候选Ck:" , Ck)
print("遍历D对每个候选计数")
for candi in Ck:
cnt = 0
for transcation in D:
if compareList(candi, transcation):
cnt += 1
if cnt >= minsupport:
print ("符合要求的项集: ", candi, "出现次数: ",cnt)
Lk.append(candi)
L.append(Lk)
return L
#test
D = loadData()
L = Aproiri(D, 2)
print (L)
#Output:
自链接加剪枝后得到的候选Ck: [[1, 2], [1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4], [3, 5], [4, 5]]
遍历D对每个候选计数
符合要求的项集: [1, 2] 出现次数: 4
符合要求的项集: [1, 3] 出现次数: 4
符合要求的项集: [1, 5] 出现次数: 2
符合要求的项集: [2, 3] 出现次数: 4
符合要求的项集: [2, 4] 出现次数: 2
符合要求的项集: [2, 5] 出现次数: 2
自链接加剪枝后得到的候选Ck: [[1, 2, 3], [1, 2, 5]]
遍历D对每个候选计数
符合要求的项集: [1, 2, 3] 出现次数: 2
符合要求的项集: [1, 2, 5] 出现次数: 2
自链接加剪枝后得到的候选Ck: []
遍历D对每个候选计数
[[], [[1], [2], [3], [4], [5]], [[1, 2], [1, 3], [1, 5], [2, 3], [2, 4], [2, 5]], [[1, 2, 3], [1, 2, 5]], []]
对于任一得到的频繁项集,首先得到其所有的非空子集,从中任取两个子集A和B,那么根据置信度的公式
confidence(A=>B) = P(B | A) = support(A U B) / support(A) = support_count(A U B) / support_count(A)
就能计算出相应的置信度,如果该置信度大于等于给定的最小置信度,则该规则就可以认为是强关联规则。