原文链接:http://blog.youkuaiyun.com/qq675927952/article/details/6707704
http://blog.youkuaiyun.com/pupingpp/article/details/8264734
关联规则的目的在于在一个数据集中找出项之间的关系,也称之为购物蓝分析 (market basketanalysis)。例如,购买鞋的顾客,有10%的可能也会买袜子,60%的买面包的顾客,也会买牛奶。这其中最有名的例子就是"尿布和啤酒"的故事了。
关联规则的应用场合。在商业销售上,关联规则可用于交叉销售,以得到更大的收入;在保险业务方面,如果出现了不常见的索赔要求组合,则可能为欺诈,需要作进一步的调查。在医疗方面,可找出可能的治疗组合;在银行方面,对顾客进行分析,可以推荐感兴趣的服务等等。
Apriori algorithm是关联规则里一项基本算法。由Rakesh Agrawal 在 1994年提出的,详细的介绍请猛击这里《Fast Algorithms for Mining Association Rules》。
首先我们来看,什么是规则?规则形如"如果…那么…(If…Then…)",前者为条件,后者为结果。例如一个顾客,如果买了可乐,那么他也会购买果汁。
如何来度量一个规则是否够好?有两个量,置信度(Confidence)和支持度(Support)。假设有如下表的购买记录。
顾客 | 项目 |
1 | orangejuice, coke |
2 | milk,orange juice, window cleaner |
3 | orangejuice, detergent |
4 | orangejuice, detergent, coke |
5 | windowcleaner |
将上表整理一下,得到如下的一个2维表
| Orange | WinCl | Milk | Coke | Detergent |
Orange | 4 | 1 | 1 | 2 | 2 |
WinCl | 1 | 2 | 1 | 0 | 0 |
Milk | 1 | 1 | 1 | 0 | 0 |
Coke | 2 | 0 | 0 | 2 | 1 |
Detergent | 1 | 0 | 0 | 0 | 2 |
上表中横栏和纵栏的数字表示同时购买这两种商品的交易条数。如购买有Orange的交易数为4,而同时购买Orange和Coke的交易数为2。
置信度表示了这条规则有多大程度上值得可信。设条件的项的集合为A,结果的集合为B。置信度计算在A中,同时也含有B的概率。即Confidence(A==>B)=P(B|A)。例如计算"如果Orange则Coke"的置信度。由于在含有Orange的4条交易中,仅有2条交易含有Coke.其置信度为0.5。
支持度计算在所有的交易集中,既有A又有B的概率。例如在5条记录中,既有Orange又有Coke的记录有2条。则此条规则的支持度为2/5=0.4。现在这条规则可表述为,如果一个顾客购买了Orange,则有50%的可能购买Coke。而这样的情况(即买了Orange会再买Coke)会有40%的可能发生。
再来考虑下述情况。
项 | 支持度 |
A | 0.45 |
B | 0.42 |
C | 0.4 |
A andB | 0.25 |
A andC | 0.2 |
B andC | 0.15 |
A,B,andC | 0.05 |
可得到下述规则
规则 | 置信度 |
If B and C thenA | 0.05/0.15*100%=33.33% |
If A and C thenB | 0.05/0.20*100%=25% |
If A and B thenC | 0.05/0.25*100%=20% |
上述的三条规则,哪一条规则有用呢?
对于规则" If B and CthenA",同时购买B和C的人中,有33.33%会购买A。而单项A的支持度有0.45,也就是说在所有交易中,会有45%的人购买A.看来使用这条规则来进行推荐,还不如不推荐,随机对顾客进荐好了。
为此引入另外一个量,即提升度(Lift),以度量此规则是否可用。描述的是相对于不用规则,使用规则可以提高多少。有用的规则的提升度大于1。计算方式为Lift(A==>B)=Confidence(A==>B)/Support(B)=Support(A==>B)/(Support(A)*Support(B))。在上例中,Lift(IfB and C The A)=0.05/(0.15*0.45)=0.74。而Lift(If A thenB)=0.25/(0.45*0.42)=1.32。也就是说对买了A的人进行推荐B,购买概率是随机推荐B的1.32倍。lift(A->B) = P(AB)/(P(A)P(B))
如何产生规则呢。可以分两步走。
首先找出频繁集(frequentitemset)。所谓频繁集指满足最小支持度或置信度的集合。其次从频繁集中找出强规则(strongrules)。强规则指既满足最小支持度又满足最小置信度的规则。
我们来看如何产生频繁集。
这其中有一个定理。即频繁集的子集也一定是频繁集。比如,如果{A,B,C}是一个3项的频繁集,则其子集{A,B},{B,C},{A,C}也一定是2项的频繁集。为方便,可以把含有k项的集合称之为k-itemsets.
下面以迭代的方式找出频繁集。首先找出1-itemsets的频繁集,然后使用这个1-itemsets,进行组合,找出2-itemsets的频繁集。如此下去,直到不再满足最小支持度或置信度的条件为止。这其中重要的两步骤分别是连接(join 连接的规则就是:两个第K级的频繁项要是可以连接形成第K+1级候选频繁项,当且仅当:两个第K级频繁项的前K-1项都相同,只是第K项不同!!) 和剪枝(prune).即从(k-1)-itemsets中的项进行组合,产生备选集(Candidateitemsets)。再从备选集中,将不符合最小支持度或置信度的项删去。例如
Frequent2-itemsets | | Candidate3-itemsets | | Frqquent3-itemsets |
I1,I2 | ==> | I1,I2,I4 | ==> | I1,I2,I4 |
I1,I4 | | I2,I3,I4 | | |
I2,I3 | | | | |
I2,I4 | | | | |
下面我们再来看一个详细的例子。
设最小支持度为2,以Ck表示k-itemsets备选集,以Lk表示k-itemsets频繁集。
ID | Items | | Itemset | Sup.count | | Itemset | | Itemset |
100 | I1,I2,I5 | | I1 | 6 | | I1 | | I1,I2 |
200 | I2,I4 | ==>C1: | I2 | 7 | ==>L1: | I2 | ==>C2 | I1,I3 |
300 | I2,I3 | | I3 | 6 | | I3 | | I1,I4 |
400 | I1,I2,I4 | | I4 | 2 | | I4 | | I1,I5 |
500 | I1,I3 | | I5 | 2 | | I5 | | I2,I3 |
600 | I2,I3 | | | | | | | I2,I4 |
700 | I1,I3 | | | | | | | I2,I5 |
800 | I1,I2,I3,I5 | | | | | | | I3,I4 |
900 | I1,I2,I3 | | | | | | | I3,I5 |
| | | | | | | | I4,I5 |
对C2进行扫描,计算支持度。
Itemset | Sup.count | | Itemset | | Itemset | Sup.count | | Itemset |
I1,I2 | 4 | ==>L2: | I1,I2 | ==>C3 | I1,I2,I3 | 2 | ==>L3: | I1,I2,I3 |
I1,I3 | 4 | | I1,I3 | | I1,I2,I5 | 2 | | I1,I2,I5 |
I1,I4 | 1 | | I1,I5 | | | | | |
I1,I5 | 2 | | I2,I3 | | | | | |
I2,I3 | 4 | | I2,I4 | | | | | |
I2,I4 | 2 | | I2,I5 | | | | | |
I2,I5 | 2 | | | | | | | |
I3,I4 | 0 | | | | | | | |
I3,I5 | 1 | | | | | | | |
I4,I5 | 0 | | | | | | | |
对于频繁集中的每一项k-itemset,可以产生非空子集,对每一个子集,可以得到满足最小置信度的规则了。例如考虑{I1,I2,I5}。其子集有{I1,I2},{I1,I5}, {I2,I5}, {I1}, {I2}, {I5}。可以产生规则,{I1,I2} => {I5} (50%), {I1,I5}=> {I2}(100%), {I2,I5} =>{I1} (100%),{I1}=> {I2,I5}(33%), {I2} =>{I1,I5} (29%), {I5}=>{I1,I2}(100%)。
也不是每个数据集都有产生强规则。例如"Thinkpad notebook" 和"Canonprinter"一起可能很难产生有效规则。因为恰好一起买这两个牌子的产品的顾客太少。但不妨将Thinkpadnotebook放到Notebook这一层次上考虑,而Canonprinter放到printer这一去层次上考虑。这样的话,一起买notebook和printer的顾客就较多了。也即Multilevelassociation rules。
下面就用C++来模拟一下,当然此算法是数据挖掘中很经典的一个算法,基于数据库的,但是我的程序就是模拟一下算法,用STL容器存储事务,代替数据库作为模拟!
- #include<iostream>
- #include<string>
- #include <vector>
- #include <map>
- #include <algorithm>
- using namespace std;
- class Apriori
- {
- public:
- Apriori(size_t is =0,unsigned int mv=0)
- {
- item_size = is;
- min_value = mv;
- }
- //~Apriori() {};
- void getItem();
- map< vector<string>,unsigned int> find_freitem();//求事务的频繁项
- //连接连个k-1级频繁项,得到第k级频繁项
- map< vector<string>,unsigned int > apri_gen(unsigned int K , map< vector<string>,unsigned int > K_item);
- //展示频繁项集
- void showAprioriItem(unsigned int K,map< vector<string>,unsigned int > showmap);
- private:
- map< int , vector<string> > item;//存储所有最开始的事务及其项
- map< vector<string>,unsigned int > K_item;//存储频繁项集
- size_t item_size;//事务数目
- unsigned int min_value;//最小阈值
- };
- void Apriori::getItem()//用户输入最初的事务集
- {
- int ci = item_size;
- for (int i=0;i<ci;i++)
- {
- string str;
- vector<string> temp;
- cout<<"请输入第 "<<i+1<<"个事务的项集(123 end):";
- while (cin>>str && str !="123")
- {
- temp.push_back(str);
- }
- sort(temp.begin(),temp.end());
- pair< map<int ,vector<string> >::iterator , bool> ret = item.insert(make_pair(i+1 ,temp));
- if (!ret.second)
- {
- --i;
- cout<<"你输入的元素已存在!请重新输入!"<<endl;
- }
- }
- cout<<"-------------运行结果如下:--------------"<<endl;
- }
- map< vector<string>,unsigned int> Apriori::find_freitem()
- {
- unsigned int i = 1;
- bool isEmpty = false;
- map< int , vector<string> >::iterator mit ;
- for (mit=item.begin();mit != item.end();mit++)
- {
- vector<string> vec = mit->second;
- if (vec.size() != 0)
- break;
- }
- if (mit == item.end())//事务集为空
- {
- isEmpty = true;
- cout<<"事务集为空!程序无法进行..."<<endl;
- map< vector<string>,unsigned int> empty;
- return empty;
- }
- while(1)
- {
- map< vector<string>,unsigned int > K_itemTemp = K_item;
- K_item = apri_gen(i++,K_item);
- if (K_itemTemp == K_item)
- {
- i = UINT_MAX;
- break;
- }
- //判断是否需要进行下一次的寻找
- map< vector<string>,unsigned int > pre_K_item = K_item;
- size_t Kitemsize = K_item.size();
- //存储应该删除的第K级频繁项集,不能和其他K级频繁项集构成第K+1级项集的集合
- if (Kitemsize != 1 && i != 1)
- {
- vector< map< vector<string>,unsigned int >::iterator > eraseVecMit;
- map< vector<string>,unsigned int >::iterator pre_K_item_it1 = pre_K_item.begin() , pre_K_item_it2;
- while (pre_K_item_it1 != pre_K_item.end() )
- {
- map< vector<string>,unsigned int >::iterator mit = pre_K_item_it1;
- bool isExist = true;
- vector<string> vec1;
- vec1 = pre_K_item_it1->first;
- vector<string> vec11(vec1.begin(),vec1.end()-1);
- while (mit != pre_K_item.end())
- {
- vector<string> vec2;
- vec2 = mit->first;
- vector<string> vec22(vec2.begin(),vec2.end()-1);
- if (vec11 == vec22)
- break;
- ++mit;
- }
- if (mit == pre_K_item.end())
- isExist = false;
- if (!isExist && pre_K_item_it1 != pre_K_item.end())
- eraseVecMit.push_back(pre_K_item_it1);//该第K级频繁项应该删除
- ++pre_K_item_it1;
- }
- size_t eraseSetSize = eraseVecMit.size();
- if (eraseSetSize == Kitemsize)
- break;
- else
- {
- vector< map< vector<string>,unsigned int >::iterator >::iterator currentErs = eraseVecMit.begin();
- while (currentErs != eraseVecMit.end())//删除所有应该删除的第K级频繁项
- {
- map< vector<string>,unsigned int >::iterator eraseMit = *currentErs;
- K_item.erase(eraseMit);
- ++currentErs;
- }
- }
- }
- else
- if(Kitemsize == 1 )
- break;
- }
- cout<<endl;
- showAprioriItem(i,K_item);
- return K_item;
- }
- map< vector<string>,unsigned int > Apriori::apri_gen(unsigned int K , map< vector<string>,unsigned int > K_item)
- {
- if (1 == K)//求候选集C1
- {
- size_t c1 = item_size;
- map< int , vector<string> >::iterator mapit = item.begin();
- vector<string> vec;
- map<string,unsigned int> c1_itemtemp;
- while (mapit != item.end() )//将原事务中所有的单项统计出来
- {
- vector<string> temp = mapit->second;
- vector<string>::iterator vecit = temp.begin();
- while (vecit != temp.end() )
- {
- pair< map<string,unsigned int>::iterator , bool > ret = c1_itemtemp.insert(make_pair(*vecit++ , 1));
- if (!ret.second)
- {
- ++ ret.first->second;
- }
- }
- ++mapit;
- }
- map<string,unsigned int>::iterator item_it = c1_itemtemp.begin();
- map< vector<string>,unsigned int > c1_item;
- while (item_it != c1_itemtemp.end() )//构造第一级频繁项集
- {
- vector<string> temp;
- if ( item_it->second >= min_value)
- {
- temp.push_back(item_it->first);
- c1_item.insert(make_pair(temp , item_it->second) );
- }
- ++item_it;
- }
- return c1_item;
- }
- else
- {
- cout<<endl;
- showAprioriItem(K-1,K_item);
- map< vector<string>,unsigned int >::iterator ck_item_it1 = K_item.begin(),ck_item_it2;
- map< vector<string>,unsigned int > ck_item;
- while (ck_item_it1 != K_item.end() )
- {
- ck_item_it2 = ck_item_it1;
- ++ck_item_it2;
- map< vector<string>,unsigned int >::iterator mit = ck_item_it2;
- //取当前第K级频繁项与其后面的第K级频繁项集联合,但要注意联合条件
- //联合条件:连个频繁项的前K-1项完全相同,只是第K项不同,然后两个联合生成第K+1级候选频繁项
- while(mit != K_item.end() )
- {
- vector<string> vec,vec1,vec2;
- vec1 = ck_item_it1->first;
- vec2 = mit->first;
- vector<string>::iterator vit1,vit2;
- vit1 = vec1.begin();
- vit2 = vec2.begin();
- while (vit1 < vec1.end() && vit2 < vec2.end() )
- {
- string str1 = *vit1;
- string str2 = *vit2;
- ++vit1;
- ++vit2;
- if ( K ==2 || str1 == str2 )
- {
- if (vit1 != vec1.end() && vit2 != vec2.end() )
- {
- vec.push_back(str1);
- }
- }
- else
- break;
- }
- if (vit1 == vec1.end() && vit2 == vec2.end() )
- {
- --vit1;
- --vit2;
- string str1 = *vit1;
- string str2 = *vit2;
- if (str1>str2)
- {
- vec.push_back(str2);
- vec.push_back(str1);
- }
- else
- {
- vec.push_back(str1);
- vec.push_back(str2);
- }
- map< int , vector<string> >::iterator base_item = item.begin();
- unsigned int Acount = 0 ;
- while (base_item != item.end() )//统计该K+1级候选项在原事务集出现次数
- {
- unsigned int count = 0 ,mincount = UINT_MAX;
- vector<string> vv = base_item->second;
- vector<string>::iterator vecit , bvit ;
- for (vecit = vec.begin();vecit < vec.end();vecit++)
- {
- string t = *vecit;
- count = 0;
- for (bvit=vv.begin();bvit < vv.end();bvit++)
- {
- if (t == *bvit)
- count++;
- }
- mincount = (count < mincount ? count : mincount );
- }
- if (mincount >=1 && mincount != UINT_MAX)
- Acount += mincount;
- ++base_item;
- }
- if (Acount >= min_value && Acount != 0)
- {
- sort(vec.begin(),vec.end());
- //该第K+1级候选项为频繁项,插入频繁项集
- pair< map< vector<string>,unsigned int >::iterator , bool> ret = ck_item.insert(make_pair(vec,Acount));
- if (! ret.second)
- {
- ret.first->second += Acount;
- }
- }
- }
- ++mit;
- }
- ++ck_item_it1;
- }
- if (ck_item.empty())//该第K+1级频繁项集为空,说明调用结束,把上一级频繁项集返回
- return K_item;
- else
- return ck_item;
- }
- }
- void Apriori::showAprioriItem(unsigned int K,map< vector<string>,unsigned int > showmap)
- {
- map< vector<string>,unsigned int >::iterator showit = showmap.begin();
- if (K != UINT_MAX)
- cout<<endl<<"第 "<<K<<" 级频繁项集为:"<<endl;
- else
- cout<<"最终的频繁项集为:"<<endl;
- cout<<"项 集"<<" \t "<<"频率"<<endl;
- while (showit != showmap.end() )
- {
- vector<string> vec = showit->first;
- vector<string>::iterator vecit = vec.begin();
- cout<<"{ ";
- while (vecit != vec.end())
- {
- cout<<*vecit<<" ";
- ++vecit;
- }
- cout<<"}"<<"\t";
- cout<<showit->second<<endl;
- ++showit;
- }
- }
- unsigned int parseNumber(const char * str)//对用户输入的数字进行判断和转换
- {
- if (str == NULL)
- return 0;
- else
- {
- unsigned int num = 0;
- size_t len = strlen(str);
- for (size_t i=0;i<len;i++)
- {
- num *= 10;
- if (str[i]>= '0' && str[i] <= '9')
- num += str[i] - '0';
- else
- return 0;
- }
- return num;
- }
- }
- void main()
- {
- //Apriori a;
- unsigned int itemsize = 0;
- unsigned int min;
- do
- {
- cout<<"请输入事务数:";
- char * str = new char;
- cin>>str;
- itemsize = parseNumber(str);
- if (itemsize == 0)
- {
- cout<<"请输入大于0正整数!"<<endl;
- }
- } while (itemsize == 0);
- do
- {
- cout<<"请输入最小阈值:";
- char * str = new char;
- cin>>str;
- min = parseNumber(str);
- if (min == 0)
- {
- cout<<"请输入大于0正整数!"<<endl;
- }
- } while (min == 0);
- Apriori a(itemsize,min);
- a.getItem();
- map< vector<string>,unsigned int> AprioriMap = a.find_freitem();
- //a.showAprioriItem(UINT_MAX,AprioriMap);
- system("pause");
- }
运行结果为(例题来源于:《数据挖掘概念与技术》机械工业 2005年版,第六章153页):
请输入事务数:9
请输入最小阈值:2
请输入第 1个事务的项集(123 end):I1 I2 I5 123
请输入第 2个事务的项集(123 end):I2 I4 123
请输入第 3个事务的项集(123 end):I2 I3 123
请输入第 4个事务的项集(123 end):I1 I2 I4 123
请输入第 5个事务的项集(123 end):I1 I3 123
请输入第 6个事务的项集(123 end):I2 I3 123
请输入第 7个事务的项集(123 end):I1 I3 123
请输入第 8个事务的项集(123 end):I1 I2 I3 I5 123
请输入第 9个事务的项集(123 end):I1 I2 I3 123
-------------运行结果如下:--------------
第 1 级频繁项集为:
项 集 频率
{ I1 } 6
{ I2 } 7
{ I3 } 6
{ I4 } 2
{ I5 } 2
第 2 级频繁项集为:
项 集 频率
{ I1 I2 } 4
{ I1 I3 } 4
{ I1 I5 } 2
{ I2 I3 } 4
{ I2 I4 } 2
{ I2 I5 } 2
第 3 级频繁项集为:
项 集 频率
{ I1 I2 I3 } 2
{ I1 I2 I5 } 2
最终的频繁项集为:
项 集 频率
{ I1 I2 I3 } 2
{ I1 I2 I5 } 2
请按任意键继续. . .
当然这只是算法的模拟实现,没怎么考虑到效率,仅供参考,当然存储使用STL容器,时间效率还是蛮高的。
有什么意见和建议,欢迎留言,谢谢,互相学习,刚刚看数据挖掘,新手一枚。