2018年12月11日 星期二

拿公投統計資料學 pandas

以前我都用 perl 處理文字資料, 例如 regexp csv 句型 都很好用。 但是現在大家都用 python, 我當然也要跟著趕一下流行啊~~ 處理試算表類型的資料時, python 程式設計師最常用 pandas 函式庫。 它也是玩機器學習或資料科學一定要學東西。 繪圖的話, matplotlib 跟 plotly 都很好用。 就讓我們拿中華臺北人很有感的十項案公投結果統計資料來當 pandas / matplotlib / plotly 的入門範例吧!

我用 python3 測試, 所以先這樣安裝: pip3 install pandas 然後進入 python3 練習。 再到 Pandas Cheat Sheet 下載一頁的 pdf 摘要。 我學的時候基本上也就只以這一頁為主, 看不懂的地方再另外搜尋補充。 但作者 Karlijn Willems 所用的版本好像有點舊, 所以 iloc 跟 iat 的語法在新版不適用。 沒關係, 有興趣的讀者可參考 這個問答; 但反正本篇也沒用到 iloc 跟 iat。

一、 資料結構與資料概觀

pandas 兩類資料結構 Pandas 主要用來處理一維陣列 (在 pandas 文件裡稱為 series) 及二維陣列 (稱為 data frame)。 程式可以用整數註標存取 series 裡的元素 (類似 python 內建的 list), 也可以用文字標籤存取 (類似內建的 dict)。 不論是整數註標或文字標籤, 通稱為 index。 程式存取 dataframe 裡的元素時, 可以按照 column (欄位) 名稱一次擷取一整直行, 或是按照 index 註標/標籤一次擷取一整橫列。 以上請見 cheatsheet 的 Pandas Data Structures 那一段的兩張圖, 也請先進入 python3 並且照著 Karlijn 的文章建立 s = pd.Series(...)df = pd.DataFrame(...) 兩個變數, 然後就可以測試以下:

  • .shape 資料有幾列? 幾行?
  • .index 資料的所有文字標籤, 或整數註標範圍
  • .columns (限用於 data frame 類型) 資料的所有欄位名稱
  • .info() (限用於 data frame 類型) 資料摘要概述
  • .count() 資料有幾列
  • .describe() 資料當中每個數字欄位的基本統計資訊: 平均值、 標準差、 最大、 最小、 中位數等等。

二、 準備測試資料

請用 wget https://ckhung.github.io/a/m/18/referendum.csv 抓回測試資料。 這是今年公投結果的分地區統計資料。 然後就可略過這節。 以下只是我自己的筆記, 記錄前置處理的過程。

首先到 g0v/referendum_report 抓回原始 (?) 資料。 在 results/ 目錄下, 有每一案按照鄉鎮市區別統計的贊成反對票數。 我用這個指令做前置處理: perl -pe 's/"(\d+(,\d+)+)"/$x=$1,$x=~s#,##g,$x/eg' *.csv | perl -F, -nale 'print join(",", @F[0..8])' | p -pe 's/第(\d+)案/sprintf("c%02d",$1)/e' | sort > ~/referendum.csv

在 perl 的置換 regexp 最後面加上 e 選項 (s/.../.../e), 表示置換字串要先當成 perl 程式碼執行 (execute)。 所以上面指令每一段的意思分別是:

  1. 把數字裡的 (每三位一個) 逗點去掉, 同時把引號去掉。
  2. 只擷取其中幾個較有趣的欄位。 詳見 perl 的 csv 句型
  3. 把 「第xx案」 改成 「cxx」

最後再手工編輯, 把最後面的標頭列移到第一列, 並刪掉重複的標頭列, 就得到本節最開始的下載檔。

三、 擷取整欄、 進行四則運算等等

我們在 python3 裡面讀入 csv 檔、 查看其中一個欄位、 驗算 「同意票數+不同意票數==有效票數」、 驗算 「有效票數+無效票數==投票數」:

import pandas as pd
refm = pd.read_csv('referendum.csv')
refm['同意票數']
x=refm['同意票數']+refm['不同意票數']-refm['有效票數']
x.describe()
x=refm['有效票數']+refm['無效票數']-refm['投票數']
x.describe()

不只可以把整欄拿來加減乘除, 也可以拿來做字串/數值比較: refm['縣市']=='全國' 這會得到一個 series, 它的值全部是 True 或 False。 這個 series 正好又可以拿來當成一個 「過濾器」 從原資料當中篩選出 (只保留) 你有興趣 (算出 True) 的那幾列。 我們可以用這個方法只保留每一案的全國總計值, 進而算出每一案的贊成比例。 請拿以下結果跟 這篇新聞 的數字比對一下。

summary = refm[refm['縣市']=='全國']
summary['同意票數']/summary['投票數']

這裡有更多 「拿 True/False series 篩選列」 的範例: 1 2 3 4 這篇 特別詳盡, 並示範 & | ~ 的用法 (對, 用 bitwise operators, 不是用英文的 and or not)。

四、 刪除列、 刪除欄

再來我們要簡化資料。 首先把全國及縣市加總資料刪掉:

refm2 = refm[~ refm['鄉鎮市區'].isnull()]
refm3 = refm.dropna(subset=['鄉鎮市區'])
# 上面兩句效果相同。 可以說第二句是第一句的簡寫。

