立法會投票數據統計

立法會會期快將結束,在新一屆立法會選舉開始前,讓我們利用 python 的 pandas 模組統計一下這一屆(第六屆立法會 2016-2020)立法會的投票紀錄。所有紀錄都可以在立法會公開數據庫找到原始數據(xml)。

統計分為三部份:

  1. 立法會整體數據分析
  2. 議員個人投票統計
  3. 拫據黨派分析投票傾向
  4. 立法會的投票傾向視像化 (30/6 updated)

導入 pandas(主要統計模組), matplotlib(製作圖表用) 及 numpy(計算時有可能會用到):

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
plt.rcParams['axes.unicode_minus']=False
sns.set_style("whitegrid")
sns.set_context("poster")
sns.set(font='SimHei', font_scale=1)

IPython 的指令,把圖表直接顯示在 notebook Output 裡

In [2]:
%matplotlib inline

把事先整合好的數據導入:

In [3]:
legco_cm = pd.read_csv('./cm/legco_cm_summary3.csv')
legco_cm.head()
Out[3]:
vote-id vote-date vote-time motion mover mover-type result 梁君彥 涂謹申 梁耀忠 ... overall-vote overall-yes overall-no overall-abstain 梁國雄 羅冠聰 姚松炎 劉小麗 梁頌恆 游蕙禎
0 20190515001 16/05/2019 10:34:10 《2019年撥款條例草案》 - 全體委員會審議 - 總目21的修正案 (修正案編號1) 胡志偉 Member Negatived Present Absent Yes ... 50 16 33 1 NaN NaN NaN NaN NaN NaN
1 20190515002 16/05/2019 10:39:52 縮短點名表決響鐘時間的議案 李慧琼 Member Passed Present Absent No ... 51 33 18 0 NaN NaN NaN NaN NaN NaN
2 20190515003 16/05/2019 10:41:42 《2019年撥款條例草案》 - 全體委員會審議 - 總目22的修正案 (修正案編號2) 鄺俊宇 Member Negatived Present Absent Yes ... 50 18 31 1 NaN NaN NaN NaN NaN NaN
3 20190515004 16/05/2019 10:43:19 《2019年撥款條例草案》 - 全體委員會審議 - 總目33的修正案 (修正案編號3) 朱凱廸 Member Negatived Present Absent Yes ... 53 11 38 4 NaN NaN NaN NaN NaN NaN
4 20190515005 16/05/2019 10:44:54 《2019年撥款條例草案》 - 全體委員會審議 - 總目42的修正案 (修正案編號4) 陳志全 Member Negatived Present Absent Yes ... 53 8 38 7 NaN NaN NaN NaN NaN NaN

5 rows × 97 columns

大概看一下數據,有 ID,日期,時間,動議,動議人,動議種類,結果,地區出席(出席沒投票),地區投票,地區贊成票,地區反對票,地區棄權票,功能組別出席(出席沒投票),投票,贊成票,反對票,棄權票,全體立法會出席(出席沒投票),投票,贊成票,反對票,棄權票,然後就是各個議員在各個動議的投票狀態。

In [4]:
legco_cm.dtypes
Out[4]:
vote-id      object
vote-date    object
vote-time    object
motion       object
mover        object
              ...  
羅冠聰          object
姚松炎          object
劉小麗          object
梁頌恆          object
游蕙禎          object
Length: 97, dtype: object
In [5]:
legco_cm.columns
Out[5]:
Index(['vote-id', 'vote-date', 'vote-time', 'motion', 'mover', 'mover-type',
       'result', '梁君彥', '涂謹申', '梁耀忠', '石禮謙', '張宇人', '李國麟', '林健鋒', '黃定光', '李慧琼',
       '陳克勤', '陳健波', '梁美芬', '黃國健', '葉劉淑儀', '謝偉俊', '毛孟靜', '田北辰', '何俊賢', '易志明',
       '胡志偉', '姚思榮', '馬逢國', '莫乃光', '陳志全', '陳恒鑌', '梁志祥', '梁繼昌', '麥美娟', '郭家麒',
       '郭偉强', '郭榮鏗', '張華峰', '張超雄', '黃碧雲', '葉建源', '葛珮帆', '廖長江', '潘兆平', '蔣麗芸',
       '盧偉國', '鍾國斌', '楊岳橋', '尹兆堅', '朱凱廸', '吳永嘉', '何君堯', '何啟明', '林卓廷', '周浩鼎',
       '邵家輝', '邵家臻', '柯創盛', '容海恩', '陳沛然', '陳振英', '陳淑莊', '張國鈞', '許智峯', '陸頌雄',
       '劉國勳', '劉業強', '鄭松泰', '鄺俊宇', '譚文豪', '范國威', '區諾軒', '鄭泳舜', '謝偉銓', '陳凱欣',
       'geo-present', 'geo-vote', 'geo-yes', 'geo-no', 'geo-abstain',
       'func-present', 'func-vote', 'func-yes', 'func-no', 'func-abstain',
       'overall-present', 'overall-vote', 'overall-yes', 'overall-no',
       'overall-abstain', '梁國雄', '羅冠聰', '姚松炎', '劉小麗', '梁頌恆', '游蕙禎'],
      dtype='object')

1. 整體投票統計

- 參與投票人數統計

平均一次投票參與人數:

In [6]:
legco_cm['overall-present'].mean()
Out[6]:
53.443120260021665

平均投票人數:(主席不投票、當中亦有連棄權也不選的情況)

In [7]:
legco_cm['overall-vote'].mean()
Out[7]:
52.24918743228602

- 動議通過率

In [8]:
passed = (legco_cm['result'] == 'Passed').sum()
print(f'紀錄動議總數: {legco_cm.shape[0]}')
print(f'獲通過的動議: {passed}')
print(f'總通過率: {passed / legco_cm.shape[0] * 100:.1f}%')

labels = ['Passed', 'Negatived']
size = [passed, legco_cm.shape[0] - passed]
fig1, ax1 = plt.subplots()
ax1.pie(size, labels=labels, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 20})
ax1.axis('equal')
ax1.set_title('For all 923 Motions:')
plt.rcParams['figure.figsize'] = (10, 10)
plt.show()
紀錄動議總數: 923
獲通過的動議: 271
總通過率: 29.4%

紀錄中把動議分為立法會成員及公務員兩種。

In [9]:
legco_cm['mover-type'].unique()
Out[9]:
array(['Member', 'Public Officer'], dtype=object)
In [10]:
member_motion = legco_cm[(legco_cm['mover-type']=='Member')].result.count()
member_motion_passed = legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Passed')].result.count()
gov_motion = legco_cm[(legco_cm['mover-type']=='Public Officer')].result.count()
gov_motion_passed = legco_cm[(legco_cm['mover-type']=='Public Officer') & (legco_cm['result']=='Passed')].result.count()

