讀古今文學網 > 機器學習實戰 > 11.5 示例:發現國會投票中的模式 >

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.getBillsByStateRecent
To see what each bill is, enter the following:
>>> for bill in bills:
...     print bill.title,bill.billId
...
Amending FAA Rulemaking Activities 13020
Prohibiting Federal Funding of National Public Radio 12939
Additional Continuing Appropriations 12888
Removing 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 sleep
from votesmart import votesmart
votesmart.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.getActionIds
bill: 12939 has actionId: 34089
bill: 12940 has actionId: 34091
bill: 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 += 2
return 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: 34089
getting 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]
...
Republican
Prohibiting Federal Funding of National Public Radio -- Yea
Removing Troops from Afghanistan – Nay 
  

上述輸出可能因Votesmart服務器返回的結果不同而有所差異。

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

>>> transDict,itemMeaning=apriori.getTransList(actionIdList, billTitles)
getting votes for actionId: 34089
getting votes for actionId: 34091
getting 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.961538461538
frozenset([22]) --> frozenset([1]) conf: 0.951351351351
                                    .
                                    .
                                    .
frozenset([25, 26, 3, 4]) --> frozenset([0, 9, 7]) conf: 0.97191011236
frozenset([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.0
frozenset([3]) --> frozenset([0]) conf: 0.995614035088
frozenset([3]) --> frozenset([0, 9]) conf: 0.995614035088
frozenset([26, 3]) --> frozenset([0, 9]) conf: 1.0
frozenset([9, 26]) --> frozenset([0, 7]) conf: 0.957547169811
                                  .
                                  .
                                  .
frozenset([23, 26, 3, 4, 7]) --> frozenset([0, 9]) conf: 1.0
frozenset([23, 25, 3, 4, 7]) --> frozenset([0, 9]) conf: 0.994764397906
frozenset([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%的情況下是成立的。我真希望在這類事情上賭一把。