web数据挖掘学习笔记-Apriori(一)-python及c++实现

理论准备

关联规则的基本概念
(1)关联规则介绍
关联规则(Association Rule)是数据中所蕴含的一类重要规律,对关联规则进行挖掘是数据挖掘中的一项根本性任务,甚至可以说是数据库和数据挖掘领域中所发明并被广泛研究的最为重要的模型。

关联规则挖掘的目标是在数据项目中找出所有的并发关系,这种关系也称为关联。

它的最经典的应用是购物篮。目的是找出顾客在商场所选购商品之间的关联。以下是一个关联规则的例子:

            奶酪 —> 啤酒 [支持度 = 10%, 置信度 = 80%]

支持度表示,10%的客户会同时购买奶酪和啤酒;置信度表示,在所有购买了奶酪的客户中,有80%的客户购买了啤酒。

(2)基本名词儿(这儿要好好理解,不然后面要绕晕。)
I=i1,i2,......im I = { i 1 , i 2 , . . . . . . i m } 是一个项目(Item)集合(上面的例子中就代表每个商品),我们以后简称它为总项集。另外在算法操作中,我们会遇到I的子集,对于这些子集,它有n个元素(n<=m),就称n项集。

T=t1t2......tn T = { t 1 , t 2 , . . . . . . t n } 是一个(数据库)事务 (Transaction)集合,其中每个事务 ti是一个项目集合,并满足 ti t i 属于I。

一个关联规则是一个如下形式的蕴涵关系:

X->Y,其中X属于I,Y属于I且X与Y的交集为空,X(或Y)是一个项目的集合,称作项集(Itemset),并称X为前件,Y为后件。例子中,啤酒是后件,奶酪是前件。

例:一个数据集文件打开后是下面这个样

Beef,Chicken,Milk

Beef,Cheese

Cheese,Boots

Beef,Chicken,Cheese

Beef,Chicken,Clothes,Cheese,Milk

Chicken,Clothes,Milk

Chicken,Milk,Clothes

事务:上面的每一行是一笔销售记录,每一笔记录就是一个事务

项目集合:[‘Beef’, ‘Chicken’, ‘Milk’, ‘Cheese’, ‘Boots’, ‘Clothes’]

(3)支持度、置信度
支持度(support)= X与 Y的同时出现的事务数目/总的T的数目(可用于评价事务的覆盖率,若太小,没有意义)

置信度(confidence)= X与 Y的同时出现的事务数目/仅出现X的事务的数目 (决定规则的可预测度Predictability)

最小支持度(minsup)预期可以接受的支持度,比这个支持度小的项集将被抛弃。

最小置信度(minconf)预期可接受的置信度。

频繁项集——指支持度高于minsup的项集

–普通关联规则需要解决的问题
算法的目的是,给定最小支持度和最小置信度,从数据集中找出满足最小支持度和置信度的所有关联规则。
在所有支持度高于预设minsup值的项集中生成置信度高于预设minconf的规则。计算置信度还是要依赖支持度,所以整个问题的解决大方向就是又快又好地计算支持度…

最简单最头铁的方式——穷举法
整体思路如下。在所有商品组成的集合中,生成所有的子集。对这些子集分别在T中搜寻,以计算它们的支持度。这个方法在小数据集上还凑合,数据量一大,它能算到天荒地老。因为子集很多…搜寻代价太大了。

–普通Apriori算法
* 向下封闭性——为减少搜寻次数而生*

这里有个技巧可以提前帮我们抛弃掉一些非频繁项集。要解决这个看似很有技巧性的问题之前,我们先寻找一些规律。也就是寻找解和子问题解之间的联系!假设{a, b, c}是一个频繁项,那么…它会和其他解有什么关系呢?我们来思考一下{a, b},{b, c}, {a, c}吧!因为这些是{a, b, c}的所有子集。如果{a, b, c}的支持度大于mincof,则{a, b}或者其他子集的支持度就不可能小于mincof。(不能说a, b, c共同出现的次数为10次,而a, b共同出现的次数为9次。这逻辑上说不通)。接着,如果{a, b, c}多加一个元素进来,我们把这个新伙伴称为d。那么{a, b, c, d}是不是频繁项呢?好像不一定。因为新元素d的出现,拉低了所有伙伴共同出场的次数。({a, b, c}出现了10次,{a, b, c, d}只能最多出现10次)那么我们改变一下原来的假设:现在假设{a, b, c}不是频繁项集。在这个新假设下,根据刚才的分析,{a, b, c}就不用再进行任何的合集操作了(有a, b, c共同参与的项集就不用考虑了),加个什么进来也不能改变它不满足minsup的事实。我们把刚才那两个规则合起来称为——向下封闭(Downward Closure Property)。
满足向下封闭性的项集不一定是频繁项集,但是不满足向下封闭性的项集一定不是频繁项集。