print(f"由議員提出的議案:{member_motion},獲得通過:{member_motion_passed},通過率:{member_motion_passed / member_motion:.3f}")
print(f"由政府提出的議案:{gov_motion},獲得通過:{gov_motion_passed},通過率:{gov_motion_passed / gov_motion:.3f}")

# Bar Chart:
negatived = [(gov_motion - gov_motion_passed), (member_motion - member_motion_passed)]
passed = [gov_motion_passed, member_motion_passed]
p1 = plt.bar([0, 1], passed, 0.35, alpha=0.5)
p2 = plt.bar([0, 1], negatived, 0.35, bottom=passed, alpha=0.5)
plt.ylabel('Number of Motions')
plt.title('Number of Motions by Member/Public Officer')
plt.xticks([0, 1], ('Public Officer', 'Members'))
plt.legend((p1[0], p2[0]), ('Passed', 'Negatived'))
plt.rcParams['figure.figsize'] = (10, 10)
plt.show()

print("Alternative: Pie charts")
labels = ['Passed', 'Negatived']
member_size = [member_motion_passed, member_motion - member_motion_passed]
gov_size = [gov_motion_passed, gov_motion - gov_motion_passed]
fig, ax = plt.subplots(1, 2)
fig.subplots_adjust(hspace=0.5, wspace=0.5)
ax[1].pie(member_size, labels=labels, autopct='%1.1f%%', startangle=90, textprops={'fontsize': 20})
ax[1].set_title('Motions by Members')

ax[0].pie(gov_size, labels=labels, autopct='%1.1f%%', startangle=0, textprops={'fontsize': 20})
ax[0].set_title('Motions by Goverment')

plt.show()
由議員提出的議案:789,獲得通過:138,通過率:0.175
由政府提出的議案:134,獲得通過:133,通過率:0.993
Alternative: Pie charts

唯一被立法會否決由政府提出的議案

投票結果:

In [11]:
oops = legco_cm[(legco_cm['mover-type']=='Public Officer') & (legco_cm['result']=='Negatived')]
oops[['motion', 'mover', 'overall-vote', 'overall-yes', 'overall-no']]
Out[11]:
motion mover overall-vote overall-yes overall-no
263 《2017年應課稅品(修訂)條例草案》 - 全體委員會審議 - 食物及衞生局局長的第二組修正... 食物及衞生局局長 41 12 29

- 動議投票時間分怖

In [12]:
legco_cm['vote-date'] = pd.to_datetime(legco_cm['vote-date'])
legco_cm['vote-time'] = pd.to_datetime(legco_cm['vote-time']).dt.time
legco_cm[['vote-id', 'vote-date', 'vote-time', 'mover-type', 'result']].head()
Out[12]:
vote-id vote-date vote-time mover-type result
0 20190515001 2019-05-16 10:34:10 Member Negatived
1 20190515002 2019-05-16 10:39:52 Member Passed
2 20190515003 2019-05-16 10:41:42 Member Negatived
3 20190515004 2019-05-16 10:43:19 Member Negatived
4 20190515005 2019-05-16 10:44:54 Member Negatived

首先看一下政府動議按月份分怖

In [13]:
legco_cm['year-month'] = legco_cm['vote-date'].dt.to_period('M')
legco_cm[legco_cm['mover-type'] == 'Public Officer'].groupby('year-month').size().plot(kind='bar')
Out[13]:
<matplotlib.axes._subplots.AxesSubplot at 0x12164b710>

整合起來再看一次...

In [14]:
legco_cm['month'] = legco_cm['vote-date'].dt.month
legco_cm[legco_cm['mover-type'] == 'Public Officer'].groupby('month').size().plot(kind='bar')
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x12175b690>

看來大部份政府議案都在5、6、10、11月提出。

然後看一下政府主要是在週幾動議投票

In [15]:
legco_cm['day-of-week'] = legco_cm['vote-date'].dt.dayofweek
legco_cm[legco_cm['mover-type'] == 'Public Officer'].groupby('day-of-week').size().plot(kind='bar')
Out[15]:
<matplotlib.axes._subplots.AxesSubplot at 0x121929650>

主要是週三、週四,其次是週五。

再來看一下投票時間分怖

In [16]:
legco_cm['hour'] = pd.to_datetime(legco_cm['vote-time'].astype('str')).dt.hour
legco_cm[legco_cm['mover-type'] == 'Public Officer'].groupby('hour').size().plot(kind='bar')
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x121b44550>

看來午飯 (12 時)及晚飯/下班前(18 時)的動議數字比較高....

- 立法會分組投票的影響力

在前面的統計中我們看到由議員提出的動議通過率只有 30% 左右,在這部份的統計我們來看一下分組投票對整體通過率的影響有多大。

算一下總體投票贊成高於反對但是被否決的動:

In [17]:
legco_cm[(legco_cm['overall-yes'] > legco_cm['overall-no']) & (legco_cm['result'] == 'Negatived')].shape[0]
Out[17]:
101

佔由議員動議而被否決的 15.51 %

2. 議員動議及投票數據統計

梁頌恆及游蕙禎沒有參與過立法會會議就被取消資格。

In [18]:
members = ['梁君彥', '涂謹申', '梁耀忠', '石禮謙', '張宇人', '李國麟', '林健鋒', '黃定光', '李慧琼',
           '陳克勤', '陳健波', '梁美芬', '黃國健', '葉劉淑儀', '謝偉俊', '毛孟靜', '田北辰', '何俊賢',
           '易志明', '胡志偉', '姚思榮', '馬逢國', '莫乃光', '陳志全', '陳恒鑌', '梁志祥', '梁繼昌', 
           '麥美娟', '郭家麒', '郭偉强', '郭榮鏗', '張華峰', '張超雄', '黃碧雲', '葉建源', '葛珮帆', 
           '廖長江', '潘兆平', '蔣麗芸', '盧偉國', '鍾國斌', '楊岳橋', '尹兆堅', '朱凱廸', '吳永嘉', 
           '何君堯', '何啟明', '林卓廷', '周浩鼎', '邵家輝', '邵家臻', '柯創盛', '容海恩', '陳沛然', 
           '陳振英', '陳淑莊', '張國鈞', '許智峯', '陸頌雄', '劉國勳', '劉業強', '鄭松泰', '鄺俊宇', 
           '譚文豪', '范國威', '區諾軒', '鄭泳舜', '謝偉銓', '陳凱欣', '梁國雄', '羅冠聰', '姚松炎', 
           '劉小麗']
# '梁頌恆', '游蕙禎'

- 議員的動議數量統計

曾經動議的議員:

