使用 BigQuery 查看广告系列财务数据

在此 Codelab 中,您将学习如何使用 BigQuery 的一些高级功能,包括:

  • JavaScript 中的用户定义函数
  • 分区表
  • 直接查询存储在 Google Cloud Storage 和 Google 云端硬盘中的数据。

您将从美国联邦选举委员会获取数据,清理这些数据,并将其加载到 BigQuery 中。此外,您还有机会就该数据集提出一些有趣的问题。

虽然本 Codelab 并不假定您具备使用 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 (console.cloud.google.com) 并创建一个新项目:

2016-02-10 12:45:26 的屏幕截图.png

请记住项目 ID,它在所有 Google Cloud 项目中都是唯一名称(很抱歉,上述名称已被占用,您无法使用!)。它稍后将在此 Codelab 中被称为 PROJECT_ID

接下来,您需要在 Cloud Console 中启用结算功能,才能使用 Google Cloud 资源。

在此 Codelab 中运行仅花费几美元,但是如果您决定使用更多资源或继续让它们运行,费用可能更高(请参阅本文档末尾的“清理”部分)。

Google Cloud Platform 的新用户有资格获享 $300 免费试用

Google Cloud Shell

虽然 Google Cloud 和 BigQuery 可以从笔记本电脑远程操作,但在此 Codelab 中,我们将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

基于 Debian 的这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5GB 主目录,并在 Google Cloud 上运行,大大增强了网络性能和身份验证。这意味着在本 Codelab 中,您只需要一个浏览器(没错,它适用于 Chromebook)。

要激活 Google Cloud Shell,只需在开发者控制台中点击右上方的按钮(配置和连接到环境应该只需要片刻时间):

activateCloudShell.png

点击“启动 Cloud Shell”按钮:

Screen Shot 2017-06-14 at 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,或在控制台信息中心内查找:

Project_ID.png

重要提示:最后,设置默认可用区和项目配置:

gcloud config set compute/zone us-central1-f

您可以选择各种不同的可用区。如需了解详情,请参阅“区域和可用区”文档

为了在本 Codelab 中运行 BigQuery 查询,您需要拥有自己的数据集。为它选择一个名称,例如 campaign_funding。在 shell 中运行以下命令(例如 CloudShell):

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

创建数据集后,您应该就可以开始使用了。运行此命令还有助于验证您是否正确设置了 bq 命令行客户端设置、身份验证是否正常运行,以及您是否对您所运行的云项目拥有写入权限。如果您有多个项目,系统会提示您从列表中选择您感兴趣的项目。

2016 年 3 月 14 日晚上 9.16.49 屏幕截图

美国联邦选举委员会的广告系列财务数据集已解压缩并复制到 GCS 存储分区 gs://campaign-funding/ 中。

让我们在本地下载一个源文件,以便查看显示效果。在命令窗口中运行以下命令:

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

此时应显示单个贡献文件的内容。在本 Codelab 中,我们将探讨三种类型的文件:个人贡献 (indiv*.txt)、候选人 (cn*.txt) 和委员会 (cm*.txt)。如果您有兴趣,请使用相同的机制检查这些其他文件中的内容。

我们不会将原始数据直接加载到 BigQuery,而是从 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 网页界面

select-bq.png

在左侧的导航窗格中找到数据集(您可能需要更改左上角的项目下拉菜单),点击红色的“COMPOSE QUERY'”按钮,然后在框中输入以下查询:

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

此举包含 Google 员工最近捐赠的 100 笔捐款。如果您愿意,不妨尝试玩一玩您的筹款活动,找到您的邮政编码对应的居民,或寻找您所在城市的捐款最多。

查询和结果将如下所示:

2016 年 3 月 14 日晚上 9.31.58 屏幕截图

但您可能会注意到,您无法真正分辨这些捐款的接收人是谁。我们需要编写一些更精细的查询来获取这些信息。

在左侧窗格中,依次点击“事务”表格和“架构”标签页。它应如下面的屏幕截图所示:

2016 年 3 月 14 日晚上 9.43.04 屏幕截图

我们可以看到与之前指定的表格定义匹配的字段列表。您可能会注意到,没有针对受助人的捐款字段,也没有找到任何方法以确定捐款支持的候选人。不过,有一个名为 CMTE_ID 的字段。这样一来,我们便能将接收捐款的委员会与该捐款关联起来。但这还不够。

接下来,点击“佣金”表以查看其架构。我们设置了 CMET_ID,可用于加入事务表。另一个字段是 CAND_ID;此字段可与候选表中的 CAND_ID 表联接。最后,我们会查看委员会表,了解交易与候选人之间的关联。

请注意,基于 GCS 的表没有预览标签页。这是因为 BigQuery 需要从外部数据源读取数据才能读取数据。让我们通过对候选表运行简单的“SELECT *' 查询”来获取数据样本。

SELECT * FROM [campaign_funding.candidates]
LIMIT 100

结果应如下所示:

2016 年 3 月 14 日晚上 9.45.52 屏幕截图

您可能会注意到,候选名称全部为大写字母,并且按“姓氏,名字”的顺序呈现。这有点令人厌烦,因为这实际上并不是我们想到的候选人;我们更愿意看到“Barack Obama”,而不是“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”一词的交易。该查询按政党派别汇总结果;这使我们能够了解向各政党分发的捐款在工程师中的分布情况。

2016 年 3 月 14 日晚上 9.56.37 的屏幕截图

我们可以看到,工程师们的构成相当均衡,为民主党和共和党提供或多或少地平均分配了资金。但“DFL”派对是什么?如果能获取全名,而不只是一个由三个字母组成的代码,不是很好吗?

各方代码在 FEC 网站上定义。有一个表格将派对代码与全名相匹配(原来是“DFL'”是“Democratic-Farmer-Labor'”)。虽然我们可以在查询中手动执行转换,这似乎需要执行大量工作,并且很难保持同步。

如果我们能够解析 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 年 3 月 14 日晚上 10.00.38 屏幕截图

接下来,删除其中包含的代码,然后输入以下代码:

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 分为两部分;第一部分是接受一行输入的函数发出解析的输出。另一个定义是将函数注册为名称为 tableParser 的用户定义函数 (UDF),表示接受名为“data'”的输入列并输出两列:代码和名称。代码列由三字母代码组成,名称列是各方的全名。

切换回“查询编辑器”标签页,然后输入以下查询:

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

运行此查询会解析原始 HTML 文件,并以结构化格式输出字段值。是不是很酷?看看您能否弄清“DFL”代表什么。

现在我们可以将派对代码转换成名称,接下来,我们尝试用另一个查询找出有趣的活动。请运行以下查询:

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 中使用用户定义的函数。

后续步骤

  • 尝试一些有趣的查询,看看谁在为此选举周期付款。

了解详情

向我们提供反馈

  • 如果您愿意,可以通过本页面左下方的链接提交问题或提供反馈!