成為初級資料分析師 | Python 與資料科學應用¶

Pandas 專題:資料處理

郭耀仁¶

Tidy datasets are all alike, but every messy dataset is messy in its own way.

Hadley Wickham

2020 總統(副總統)各投票所明細與概況¶

https://db.cec.gov.tw/ElecTable/Election/ElecTickets?dataType=tickets&typeId=ELC&subjectId=P0&legisId=00&themeId=1f7d9f4f6bfe06fdaf4db7df2ed4d60c&dataLevel=N&prvCode=00&cityCode=000&areaCode=00&deptCode=000&liCode=0000

Source: https://db.cec.gov.tw/ElecTable/Election?type=President

大綱¶

  • 觀察處理前資料
  • 盤點單張試算表處理步驟
  • 盤點多張試算表處理步驟
  • 輸出

觀察處理前資料¶

投票所明細原始資料¶

以台北市為例

Imgur

盤點單張試算表處理步驟¶

處理前資料的問題¶

  • 合併儲存格
  • 總計、小計與明細資訊夾雜
  • 有遺漏值

單張試算表處理步驟¶

  • 建立具有中文的資料來源網址
  • 讀入試算表為資料框
  • 判斷候選人人數
  • 給定欄位名
  • 填補行政區缺失
  • 清理行政區的空字串
  • 刪除得票數總計、小計列

建立具有中文的資料來源網址¶

In [1]:
from urllib.parse import quote

file_name = "總統-A05-4-候選人得票數一覽表-各投開票所(臺北市).xls"
file_name_url = quote(file_name)
print(file_name_url)
%E7%B8%BD%E7%B5%B1-A05-4-%E5%80%99%E9%81%B8%E4%BA%BA%E5%BE%97%E7%A5%A8%E6%95%B8%E4%B8%80%E8%A6%BD%E8%A1%A8-%E5%90%84%E6%8A%95%E9%96%8B%E7%A5%A8%E6%89%80%28%E8%87%BA%E5%8C%97%E5%B8%82%29.xls

讀入試算表為資料框¶

In [2]:
import pandas as pd