In [19]:
print(legco_cm[(legco_cm['mover-type']=='Member')].mover.unique())
print(len(legco_cm[(legco_cm['mover-type']=='Member')].mover.unique()), '人')
['胡志偉' '李慧琼' '鄺俊宇' '朱凱廸' '陳志全' '區諾軒' '范國威' '林卓廷' '譚文豪' '郭家麒' '楊岳橋' '鄭松泰'
 '毛孟靜' '葉建源' '許智峯' '尹兆堅' '李國麟' '梁美芬' '梁志祥' '郭偉强' '吳永嘉' '蔣麗芸' '張超雄' '劉小麗'
 '田北辰' '黃國健' '葉劉淑儀' '張國鈞' '陳恒鑌' '陳克勤' '梁繼昌' '涂謹申' '葛珮帆' '邵家輝' '易志明' '謝偉俊'
 '陳淑莊' '郭榮鏗' '梁耀忠' '邵家臻' '盧偉國' '謝偉銓' '麥美娟' '柯創盛' '何君堯' '莫乃光' '潘兆平' '周浩鼎'
 '黃碧雲' '廖長江' '何啟明' '陸頌雄' '容海恩' '陳凱欣' '陳沛然' '姚松炎' '林健鋒' '劉國勳' '張華峰' '羅冠聰'
 '梁國雄' '張宇人' '姚思榮' '陳健波' '黃定光' '鄭泳舜' '馬逢國' '何俊賢']
68 人

把動議數、通過 / 否決數作成一個圖表

In [20]:
move_count = legco_cm[(legco_cm['mover-type']=='Member')].groupby('mover').size().reset_index(name='counts').sort_values('counts')
move_pass = legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Passed')].groupby('mover').size().reset_index(name='passed')
move_neg = legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Negatived')].groupby('mover').size().reset_index(name='negatived')
move_count = pd.merge(move_count, move_pass, on='mover', how='outer')
move_count = pd.merge(move_count, move_neg, on='mover', how='outer')
move_count[['passed', 'negatived']] = move_count[['passed', 'negatived']].fillna(0).astype('int')

plt.rcParams['figure.figsize'] = (10, 20)

inp = np.arange(move_count.shape[0])
p1 = plt.barh(inp, move_count['passed'], 1, alpha=0.7)
p2 = plt.barh(inp, move_count['negatived'], 1, left=move_count['passed'], alpha=0.7)
plt.title('Number of Motions by Member')
plt.yticks(inp, move_count['mover'])
plt.legend((p1[0], p2[0]), ('Passed', 'Negatived'), loc='center right')
plt.show()

作圖的話 73 名議員太長不太好看...

- 議員投票率統計

In [21]:
df = pd.DataFrame([legco_cm.groupby(member).size() for member in members])
df.fillna(0).astype('int')
df['member'] = members
df['vote_num'] = df.fillna(0)['Yes'] + df.fillna(0)['No'] + df.fillna(0)['Abstain']
df['vote_rate'] = df['vote_num'] / (df['vote_num'] + df.fillna(0)['Present'] + df.fillna(0)['Absent']) * 100
df.sort_values('vote_rate').head(10).fillna(0)
Out[21]:
Absent Present Abstain No Yes member vote_num vote_rate
0 24.0 899.0 0.0 0.0 0.0 梁君彥 0.0 0.000000
71 189.0 0.0 7.0 49.0 62.0 姚松炎 118.0 38.436482
30 472.0 1.0 22.0 112.0 316.0 郭榮鏗 450.0 48.754063
3 449.0 3.0 8.0 333.0 130.0 石禮謙 471.0 51.029252
1 420.0 19.0 46.0 95.0 343.0 涂謹申 484.0 52.437703
60 413.0 0.0 11.0 375.0 124.0 劉業強 510.0 55.254605
16 399.0 0.0 37.0 309.0 178.0 田北辰 524.0 56.771398
14 380.0 2.0 18.0 329.0 194.0 謝偉俊 541.0 58.613218
40 378.0 0.0 27.0 341.0 177.0 鍾國斌 545.0 59.046587
5 377.0 1.0 50.0 118.0 377.0 李國麟 545.0 59.046587

- 把投票統計和動議統計結合就成為議員的一個簡單成積表

In [22]:
member_summary = pd.merge(move_count, df[['Absent', 'Present', 'vote_num', 'vote_rate', 'member']], 
                          right_on='member', 
                          left_on='mover', 
                          how='outer')
member_summary = member_summary.drop(columns=['mover']).fillna(0)
member_summary.head(10)
Out[22]:
counts passed negatived Absent Present vote_num vote_rate member
0 1.0 0.0 1.0 115.0 0.0 808.0 87.540628 何俊賢
1 1.0 1.0 0.0 42.0 0.0 494.0 92.164179 鄭泳舜
2 1.0 1.0 0.0 4.0 0.0 313.0 98.738170 陳凱欣
3 1.0 1.0 0.0 217.0 1.0 705.0 76.381365 張宇人
4 1.0 0.0 1.0 50.0 2.0 871.0 94.366197 潘兆平
5 2.0 0.0 2.0 212.0 0.0 711.0 77.031419 何君堯
6 2.0 1.0 1.0 130.0 1.0 792.0 85.807151 劉國勳
7 2.0 1.0 1.0 328.0 0.0 595.0 64.463705 馬逢國
8 2.0 2.0 0.0 339.0 0.0 584.0 63.271939 陳沛然
9 2.0 1.0 1.0 64.0 1.0 858.0 92.957746 陳健波

- 個別議員的表現分析

動議完全沒有獲通過的議員

In [23]:
loser = []
for name in legco_cm[(legco_cm['mover-type']=='Member')].mover.unique():
    if name not in legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Passed')].mover.unique():
        loser.append(name)

print('Loser name list: ', loser)
print(len(loser), '人')
Loser name list:  ['朱凱廸', '陳志全', '范國威', '林卓廷', '楊岳橋', '鄭松泰', '許智峯', '劉小麗', '涂謹申', '柯創盛', '何君堯', '潘兆平', '姚松炎', '羅冠聰', '梁國雄', '何俊賢']
16 人
In [24]:
member_summary[member_summary['member'].isin(loser)]
Out[24]:
counts passed negatived Absent Present vote_num vote_rate member
0 1.0 0.0 1.0 115.0 0.0 808.0 87.540628 何俊賢
4 1.0 0.0 1.0 50.0 2.0 871.0 94.366197 潘兆平
5 2.0 0.0 2.0 212.0 0.0 711.0 77.031419 何君堯
13 3.0 0.0 3.0 56.0 0.0 867.0 93.932828 柯創盛
15 3.0 0.0 3.0 189.0 0.0 118.0 38.436482 姚松炎
42 8.0 0.0 8.0 88.0 0.0 835.0 90.465872 鄭松泰
46 11.0 0.0 11.0 52.0 0.0 364.0 87.500000 范國威
49 12.0 0.0 12.0 349.0 7.0 567.0 61.430119 許智峯
51 15.0 0.0 15.0 220.0 6.0 697.0 75.514626 林卓廷
54 17.0 0.0 17.0 249.0 7.0 667.0 72.264355 楊岳橋
56 17.0 0.0 17.0 107.0 0.0 200.0 65.146580 羅冠聰
58 20.0 0.0 20.0 420.0 19.0 484.0 52.437703 涂謹申
61 25.0 0.0 25.0 62.0 2.0 243.0 79.153094 劉小麗
62 26.0 0.0 26.0 77.0 0.0 230.0 74.918567 梁國雄
66 47.0 0.0 47.0 206.0 2.0 715.0 77.464789 朱凱廸
67 150.0 0.0 150.0 70.0 5.0 848.0 91.874323 陳志全

