首页 » 机器学习实战 » 机器学习实战全文在线阅读

《机器学习实战》11.5 示例:发现国会投票中的模式

关灯直达底部

前面我们已经发现频繁项集及关联规则,现在是时候把这些工具用在真实数据上了。那么可以使用什么样的数据呢?购物是一个很好的例子,但是前面已经用过了。另一个例子是搜索引擎中的查询词。这个示例听上去不错,不过下面看到的是一个更有趣的美国国会议员投票的例子。

加州大学埃文分校的机器学习数据集合中有一个自1984年起的国会投票记录的数据集:http://archive.ics.uci.edu/ml/datasets/Congressional+Voting+Records。这个数据集有点偏旧,而且其中的议题对我来讲意义也不大。我们想尝试一些更新的数据。目前有不少组织致力于将政府数据公开化,其中的一个组织是智能投票工程(Project Vote Smart,网址:http://www.votesmart.org),它提供了一个公共的API。下面会看到如何从Votesmart.org获取数据,并将其转化为用于生成频繁项集与关联规则的格式。该数据可以用于竞选的目的或者预测政治家如何投票。

示例:在美国国会投票记录中发现关联规则

  1. 收集数据:使用votesmart模块来访问投票记录。
  2. 准备数据:构造一个函数来将投票转化为一串交易记录。
  3. 分析数据:在Python提示符下查看准备的数据以确保其正确性。
  4. 训练算法:使用本章早先的apriorigenerateRules函数来发现投票记录中的有趣信息。
  5. 测试算法:不适用,即没有测试过程。
  6. 使用算法:这里只是出于娱乐的目的,不过也可以使用分析结果来为政治竞选活动服务,或者预测选举官员会如何投票。

接下来,我们将处理投票记录并创建一个交易数据库。这需要一些创造性思维。最后,我们会使用本章早先的代码来生成频繁项集和关联规则的列表。

11.5.1 收集数据:构建美国国会投票记录的事务数据集

智能投票工程已经收集了大量的政府数据,他们同时提供了一个公开的API来访问该数据http://api.votesmart.org/docs/terms.html。Sunlight 实验室写过一个Python模块用于访问该数据,该模块在https://github.com/sunlightlabs/python-votesmart 中有很多可供参考的文档。下面要从美国国会获得一些最新的投票记录并基于这些数据来尝试学习一些关联规则。

我们希望最终数据的格式与图11-1中的数据相同,即每一行代表美国国会的一个成员,而每列都是他们投票的对象。接下来从国会议员最近投票的内容开始。如果没有安装python-votesmart,或者没有获得API key,那么需要先完成这两件事。关于如何安装python-votesmart可以参考附录A 。

要使用votesmartAPI,需要导入votesmart模块:

>>> from votesmart import votesmart  

接下来,输入你的API key1:

>>> votesmart.apikey = '49024thereoncewasamanfromnantucket94040'    

1. 这里的key只是一个例子。你需要在http://votesmart.org/share/api/register申请自己的key。

现在就可以使用votesmartAPI了。为了获得最近的100条议案,输入:

>>> bills = votesmart.votes.getBillsByStateRecent  

为了看看每条议案的具体内容,输入:

>>> bills = votesmart.votes.getBillsByStateRecentTo see what each bill is, enter the following:>>> for bill in bills:...     print bill.title,bill.billId...Amending FAA Rulemaking Activities 13020Prohibiting Federal Funding of National Public Radio 12939Additional Continuing Appropriations 12888Removing Troops from Afghanistan 12940                           .                           .                           ."Whistleblower Protection" for Offshore Oil Workers 11820  

读者在看本书时,最新的100条议案内容将会有所改变。所以这里我将上述100条议案的标题及ID号(billId)保存为recent100bills.txt文件。

可以通过getBill方法,获得每条议案的更多内容。比如,对刚才的最后一条议案 “Whistleblower Protection” ,其ID号为11820。下面看看实际结果:

>>> bill = votesmart.votes.getBill(11820)  

上述命令会返回一个BillDetail对象,其中包含大量完整信息。我们可以查看所有信息,不过这里我们所感兴趣的只是围绕议案的所有行为。可以通过输入下列命令来查看实际结果:

>>> bill.actions  

上述命令会返回许多行为,议案包括议案被提出时的行为以及议案在投票时的行为。我们对投票发生时的行为感兴趣,可以输入下面命令来获得这些信息:

>>> for action in bill.actions:...     if action.stage=='Passage':...         print action.actionId...31670    

上述信息并不完整,一条议案会经历多个阶段。一项议案被提出之后,经由美国国会和众议院投票通过后,才能进入行政办公室。其中的Passage(议案通过)阶段可能存在欺骗性,因为这有可能是行政办公室的Passage阶段,那里并没有任何投票。