–逐级搜寻

刚才得到的向下封闭属性似乎可以减少搜寻次数,但是还少了点可以操作的味道。我们下面来找一个这个属性的服用方法。我们刚才总结出的向下封闭性是n项,n-1项和n+1项之间的关系。{a, b}, {b, c}, {a, c}这都是2项,{a, b, c}是三项,{a, b, c, d}是四项。下面把有n项的项集称为n项集或者说这个项集维度为n。那么我们在进行操作的时候就一步一步来吧。我们可以先搜索所有一项集的频繁度,对不符合支持度要求的项集,直接删去。再搜索二项集,三项集……我们给这个“一步一步来”起个高大上的名字叫逐级搜索(Level-wise Search)。

–搜寻过程

    1.拿到数据,生成总项集I。针对总项集I,生成所有的一项集。['Beef', 'Chicken', 'Milk', 
    'Cheese', 'Boots', 'Clothes']这是总项集,所有的一项集有6个,分别就是里面的字符串了。

    2.对这些子项集分别在所有事务中搜寻,如果有某个事项包含这个项集。那么这个子项集的支持度+1.

       搜寻完成后删除支持度小于minsup的子项集。这些子项集就是满足支持度要求的频繁项(它们的项数都是一样的)。

    3.对步2得到的所有子项集(假设它们的项数为n)分别对其他子项集进行合集操作,保留那些项数为n+1的项集。

    4.对步3的所有子项集分别进行减一维操作。对其中一个子项集(比如目前的子项集叫x吧,设项集x有n项),方法是删除
    这个子项集的一个元素,所以这个n项的子项集会产生n个结果。拿着这n个结果去2步的所有项集里去找,如果这n个
    结果里有一个不在2的所有项集里,则删去这个子项集(x就被删去了。这步的操作是为了验证生成的所有n项子项集是否
    满足向下封闭性。n=2时,这一步删不掉什么东西。)

    5.对经历了4的洗礼还剩下的所有子项集进行频繁度搜寻。到这儿步骤就返回到2,如此循环...其中2-5步是代码
    中candidateGen()函数的主要过程。

–规则生成

一个看似可行的算法:设a为f的任一非空子集,则可以生成以下一条关联规则:

f的支持度,(f-a)代表f和a的差集。这里的支持度不需要再扫描数据集了,因为所有结果已经在频繁项搜寻过程中得到了。

这个方法是穷举法,效率比较低。我们可以简化一下规则生成过程。想一下…如果(f-a) -> a是一条关联规则,那么所有的(f - asub a s u b ) -> asub a s u b 也是关联规则,其中 asub a s u b 是a的一个非空子集。例如,给定一个项集{A, B, C, D},如果(A, B -> C, D)是一条关联规则,那么(A, B, C -> D)和(A, B, D -> C)必然都是关联规则。

整体思路有了。给定一个频繁项目集f,如果一条关联规则的后件为a,那么所有以a的任一非空子集为后件的后选规则都是关联规则。那么我们可以采用逐级1->2->3….生成规则,每升一级,就把后件送到candidateGen()函数中去生成候选后件…

下章介绍差别支持度的apriori

https://mp.csdn.net/mdeditor/79844411


Python及C++实现

python代码可优化:判断是否是子集or超集:set1.issubset(set2) or set1.issubset(set2)

一、Python+Linux环境
1.保存代码到home目录,命名为’apriori.py’
2.启动终端(ALT+CTRL+T)
3.(跳转到home目录,命令是下一行)
cd ~
4.(启动python2)
python2
5.(导入apriori模块)
import apriori
6.(运行函数)
apriori.apriori(fileName = ‘transaction.txt’, f = 3, n = 3)
结果如下

