透過 BigQuery 查看廣告活動財務資料

本程式碼研究室將介紹一些 BigQuery 進階功能的使用方法,包括:

  • JavaScript 中的使用者定義函式
  • 分區資料表
  • 直接查詢 Google Cloud Storage 和 Google 雲端硬碟中的資料。

您將取得美國聯邦選舉委員會的資料,予以清理並載入至 BigQuery。您也有機會對該資料集提出一些有趣的問題。

雖然這個程式碼研究室不認為之前有 BigQuery 經驗,但熟悉 SQL 將有助於您充分運用這項服務。

課程內容

  • 如何使用 JavaScript 使用者定義的函式來執行 SQL 中難以執行的操作。
  • 如何使用 BigQuery 對儲存在其他資料儲存庫 (例如 Google Cloud Storage 和 Google 雲端硬碟) 中的資料執行 ETL (擷取、轉換、載入) 作業。

軟硬體需求

  • Google Cloud 專案已啟用計費功能。
  • Google Cloud Storage 值區
  • 已安裝 Google Cloud SDK

您要如何使用本教學課程?

唯讀閱讀 閱讀內容並完成練習

您對於 BigQuery 使用體驗的評價如何?

初級 中級 專業

自行調整環境設定

如果您還沒有 Google 帳戶 (Gmail 或 Google Apps),請先建立帳戶。登入 Google Cloud Platform 主控台 (console.cloud.google.com),然後建立新專案:

2016-02-10 12:45:26.png 的螢幕擷取畫面

提醒您,專案編號是所有 Google Cloud 專案的不重複名稱 (使用上述名稱後就無法使用,敬請見諒!)此程式碼研究室稍後將稱為 PROJECT_ID

接著,您必須在 Cloud Console 中啟用計費功能,才能使用 Google Cloud 資源。

完成這個程式碼研究室的成本應該不會超過新臺幣 $300 元,但如果您決定繼續使用更多資源,或是讓資源繼續運作 (請參閱本文件結尾的「清除設定」一節),就有可能需要更多成本。

新加入 Google Cloud Platform 的使用者可免費試用 $300 美元

Google Cloud Shell

雖然 Google Cloud 和 BigQuery 可以在您的筆記型電腦上執行遠端作業,但在這個程式碼研究室中,我們會使用 Google Cloud Shell,這是一個在 Cloud 上執行的指令列環境。

這款以 Debian 為基礎的虛擬機器會載入您需要的所有開發工具。這項服務提供永久性的 5GB 主目錄,可在 Google Cloud 中運作,大幅提升網路效能和驗證效能。也就是說,這個程式碼研究室只需使用瀏覽器 (是,您可以在 Chromebook 上使用)。

如要啟用 Google Cloud Shell,只要在開發人員控制台中,按一下右上角的按鈕 (只需幾分鐘即可佈建並連線至環境):

啟用 Cloud Shell.png

按一下 [Start Cloud Shell] 按鈕:

Screen Shot 2017-06-14 晚上 10.13.43 PM.png

連線至 Cloud Shell 之後,您應該會看到您已經通過驗證,且專案已設為 PROJECT_ID

gcloud auth list

指令輸出

Credentialed accounts:
 - <myaccount>@<mydomain>.com (active)
gcloud config list project

指令輸出

[core]
project = <PROJECT_ID>

根據預設,Cloud Shell 也會設定某些環境變數,當您執行未來指令時可能會用到這個變數。

echo $GOOGLE_CLOUD_PROJECT

指令輸出

<PROJECT_ID>

如果因為某些原因而無法設定專案,只要發出下列指令即可:

gcloud config set project <PROJECT_ID>

正在尋找您的 PROJECT_ID 嗎?您可以查看在設定步驟中使用的 ID,或是在控制台資訊主頁查詢 ID:

專案 ID.png

重要事項:最後,請設定預設區域和專案設定:

gcloud config set compute/zone us-central1-f

您可以選擇多種不同的區域。詳情請參閱地區和區域說明文件

為了在此程式碼研究室中執行 BigQuery 查詢,您需要自己的資料集。選擇一個名稱,例如 campaign_funding。在殼層中執行下列指令 (例如 Cloud Shell):