計算 「贊成比例」 並且把 「鄉鎮市區」 冠上縣市名稱。 只留下 「案件」、 「鄉鎮市區」、 「贊成比例」。

refm = refm3
refm['贊成比例'] = refm['同意票數']/refm['投票數']
refm['鄉鎮市區'] = refm['縣市'] + refm['鄉鎮市區']
refm2 = refm.drop(columns=['縣市', '同意票數', '不同意票數', '有效票數', '無效票數', '投票數', '投票權人數'])
refm3 = refm[['案件', '鄉鎮市區', '贊成比例']]
# 上面兩句效果相同。

五、 疊疊樂

再來, 想把資料大整形, 變成 「以鄉鎮市區為 index, 以公投案件代號為 columns」。 搜尋不到簡單的方法, 只好寫迴圈。 不過寫迴圈之前, 先測試一下即將放在迴圈內的一個基本動作:

refm = refm3
x07 = refm[refm['案件']=='c07'][['鄉鎮市區','贊成比例']]
x08 = refm[refm['案件']=='c08'][['鄉鎮市區','贊成比例']]
tmp = pd.concat([x07.set_index('鄉鎮市區'), x08.set_index('鄉鎮市區')], axis=1, sort=True)
tmp.columns = ['c07', 'c08']

上面先分別撈出第七案的所有資料、 第八案的所有資料, 然後照著 這個問答 用強大的 .concat() 把兩份資料左右疊在一起。 疊的時候必須先讓兩者都改採 「鄉鎮市區」 作為 index。 疊完之後變成有兩個 column 撞名, 都叫做 「贊成比例」, 所以再根據 這個問答, 把它們的名字分別改成 'c07' 跟 'c08'。 注意: 還記得嗎? index 是標籤, 不算在 columns 裡面!

呼叫這個函數時, 如果傳入 axis=0 (預設), 則是上下相疊; 如果傳入 axis=1 則是左右相疊。 左右相疊時, 兩邊的 index 可能對不齊 (有幾列只有左邊的 dataframe 有; 另幾列只有右邊的 dataframe 有), 這時用 join='outer' 指定求聯集, 用 join='inner' 只保留交集。 請圖片搜尋 pandas concat, 有很多張圖可以讓你一眼秒懂上面這段話。

現在我們就知道該怎麼用迴圈把 10 案公投結果橫向疊起來了:

summary = refm[refm['案件']=='c07'][['鄉鎮市區','贊成比例']].set_index('鄉鎮市區')
cnames = refm['案件'].unique()
for c in cnames[1:] :
    tmp = refm[refm['案件']==c][['鄉鎮市區','贊成比例']]
    summary = pd.concat([summary, tmp.set_index('鄉鎮市區')], axis=1, sort=True)

summary.columns = cnames

六、 用 matplotlib 畫圖

Matplotlib 是傳統的 python 繪圖函式庫。 先前我寫過一個 scatterplot 範例, 包含解決中文問題。 Pandas 的 dataframe 可以直接呼叫 matplotlib 的繪圖函數, 像這樣: drawing=summary.plot(kind='scatter', x='c07', y='c08') 但這只是產生一個圖片物件; 要秀在螢幕上, 還必須:

import matplotlib.pyplot as plt
plt.savefig('ref-07-08.svg')
plt.show()

注意: 如果想要存檔, 就必須在顯示之前先存。 更多選項詳見 手冊

七、 用 plotly 畫圖

跟 matplotlib 繪圖函式庫比起來, plotly 算是更高階、 互動性更高的函式庫, 缺點是它的相依套件較多、 吃較多資源、 svg 存檔比較麻煩 離線版的 plotly 繪圖有兩種方式: 採用 jupyter notebook 或直接產生一個 html 檔。 我跟 jupyter 不熟, 那就用最簡單的 html 版吧:

import plotly
import plotly.graph_objs as go
plotly.offline.plot([
    go.Scatter(
        mode='markers',
        x=summary['c07'],
        y=summary['c08'],
        text=summary.index
    )],
    filename='ref-07-08.html'
)

這會在瀏覽器上開啟一個新分頁, 顯示一個 html 檔, 裡面包含圖片跟提供互動功能的 js 程式碼。 例如, 滑鼠移到圓點旁邊時, 會出現鄉鎮市區的名稱。 使用者還可以 拉座標軸來縮放/平移圖片

餵給 plotly 的資料不需要是 pandas 的 dataframe 或是 numpy 的 array; 單純的 python list 也可以, 例如:

import math
N = 60
X=[(float(k)/N-0.5)*math.pi*2 for k in range(N+1)]
Y=[math.sin(x) for x in X]
plotly.offline.plot([go.Scatter(mode='lines+markers', x=X, y=Y)])

八、 結語

Pandas 擷取欄位的語法蠻直覺的, 讓寫程式幾乎變得跟說話一樣精簡扼要。 尤其跟 matplotlib 或 plotly 搭配畫圖時, 一句話就把圖畫出來, 很方便。

公投 11 跟 12 案 (都是反同團體所提) 呈現正現相、 11 跟 15 案 (兩者都談性教育, 後者為平權團體所提) 呈負相關、 最友善同志的是臺北市大同區與中山區、 最恐同的是宜蘭縣大同鄉、... 視覺化可以看出很多有趣的訊息呵 :-)

沒有留言:

張貼留言