说明:apriori.apriori()函数接受三个参数,分别是:
1.数据集文件。此文件保存所有消费记录,每一行储存一笔交易记录,每笔记录的商品之间用’,’隔开
2.f参数(int类型)。f是我们可以接受的频繁度。
3.n参数(int类型)。n是最终生成的频繁项目集的项目数,传入的n要大于等于2。如果n等于1,程序会当成2处理(目前还是个bug)。
说明:可以按照上一个说明里第一项规则自己存写数据集。当然也可以复制粘贴下面的,保存名称为’transaction.txt’
Beef,Chicken,Milk
Beef,Cheese
Cheese,Boots
Beef,Chicken,Cheese
Beef,Chicken,Clothes,Cheese,Milk
Chicken,Clothes,Milk
Chicken,Milk,Clothes
说明:此程序使用字典来储存项目集。字典有一个特点,它的键必须是不可变类型。而进行项目集合并、子集存在判断等操作使用了集合。集合是可变数据类型,不可以作为字典的键。所以程序使用了多次数据类型转换,不排除有一些操作存在重复性。故储存和运算占用资源较多,在大规模数据集上使用效果不好。也许存在合适的方法和合适的数据类型,可以减少操作,提高程序运行效率。总结:此程序现在还是个玩具。
说明:这个程序暂时还没有规则生成函数,有时间续上写。
函数解析:
transactionGet()
这个函数功能是产生全事件集,只接收一个参数。参数类型为字符串,实际意义是数据集文件名称。返回数据为嵌套列表,总列表是全事件集,子列表代表每个事项。

candidateGen()
候选项集函数。接收两个参数,分别为预处理的事项集(数据类型为字典)和频繁度。事项集字典的key是事项,它对应的value值是这个事项出现的频度。函数首先会分析传入事件集字典的维数(也就是n),度量方式是判断字典的第一个key的长度。之后函数会把所有key分别和其他key做合集操作,合集后保留高1维度的事项。再分别判断这些事项的所有子集是否存在于预处理的事项集中,删去不符合的事项集。最后把这些事项分别作为key生成字典,送到candidateValueCount()函数计算所有事项的
频度,减去频度不符合预期频度(f)要求的事项。返回候选事项集,返回类型为字典。
candidateValueCount()
接收两个参数。一个是需要计算事项频繁度的事项集(空值字典),第二个参数是全事件集列表。返回带有频繁度的事项集字典。

#----------------------------------------------------- 
#   function:Apriori 
#   author:hanshuo   
#   date:2018-4-04
#   tools:Python 2.7.6  
#   system:linux or Windows
#-----------------------------------------------------
def transactionGet(fileName = 'transaction.txt'):
    f = open(fileName)
    transactionList = []

    for line in f.readlines():
        lineTuple = line.strip().split(',')
        transactionList.append(lineTuple)

    return transactionList

def candidateGen(dict1, f):
    dicKeys = dict1.keys()
    dict2 = {}
    j = len(dicKeys[0])
    transactionList = transactionGet('transaction.txt')

    for value in dicKeys:
        for eachValue in dicKeys:
            canKey = tuple(set(value) | set(eachValue))

            if len(canKey) == j + 1:
                dict2[canKey] = None

                for reValue in canKey:
                    subSet = list(canKey)
                    subSet.remove(reValue)
                    if set(subSet) not in [set(i) for i in dicKeys]:
                        del dict2[tuple(canKey)]
                        break
    dict2 = candidateValueCount(dict2, transactionList)

    for key, value in dict2.items():
        if value < f:
            del dict2[key]

    return dict2, j+1

def candidateValueCount(dict1, transactionList):
    for value in dict1.keys():
        listTemp = []
        for eachGood in value:
            listTemp.append(eachGood)

        valueCount = 0
        for issue in transactionList:
            if set(value) == set(value) & set(issue):
                valueCount += 1
        dict1[value] = valueCount

    return dict1

def apriori(fileName = 'transaction.txt', f = 3, n = 3):
    tranList = transactionGet(fileName)
    goodSet = set()
    dict0 = {}
    itemNum = 0
    for issue in tranList:
        goodSet = goodSet | set(issue)

    goodTupleList = []

    for good in goodSet:
        list1 = []
        list1.append(good)
        goodTupleList.append(tuple(list1))

    dict0 = dict.fromkeys(goodTupleList)

    while itemNum<n:
        dict0, itemNum = candidateGen(dict0, f)

    return dict0

