示例:2012联邦选举委员会数据库

美国联邦选举委员会发布了有关政治竞选赞助方面的数据。其中包括赞助者的姓名、职业、雇主、地址以及出资额等信息。我们对2012年美国总统大选的数据集比较感兴趣(http://www.fec.gov/disclosurep/PDownload.do)。到编写本书时为止(2012年6月),涵盖全美各州的完整数据集是一个150MB的CSV文件(P00000001-ALL.csv),我们先用pandas.read_csv将其加载进来:

  1. In [13]: fec = pd.read_csv('ch09/P00000001-ALL.csv')
  2.  
  3. In [14]: fec
  4. Out[14]:
  5. <class 'pandas.core.frame.DataFrame'>
  6. Int64Index: 1001731 entries, 0 to 1001730
  7. Data columns:
  8. cmte_id 1001731 non-null values
  9. cand_id 1001731 non-null values
  10. cand_nm 1001731 non-null values
  11. contbr_nm 1001731 non-null values
  12. contbr_city 1001716 non-null values
  13. contbr_st 1001727 non-null values
  14. contbr_zip 1001620 non-null values
  15. contbr_employer 994314 non-null values
  16. contbr_occupation 994433 non-null values
  17. contb_receipt_amt 1001731 non-null values
  18. contb_receipt_dt 1001731 non-null values
  19. receipt_desc 14166 non-null values
  20. memo_cd 92482 non-null values
  21. memo_text 97770 non-null values
  22. form_tp 1001731 non-null values
  23. file_num 1001731 non-null values
  24. dtypes: float64(1), int64(1), object(14)

该DataFrame中的记录如下所示:

  1. In [15]: fec.ix[123456]
  2. Out[15]:
  3. cmte_id C00431445
  4. cand_id P80003338
  5. cand_nm Obama, Barack
  6. contbr_nm ELLMAN, IRA
  7. contbr_city TEMPE
  8. contbr_st AZ
  9. contbr_zip 852816719
  10. contbr_employer ARIZONA STATE UNIVERSITY
  11. contbr_occupation PROFESSOR
  12. contb_receipt_amt 50
  13. contb_receipt_dt 01-DEC-11
  14. receipt_desc NaN
  15. memo_cd NaN
  16. memo_text NaN
  17. form_tp SA17A
  18. file_num 772372
  19. Name: 123456

你可能已经想出了许多办法从这些竞选赞助数据中抽取有关赞助人和赞助模式的统计信息。我将在接下来的内容中介绍几种不同的分析工作(运用到目前为止已经学到的技术)。

不难看出,该数据中没有党派信息,因此最好把它加进去。通过unique,你可以获取全部的候选人名单(注意,NumPy不会输出信息中字符串两侧的引号):

  1. In [16]: unique_cands = fec.cand_nm.unique()
  2.  
  3. In [17]: unique_cands
  4. Out[17]:
  5. array([Bachmann, Michelle, Romney, Mitt, Obama, Barack,
  6. Roemer, Charles E. 'Buddy' III, Pawlenty, Timothy,
  7. Johnson, Gary Earl, Paul, Ron, Santorum, Rick, Cain, Herman,
  8. Gingrich, Newt, McCotter, Thaddeus G, Huntsman, Jon, Perry, Rick],
  9. dtype=object)
  10.  
  11. In [18]: unique_cands[2]
  12. Out[18]: 'Obama, Barack'

最简单的办法是利用字典说明党派关系注1

  1. parties = {'Bachmann, Michelle': 'Republican',
  2. 'Cain, Herman': 'Republican',
  3. 'Gingrich, Newt': 'Republican',
  4. 'Huntsman, Jon': 'Republican',
  5. 'Johnson, Gary Earl': 'Republican',
  6. 'McCotter, Thaddeus G': 'Republican',
  7. 'Obama, Barack': 'Democrat',
  8. 'Paul, Ron': 'Republican',
  9. 'Pawlenty, Timothy': 'Republican',
  10. 'Perry, Rick': 'Republican',
  11. "Roemer, Charles E. 'Buddy' III": 'Republican',
  12. 'Romney, Mitt': 'Republican',
  13. 'Santorum, Rick': 'Republican'}

现在,通过这个映射以及Series对象的map方法,你可以根据候选人姓名得到一组党派信息:

  1. In [20]: fec.cand_nm[123456:123461]
  2. Out[20]:
  3. 123456 Obama, Barack
  4. 123457 Obama, Barack
  5. 123458 Obama, Barack
  6. 123459 Obama, Barack
  7. 123460 Obama, Barack
  8. Name: cand_nm
  9.  
  10. In [21]: fec.cand_nm[123456:123461].map(parties)
  11. Out[21]:
  12. 123456 Democrat
  13. 123457 Democrat
  14. 123458 Democrat
  15. 123459 Democrat
  16. 123460 Democrat
  17. Name: cand_nm
  18.  
  19. # 将其添加为一个新列
  20. In [22]: fec['party'] = fec.cand_nm.map(parties)
  21.  
  22. In [23]: fec['party'].value_counts()
  23. Out[23]:
  24. Democrat 593746
  25. Republican 407985

这里有两个需要注意的地方。第一,该数据既包括赞助也包括退款(负的出资额):

  1. In [24]: (fec.contb_receipt_amt > 0).value_counts()
  2. Out[24]:
  3. True 991475
  4. False 10256

为了简化分析过程,我限定该数据集只能有正的出资额:

  1. In [25]: fec = fec[fec.contb_receipt_amt > 0]

由于Barack Obama和Mitt Romney是最主要的两名候选人,所以我还专门准备了一个子集,只包含针对他们两人的竞选活动的赞助信息:

  1. In [26]: fec_mrbo = fec[fec.cand_nm.isin(['Obama, Barack', 'Romney, Mitt'])]

根据职业和雇主统计赞助信息

基于职业的赞助信息统计是另一种经常被研究的统计任务。例如,律师们更倾向于资助民主党,而企业主则更倾向于资助共和党。你可以不相信我,自己看那些数据就知道了。首先,根据职业计算出资总额,这很简单:

  1. In [27]: fec.contbr_occupation.value_counts()[:10]
  2. Out[27]:
  3. RETIRED 233990
  4. INFORMATION REQUESTED 35107
  5. ATTORNEY 34286
  6. HOMEMAKER 29931
  7. PHYSICIAN 23432
  8. INFORMATION REQUESTED PER BEST EFFORTS 21138
  9. ENGINEER 14334
  10. TEACHER 13990
  11. CONSULTANT 13273
  12. PROFESSOR 12555

不难看出,许多职业都涉及相同的基本工作类型,或者同一样东西有多种变体。下面的代码片段可以清理一些这样的数据(将一个职业信息映射到另一个)。注意,这里巧妙地利用了dict.get,它允许没有映射关系的职业也能“通过”:

  1. occ_mapping = {
  2. 'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
  3. 'INFORMATION REQUESTED' : 'NOT PROVIDED',
  4. 'INFORMATION REQUESTED (BEST EFFORTS)' : 'NOT PROVIDED',
  5. 'C.E.O.': 'CEO'
  6. }
  7.  
  8. # 如果没有提供相关映射,则返回x
  9. f = lambda x: occ_mapping.get(x, x)
  10. fec.contbr_occupation = fec.contbr_occupation.map(f)

我对雇主信息也进行了同样的处理:

  1. emp_mapping = {
  2. 'INFORMATION REQUESTED PER BEST EFFORTS' : 'NOT PROVIDED',
  3. 'INFORMATION REQUESTED' : 'NOT PROVIDED',
  4. 'SELF' : 'SELF-EMPLOYED',
  5. 'SELF EMPLOYED' : 'SELF-EMPLOYED',
  6. }
  7.  
  8. # 如果没有提供相关映射,则返回x
  9. f = lambda x: emp_mapping.get(x, x)
  10. fec.contbr_employer = fec.contbr_employer.map(f)

现在,你可以通过pivot_table根据党派和职业对数据进行聚合,然后过滤掉总出资额不足200万美元的数据:

  1. In [34]: by_occupation = fec.pivot_table('contb_receipt_amt',
  2. ...: rows='contbr_occupation',
  3. ...: cols='party', aggfunc='sum')
  4.  
  5. In [35]: over_2mm = by_occupation[by_occupation.sum(1) > 2000000]
  6.  
  7. In [36]: over_2mm
  8. Out[36]:
  9. party Democrat Republican
  10. contbr_occupation
  11. ATTORNEY 11141982.97 7477194.430000
  12. CEO 2074974.79 4211040.520000
  13. CONSULTANT 2459912.71 2544725.450000
  14. ENGINEER 951525.55 1818373.700000
  15. EXECUTIVE 1355161.05 4138850.090000
  16. HOMEMAKER 4248875.80 13634275.780000
  17. INVESTOR 884133.00 2431768.920000
  18. LAWYER 3160478.87 391224.320000
  19. MANAGER 762883.22 1444532.370000
  20. NOT PROVIDED 4866973.96 20565473.010000
  21. OWNER 1001567.36 2408286.920000
  22. PHYSICIAN 3735124.94 3594320.240000
  23. PRESIDENT 1878509.95 4720923.760000
  24. PROFESSOR 2165071.08 296702.730000
  25. REAL ESTATE 528902.09 1625902.250000
  26. RETIRED 25305116.38 23561244.489999
  27. SELF-EMPLOYED 672393.40 1640252.540000

把这些数据做成柱状图看起来会更加清楚('barh'表示水平柱状图,如图9-2所示):

  1. In [38]: over_2mm.plot(kind='barh')

示例:2012联邦选举委员会数据库 - 图1

图9-2:对各党派总出资额最高的职业

你可能还想了解一下对Obama和Romney总出资额最高的职业和企业。为此,我们先对候选人进行分组,然后使用本章前面介绍的那种求取最大值的方法:

  1. def get_top_amounts(group, key, n=5):
  2.   totals = group.groupby(key)['contb_receipt_amt'].sum()
  3.  
  4. # 根据key对totals进行降序排列
  5. return totals.order(ascending=False)[n:]

然后根据职业和雇主进行聚合:

  1. In [40]: grouped = fec_mrbo.groupby('cand_nm')
  2.  
  3. In [41]: grouped.apply(get_top_amounts, 'contbr_occupation', n=7)
  4. Out[41]:
  5. cand_nm contbr_occupation
  6. Obama, Barack RETIRED 25305116.38
  7. ATTORNEY 11141982.97
  8. INFORMATION REQUESTED 4866973.96
  9. HOMEMAKER 4248875.80
  10. PHYSICIAN 3735124.94
  11. LAWYER 3160478.87
  12. CONSULTANT 2459912.71
  13. Romney, Mitt RETIRED 11508473.59
  14. INFORMATION REQUESTED PER BEST EFFORTS 11396894.84
  15. HOMEMAKER 8147446.22
  16. ATTORNEY 5364718.82
  17. PRESIDENT 2491244.89
  18. EXECUTIVE 2300947.03
  19. C.E.O. 1968386.11
  20. Name: contb_receipt_amt
  21.  
  22. In [42]: grouped.apply(get_top_amounts, 'contbr_employer', n=10)
  23. Out[42]:
  24. cand_nm contbr_employer
  25. Obama, Barack RETIRED 22694358.85
  26. SELF-EMPLOYED 17080985.96
  27. NOT EMPLOYED 8586308.70
  28. INFORMATION REQUESTED 5053480.37
  29. HOMEMAKER 2605408.54
  30. SELF 1076531.20
  31. SELF EMPLOYED 469290.00
  32. STUDENT 318831.45
  33. VOLUNTEER 257104.00
  34. MICROSOFT 215585.36
  35. Romney, Mitt INFORMATION REQUESTED PER BEST EFFORTS 12059527.24
  36. RETIRED 11506225.71
  37. HOMEMAKER 8147196.22
  38. SELF-EMPLOYED 7409860.98
  39. STUDENT 496490.94
  40. CREDIT SUISSE 281150.00
  41. MORGAN STANLEY 267266.00
  42. GOLDMAN SACH & CO. 238250.00
  43. BARCLAYS CAPITAL 162750.00
  44. H.I.G. CAPITAL 139500.00
  45. Name: contb_receipt_amt

对出资额分组

还可以对该数据做另一种非常实用的分析:利用cut函数根据出资额的大小将数据离散化到多个面元中:

  1. In [43]: bins = np.array([0, 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000])
  2.  
  3. In [44]: labels = pd.cut(fec_mrbo.contb_receipt_amt, bins)
  4.  
  5. In [45]: labels
  6. Out[45]:
  7. Categorical: contb_receipt_amt
  8. array([(10, 100], (100, 1000], (100, 1000], ..., (1, 10], (10, 100],
  9. (100, 1000]], dtype=object)
  10. Levels (8): Index([(0, 1], (1, 10], (10, 100], (100, 1000], (1000, 10000],
  11.     (10000, 100000], (100000, 1000000], (1000000, 10000000]], dtype=object)

然后根据候选人姓名以及面元标签对数据进行分组:

  1. In [46]: grouped = fec_mrbo.groupby(['cand_nm', labels])
  2.  
  3. In [47]: grouped.size().unstack(0)
  4. Out[47]:
  5. cand_nm Obama, Barack Romney, Mitt
  6. contb_receipt_amt
  7. (0, 1] 493 77
  8. (1, 10] 40070 3681
  9. (10, 100] 372280 31853
  10. (100, 1000] 153991 43357
  11. (1000, 10000] 22284 26186
  12. (10000, 100000] 2 1
  13. (100000, 1000000] 3 NaN
  14. (1000000, 10000000] 4 NaN

从这个数据中可以看出,在小额赞助方面,Obama获得的数量比Romney多得多。你还可以对出资额求和并在面元内规格化,以便图形化显示两位候选人各种赞助额度的比例:

  1. In [48]: bucket_sums = grouped.contb_receipt_amt.sum().unstack(0)
  2.  
  3. In [49]: bucket_sums
  4. Out[49]:
  5. cand_nm Obama, Barack Romney, Mitt
  6. contb_receipt_amt
  7. (0, 1] 318.24 77.00
  8. (1, 10] 337267.62 29819.66
  9. (10, 100] 20288981.41 1987783.76
  10. (100, 1000] 54798531.46 22363381.69
  11. (1000, 10000] 51753705.67 63942145.42
  12. (10000, 100000] 59100.00 12700.00
  13. (100000, 1000000] 1490683.08 NaN
  14. (1000000, 10000000] 7148839.76 NaN
  15.  
  16. In [50]: normed_sums = bucket_sums.div(bucket_sums.sum(axis=1), axis=0)
  17.  
  18. In [51]: normed_sums
  19. Out[51]:
  20. cand_nm Obama, Barack Romney, Mitt
  21. contb_receipt_amt
  22. (0, 1] 0.805182 0.194818
  23. (1, 10] 0.918767 0.081233
  24. (10, 100] 0.910769 0.089231
  25. (100, 1000] 0.710176 0.289824
  26. (1000, 10000] 0.447326 0.552674
  27. (10000, 100000] 0.823120 0.176880
  28. (100000, 1000000] 1.000000 NaN
  29. (1000000, 10000000] 1.000000 NaN
  30.  
  31. In [52]: normed_sums[:-2].plot(kind='barh', stacked=True)

我排除了两个最大的面元,因为这些不是由个人捐赠的。最终的结果如图9-3所示。

示例:2012联邦选举委员会数据库 - 图2

图9-3:两位候选人收到的各种捐赠额度的总额比例

当然,还可以对该分析过程做许多的提炼和改进。比如说,可以根据赞助人的姓名和邮编对数据进行聚合,以便找出哪些人进行了多次小额捐款,哪些人又进行了一次或多次大额捐款。我强烈建议你下载这些数据并自己摸索一下。

根据州统计赞助信息

首先自然是根据候选人和州对数据进行聚合:

  1. In [53]: grouped = fec_mrbo.groupby(['cand_nm', 'contbr_st'])
  2.  
  3. In [54]: totals = grouped.contb_receipt_amt.sum().unstack(0).fillna(0)
  4.  
  5. In [55]: totals = totals[totals.sum(1) > 100000]
  6.  
  7. In [56]: totals[:10]
  8. Out[56]:
  9. cand_nm Obama, Barack Romney, Mitt
  10. contbr_st
  11. AK 281840.15 86204.24
  12. AL 543123.48 527303.51
  13. AR 359247.28 105556.00
  14. AZ 1506476.98 1888436.23
  15. CA 23824984.24 11237636.60
  16. CO 2132429.49 1506714.12
  17. CT 2068291.26 3499475.45
  18. DC 4373538.80 1025137.50
  19. DE 336669.14 82712.00
  20. FL 7318178.58 8338458.81

如果对各行除以总赞助额,就会得到各候选人在各州的总赞助额比例:

  1. In [57]: percent = totals.div(totals.sum(1), axis=0)
  2.  
  3. In [58]: percent[:10]
  4. Out[58]:
  5. cand_nm Obama, Barack Romney, Mitt
  6. contbr_st
  7. AK 0.765778 0.234222
  8. AL 0.507390 0.492610
  9. AR 0.772902 0.227098
  10. AZ 0.443745 0.556255
  11. CA 0.679498 0.320502
  12. CO 0.585970 0.414030
  13. CT 0.371476 0.628524
  14. DC 0.810113 0.189887
  15. DE 0.802776 0.197224
  16. FL 0.467417 0.532583

我认为在地图上看这些数据会比较有意思(第8章中介绍过相关技术)。在找到有关州界的shape file(http://nationalatlas.gov/atlasftp.html?openChapters=chpbound)并稍微学习一下matplotlib及其basemap工具包(Thomas Lecocq的博客帮了我的大忙注2)之后,我终于用下面这段代码画出了刚才算出来的相对百分比:译注7

  1. from mpl_toolkits.basemap import Basemap, cm
  2. import numpy as np
  3. from matplotlib import rcParams
  4. from matplotlib.collections import LineCollection
  5. import matplotlib.pyplot as plt
  6.  
  7. from shapelib import ShapeFile
  8. import dbflib
  9.  
  10. obama = percent['Obama, Barack']
  11.  
  12. fig = plt.figure(figsize=(12, 12))
  13. ax = fig.add_axes([0.1,0.1,0.8,0.8])
  14.  
  15. lllat = 21; urlat = 53; lllon = -118; urlon = -62
  16.  
  17. m = Basemap(ax=ax, projection='stere',
  18. lon_0=(urlon + lllon) / 2, lat_0=(urlat + lllat) / 2,
  19. llcrnrlat=lllat, urcrnrlat=urlat, llcrnrlon=lllon,
  20. urcrnrlon=urlon, resolution='l')
  21. m.drawcoastlines()
  22. m.drawcountries()
  23.  
  24. shp = ShapeFile('../states/statesp020')
  25. dbf = dbflib.open('../states/statesp020')
  26.  
  27. for npoly in range(shp.info()[0]):
  28. # 在地图上绘制彩色多边形
  29. shpsegs = []
  30. shp_object = shp.read_object(npoly)
  31. verts = shp_object.vertices()
  32. rings = len(verts)
  33. for ring in range(rings):
  34. lons, lats = zip(*verts[ring])
  35. x, y = m(lons, lats)
  36. shpsegs.append(zip(x,y))
  37. if ring == 0:
  38. shapedict = dbf.read_record(npoly)
  39. name = shapedict['STATE']
  40. lines = LineCollection(shpsegs,antialiaseds=(1,))
  41.  
  42. # state_to_code字典,例如'ALASKA' -> 'AK', omitted
  43. try:
  44. per = obama[state_to_code[name.upper()]]
  45. except KeyError:
  46. continue
  47.  
  48. lines.set_facecolors('k')
  49. lines.set_alpha(0.75 * per) # 把“百分比”变小一点
  50. lines.set_edgecolors('k')
  51. lines.set_linewidth(0.3)
  52.  
  53. plt.show()

最终结果如图9-4所示。

示例:2012联邦选举委员会数据库 - 图3

图9-4:汇集了所有赞助统计信息的美国地图(颜色越深表示越支持民主党)

注1:为了简单起见,这里假设Gary Johnson是一名共和党员,虽然他后来成为自由党的候选人。

注2http://www.geophysique.be/2011/01/27/matplotlib-basemap-tutorial-07-shapefiles-unleached/。

译注7:惭愧,折腾了整整两天,愣是没做出来。太郁闷了,照着输入都不行。在网上找了一个比较有效的办法,不过由于时间太紧就没完成,希望读者在尝试成功之后一定在网上发布一下,以飨更多读者。