file_path = "https://taiwan-election-data.s3-ap-northeast-1.amazonaws.com/presidential_2020/{}".format(file_name_url)
xls_df = pd.read_excel(file_path, skiprows=[0, 1, 3, 4])
xls_df.head()
Out[2]:
Unnamed: 0 Unnamed: 1 Unnamed: 2 (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 Unnamed: 6 Unnamed: 7 Unnamed: 8 Unnamed: 9 Unnamed: 10 Unnamed: 11 Unnamed: 12 Unnamed: 13
0 總 計 NaN NaN 70,769 685,830 875,854 1,632,453 21,381 1,653,834 143 1,653,977 513,287 2,167,264 76.3098
1 松山區 NaN NaN 5,436 55,918 64,207 125,561 1,762 127,323 2 127,325 37,329 164,654 77.3276
2 NaN 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
3 NaN 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
4 NaN 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811

判斷候選人人數¶

In [3]:
column_names = list(xls_df.columns)
n_candidates = len(column_names) - 11
candidate_numbers_names = column_names[3:(3+n_candidates)]
print(candidate_numbers_names)
['(1)\n宋楚瑜\n余湘', '(2)\n韓國瑜\n張善政', '(3)\n蔡英文\n賴清德']

給定欄位名¶

In [4]:
from string import ascii_uppercase

column_names = ["district", "village", "office"] + candidate_numbers_names + list(ascii_uppercase[:8])
xls_df.columns = column_names
xls_df.head()
Out[4]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 總 計 NaN NaN 70,769 685,830 875,854 1,632,453 21,381 1,653,834 143 1,653,977 513,287 2,167,264 76.3098
1 松山區 NaN NaN 5,436 55,918 64,207 125,561 1,762 127,323 2 127,325 37,329 164,654 77.3276
2 NaN 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
3 NaN 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
4 NaN 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811

隨堂練習:填補行政區缺失¶

In [7]:
xls_df.head()
Out[7]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 總 計 NaN NaN 70,769 685,830 875,854 1,632,453 21,381 1,653,834 143 1,653,977 513,287 2,167,264 76.3098
1 松山區 NaN NaN 5,436 55,918 64,207 125,561 1,762 127,323 2 127,325 37,329 164,654 77.3276
2 松山區 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
3 松山區 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
4 松山區 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811

隨堂練習:清理行政區的空字串¶

In [8]:
print(xls_df["district"].unique())
['總\u3000計' '\u3000松山區' '\u3000信義區' '\u3000大安區' '\u3000中山區' '\u3000中正區'
 '\u3000大同區' '\u3000萬華區' '\u3000文山區' '\u3000南港區' '\u3000內湖區' '\u3000士林區'
 '\u3000北投區']
In [10]:
print(xls_df["district"].unique())
['總計' '松山區' '信義區' '大安區' '中山區' '中正區' '大同區' '萬華區' '文山區' '南港區' '內湖區' '士林區'
 '北投區']

隨堂練習:刪除得票數小計、總計列¶

In [12]:
xls_df.head()
Out[12]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 松山區 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
1 松山區 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
2 松山區 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811
3 松山區 莊敬里 576.0 43 389 462 894 14 908 0 908 271 1,179 77.0144
4 松山區 東榮里 577.0 38 431 545 1,014 18 1,032 0 1,032 272 1,304 79.1411

隨堂練習:將處理單張試算表的程式封裝為一個函數 get_tidy_data()¶

In [14]:
file_name = "總統-A05-4-候選人得票數一覽表-各投開票所(新北市).xls"
file_name_url = quote(file_name)
file_path = "https://taiwan-election-data.s3-ap-northeast-1.amazonaws.com/presidential_2020/{}".format(file_name_url)
get_tidy_data(file_path).head()
Out[14]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 板橋區 留侯里 1124.0 58 430 557 1,045 24 1,069 0 1,069 304 1,373 77.8587
1 板橋區 流芳里 1125.0 50 345 534 929 6 935 0 935 311 1,246 75.0401
2 板橋區 赤松里 1126.0 24 218 336 578 15 593 0 593 153 746 79.4906
3 板橋區 黃石里 1127.0 35 248 423 706 13 719 0 719 230 949 75.7640
4 板橋區 挹秀里 1128.0 37 289 351 677 13 690 0 690 188 878 78.5877

盤點多張試算表處理步驟¶

單張試算表已經處理妥當¶

更換資料來源,就可以讀入 22 個縣市的投票所明細

準備 ITERABLES¶

In [15]:
admin_areas = ["臺北市", "新北市", "桃園市", "臺中市", "臺南市", "高雄市", "新竹縣", "苗栗縣", "彰化縣", "南投縣", "雲林縣", "嘉義縣", "屏東縣", "宜蘭縣", "花蓮縣", "臺東縣", "澎湖縣", "基隆市", "新竹市", "嘉義市", "金門縣", "連江縣"]
file_names = ["總統-A05-4-候選人得票數一覽表-各投開票所({}).xls".format(admin_area) for admin_area in admin_areas]
file_name_urls = [quote(file_name) for file_name in file_names]
file_paths = ["https://taiwan-election-data.s3-ap-northeast-1.amazonaws.com/presidential_2020/{}".format(file_name_url) for file_name_url in file_name_urls]

讀入 22 個縣市的投票所明細¶

In [16]:
df_dict = {}
for file_path, admin_area in zip(file_paths, admin_areas):
    tidy_df = get_tidy_data(file_path)
    df_dict[admin_area] = tidy_df
    print("現在正在處理{}的資料...".format(admin_area))
    print("資料外觀為:", tidy_df.shape)
現在正在處理臺北市的資料...
資料外觀為: (1728, 14)
現在正在處理新北市的資料...
資料外觀為: (2559, 14)
現在正在處理桃園市的資料...
資料外觀為: (1242, 14)
現在正在處理臺中市的資料...
資料外觀為: (1849, 14)
現在正在處理臺南市的資料...
資料外觀為: (1507, 14)
現在正在處理高雄市的資料...
資料外觀為: (2006, 14)
現在正在處理新竹縣的資料...
資料外觀為: (445, 14)
現在正在處理苗栗縣的資料...
資料外觀為: (480, 14)
現在正在處理彰化縣的資料...
資料外觀為: (1071, 14)
現在正在處理南投縣的資料...
資料外觀為: (492, 14)
現在正在處理雲林縣的資料...
資料外觀為: (609, 14)
現在正在處理嘉義縣的資料...
資料外觀為: (530, 14)
現在正在處理屏東縣的資料...
資料外觀為: (705, 14)
現在正在處理宜蘭縣的資料...
資料外觀為: (421, 14)
現在正在處理花蓮縣的資料...
資料外觀為: (324, 14)
現在正在處理臺東縣的資料...
資料外觀為: (236, 14)
現在正在處理澎湖縣的資料...
資料外觀為: (119, 14)
現在正在處理基隆市的資料...
資料外觀為: (274, 14)
現在正在處理新竹市的資料...
資料外觀為: (344, 14)
現在正在處理嘉義市的資料...
資料外觀為: (191, 14)
現在正在處理金門縣的資料...
資料外觀為: (84, 14)
現在正在處理連江縣的資料...
資料外觀為: (10, 14)
In [17]:
df_dict['臺北市'].head()
Out[17]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 松山區 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
1 松山區 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
2 松山區 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811
3 松山區 莊敬里 576.0 43 389 462 894 14 908 0 908 271 1,179 77.0144
4 松山區 東榮里 577.0 38 431 545 1,014 18 1,032 0 1,032 272 1,304 79.1411
In [18]:
df_dict['新北市'].head()
Out[18]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 板橋區 留侯里 1124.0 58 430 557 1,045 24 1,069 0 1,069 304 1,373 77.8587
1 板橋區 流芳里 1125.0 50 345 534 929 6 935 0 935 311 1,246 75.0401
2 板橋區 赤松里 1126.0 24 218 336 578 15 593 0 593 153 746 79.4906
3 板橋區 黃石里 1127.0 35 248 423 706 13 719 0 719 230 949 75.7640
4 板橋區 挹秀里 1128.0 37 289 351 677 13 690 0 690 188 878 78.5877

有什麼不足的嗎?¶

使用 22 個資料框無法做「跨縣市」的分析

採用垂直合併 22 個資料框¶

多張試算表處理步驟¶

  • 將歸屬於「投票所」的變數欄位捨棄
  • 轉置資料框將得票數歸屬到「候選人」
  • 垂直合併 22 個資料框
  • 新增「候選人號碼」與「候選人姓名」
  • 新增「政黨」
  • 調整變數的型別

隨堂練習:將歸屬於「投票所」的變數欄位捨棄¶

In [19]:
tpe = df_dict["臺北市"]
tpe.head()
Out[19]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德 A B C D E F G H
0 松山區 莊敬里 573.0 36 391 429 856 14 870 0 870 230 1,100 79.0909
1 松山區 莊敬里 574.0 46 382 438 866 12 878 0 878 259 1,137 77.2208
2 松山區 莊敬里 575.0 48 393 389 830 22 852 0 852 262 1,114 76.4811
3 松山區 莊敬里 576.0 43 389 462 894 14 908 0 908 271 1,179 77.0144
4 松山區 東榮里 577.0 38 431 545 1,014 18 1,032 0 1,032 272 1,304 79.1411
In [21]:
tpe.head()
Out[21]:
district village office (1)\n宋楚瑜\n余湘 (2)\n韓國瑜\n張善政 (3)\n蔡英文\n賴清德
0 松山區 莊敬里 573.0 36 391 429
1 松山區 莊敬里 574.0 46 382 438
2 松山區 莊敬里 575.0 48 393 389
3 松山區 莊敬里 576.0 43 389 462
4 松山區 東榮里 577.0 38 431 545

轉置資料框將得票數歸屬到「候選人」¶

In [22]:
candidate_infos = list(tpe.columns[3:])
tpe = tpe.melt(id_vars=['district', 'village', 'office'], value_vars=candidate_infos, var_name="candidate_info", value_name='votes')
tpe.head()
Out[22]:
district village office candidate_info votes
0 松山區 莊敬里 573.0 (1)\n宋楚瑜\n余湘 36
1 松山區 莊敬里 574.0 (1)\n宋楚瑜\n余湘 46
2 松山區 莊敬里 575.0 (1)\n宋楚瑜\n余湘 48
3 松山區 莊敬里 576.0 (1)\n宋楚瑜\n余湘 43
4 松山區 東榮里 577.0 (1)\n宋楚瑜\n余湘 38

隨堂練習:垂直合併 22 個資料框¶

In [24]:
print(presidential_votes.shape)
presidential_votes.head()
(51678, 6)
Out[24]:
district village office candidate_info votes admin_area
0 松山區 莊敬里 573.0 (1)\n宋楚瑜\n余湘 36 臺北市
1 松山區 莊敬里 574.0 (1)\n宋楚瑜\n余湘 46 臺北市
2 松山區 莊敬里 575.0 (1)\n宋楚瑜\n余湘 48 臺北市
3 松山區 莊敬里 576.0 (1)\n宋楚瑜\n余湘 43 臺北市
4 松山區 東榮里 577.0 (1)\n宋楚瑜\n余湘 38 臺北市
In [25]:
presidential_votes.tail()
Out[25]:
district village office candidate_info votes admin_area
51673 北竿鄉 塘岐村、后沃村 6.0 (3)\n蔡英文\n賴清德 121 連江縣
51674 北竿鄉 橋仔村、芹壁村、坂里村、白沙村 7.0 (3)\n蔡英文\n賴清德 80 連江縣
51675 莒光鄉 青帆村、田沃村、西坵村 8.0 (3)\n蔡英文\n賴清德 43 連江縣
51676 莒光鄉 福正村、大坪村 9.0 (3)\n蔡英文\n賴清德 62 連江縣
51677 東引鄉 中柳村、樂華村 10.0 (3)\n蔡英文\n賴清德 136 連江縣

新增「候選人號碼」與「候選人姓名」¶

In [26]:
split_candidate_info = presidential_votes["candidate_info"].str.split("\n", expand=True)
presidential_votes["number"] = split_candidate_info[0].str.replace('\(', '').str.replace('\)', '')
presidential_votes["candidates"] = split_candidate_info[1].str.cat(split_candidate_info[2], '/')
presidential_votes.head()
Out[26]:
district village office candidate_info votes admin_area number candidates
0 松山區 莊敬里 573.0 (1)\n宋楚瑜\n余湘 36 臺北市 1 宋楚瑜/余湘
1 松山區 莊敬里 574.0 (1)\n宋楚瑜\n余湘 46 臺北市 1 宋楚瑜/余湘
2 松山區 莊敬里 575.0 (1)\n宋楚瑜\n余湘 48 臺北市 1 宋楚瑜/余湘
3 松山區 莊敬里 576.0 (1)\n宋楚瑜\n余湘 43 臺北市 1 宋楚瑜/余湘
4 松山區 東榮里 577.0 (1)\n宋楚瑜\n余湘 38 臺北市 1 宋楚瑜/余湘

隨堂練習:新增「政黨」¶

In [28]:
presidential_votes.head()
Out[28]:
district village office candidate_info votes admin_area number candidates party
0 松山區 莊敬里 573.0 (1)\n宋楚瑜\n余湘 36 臺北市 1 宋楚瑜/余湘 親民黨
1 松山區 莊敬里 574.0 (1)\n宋楚瑜\n余湘 46 臺北市 1 宋楚瑜/余湘 親民黨
2 松山區 莊敬里 575.0 (1)\n宋楚瑜\n余湘 48 臺北市 1 宋楚瑜/余湘 親民黨
3 松山區 莊敬里 576.0 (1)\n宋楚瑜\n余湘 43 臺北市 1 宋楚瑜/余湘 親民黨
4 松山區 東榮里 577.0 (1)\n宋楚瑜\n余湘 38 臺北市 1 宋楚瑜/余湘 親民黨

隨堂練習:調整變數位置為 ["admin_area", "district", "village", "office", "number", "party", "candidates", "votes"]¶

In [30]:
presidential_votes.head()
Out[30]:
admin_area district village office number party candidates votes
0 臺北市 松山區 莊敬里 573.0 1 親民黨 宋楚瑜/余湘 36
1 臺北市 松山區 莊敬里 574.0 1 親民黨 宋楚瑜/余湘 46
2 臺北市 松山區 莊敬里 575.0 1 親民黨 宋楚瑜/余湘 48
3 臺北市 松山區 莊敬里 576.0 1 親民黨 宋楚瑜/余湘 43
4 臺北市 松山區 東榮里 577.0 1 親民黨 宋楚瑜/余湘 38

調整變數的型別¶

In [31]:
presidential_votes['number'] = presidential_votes['number'].astype(int)
presidential_votes['office'] = presidential_votes['office'].astype(int)
presidential_votes['votes'] = presidential_votes['votes'].astype(str)
presidential_votes['votes'] = presidential_votes['votes'].str.replace(',', '').astype(int)
In [32]:
presidential_votes.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 51678 entries, 0 to 51677
Data columns (total 8 columns):
admin_area    51678 non-null object
district      51678 non-null object
village       51678 non-null object
office        51678 non-null int64
number        51678 non-null int64
party         51678 non-null object
candidates    51678 non-null object
votes         51678 non-null int64
dtypes: int64(3), object(5)
memory usage: 3.2+ MB

輸出¶

隨堂練習:輸出成為 .csv 檔案¶