最少有一個動議獲通過的議員

In [25]:
print(legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Passed')].mover.unique())
print(len(legco_cm[(legco_cm['mover-type']=='Member') & (legco_cm['result']=='Passed')].mover.unique()), '人')
['李慧琼' '李國麟' '梁美芬' '郭偉强' '黃國健' '葉劉淑儀' '張國鈞' '陳恒鑌' '陳克勤' '葛珮帆' '鄺俊宇' '胡志偉'
 '邵家輝' '易志明' '張超雄' '梁志祥' '梁耀忠' '郭家麒' '區諾軒' '邵家臻' '莫乃光' '周浩鼎' '謝偉俊' '廖長江'
 '盧偉國' '何啟明' '陸頌雄' '麥美娟' '葉建源' '梁繼昌' '容海恩' '陳凱欣' '謝偉銓' '陳沛然' '黃碧雲' '尹兆堅'
 '張宇人' '鄭泳舜' '陳淑莊' '劉國勳' '陳健波' '黃定光' '張華峰' '毛孟靜' '譚文豪' '田北辰' '姚思榮' '蔣麗芸'
 '吳永嘉' '林健鋒' '馬逢國' '郭榮鏗']
52 人
In [26]:
member_summary[~member_summary['member'].isin(loser)]
Out[26]:
counts passed negatived Absent Present vote_num vote_rate member
1 1.0 1.0 0.0 42.0 0.0 494.0 92.164179 鄭泳舜
2 1.0 1.0 0.0 4.0 0.0 313.0 98.738170 陳凱欣
3 1.0 1.0 0.0 217.0 1.0 705.0 76.381365 張宇人
6 2.0 1.0 1.0 130.0 1.0 792.0 85.807151 劉國勳
7 2.0 1.0 1.0 328.0 0.0 595.0 64.463705 馬逢國
8 2.0 2.0 0.0 339.0 0.0 584.0 63.271939 陳沛然
9 2.0 1.0 1.0 64.0 1.0 858.0 92.957746 陳健波
10 3.0 2.0 1.0 61.0 0.0 862.0 93.391116 姚思榮
11 3.0 3.0 0.0 182.0 1.0 740.0 80.173348 廖長江
12 3.0 2.0 1.0 72.0 0.0 851.0 92.199350 張國鈞
14 3.0 2.0 1.0 241.0 0.0 682.0 73.889491 林健鋒
16 3.0 3.0 0.0 70.0 0.0 853.0 92.416035 易志明
17 4.0 3.0 1.0 280.0 0.0 643.0 69.664139 葉劉淑儀
18 4.0 3.0 1.0 94.0 0.0 829.0 89.815818 容海恩
19 4.0 2.0 2.0 60.0 1.0 862.0 93.391116 黃定光
20 4.0 2.0 2.0 98.0 7.0 818.0 88.624052 黃國健
21 4.0 1.0 3.0 61.0 0.0 862.0 93.391116 周浩鼎
22 4.0 2.0 2.0 377.0 1.0 545.0 59.046587 李國麟
23 5.0 3.0 2.0 145.0 0.0 778.0 84.290358 陳恒鑌
24 5.0 2.0 3.0 56.0 6.0 861.0 93.282774 郭偉强
25 5.0 4.0 1.0 61.0 0.0 862.0 93.391116 陳克勤
26 5.0 1.0 4.0 61.0 0.0 862.0 93.391116 邵家輝
27 5.0 1.0 4.0 380.0 2.0 541.0 58.613218 謝偉俊
28 5.0 2.0 3.0 229.0 0.0 694.0 75.189599 蔣麗芸
29 5.0 1.0 4.0 399.0 0.0 524.0 56.771398 田北辰
30 5.0 1.0 4.0 87.0 0.0 836.0 90.574215 梁志祥
31 6.0 3.0 3.0 179.0 0.0 744.0 80.606717 梁美芬
32 6.0 3.0 3.0 317.0 0.0 606.0 65.655471 邵家臻
33 6.0 5.0 1.0 82.0 6.0 835.0 90.465872 麥美娟
34 6.0 2.0 4.0 81.0 6.0 836.0 90.574215 陸頌雄
35 6.0 2.0 4.0 32.0 0.0 504.0 94.029851 謝偉銓
36 6.0 5.0 1.0 200.0 0.0 723.0 78.331528 吳永嘉
37 7.0 1.0 6.0 304.0 0.0 619.0 67.063922 葉建源
38 7.0 4.0 3.0 134.0 1.0 788.0 85.373781 張華峰
39 7.0 3.0 4.0 222.0 0.0 701.0 75.947996 莫乃光
40 7.0 6.0 1.0 36.0 0.0 887.0 96.099675 葛珮帆
41 8.0 1.0 7.0 267.0 5.0 651.0 70.530878 鄺俊宇
43 9.0 1.0 8.0 472.0 1.0 450.0 48.754063 郭榮鏗
44 9.0 7.0 2.0 38.0 6.0 832.0 94.977169 何啟明
45 10.0 4.0 6.0 286.0 0.0 637.0 69.014085 梁耀忠
47 11.0 1.0 10.0 141.0 26.0 756.0 81.906826 譚文豪
48 11.0 7.0 4.0 11.0 0.0 912.0 98.808234 盧偉國
50 13.0 2.0 11.0 326.0 15.0 582.0 63.055255 陳淑莊
52 15.0 1.0 14.0 276.0 15.0 632.0 68.472373 黃碧雲
53 17.0 14.0 3.0 93.0 24.0 806.0 87.323944 李慧琼
55 17.0 5.0 12.0 303.0 5.0 615.0 66.630553 尹兆堅
57 19.0 4.0 15.0 302.0 3.0 618.0 66.955580 梁繼昌
59 21.0 2.0 19.0 269.0 6.0 648.0 70.205850 胡志偉
60 21.0 1.0 20.0 203.0 0.0 720.0 78.006501 毛孟靜
63 28.0 2.0 26.0 67.0 1.0 348.0 83.653846 區諾軒
64 32.0 1.0 31.0 276.0 6.0 641.0 69.447454 郭家麒
65 36.0 3.0 33.0 282.0 0.0 641.0 69.447454 張超雄
68 0.0 0.0 0.0 24.0 899.0 0.0 0.000000 梁君彥
69 0.0 0.0 0.0 449.0 3.0 471.0 51.029252 石禮謙
70 0.0 0.0 0.0 378.0 0.0 545.0 59.046587 鍾國斌
71 0.0 0.0 0.0 23.0 3.0 897.0 97.183099 陳振英
72 0.0 0.0 0.0 413.0 0.0 510.0 55.254605 劉業強

完全沒有動議的議員

In [27]:
member_summary[member_summary['counts'] == 0].sort_values(['Absent'], ascending=False)
Out[27]:
counts passed negatived Absent Present vote_num vote_rate member
69 0.0 0.0 0.0 449.0 3.0 471.0 51.029252 石禮謙
72 0.0 0.0 0.0 413.0 0.0 510.0 55.254605 劉業強
70 0.0 0.0 0.0 378.0 0.0 545.0 59.046587 鍾國斌
68 0.0 0.0 0.0 24.0 899.0 0.0 0.000000 梁君彥
71 0.0 0.0 0.0 23.0 3.0 897.0 97.183099 陳振英

能看得出 劉業強、鍾國斌、石禮謙、不但完全沒動議,連投票率也不到 60%

投票率最高的 10 名議員:

In [28]:
member_summary.sort_values('vote_num', ascending=False).head(10)
Out[28]:
counts passed negatived Absent Present vote_num vote_rate member
48 11.0 7.0 4.0 11.0 0.0 912.0 98.808234 盧偉國
71 0.0 0.0 0.0 23.0 3.0 897.0 97.183099 陳振英
40 7.0 6.0 1.0 36.0 0.0 887.0 96.099675 葛珮帆
4 1.0 0.0 1.0 50.0 2.0 871.0 94.366197 潘兆平
13 3.0 0.0 3.0 56.0 0.0 867.0 93.932828 柯創盛
25 5.0 4.0 1.0 61.0 0.0 862.0 93.391116 陳克勤
10 3.0 2.0 1.0 61.0 0.0 862.0 93.391116 姚思榮
19 4.0 2.0 2.0 60.0 1.0 862.0 93.391116 黃定光
21 4.0 1.0 3.0 61.0 0.0 862.0 93.391116 周浩鼎
26 5.0 1.0 4.0 61.0 0.0 862.0 93.391116 邵家輝

投票率最低的 10 名議員:

In [29]:
member_summary.sort_values('vote_rate', ascending=True).head(10)
Out[29]:
counts passed negatived Absent Present vote_num vote_rate member
68 0.0 0.0 0.0 24.0 899.0 0.0 0.000000 梁君彥
15 3.0 0.0 3.0 189.0 0.0 118.0 38.436482 姚松炎
43 9.0 1.0 8.0 472.0 1.0 450.0 48.754063 郭榮鏗
69 0.0 0.0 0.0 449.0 3.0 471.0 51.029252 石禮謙
58 20.0 0.0 20.0 420.0 19.0 484.0 52.437703 涂謹申
72 0.0 0.0 0.0 413.0 0.0 510.0 55.254605 劉業強
29 5.0 1.0 4.0 399.0 0.0 524.0 56.771398 田北辰
27 5.0 1.0 4.0 380.0 2.0 541.0 58.613218 謝偉俊
70 0.0 0.0 0.0 378.0 0.0 545.0 59.046587 鍾國斌
22 4.0 2.0 2.0 377.0 1.0 545.0 59.046587 李國麟

3. 根據政黨的投票分析

從 Wikipedia 上找到各個議員的所屬政黨

In [30]:
from bs4 import BeautifulSoup
In [31]:
with open('./party.html', 'r') as f:
    party = f.read()
party_soup = BeautifulSoup(party, 'html.parser')
tr = party_soup.find_all('tr')
tr_text = []
for i in tr:
    j = i.text.strip().split('\n\n')
    a = j[0].split('\n')[-1]
    b = j[-1].split('\n')[0]
    tr_text.append([a, b])
tr_text.remove(['備註', '席位'])
tr_text.remove(['懸空', ''])
for _ in range(3):
    tr_text.remove(['懸空', '懸空'])
party_pd = pd.DataFrame(tr_text)
# correct error due to format
party_pd.loc[party_pd[0] == '吳永嘉', 1] = '經民聯'
party_pd.loc[party_pd[0] == '邵家輝', 1] = '自由黨'
# add missing members
missing = pd.DataFrame([{0: '張華峰', 1: '經民聯'},
 {0: '何啟明', 1: '工聯會'},
 {0: '范國威', 1: '香港本土'},
 {0: '區諾軒', 1: '獨立民主派'},
 {0: '梁國雄', 1: '社民連'}, 
 {0: '羅冠聰', 1: '眾志'},
 {0: '姚松炎', 1: '專業議政'},
 {0: '劉小麗', 1: '工黨'}, 
#  {0: '梁頌恆', 1: '青年新政'},
#  {0: '游蕙禎', 1: '青年新政'}
])
party_pd = party_pd.append(missing, ignore_index=True)
party_pd.replace('公民黨/專業議政', '公民黨', inplace=True)
party_pd.replace('教協/專業議政', '專業議政', inplace=True)
party_pd.replace('民建聯/新界社團聯會', '民建聯', inplace=True)
party_pd.replace('新民黨/公民力量', '新民黨', inplace=True)
party_pd.replace('公專聯/專業議政', '公專聯', inplace=True)
party_pd.replace('經民聯/西九新動力', '經民聯', inplace=True)
In [32]:
party_pd[1].unique()
Out[32]:
array(['新民黨', '工聯會', '民主黨', '民建聯', '公民黨', '經民聯', '香港本土', '獨立建制派', '獨立民主派',
       '實政圓桌', '熱血公民', '工黨', '人民力量', '自由黨', '專業議政', '公專聯', '獨立中間派', '勞聯',
       '新論壇', '街工', '社民連', '眾志'], dtype=object)
In [33]:
party_pd.head()
Out[33]:
0 1
0 葉劉淑儀 新民黨
1 郭偉强 工聯會
2 許智峯 民主黨
3 張國鈞 民建聯
4 陳淑莊 公民黨

- 以政黨作單位的投票率

In [34]:
member_summary_party = pd.merge(member_summary, party_pd, left_on='member', right_on=0, how='left')
member_summary_party.drop(columns=0, inplace=True)
member_summary_party
Out[34]:
counts passed negatived Absent Present vote_num vote_rate member 1
0 1.0 0.0 1.0 115.0 0.0 808.0 87.540628 何俊賢 民建聯
1 1.0 1.0 0.0 42.0 0.0 494.0 92.164179 鄭泳舜 民建聯
2 1.0 1.0 0.0 4.0 0.0 313.0 98.738170 陳凱欣 獨立建制派
3 1.0 1.0 0.0 217.0 1.0 705.0 76.381365 張宇人 自由黨
4 1.0 0.0 1.0 50.0 2.0 871.0 94.366197 潘兆平 勞聯
... ... ... ... ... ... ... ... ... ...
68 0.0 0.0 0.0 24.0 899.0 0.0 0.000000 梁君彥 經民聯
69 0.0 0.0 0.0 449.0 3.0 471.0 51.029252 石禮謙 經民聯
70 0.0 0.0 0.0 378.0 0.0 545.0 59.046587 鍾國斌 自由黨
71 0.0 0.0 0.0 23.0 3.0 897.0 97.183099 陳振英 獨立建制派
72 0.0 0.0 0.0 413.0 0.0 510.0 55.254605 劉業強 經民聯

73 rows × 9 columns

In [35]:
pty_vote_rate = member_summary_party.groupby(1).vote_rate.mean().sort_values()

plt.rcParams['figure.figsize'] = (10, 10)

ax = pty_vote_rate.plot(kind='barh', alpha=0.7, title='各政黨的平均投票率')
ax.set_xlabel('投票率(%)')
ax.set_ylabel("政黨")
ax.axvline(x=50, color='red', ls='--', alpha=1, label='50%')
Out[35]:
<matplotlib.lines.Line2D at 0x12288eb90>

- 各政黨投票的統一性分析

為分析各政黨議員投票的統一性,我們先定義一個統一性的分數。

$$Score = \frac{A (Yes - No)^2 + B (Yes - Abstain)^2 + C (No - Abstain)^2}{(Yes + No + Abstain)^2} $$

Score = 1 為同一次投票內選擇完全一致(不包括缺席),越分散分數越低。

就每一次的投票結果而言 Yes 和 No 及 Abstain 是對立的,但 No 和 Abstain 雖然立場有不同但做成結果一致,所以把 Yes-No 和 Yes-Abstain 的比重 (A, B) 設成 2,而 No-Abstain (C) 則設成 0.5。把 function 寫成可以改變比重的模式方便日後(反悔時)調整。

In [36]:
legco_member_vote = legco_cm[members]

def diff_vote(inputList, weight=[2, 2, 0.5]):
    yes = 0
    no = 0
    abstain = 0
    for vote in inputList:
        if vote == 'Yes':
            yes += 1
        elif vote == 'No':
            no += 1
        elif vote == 'Abstain':
            abstain += 1
    if (yes**2 + no**2 + abstain**2) > 0:
        diff = 1 - (weight[0] * 2 * (yes * no) + weight[2] * 2 * (no * abstain) + weight[1] * 2 * (yes * abstain)) / (yes + no + abstain)**2
    else:
        diff = np.nan
    return diff

測試:

In [37]:
party_member_list = list(party_pd.groupby(1).get_group('民建聯')[0])
test = []
for i in range(legco_member_vote.shape[0]):
    diff = diff_vote(legco_member_vote[(member for member in legco_member_vote.columns if member in party_member_list)].loc[i])
    test.append(diff)
legco_cm['民建聯'] = test
print(legco_cm['民建聯'].describe())
legco_cm[legco_cm['民建聯'] < 1][[member for member in legco_cm.columns if member in party_member_list]+['motion']]
count    923.000000
mean       0.997819
std        0.028279
min        0.479290
25%        1.000000
50%        1.000000
75%        1.000000
max        1.000000
Name: 民建聯, dtype: float64
Out[37]:
黃定光 李慧琼 陳克勤 何俊賢 陳恒鑌 梁志祥 葛珮帆 蔣麗芸 周浩鼎 柯創盛 張國鈞 劉國勳 鄭泳舜 motion
97 Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain No Abstain Abstain 根據《立法會(權力及特權)條例》動議的議案
125 Abstain Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes NaN 縮短點名表決響鐘時間的議案
463 No No Yes No No No No Absent Absent No No No NaN 《2017年撥款條例草案》 - 全體委員會審議階段 - 總目90的修正案 (修正案編號72)
603 No Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Absent Abstain Abstain 郭家麒議員對何君堯議員的「活化強制性公積金」議案作出的修正案
730 Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Abstain Yes Yes 陸頌雄議員對何啟明議員的「全面檢討勞工法例,改善勞工權益」議案作出的修正案
786 Absent Absent Yes Abstain Yes Yes Yes Yes Yes Yes Absent Absent Yes 修訂《2018年食物攙雜(金屬雜質含量)(修訂)規例》的擬議決議案
828 No No No No No No No No No No No No Yes 《國歌條例草案》 - 全體委員會審議 - 陳志全議員的第十四及十五項修正案 (修正案編號18...

如果每一個政黨都要把投票不一的議題全部列出太貼位置,把迴圈修改為只列出 motion id。同時亦把只有一名立法會議員的政黨除掉。

In [38]:
party_vote_summary = []

for party in party_pd[1].unique():
    party_member_list = list(party_pd.groupby(1).get_group(party)[0])
    test = []
    diff_list = []
    for i in range(legco_member_vote.shape[0]):
        diff = diff_vote(legco_member_vote[(member for member in legco_member_vote.columns if member in party_member_list)].loc[i])
        test.append(diff)
        if diff < 1:
            diff_list.append(i)
    legco_cm[party] = test
    party_vote = {'party-name': party,
                  'num-members': len([member for member in legco_member_vote.columns if member in party_member_list]),
                  'involed-num-motion': legco_cm[party].count(), 
                  'num-motion-not-unify': len(diff_list),
                  'motion-list': diff_list,
                  'score-mean': legco_cm[party].mean(),
                  'score-sd': legco_cm[party].std()
                 }
    party_vote_summary.append(party_vote)

party_vote_summary_df = pd.DataFrame(party_vote_summary)
party_vote_summary_df[party_vote_summary_df['num-members'] > 1].sort_values('score-mean')
#     print(legco_cm[legco_cm[party] < 1][[member for member in legco_cm.columns if member in party_member_list]+['motion']])
Out[38]:
party-name num-members involed-num-motion num-motion-not-unify motion-list score-mean score-sd
8 獨立民主派 4 864 201 [8, 9, 11, 12, 13, 14, 15, 16, 17, 18, 19, 22,... 0.808489 0.364708
7 獨立建制派 8 918 143 [2, 63, 71, 72, 73, 75, 76, 77, 78, 86, 87, 88... 0.914695 0.234340
15 公專聯 2 739 46 [8, 35, 45, 48, 93, 94, 110, 167, 211, 272, 29... 0.951962 0.205512
5 經民聯 8 923 69 [65, 71, 76, 87, 88, 95, 97, 99, 105, 145, 146... 0.968870 0.137125
6 香港本土 2 751 26 [601, 609, 610, 611, 612, 628, 638, 639, 719, ... 0.970373 0.165945
14 專業議政 2 639 21 [65, 66, 72, 155, 287, 288, 289, 291, 295, 296... 0.977700 0.138536
11 工黨 2 727 15 [65, 66, 72, 81, 283, 292, 294, 389, 555, 588,... 0.987620 0.100882
13 自由黨 4 923 14 [0, 86, 87, 88, 112, 210, 261, 272, 395, 611, ... 0.992672 0.070618
2 民主黨 7 829 9 [55, 214, 215, 216, 217, 284, 285, 399, 837] 0.993640 0.067003
4 公民黨 5 877 6 [79, 270, 720, 797, 800, 810] 0.995634 0.055960
3 民建聯 13 923 7 [97, 125, 463, 603, 730, 786, 828] 0.997819 0.028279
1 工聯會 5 914 3 [60, 261, 549] 0.998065 0.037059
0 新民黨 2 867 1 [292] 0.998847 0.033962

可以看到以獨立民主派分歧最大,新民黨最統一(但只有 2 人),其次是工聯會和民建聯。

In [39]:
party_vote_summary = party_vote_summary_df[party_vote_summary_df['num-members'] > 1].sort_values('score-mean')
inp = np.arange(party_vote_summary.shape[0])

plt.barh(inp, party_vote_summary['score-mean'], alpha=0.7)
plt.title('Voting Score of Parties')
plt.yticks(inp, party_vote_summary['party-name'])
plt.show()

- 接下來根據 wikipedia 的定義把政黨分為泛民和建制兩大陣營作分析

分類根據 wikipedia 對建制和泛民的定義

In [40]:
proBJ = ['民建聯', '工聯會', '經民聯', '自由黨', '新民黨', '實政圓桌', '新論壇', '勞聯']
proDem = ['民主黨', '公民黨', '工黨', '街工', '公專聯', '人民力量', '社民連']
all_parties = ['新民黨', '工聯會', '民主黨', '民建聯', '公民黨', '經民聯', '香港本土', '獨立建制派', '獨立民主派',
       '實政圓桌', '熱血公民', '工黨', '人民力量', '自由黨', '專業議政', '公專聯', '獨立中間派', '勞聯',
       '新論壇', '街工', '社民連', '眾志']
In [41]:
def getPartyMember(partyList):
    memberList = []
    for party in partyList:
        memberList += list(party_pd.groupby(1).get_group(party)[0])
    return memberList
In [42]:
side_vote_summary = []
side = [{'name': '建制', 'party-list': proBJ}, 
        {'name': '泛民', 'party-list': proDem},
        {'name': '立法會全體', 'party-list': all_parties}
        ]

for s in side:
    party_member_list = getPartyMember(s['party-list'])
    test = []
    diff_list = []
    for i in range(legco_member_vote.shape[0]):
        diff = diff_vote(legco_member_vote[(member for member in legco_member_vote.columns if member in party_member_list)].loc[i])
        test.append(diff)
        if diff < 1:
            diff_list.append(i)
    legco_cm[s['name']] = test
    party_vote = {'party-name': s['name'],
                  'num-members': len([member for member in legco_member_vote.columns if member in party_member_list]),
                  'involed-num-motion': legco_cm[s['name']].count(), 
                  'num-motion-not-unify': len(diff_list),
                  'motion-list': diff_list,
                  'score-mean': legco_cm[s['name']].mean(),
                  'score-sd': legco_cm[s['name']].std()
                  }
    side_vote_summary.append(party_vote)
In [43]:
side_summary_df = pd.DataFrame(side_vote_summary)
side_summary_df
Out[43]:
party-name num-members involed-num-motion num-motion-not-unify motion-list score-mean score-sd
0 建制 35 923 219 [0, 60, 61, 62, 63, 64, 65, 71, 72, 73, 74, 75... 0.876516 0.270388
1 泛民 19 922 339 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, ... 0.759984 0.367609
2 立法會全體 73 923 887 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,... 0.271340 0.275440
In [44]:
inp = np.arange(side_summary_df.shape[0])
plt.bar(inp, side_summary_df['score-mean'], alpha=0.7)
plt.title('Voting Score of Parties')
plt.xticks(inp, ['建制', '泛民', '立法會全體'])
plt.show()

- 分析:如果...

在把立法會議員分成建制及泛民兩邊時我們不難發現建制派有著人數上的優勢,如果把獨立建制派也加在內的話總人數達 43 人,也就是 2/3 的總人數。但是把投票並沒有完全統一及缺席也加算在內的話,在議員動議統計也不難發現建制派的動議被否決或是泛民派的動議通過。在這部份我們將探討一下有多少動議是如果泛民派足夠團結/全員出席後有機會改變結果。

In [45]:
proBJ += ['獨立建制派']
proBjMember = [x for x in members if (member_summary_party[(member_summary_party['member'] == x)][1].values in proBJ)]
proBJPass = np.zeros(legco_member_vote.shape[0])
proBJNeg = np.zeros(legco_member_vote.shape[0])
proBJAbs = np.zeros(legco_member_vote.shape[0])

for i in range(legco_member_vote.shape[0]):
    p = 0
    n = 0
    absend = 0
    for member in proBjMember:
        if legco_member_vote[member].iloc[i] in ['No', 'Astain']:
            n += 1
        elif legco_member_vote[member].iloc[i] == 'Yes':
            p += 1
        else:
            absend += 1
    proBJPass[i] = p
    proBJNeg[i] = n
    proBJAbs[i] = absend
    
legco_cm['Pro-BJ-pass'] = proBJPass
legco_cm['Pro-BJ-neg'] = proBJNeg
legco_cm['Pro-BJ-abs'] = proBJAbs
legco_cm[['Pro-BJ-pass', 'Pro-BJ-neg', 'Pro-BJ-abs', 'overall-yes', 'overall-no', 'result']].head()
Out[45]:
Pro-BJ-pass Pro-BJ-neg Pro-BJ-abs overall-yes overall-no result
0 0.0 32.0 11.0 16 33 Negatived
1 33.0 0.0 10.0 33 18 Passed
2 0.0 31.0 12.0 18 31 Negatived
3 0.0 35.0 8.0 11 38 Negatived
4 0.0 35.0 8.0 8 38 Negatived

假如加上獨立民立派 4 名議員,泛民主派議員人數一共 23 人,查看一下有沒有 23 票能改變的議題。我們先把建制派的投票中支持票及反對票差別少於 23 票的動議找出來。(由議員提出的動議需要分組投票才能通過,為簡化過程這部份我們只看由政府提出的動議。)

In [46]:
legco_cm['pro-BJ-diff'] = abs(legco_cm['Pro-BJ-pass'] -  legco_cm['Pro-BJ-neg'])
legco_cm[(legco_cm['pro-BJ-diff'] <= 23) & (legco_cm['mover-type'] == 'Public Officer')][['vote-id', 'Pro-BJ-pass', 'Pro-BJ-neg', 'overall-vote', 'overall-yes', 'result']]
Out[46]:
vote-id Pro-BJ-pass Pro-BJ-neg overall-vote overall-yes result
104 20180328004 23.0 0.0 44 44 Passed
263 20180207002 7.0 21.0 41 12 Negatived
275 20170524001 16.0 0.0 35 34 Passed
276 20170524002 16.0 0.0 35 35 Passed
277 20170524003 22.0 0.0 39 38 Passed
278 20170524004 20.0 0.0 36 35 Passed
279 20170524005 20.0 0.0 37 36 Passed
280 20170524006 19.0 0.0 36 35 Passed
282 20170524008 21.0 0.0 38 38 Passed
560 20170517163 23.0 0.0 38 23 Passed
902 20180509067 21.0 0.0 29 22 Passed

當中由政府提出的 20170517163 號及 20180509067 號看來是泛民主派有機會以及想要拉倒的動議。

In [47]:
legco_cm[legco_cm['vote-id'] == '20170517163'][['motion', 'mover', 'overall-vote', 'overall-yes', 'Pro-BJ-pass', 'overall-no']]
Out[47]:
motion mover overall-vote overall-yes Pro-BJ-pass overall-no
560 《2017年撥款條例草案》 - 全體委員會審議階段 - 總目21、22、25、26、28、3... 財政司司長 38 23 23.0 15
In [48]:
legco_cm[legco_cm['vote-id'] == '20180509067'][['motion', 'mover', 'overall-vote', 'overall-yes', 'Pro-BJ-pass', 'overall-no']]
Out[48]:
motion mover overall-vote overall-yes Pro-BJ-pass overall-no
902 《2018年撥款條例草案》 - 全體委員會審議 - 總目21、22、28、30、33、44、... 財政司司長 29 22 21.0 5

都是年度撥款條例草案,然而參與投票不足下未能達到想要的結果。(當然在現實建制派發現泛民主派投票人數增加也有動員參加投票的可能,所以並不能說現實上如果泛民主派努力一點團結一點就能成功。在這裡只是以這個假想情況作例子示範一下 pandas 如何幫我們找到想要的數據。)

4. 立法會的投票取向視像化

在這個部份我們試著把各議員的投票傾向分類和視像化。首先我們會用上面提及的評分方法計算議員之間的距離評分,議員之間投票的相似度可以用 heat map 來視像化。然後我們會用 Multidimensional Scaling (MDS)方法把不易看懂的 heat map 轉換成二維坐標圖分析。

- 議員投票距離距陣

這裡以之前定義的評分來計算投票距離 (1-Score),使 0 為最近,投票方向越不同距離越大。

In [49]:
member_vote = legco_cm[members].drop(columns='梁君彥')    # drop him as he is the chairman of cm who did not vote at all


#    This script took more then 30 mins to finish in my notebook...
matrix = []
timer = 0
print(str(timer) + "/73", end=' ')
for member1 in member_vote.columns:
    mem_dict = {}
    for member2 in member_vote.columns:
        diff_list = np.zeros(member_vote.shape[0])
        for i in range(member_vote.shape[0]):
            diff = diff_vote(member_vote[[member1, member2]].loc[i])
            diff_list[i] = diff
        mem_dict[member2] = 1 - np.nanmean(diff_list)
    matrix.append(mem_dict)
    timer += 1
    print(str(timer) + "/73", end=' ')    # just to enusre the program is running

print()
matrix_df = pd.DataFrame(matrix)
0/73 1/73 2/73 3/73 4/73 5/73 6/73 7/73 8/73 9/73 10/73 11/73 12/73 13/73 14/73 15/73 16/73 17/73 18/73 19/73 20/73 21/73 22/73 23/73 24/73 25/73 26/73 27/73 28/73 29/73 30/73 31/73 32/73 33/73 34/73 35/73 36/73 37/73 38/73 39/73 40/73 41/73 42/73 43/73 44/73 45/73 46/73 47/73 48/73 49/73 50/73 51/73 52/73 53/73 54/73 55/73 56/73 57/73 58/73 59/73 60/73 61/73 62/73 63/73 64/73 65/73 66/73 67/73 68/73 69/73 70/73 71/73 72/73 

把 index 換成名字。

In [50]:
rename_dict = {}
i = 0
for member1 in member_vote.columns:
    rename_dict[i] = member1
    i += 1
matrix_df.rename(rename_dict, inplace=True)
matrix_df.head()
Out[50]:
涂謹申 梁耀忠 石禮謙 張宇人 李國麟 林健鋒 黃定光 李慧琼 陳克勤 陳健波 ... 譚文豪 范國威 區諾軒 鄭泳舜 謝偉銓 陳凱欣 梁國雄 羅冠聰 姚松炎 劉小麗
涂謹申 0.000000 0.115141 0.304269 0.346774 0.071321 0.368205 0.337100 0.368750 0.370413 0.364872 ... 0.075898 0.080556 0.091052 0.330251 0.350293 0.204473 0.057258 0.039101 0.038986 0.043582
梁耀忠 0.115141 0.000000 0.417655 0.585812 0.143733 0.481287 0.544711 0.564302 0.572987 0.573562 ... 0.081528 0.035663 0.029078 0.477149 0.508916 0.325403 0.018342 0.011954 0.018321 0.013926
石禮謙 0.304269 0.417655 0.000000 0.013654 0.310056 0.006275 0.023687 0.023714 0.023783 0.021216 ... 0.425733 0.272196 0.250389 0.012327 0.011960 0.005426 0.089370 0.111250 0.043986 0.125000
張宇人 0.346774 0.585812 0.013654 0.000000 0.377481 0.021825 0.060211 0.071266 0.072456 0.035675 ... 0.555459 0.333333 0.297990 0.043147 0.028958 0.033641 0.167197 0.142674 0.082882 0.170354
李國麟 0.071321 0.143733 0.310056 0.377481 0.000000 0.328125 0.348077 0.368664 0.387472 0.390660 ... 0.117034 0.128582 0.136364 0.317760 0.366373 0.275920 0.032143 0.017988 0.021084 0.017085

5 rows × 72 columns

把距陣以 heatmap 展視。(基本上看不出什麼...)

In [51]:
ax = sns.heatmap(matrix_df, vmin=0, vmax=1)

導入 scikit learn 的 MDS 模組。

In [52]:
from sklearn.manifold import MDS

model = MDS(n_components=2, dissimilarity='precomputed', random_state=1)
In [53]:
out = model.fit_transform(matrix_df)
member_scatt = pd.DataFrame({'member': matrix_df.columns, 
                            'x': out[:, 0], 
                            'y': out[:, 1]})
member_scatt = member_scatt.merge(member_summary_party[['member', 1]], on='member')
member_scatt.head()
Out[53]:
member x y 1
0 涂謹申 0.089394 0.165066 民主黨
1 梁耀忠 0.127461 0.307847 街工
2 石禮謙 -0.065369 -0.111455 經民聯
3 張宇人 -0.116345 -0.132765 自由黨
4 李國麟 0.126469 0.148297 獨立民主派
In [54]:
plt.rcParams['figure.figsize'] = (15, 15)

sns.scatterplot(member_scatt['x'], member_scatt['y'], hue=member_scatt[1], s=100)
def label_point(x, y, val, ax):
    for i in range(len(x)):
        ax.text(x[i]+.005, y[i]-0.002, str(val[i]))

label_point(out[:, 0], out[:, 1], matrix_df.columns, plt.gca())

在這個圖可以看出建制派整體的投票比較一致,泛民主派雖然投票傾向遠離建制派,但分怖比較離散。