$ DATASET=campaign_funding
$ bq mk -d ${DATASET}
Dataset 'bq-campaign:campaign_funding' successfully created.

資料集建立完成後,您應該就可以開始執行了。執行這個步驟也應該有助於確認是否已正確設定 bq 指令列用戶端、驗證作業是否正常運作,以及您具備操作的雲端專案寫入權限。如果您有多個專案,系統會提示您從清單中選取您感興趣的專案。

2016-03-14 晚上 9.16.49 PM.png

美國聯邦選舉委員會的廣告活動財務資料集已壓縮並複製到 GCS 值區 gs://campaign-funding/

讓我們將本機來源檔案下載到本機,以便檢視其中內容。在命令視窗中執行下列指令:

$ gsutil cp gs://campaign-funding/indiv16.txt .
$ tail indiv16.txt

系統應會顯示個別貢獻檔案的內容。我們為這個程式碼研究室尋找了三類檔案,包括:個人貢獻 (indiv*.txt)、求職者 (cn*.txt) 和委員會 (cm*.txt)。如果您有興趣,請運用相同機制,看看其中有哪些檔案。

我們不會直接在原始資料中載入原始資料,而是從 Google Cloud Storage 查詢資料。因此,我們必須瞭解架構以及一些相關資訊。

如要瞭解此資料集,請參閱聯邦選舉網站。我們看到的表格結構定義如下:

為了連結到資料表,我們必須為包含結構定義的表格建立資料表定義。執行下列指令來產生個別資料表定義:

$ bq mkdef --source_format=CSV \
    gs://campaign-funding/indiv*.txt \
"CMTE_ID, AMNDT_IND, RPT_TP, TRANSACTION_PGI, IMAGE_NUM, TRANSACTION_TP, ENTITY_TP, NAME, CITY, STATE, ZIP_CODE, EMPLOYER, OCCUPATION, TRANSACTION_DT, TRANSACTION_AMT:FLOAT, OTHER_ID, TRAN_ID, FILE_NUM, MEMO_CD, MEMO_TEXT, SUB_ID" \
> indiv_def.json

使用您慣用的文字編輯器開啟 indiv_dev.json 檔案並查看內容,其中會包含說明如何解讀 FEC 資料檔案的 json 檔案。

我們必須對 csvOptions 區段進行小幅修改。加入「||」的 fieldDelimiter 值,並將 quote 的值設為 "" (空字串)。這是必要的,因為資料檔案實際上是以半形逗號分隔,而且是用直立線分隔:

$ sed -i 's/"fieldDelimiter": ","/"fieldDelimiter": "|"/g; s/"quote": "\\""/"quote":""/g' indiv_def.json

indiv_dev.json 檔案現在應已讀取:

    "fieldDelimiter": "|", 
    "quote":"", 

由於委員會和候選表格的表格定義類似,而且架構包含公式化的公式,因此請下載這些檔案。

$ gsutil cp gs://campaign-funding/candidate_def.json .
Copying gs://campaign-funding/candidate_def.json...
/ [1 files][  945.0 B/  945.0 B]                                                
Operation completed over 1 objects/945.0 B. 

$ gsutil cp gs://campaign-funding/committee_def.json .
Copying gs://campaign-funding/committee_def.json...
/ [1 files][  949.0 B/  949.0 B]                                                
Operation completed over 1 objects/949.0 B.  

這些檔案的格式與 indiv_dev.json 檔案類似。請注意,您也可以下載 indiv_def.json 檔案,以協助獲得正確的值。

接著,我們開始將 BigQuery 資料表連結至這些檔案。執行下列指令:

$ bq mk --external_table_definition=indiv_def.json -t ${DATASET}.transactions 
Table 'bq-campaign:campaign_funding.transactions' successfully created.

$ bq mk --external_table_definition=committee_def.json -t ${DATASET}.committees 
Table 'bq-campaign:campaign_funding.committees' successfully created.

$ bq mk --external_table_definition=candidate_def.json -t ${DATASET}.candidates 
Table 'bq-campaign:campaign_funding.candidates' successfully created.