为获得某条特定议案的投票信息,使用getBillActionVotes方法:

>>> voteList = votesmart.votes.getBillActionVotes(31670)  

其中,voteList是一个包含Vote对象的列表。输入下面的命令来看一下里面包含的内容:

>>> voteList[22]Vote({u'action': u'No Vote', u'candidateId': u'430', u'officeParties':u'Democratic', u'candidateName': u'Berry, Robert'})>>> voteList[21]Vote({u'action': u'Yea', u'candidateId': u'26756', u'officeParties':u'Democratic', u'candidateName': u'Berman, Howard'})  

现在为止,我们已经用过这些相关API,可以将它们组织到一块了。接下来会给出一个函数将文本文件中的billId转化为actionId。如前所述,并非所有的议案都被投票过,另外可能有一些议案在多处进行了议案投票。也就是说需要对actionId进行过滤只保留包含投票数据的actionId。这样处理之后将100个议案过滤到只剩20个议案,这些剩下的议案都是我认为有趣的议案,它们被保存在文件recent20bills.txt中。下面给出一个getActionIds函数来处理actionIds的过滤。打开apriori.py文件,输入下面的代码2。

2. 不要忘了使用你自己的API key来代替例子中的key!

程序清单11-4 收集美国国会议案中action ID的函数

from time import sleepfrom votesmart import votesmartvotesmart.apikey = '49024thereoncewasamanfromnantucket94040'def getActionIds:actionIdList = ; billTitleList = fr = open('recent20bills.txt')for line in fr.readlines:    billNum = int(line.split('/t')[0])    try:        billDetail = votesmart.votes.getBill(billNum)        for action in billDetail.actions:        #❶(以下两行)过滤出包含投票的行为        if action.level == 'House' and (action.stage == 'Passage' or action.stage == 'Amendment Vote'):            actionId = int(action.actionId)            print 'bill: %d has actionId: %d' % (billNum, actionId)            actionIdList.append(actionId)            billTitleList.append(line.strip.split('/t')[1])    except:        print "problem getting bill %d" % billNum    sleep(1)#❷ 为礼貌访问网站而做些延迟return actionIdList, billTitleList  

上述程序中导入了votesmart模块并通过引入sleep函数来延迟API调用。getActionsIds函数会返回存储在recent20bills.txt文件中议案的actionId。程序先导入API key,然后创建两个空列表。这两个列表分别用来返回actionsId和标题。首先打开recent20bills.txt文件,对每一行内不同元素使用tab进行分隔,之后进入try-except模块。由于在使用外部API时可能会遇到错误,并且也不想让错误占用数据获取的时间,上述try-except模块调用是一种非常可行的做法。所以,首先尝试使用getBill方法来获得一个billDetail对象。接下来遍历议案中的所有行为,来寻找有投票数据的行为。在Passage阶段与Amendment Vote(修正案投票)阶段都会有投票数据,要找的就是它们。现在,在行政级别上也有一个Passage阶段,但那个阶段并不包含任何投票数据,所以要确保这个阶段是发生在众议院❷。如果确实如此,程序就会将actionId打印出来并将它添加到actionIdList中。同时,也会将议案的标题添加到billTitleList中。如果在API调用时发生错误,就不会执行actionIdList的添加操作。一旦有错误就会执行except模块并将错误信息输出。最后,程序会休眠1秒钟,以避免对Votesmart.org网站的过度频繁访问。程序运行结束时,actionIdListbillTitleList会被返回用于进一步的处理。

下面看一下实际运行效果。将程序清单11-4中的代码加入到apriori.py文件后,输入如下命令:

>>> reload(apriori)<module 'apriori' from 'apriori.py'>>>> actionIdList,billTitles = apriori.getActionIdsbill: 12939 has actionId: 34089bill: 12940 has actionId: 34091bill: 12988 has actionId: 34229                     .                     .                     .     

可以看到actionId显示了出来,它同时也被添加到actionIdList中输出,以后我们可以使用这些actionId了。如果程序运行错误,则尝试使用try..except代码来捕获错误。我自己就曾经在获取所有actiondId时遇到一个错误。接下里可以继续来获取这些actionId的投票信息。

选举人可以投是或否的表决票,也可以弃权。需要一种方法来将这些上述信息转化为类似于项集或者交易数据库之类的东西。前面提到过,一条交易记录数据只包含一个项的出现或不出现信息,并不包含项出现的次数。基于上述投票数据,可以将投票是或否看成一个元素。