二、C++和Linux环境
1.保存代码到home目录,命名为’apriori.cpp’
2.启动终端
3.(使用g++编译程序,直接把下面命令粘贴到终端,敲回车)g++ apriori.cpp -o apr
4.(运行程序)

#include<iostream>
#include<set>
#include<map>
#include<string>
#include<vector>
using namespace std;
typedef map<set<string>,int> map_s;
​
map_s Ck ;//候选集Ck
map_s Lk ; //频繁项集Lk
vector<set<string> >  data; //原始数据
vector<set<string> >  L;    //频繁项集合
vector<string>        data2; //分割后的原始数据
int n,m;
int minSup = 2;
int minConf = 0.2;
set<string> s;
string in;
void Delete(map_s &Ck)
{
   for(  map_s::iterator l_it=Ck.begin();l_it!=Ck.end();l_it++ )
   {
      if( l_it->second< minSup)
      {
        Ck.erase(l_it);
      }
   }
}
​
int compset(set<string> s1,set<string> s2)
{
  int flag=0;
  //判断集合s1是不是s2的子集
  for( set<string>::iterator it=s1.begin(); it!=s1.end();it++ )
  {
     //s1有元素不在s2中
     if( s2.find(*it)==s2.end() )
     {
         flag=10;
         break;
     }
  }
  for( set<string>::iterator it=s2.begin(); it!=s2.end();it++ )
  {
     //s2有元素不在s1中
      if( s1.find(*it)==s1.end() )
      {
         flag+=1;               
         break;
      }
  }
  /*当flag==0,s1元素全部在s2中,s2元素也全部在s1中,s1==s2
    当flag==10,s1有元素不在s2中,s2所有元素都在s1中,s1包含了s2
    当flag==1,s1所有元素在s2中,s2有元素不在s1中,s2包含了s1
    当flag==11,s1 s2集合互不包含
  */
  return flag;
}
​
map_s apriori_gen(map_s &Ck,int k)
{
   //生成子集
   map_s Ck_temp;
   set<string> s_temp;
   for( map_s::iterator l_it1=Ck.begin();l_it1!=Ck.end();l_it1++ )
   {
      for( map_s::iterator l_it2=Ck.begin();l_it2!=Ck.end();l_it2++ )
      {
         //如果两个set一样,则说明是同一个KEY,跳过
         if(!((l_it1->first > l_it2->first)||(l_it1->first < l_it2->first)))
                   continue;
         //否则开始组装,遍历整个Ck
         for( set<string>::iterator s_it=l_it2->first.begin();s_it!=l_it2->first.end();s_it++ )
         {
               //如果该值在l_it1 set里面可以找到,不能组装
               if( l_it1->first.find(*s_it)!=l_it1->first.end())
                  continue;
               //否则进行组装,先把l_it1的set复制进去
               s_temp = l_it1->first;
               //再把l_it2的值放进去
               s_temp.insert(*s_it);
               //判断该组装的set是否已在生成集合中,如果之前已生成,则不需要往下运算
               if(Ck_temp.find(s_temp)!=Ck_temp.end())
                    continue;
               else  //否则放到生成的子集中
               {
                  Ck_temp.insert(pair<set<string>,int >(s_temp,0));
               }
         }
      }
   }

   //对于k=2的情况,需要扫描原始数据得出计数值
   if( 2 == k )
   {  
     for( map_s::iterator l_it=Ck_temp.begin();l_it!=Ck_temp.end();l_it++ ) 
      for(int i=0;i<data.size();i++)
       //l_it集合被data[i]完整包含,则计数值+1
       if( (10 == compset(data[i],l_it->first)) || (0 == compset(data[i],l_it->first))  )
                Ck_temp[l_it->first]++;

       //扫描完之后排除 非频繁项
     for( map_s::iterator l_it=Ck_temp.begin();l_it!=Ck_temp.end();l_it++ )
         if( Ck_temp[l_it->first] < minSup )
             Ck_temp.erase(l_it);
   }
   //如果是大于2的情况,扫描k-1的频繁项子集
   if( 2 < k )
   {
      //每次都循环获取每个Ck的k-1子集元素
      //如{I1,I2,I3}C3的子集是{I1,I2} {I2,I3} {I3,I4}
      //如果Ck的子集不在k-1的频繁项子集中,则去掉该Ck项
      for( map_s::iterator l_it=Ck_temp.begin();l_it!=Ck_temp.end();l_it++ )
      {
         int flag;
         for( set<string>::iterator s_it=l_it->first.begin();s_it!=l_it->first.end();s_it++ ) 
         {
           //开始求子集
           //首先把当前Ck项的集合保存
           s_temp=l_it->first;
           //去掉一个元素,即是它的k-1子集
           s_temp.erase(*s_it);
           //遍历频繁项集合L,看看是不是在频繁集中
           flag=1;
           for( int i=0;i<L.size();i++  )
           {
             //如果K-1子集在频繁项集中存在,则保留
             if( 0 == compset(s_temp,L[i]) )
             {
                  flag=0;
                  break;
             }
           }
           //如果找到了哪怕一个k-1子集项不在频繁项集中,直接退出
           if( flag ) break;
         }
         //只有所有的k-1子集在频繁项集中,才保留该Ck项
         if( flag ) Ck_temp.erase(l_it);
      }
   }
​
   cout<<"由L"<<k-1<<"产生的候选集C"<<k<<"   "<<"cout数(k=2以上不做计数)"<<endl;
   for( map_s::iterator l_it=Ck_temp.begin();l_it!=Ck_temp.end();l_it++ )
   {
        for( set<string>::iterator s_it=l_it->first.begin();s_it!=l_it->first.end();s_it++ )
             cout<<*s_it<<"  ";
        cout<<l_it->second<<endl;
   }
   return Ck_temp;
}
​
int main()
{ 
  cout<<"请输入事务,第一行输入事务数。每行第一个值输入该事务的item数"<<endl;
  //生成原始数据集
  cin>>n;
  for(int i=0;i<n;i++)
  { 
     s.clear();
     cin>>m;
     for(int j=0;j<m;j++)
     {
        cin>>in;
        s.insert(in);
        data2.push_back(in);
     }
     data.push_back(s); 
  }
  //扫描数据集D,生成C1
     //对于每个候选集里面的元素
     for( int j=0; j<data2.size();j++ )
     {
      int flag=1;
      //如果C1中存在该元素,计数值加1
      for( map_s::iterator l_it=Ck.begin();l_it!=Ck.end();l_it++ )
      {
        if( (l_it->first).find(data2[j]) != (l_it->first).end() )
        {
           Ck[l_it->first]++;
           flag=0;
           break;
        }
      }
     //不存在,插入到C1集合中
     if(flag)
     { 
          s.clear();
          s.insert(data2[j]);
          Ck.insert(pair<set<string>,int>(s,1));
     }
    }
    //去掉支持度不足的
    for(  map_s::iterator l_it=Ck.begin();l_it!=Ck.end();l_it++ )
    {
      if( l_it->second< minSup)
        Ck.erase(l_it);
    }
​
​
​
  cout<<"C1候选集:"<<endl;
  cout<<"项集"<<"     "<<"支持度计数"<<endl;

  for( map_s::iterator l_it=Ck.begin();l_it!=Ck.end();l_it++ )
  {
      for( set<string>::iterator s_it=(l_it->first).begin(); s_it!=(l_it->first).end(); s_it++)
         cout<<*s_it<<" "<<l_it->second<<endl;
  }

  int f_count=2;
  while( f_count )
  {
      //将Ck内的k-1频繁集全部保存到L集中
      for( map_s::iterator l_it=Ck.begin();l_it!=Ck.end();l_it++ )
        L.push_back(l_it->first);
      //获取Ck集,已清除掉小于支持度的候选集
      Ck=apriori_gen(Ck,f_count);     

      if( Ck.empty() )
      {
          break;
      }else{ f_count++; }
  }

  cout<<"最终的频繁集集合"<<endl;
  for( int i=0; i<L.size(); i++ )
  {
      for( set<string>::iterator s_it=L[i].begin(); s_it!=L[i].end(); s_it++)
         cout<<*s_it<<" ";
      cout<<endl;
  }
​
}


目前只实现到产生频繁集合,支持度默认计数2

测试数据如下:

4
3 I1 I2 I6
4 I1 I2 I3 I5
3 I2 I3 I7
5 I1 I3 I5 I6 I2

当然目前还只是儿童玩具,因为所有的数据都是放在内存里的,数据量一大这个代码就不实用了。
结果如下

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值