這項操作會建立三個 BigQuery 資料表:交易、修訂版本和候選項目。您可以查詢這些資料表,就像一般的 BigQuery 資料表一樣,但實際上不儲存在 BigQuery 中,而是儲存在 Google Cloud Storage 中。如果您更新基礎檔案,變更內容會立即反映在您所執行的查詢中。

接著,我們開始嘗試執行幾項查詢。開啟 BigQuery 網頁 UI

選取-bq.png

在左側導覽窗格中找出資料集 (您可能需要變更左上角的專案下拉式選單),按一下紅色的 [COMQUERYE QUERY'] 按鈕,然後在方塊中輸入下列查詢:

SELECT * FROM [campaign_funding.transactions] 
WHERE EMPLOYER contains "GOOGLE" 
ORDER BY TRANSACTION_DT DESC
LIMIT 100

這樣會找到 Google 員工最近捐款的 100 項廣告活動。如果您願意,也可以試著尋找您居住地的居民捐款活動,或尋找市內最大的捐款金額。

查詢和結果看起來會像這樣:

Screen Shot 2016-03-14 晚上 9.31.58 PM.png

不過,您也許會注意到,您無法確實判斷受款人是誰。為了獲得這些資訊,我們必須提出一些更籠統的查詢。

按一下左側窗格中的交易表格,然後按一下 [架構] 標籤。螢幕截圖應如下所示:

Screen Shot 2016-03-14 晚上 9.43.04 PM.png

我們可以看到一份欄位,與先前指定的表格定義相符。你可能會發現沒有填寫欄位,或以任何方式來確認你支持的捐款項目。不過,有一個名為 CMTE_ID 的欄位。方便我們為捐助者的捐款連至該委員會。這樣還是不實用。

接下來,按一下「佣金」資料表即可查看其結構定義。我們有一組 CMET_ID,可加入交易表格。另一個欄位為 CAND_ID;可與候選表格中的 CAND_ID 資料表合併。最後,我們將透過佣金表格,針對交易和候選人之間建立連結。

請注意,GCS 表格沒有預覽分頁。這是因為為了讀取資料,BigQuery 需要從外部資料來源讀取資料。執行簡單的「SELECT *'」查詢候選項目表格,取得資料樣本。

SELECT * FROM [campaign_funding.candidates]
LIMIT 100

結果應如下所示:

Screen Shot 2016-03-14 晚上 9.45.52 PM.png

您可能會注意到,候選者的名字都是「全大寫」,並以「姓氏、名字」的順序顯示。這有點令人困擾,因為其實這並不代表我們對於候選者的看法;我們不認為「歐巴馬」要比「OBAMA, BARACK」多了。此外,交易表格中的交易日期 (TRANSACTION_DT) 也有點尷尬,格式為 YYYYMMDD 的字串值。我們將在下一節說明這些疑難雜症。

釐清交易與候選資料的關聯後,現在您可以執行查詢,看看有哪些人為對象。剪下以下查詢,並貼到撰寫方塊中:

SELECT affiliation, SUM(amount) AS amount
FROM (
  SELECT *
  FROM (
    SELECT
      t.amt AS amount,
      t.occupation AS occupation,
      c.affiliation AS affiliation,
    FROM (
      SELECT
        trans.TRANSACTION_AMT AS amt,
        trans.OCCUPATION AS occupation,
        cmte.CAND_ID AS CAND_ID
      FROM [campaign_funding.transactions] trans
      RIGHT OUTER JOIN EACH (
        SELECT
          CMTE_ID,
          FIRST(CAND_ID) AS CAND_ID
        FROM [campaign_funding.committees]
        GROUP EACH BY CMTE_ID ) cmte
      ON trans.CMTE_ID = cmte.CMTE_ID) AS t
    RIGHT OUTER JOIN EACH (
      SELECT
        CAND_ID,
        FIRST(CAND_PTY_AFFILIATION) AS affiliation,
      FROM [campaign_funding.candidates]
      GROUP EACH BY CAND_ID) c
    ON t.CAND_ID = c.CAND_ID )
  WHERE occupation CONTAINS "ENGINEER")
GROUP BY affiliation
ORDER BY amount DESC

這個查詢會將交易資料表彙整至資料表資料表,再加入候選資料表。只會查看職稱中「ENGINEER」一詞的交易。這項查詢會彙整各方聯盟的結果,方便我們查看工程師在各政黨中的分佈情形。

Screen Shot 2016-03-14 晚上 9.56.37 PM.png

我們發現,工程師的平衡相當成熟,讓民主和共和國民眾平均增加或減少。但什麼是「DFL' Party」?如果只輸入 3 個字母的代碼,不算實際會用到全名,這樣是不是很好的?

派對代碼是由 FEC 網站定義。有一份表格比對派對代碼與全名 (例如「DFL' is “Democratic-Farmer-Labor'{/1}」)。雖然我們可以手動在查詢中執行翻譯,但這似乎是相當費工的工作,而且很難保持同步。

如果我們可以在查詢中剖析 HTML,那該怎麼處理呢?在網頁上的任何位置按一下滑鼠右鍵,然後查看「檢視網頁原始碼」。來源中有許多標頭 / 樣板資訊,但要找出 <table> 標記。每個對應資料列都屬於 HTML <tr> 元素,因此名稱和程式碼會分別納入 <td> 元素中。每列如下所示:

HTML 格式如下所示:

<tr bgcolor="#F5F0FF">
    <td scope="row"><div align="left">ACE</div></td>
    <td scope="row">Ace Party</td>
    <td scope="row"></td>
</tr>

請注意,BigQuery 無法直接從網路讀取檔案,這是因為 BigQuery 能夠同時從數千個工作站擷取來源。如果已允許隨機網頁執行,基本上就是分散式阻斷服務攻擊 (DDoS)。FEC 網頁的 HTML 檔案會儲存在 gs://campaign-funding 值區中。

我們需要依據廣告活動的資金資料建立表格。這類似於我們建立的其他 GCS 支援資料表。差別在於,我們實際上並沒有採用結構定義,因此每列只會使用一個欄位,並命名為「data'」。我們會將該欄視為 CSV 檔案,但不含逗號分隔,而是使用大分隔符號 (`) 且不含引號字元。

如要建立派對資料表,請從指令列執行下列指令:

$ echo '{"csvOptions": {"allowJaggedRows": false, "skipLeadingRows": 0, "quote": "", "encoding": "UTF-8", "fieldDelimiter": "`", "allowQuotedNewlines": false}, "ignoreUnknownValues": true, "sourceFormat": "CSV", "sourceUris": ["gs://campaign-funding/party_codes.shtml"], "schema": {"fields": [{"type": "STRING", "name": "data"}]}}' > party_raw_def.json
$ bq mk --external_table_definition=party_raw_def.json \
   -t ${DATASET}.raw_party_codes 
Table 'bq-campaign:campaign_funding.raw_party_codes' successfully created.

現在我們將使用 JavaScript 剖析檔案。「BigQuery 查詢編輯器」的右上角應該有 [UDF 編輯器] 按鈕。按一下即可切換以編輯 JavaScript UDF。UDF 編輯器會填入一些已加註的樣板。

2016-03-14 10.00.38 PM.png

請直接刪除當中的程式碼,然後輸入以下程式碼:

function tableParserFun(row, emitFn) {
  if (row.data != null && row.data.match(/<tr.*<\/tr>/) !== null) {
    var txt = row.data
    var re = />\s*(\w[^\t<]*)\t*<.*>\s*(\w[^\t<]*)\t*</;
    matches = txt.match(re);
    if (matches !== null && matches.length > 2) {
        var result = {code: matches[1], name: matches[2]};
        emitFn(result);
    } else {
        var result = { code: 'ERROR', name: matches};
        emitFn(result);
    }
  }
}

bigquery.defineFunction(
  'tableParser',               // Name of the function exported to SQL
  ['data'],                    // Names of input columns
  [{'name': 'code', 'type': 'string'},  // Output schema
   {'name': 'name', 'type': 'string'}],
  tableParserFun // Reference to JavaScript UDF
);

這裡的 JavaScript 分成兩部分,第一種函式會擷取一列的輸入內容,以產生剖析的輸出。另一個則是將函式登錄為「使用者定義函式 (UDF)」中,名稱為「tableParser」,代表其會採用名為「data&#39」的輸入資料欄,並輸出兩個資料欄、程式碼和名稱。代碼欄是 3 個字母的代碼,「名稱」欄則是政黨的全名。

切換回「查詢編輯器」分頁,然後輸入以下查詢:

SELECT code, name FROM tableParser([campaign_funding.raw_party_codes])
ORDER BY code

執行這項查詢會剖析原始 HTML 檔案,並以結構化格式輸出欄位值。非常漂亮,對吧?試著找出「DFL&#39」是什麼意思。

現在,我們可以將派對代碼轉換成名字,再嘗試改用另一個查詢來找出有趣的查詢。執行下列查詢:

SELECT
  candidate,
  election_year,
  FIRST(candidate_affiliation) AS affiliation,
  SUM(amount) AS amount
FROM (
  SELECT 
    CONCAT(REGEXP_EXTRACT(c.candidate_name,r'\w+,[ ]+([\w ]+)'), ' ',
      REGEXP_EXTRACT(c.candidate_name,r'(\w+),')) AS candidate,
    pty.candidate_affiliation_name AS candidate_affiliation,
    c.election_year AS election_year,
    t.amt AS amount,
  FROM (
    SELECT
      trans.TRANSACTION_AMT AS amt,
      cmte.committee_candidate_id AS committee_candidate_id
    FROM [campaign_funding.transactions] trans
    RIGHT OUTER JOIN EACH (
      SELECT
        CMTE_ID,
        FIRST(CAND_ID) AS committee_candidate_id
      FROM [campaign_funding.committees]
      GROUP BY CMTE_ID ) cmte
    ON trans.CMTE_ID = cmte.CMTE_ID) AS t
  RIGHT OUTER JOIN EACH (
    SELECT
      CAND_ID AS candidate_id,
      FIRST(CAND_NAME) AS candidate_name,
      FIRST(CAND_PTY_AFFILIATION) AS affiliation,
      FIRST(CAND_ELECTION_YR) AS election_year,
    FROM [campaign_funding.candidates]
    GROUP BY candidate_id) c
  ON t.committee_candidate_id = c.candidate_id
  JOIN (
    SELECT
      code,
      name AS candidate_affiliation_name
    FROM (tableParser([campaign_funding.raw_party_codes]))) pty
  ON pty.code = c.affiliation )
GROUP BY candidate, election_year
ORDER BY amount DESC
LIMIT 100

這項查詢會顯示哪些候選人獲得最多廣告活動捐款,並拼出自己的政黨。

這些表格的大小不大,查詢大約需要 30 秒。如果要為表格進行許多工作,建議您將這些資料匯入 BigQuery。您可以對資料表執行 ETL 查詢,將資料整理成易於使用的操作,然後將其儲存為永久資料表。這表示,您不一定要記住如何翻譯派對代碼,您也可以在執行搜尋時排除錯誤資料。

按一下 [顯示選項] 按鈕,然後按一下 [選取表格] 按鈕旁的「選取表格」按鈕。Destination Table挑選您的campaign_funding資料集,並輸入表格 ID 為「summary'」。選取「allow large results' 核取方塊」。

現在,請執行下列查詢:

SELECT 
CONCAT(REGEXP_EXTRACT(c.candidate_name,r'\w+,[ ]+([\w ]+)'), ' ', REGEXP_EXTRACT(c.candidate_name,r'(\w+),')) 
  AS candidate,
pty.candidate_affiliation_name as candidate_affiliation,
INTEGER(c.election_year) as election_year,
c.candidate_state as candidate_state,
c.office as candidate_office,
t.name as name,
t.city as city,
t.amt as amount,
c.district as candidate_district,
c.ici as candidate_ici,
c.status as candidate_status,

t.memo as memo,
t.state as state,
LEFT(t.zip_code, 5) as zip_code,
t.employer as employer,
t.occupation as occupation,
USEC_TO_TIMESTAMP(PARSE_UTC_USEC(
CONCAT(RIGHT(t.transaction_date, 4), "-", 
      LEFT(t.transaction_date,2), "-", 
      RIGHT(LEFT(t.transaction_date,4), 2),
      " 00:00:00"))) as transaction_date,
t.committee_name as committee_name,
t.committe_designation as committee_designation,
t.committee_type as committee_type,
pty_cmte.committee_affiliation_name as committee_affiliation,
t.committee_org_type as committee_organization_type,
t.committee_connected_org_name as committee_organization_name,
t.entity_type as entity_type,
FROM (
SELECT 
trans.ENTITY_TP as entity_type,
trans.NAME as name,
trans.CITY as city,
trans.STATE as state,
trans.ZIP_CODE as zip_code,
trans.EMPLOYER as employer,
trans.OCCUPATION as occupation,
trans.TRANSACTION_DT as transaction_date,
trans.TRANSACTION_AMT as amt,
trans.MEMO_TEXT as memo,
cmte.committee_name as committee_name,
cmte.committe_designation as committe_designation,
cmte.committee_type as committee_type,
cmte.committee_affiliation as committee_affiliation,
cmte.committee_org_type as committee_org_type,
cmte.committee_connected_org_name as committee_connected_org_name,
cmte.committee_candidate_id as committee_candidate_id
FROM [campaign_funding.transactions] trans
RIGHT OUTER JOIN EACH (
SELECT
CMTE_ID,
FIRST(CMTE_NM) as committee_name,
FIRST(CMTE_DSGN) as committe_designation,
FIRST(CMTE_TP) as committee_type,
FIRST(CMTE_PTY_AFFILIATION) as committee_affiliation,
FIRST(ORG_TP) as committee_org_type,
FIRST(CONNECTED_ORG_NM) as committee_connected_org_name,
FIRST(CAND_ID) as committee_candidate_id
FROM [campaign_funding.committees]
GROUP BY CMTE_ID
) cmte 
ON trans.CMTE_ID = cmte.CMTE_ID) as t
RIGHT OUTER JOIN EACH 
  (SELECT CAND_ID as candidate_id,
      FIRST(CAND_NAME) as candidate_name,
      FIRST(CAND_PTY_AFFILIATION) as affiliation,
      INTEGER(FIRST(CAND_ELECTION_YR)) as election_year,
      FIRST(CAND_OFFICE_ST) as candidate_state,
      FIRST(CAND_OFFICE) as office,
      FIRST(CAND_OFFICE_DISTRICT) as district,
      FIRST(CAND_ICI) as ici,
      FIRST(CAND_STATUS) as status,
   FROM  [campaign_funding.candidates]
   GROUP BY candidate_id) c 
ON t.committee_candidate_id = c.candidate_id
JOIN (
SELECT code, name as candidate_affiliation_name 
FROM (tableParser([campaign_funding.raw_party_codes]))) pty
ON pty.code = c.affiliation
JOIN (
SELECT code, name as committee_affiliation_name 
FROM (tableParser([campaign_funding.raw_party_codes]))) pty_cmte
ON pty_cmte.code = t.committee_affiliation
WHERE t.amt > 0.0 and REGEXP_MATCH(t.state, "^[A-Z]{2}$") and t.amt < 1000000.0

此查詢大幅延長,且還包含其他清理選項。舉例來說,系統會忽略金額超過 $100 萬美元的任何金額。它也會使用規則運算式將「LASTNAME, FIRSTNAME」改為「FIRSTNAME LASTNAME」。如果您喜歡嘗試新方法,請試著撰寫 UDF 來做到更好,並修正大小寫 (例如「Firstname Lastname」)。

最後,請試著對 campaign_funding.summary 資料表執行幾項查詢,確認對該查詢的查詢速度較快。別忘了先移除目的地資料表查詢選項,否則可能會覆寫摘要表格!

您已經從 FEC 網站清理資料並匯入 BigQuery!

適用範圍

  • 在 BigQuery 中使用 GCS 支援的資料表。
  • 在 BigQuery 中使用使用者定義的函式。

後續步驟

  • 嘗試一些有趣的查詢,看看誰為這個選舉週期提供金錢。

瞭解詳情

請提供您寶貴的意見

  • 歡迎透過本網頁左下角的連結來提交問題或提供意見!