美国有两个主要政党:共和党与民主党。下面也会对这些信息进行编码并写到事务数据库中。幸运的是,这些信息在投票数据中已经包括。下面给出构建事务数据库的流程:首先创建一个字典,字典中使用政客的名字作为键值。当某政客首次出现时,将他及其所属政党(民主党或者共和党)添加到字典中,这里使用0来代表民主党,1来代表共和党。下面介绍如何对投票进行编码。对每条议案创建两个条目:bill+'Yea'以及 bill+'Nay'。该方法允许在某个政客根本没有投票时也能合理编码。图11-5给出了从投票信息到元素项的转换结果。

图11-5 美国国会信息到元素(项)编号之间的映射示意图

现在,我们已经有一个可以将投票编码为元素项的系统,接下来是时候生成事务数据库了。一旦有了事务数据库,就可以应用早先写的Apriori代码。下面将构建一个使用actionId串作为输入并利用votesmart的API来抓取投票记录的函数。然后将每个选举人的投票转化为一个项集。每个选举人对应于一行或者说事务数据库中的一条记录。下面看一下实际的效果,打开apriori.py文件并添加下面清单中的代码。

程序清单11-5 基于投票数据的事务列表填充函数

def getTransList(actionIdList, billTitleList):    itemMeaning = ['Republican', 'Democratic']    for billTitle in billTitleList:        #❶(以下三行)填充itemMeaning列表            itemMeaning.append('%s -- Nay' % billTitle)        itemMeaning.append('%s -- Yea' % billTitle)    transDict = {}    voteCount = 2    for actionId in actionIdList:        sleep(3)        print 'getting votes for actionId: %d' % actionId        try:            voteList = votesmart.votes.getBillActionVotes(actionId)            for vote in voteList:                if not transDict.has_key(vote.candidateName):                    transDict[vote.candidateName] =                     if vote.officeParties == 'Democratic':                        transDict[vote.candidateName].append(1)                    elif vote.officeParties == 'Republican':                        transDict[vote.candidateName].append(0)            if vote.action == 'Nay':                transDict[vote.candidateName].append(voteCount)           elif vote.action == 'Yea':                transDict[vote.candidateName].append(voteCount + 1)    except:        print "problem getting actionId: %d" % actionId    voteCount += 2return transDict, itemMeaning  

函数getTransList会创建一个事务数据库,于是在此基础上可以使用前面的Apriori代码来生成频繁项集与关联规则。该函数也会创建一个标题列表,所以很容易了解每个元素项的含义。一开始使用前两个元素“Repbulican”和“Democratic”创建一个含义列表itemMeaning。当想知道某些元素项的具体含义时,需要做的是以元素项的编号作为索引访问itemMeaning即可。接下来遍历所有议案,然后在议案标题后添加Nay(反对)或者Yea(同意)并将它们放入itemMeaning列表中❶。接下来创建一个空字典用于加入元素项,然后遍历函数getActionIds返回的每一个actionId。遍历时要做的第一件事是休眠,即在for循环中一开始调用sleep函数来延迟访问,这样做可以避免过于频繁的API调用。接着将运行结果打印出来,以便知道程序是否在正常工作。再接着通过try..except块来使用VotesmartAPI获取某个特定actionId相关的所有投票信息。然后,遍历所有的投票信息(通常voteList会超过400个投票)。在遍历时,使用政客的名字作为字典的键值来填充transDict。如果之前没有遇到该政客,那么就要获取他的政党信息。字典中的每个政客都有一个列表来存储他投票的元素项或者他的政党信息。接下来会看到该政客是否对当前议案投了赞成(Yea)或反对(Nay)票。如果他们之前有投票,那么不管是投赞成票还是反对票,这些信息都将添加到列表中。如果API调用中发生了什么错误,except模块中的程序就会被调用并将错误信息输出到屏幕上,之后函数仍然继续执行。最后,程序返回事务字典transDict及元素项含义类表itemMeaning

下面看一下投票信息的前两项,了解上述代码是否正常工作:

>>> reload(apriori)<module 'apriori' from 'apriori.py'>>>>transDict,itemMeaning=apriori.getTransList(actionIdList[:2],billTitles[:2])getting votes for actionId: 34089getting votes for actionId: 34091  

下面看一下transDict中包含的具体内容:

>>> for key in transDict.keys:...     print transDict[key][1, 2, 5][1, 2, 4][0, 3, 4][0, 3, 4][1, 2, 4][0, 3, 4][1][1, 2, 5][1, 2, 4][1][1, 2, 4][0, 3, 4][1, 2, 5][1, 2, 4][0, 3, 4]   

如果上面许多列表看上去都类似的话,读者也不要太过担心。许多政客的投票结果都很类似。现在如果给定一个元素项列表,那么可以使用itemMeaning列表来快速“解码”出它的含义:

>>> transDict.keys[6]u' Doyle, Michael 'Mike''>>> for item in transDict[' Doyle, Michael 'Mike'']:...     print itemMeaning[item]...RepublicanProhibiting Federal Funding of National Public Radio -- YeaRemoving Troops from Afghanistan – Nay   

上述输出可能因Votesmart服务器返回的结果不同而有所差异。

下面看看完整列表下的结果:

>>> transDict,itemMeaning=apriori.getTransList(actionIdList, billTitles)getting votes for actionId: 34089getting votes for actionId: 34091getting votes for actionId: 34229                    .                    .                    .  

接下来在使用前面开发的Apriori算法之前,需要构建一个包含所有事务项的列表。可以使用类似于前面for循环的一个列表处理过程来完成:

>>> dataSet = [transDict[key] for key in transDict.keys]  

上面这样的做法会去掉键值(即政客)的名字。不过这无关紧要,这些信息不是我们感兴趣的内容。我们感兴趣的是元素项以及它们之间的关联关系。接下来将使用Apriori算法来挖掘上面例子中的频繁项集与关联规则。

11.5.2 测试算法:基于美国国会投票记录挖掘关联规则

现在可以应用11.3节的Apriori算法来进行处理。如果使用默认的支持度阈值50%,那么应该不会产生太多的频繁项集:

>>> L,suppData=apriori.apriori(dataSet, minSupport=0.5)>>> L[[frozenset([4]), frozenset([13]), frozenset([0]), frozenset([21])],[frozenset([13, 21])], ]      

使用一个更小的支持度阈值30%会得到更多频繁项集:

>>> L,suppData=apriori.apriori(dataSet, minSupport=0.3)>>> len(L)8   

当使用30%的支持度阈值时,会得到许多频繁项集,甚至可以得到包含所有7个元素项的6个频繁集。

>>> L[6][frozenset([0, 3, 7, 9, 23, 25, 26]), frozenset([0, 3, 4, 9, 23, 25, 26]),frozenset([0, 3, 4, 7, 9, 23, 26]), frozenset([0, 3, 4, 7, 9, 23, 25]),frozenset([0, 4, 7, 9, 23, 25, 26]), frozenset([0, 3, 4, 7, 9, 25, 26])]  

获得频繁项集之后就可以结束,也可以尝试使用11.4节的代码来生成关联规则。首先将最小可信度值设为0.7:

>>> rules = apriori.generateRules(L,suppData)    

这样会产生太多规则,于是可以加大最小可信度值。

>>> rules = apriori.generateRules(L,suppData, minConf=0.95)frozenset([15]) --> frozenset([1]) conf: 0.961538461538frozenset([22]) --> frozenset([1]) conf: 0.951351351351                                    .                                    .                                    .frozenset([25, 26, 3, 4]) --> frozenset([0, 9, 7]) conf: 0.97191011236frozenset([0, 25, 26, 4]) --> frozenset([9, 3, 7]) conf: 0.950549450549   

继续增加可信度值:

>>> rules = apriori.generateRules(L,suppData, minConf=0.99)frozenset([3]) --> frozenset([9]) conf: 1.0frozenset([3]) --> frozenset([0]) conf: 0.995614035088frozenset([3]) --> frozenset([0, 9]) conf: 0.995614035088frozenset([26, 3]) --> frozenset([0, 9]) conf: 1.0frozenset([9, 26]) --> frozenset([0, 7]) conf: 0.957547169811                                  .                                  .                                  .frozenset([23, 26, 3, 4, 7]) --> frozenset([0, 9]) conf: 1.0frozenset([23, 25, 3, 4, 7]) --> frozenset([0, 9]) conf: 0.994764397906frozenset([25, 26, 3, 4, 7]) --> frozenset([0, 9]) conf: 1.0  

上面给出了一些有趣的规则。如果要找出每一条规则的含义,则可以将规则号作为索引输入到itemMeaning中:

>>> itemMeaning[26]'Prohibiting the Use of Federal Funds for NASCAR Sponsorships -- Nay'>>> itemMeaning[3]'Prohibiting Federal Funding of National Public Radio -- Yea'>>> itemMeaning[9]'Repealing the Health Care Bill -- Yea'    

在图11-6中列出了下面的几条规则:{3} ➞ {0}、{22} ➞ {1}及{9,26} ➞ {0,7}。

图11-6 关联规则{3}➞{0}、{22}➞{1}与{9,26}➞{0,7}的含义及可信度

数据中还有更多有趣或娱乐性十足的规则。还记得前面最早使用的支持度30%吗?这意味着这些规则至少出现在30%以上的记录中。由于至少会在30%的投票记录中看到这些规则,所以这是很有意义。对于{3} ➞ {0}这条规则,在99.6%的情况下是成立的。我真希望在这类事情上赌一把。