+ BigQueryで学ぶ
COVID-19データ分析
チャレンジラボ
+ 完全攻略ガイド
+
+
+ 公開データセット
+ bigquery-public-data.covid19_open_data.covid19_open_data
+ に対する10個のSQLタスクを、集計・ウィンドウ関数・CTEの実務的な考え方から解説します。
+
+ このラボは値がランダム化されます。
+ Date・Death Count・Confirmed Cases・Month・Limit Value
+ は受講者ごとに異なる数値で出題されます。本ガイドのコード中の
+ <Date>
+ のような表記は、自分のラボ画面に表示された実際の値に置き換えてください。
+
ラボ全体の流れをつかむ
++ 個別のSQLに入る前に、まず全体の流れを図で押さえます。BigQueryコンソールを開いてからLooker + Studioでレポートを完成させるまでの一連の流れです。 +
++ 「Check my + progressで採点する」は自動採点システムによる判定です。不合格の場合は該当タスクのSQLを見直して再実行します。 +
+データセットの構造を理解する(最重要の前提知識)
++ 10個のタスクすべてに共通して関わる構造上の注意点です。ここを理解しないままクエリを書くと、一見正しく動くのに集計結果が水増しされる、という事故が起きやすくなります。 +
+ +| カラム名 | +意味 | +補足 | +
|---|---|---|
date |
+ その行のデータの対象日 | +DATE型 | +
country_code |
+ 国コード(例: US) |
+ ISO準拠のコード | +
country_name |
+ 国名(例: United States of America) |
+ 表記ゆれに注意 | +
subregion1_name |
+ 州・省などの地域名 | +国レベルの行ではNULL |
+
cumulative_confirmed |
+ その日までの累積確定症例数 | +「新規」ではなく「累積」 | +
cumulative_deceased |
+ その日までの累積死亡者数 | +同上 | +
cumulative_recovered |
+ その日までの累積回復者数 | +同上 | +
+ なぜ単純にSUM(cumulative_confirmed)だけでは危険なのか。このテーブルは「国レベルの行」「州レベルの行」「郡レベルの行」を同じ1つのフラットなテーブルの中に混在させて格納しています。行を親子関係でネストしているのではなく、粒度の異なる行が並列に並んでいる、という点がポイントです。
+
+ 矢印は「親から子」のリレーションではなく、同じ日付・同じ国に対して粒度違いの行が複数存在することを表しています。 +
+ +
+ 例えばアメリカの場合、country_name = "United States of America"という条件だけで絞り込むと、国全体の1行・50州分の行・数千の郡の行がすべて同時にヒットします。この挙動はデータセット提供元の公式リポジトリでも明記されており、subregion1_codeがNULLであれば国レベル、値が入っていれば州レベルの集計であるとされています(GoogleCloudPlatform/covid-19-open-data README
+ )。
+
+ 実務での回避策:地域別に集計したいときは、必ずsubregion1_name IS NOT NULL(州レベルだけ)やsubregion1_name IS NULL AND subregion2_name IS NULL(国レベルだけ)のように、対象の粒度を明示的にWHERE句で絞り込みます。ただしタスク1のように「日付だけで単純にSUMする」ことが公式の想定解になっているタスクもあり、これは採点システムの期待値がその単純な合計に合わせて作られているためです。
+
-
+
- 累積値 +
- + ある時点までの合計。前日までの値に当日分を足し込んだ値で、「その日単体の新規件数」ではない + +
- 集計レベル +
- + データがどの地理的粒度(国・州・郡)で集計されているかを表す区分 + +
全タスクに共通するベストプラクティス
++ 個別タスクに入る前に、10個のタスクを通して繰り返し使うSQLパターンをまとめます。 +
+ +GoogleSQLダイアレクトを使う
+
+ BigQueryのクエリエディタは既定でGoogleSQL(旧称: Standard
+ SQL)です。古い記事の[project:dataset.table]という角カッコ表記(Legacy
+ SQL)ではなく、本ガイドはすべて`project.dataset.table`形式のGoogleSQLで統一しています(GoogleSQLの名称について )。
+
集計後の値で絞り込みたいときはHAVINGを使う
+
+ なぜ単純なWHERE 集計結果 > 100では解けないのか。WHERE句はグループ化(GROUP BY)が行われる前の生の行に対して評価されます。SUM(...)のような集計結果はグループ化が終わった後に初めて存在する値なので、WHEREの中では参照できません。
+
| 方法 | +書き方 | +向いている場面 | +
|---|---|---|
HAVINGを使う |
+ GROUP BY 列 HAVING 集計結果 > 100 |
+ 集計とその後の絞り込みだけで完結する場合 | +
| サブクエリ / CTEで包む | +内側で集計し外側のWHEREで絞り込む |
+ 絞り込んだ後にさらに計算を続ける場合 | +
前日比較にはウィンドウ関数(LAG)を使う
+
+ なぜ自己結合では非効率なのか。日付テーブルを自分自身とJOINして「1日前のレコード」を探す書き方もできますが、自己結合は出力行数が膨らみやすくパフォーマンスの問題を起こしやすいとBigQueryの公式ドキュメントでも指摘されています。同じ目的はLAGなどのウィンドウ関数で書き直すことが推奨されます(BigQueryクエリプランの解説
+ )。
+
| 関数 | +やること | +使うべき場面 | +
|---|---|---|
LAG(値) OVER (ORDER BY 日付) |
+ 1つ前の行の値を取得する | +前日比・前月比などの差分計算(タスク6・7) | +
LEAD(値) OVER (ORDER BY 日付) |
+ 1つ後の行の値を取得する | +未来方向の値を同じ行に並べる(タスク9) | +
ROW_NUMBER() OVER (...) |
+ 重複のない連番を振る | +同率を区別して上位N件だけ取りたいとき | +
RANK() / DENSE_RANK() OVER (...) |
+ 同率に同じ順位を振る | +ランキングで同率を同じ順位に見せたいとき | +
+ LAGやLEADはいずれもOVER句とセットでなければ使えません。書き忘れるとエラーになる点は、後述するタスク9のデバッグで重要になります(ナビゲーション関数リファレンス )。
+
割り算にはSAFE_DIVIDEを検討する
+
+ 致死率・回復率・増加率のような割り算では、分母が0になるとエラーで処理全体が止まります。SAFE_DIVIDE(分子, 分母)は分母が0のときエラーではなくNULLを返してくれます(数学関数リファレンス )。本ガイドでは割り算を行うすべてのタスクでこの関数を使います。
+
-
+
- ウィンドウ関数 +
- + 行を1行に集約する集計関数とは違い、各行を保ったまま前後の値などを計算できる関数 + +
- OVER句 +
- ウィンドウ関数がどの範囲・並び順で計算するかを指定する句 +
- SAFE_DIVIDE +
-
+ ゼロ除算が起きてもエラーにせず
NULLを返す安全な割り算関数 +
+
全世界の確定症例数の合計
+cumulative_confirmedを1行に集計するクエリです。
+ SELECT
+ SUM(cumulative_confirmed) AS total_cases_worldwide
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ date = "<Date>"
+ -- date は "YYYY-MM-DD" 形式の文字列リテラルとして渡す
+
+ 処理の流れ:
+ WHERE date = "<Date>"で対象日の行だけに絞り込み、全行のcumulative_confirmedをSUMで合計します。
+
+ 実務コラム:Step
+ 1で説明した通り、このWHERE句には地域粒度の絞り込みが入っていないため、国・州・郡レベルの行がすべて合算されます。ラボの採点はこの単純な合計を期待値としているため、このまま提出して問題ありません。自社のダッシュボードなど実務でこのデータセットを使う場合は、subregion1_name IS NULL AND subregion2_name IS NULLを加えて国レベルの行だけに絞り込むほうが安全です。
+
被害が大きい地域を特定する
+SELECT
+ COUNT(*) AS count_of_states
+FROM (
+ SELECT
+ subregion1_name AS state,
+ SUM(cumulative_deceased) AS death_count
+ FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+ WHERE
+ country_name = "United States of America"
+ AND date = "<Date>"
+ AND subregion1_name IS NOT NULL -- 国全体の1行を除外する
+ GROUP BY
+ subregion1_name
+)
+WHERE
+ death_count > <Death Count>
+ 処理の流れ:
+-
+
-
+ 内側のサブクエリで州ごとに
cumulative_deceasedを集計しdeath_countを作る +
+ -
+
subregion1_name IS NOT NULLで国レベルの合計行を弾く。忘れると「51件目の州」として国全体の行が混ざり件数がずれる +
+ -
+ 外側の
WHERE death_count > <Death Count>でしきい値を超えた州だけを残す +
+ COUNT(*)で残った州の件数を数える
+
+ country_code = "US"ではなくcountry_name = "United States of America"を条件に使っています。どちらのカラムで絞り込んでいるかは常に意識しておくとよい習慣です。
+
ホットスポットを特定する
+SELECT
+ subregion1_name AS state,
+ SUM(cumulative_confirmed) AS total_confirmed_cases
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ country_code = "US"
+ AND date = "<Date>"
+ AND subregion1_name IS NOT NULL
+GROUP BY
+ subregion1_name
+HAVING
+ total_confirmed_cases > <Confirmed Cases>
+ORDER BY
+ total_confirmed_cases DESC
+
+ タスク2ではサブクエリ、タスク3ではHAVINGを使いました。どちらも「集計後の値で絞り込む」という同じ目的のための書き方の違いです。今回はこの後さらに計算を続けないので、HAVINGのほうが行数の少ないシンプルな書き方になります。
+
致死率(Case-Fatality Ratio)を計算する
+SELECT
+ SUM(cumulative_confirmed) AS total_confirmed_cases,
+ SUM(cumulative_deceased) AS total_deaths,
+ SAFE_DIVIDE(SUM(cumulative_deceased), SUM(cumulative_confirmed)) * 100 AS case_fatality_ratio
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ country_name = "Italy"
+ AND date BETWEEN "<Monthの初日, 例: 2020-04-01>" AND "<Monthの末日, 例: 2020-04-30>"
+
+ 月末日を手で数える手間を減らしたい場合:閏年の2月など月末日を間違えやすいケースがあります。EXTRACT(YEAR FROM date)とEXTRACT(MONTH FROM date)で絞り込めば、月末日を意識せずに書けます。
+
SELECT
+ SUM(cumulative_confirmed) AS total_confirmed_cases,
+ SUM(cumulative_deceased) AS total_deaths,
+ SAFE_DIVIDE(SUM(cumulative_deceased), SUM(cumulative_confirmed)) * 100 AS case_fatality_ratio
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ country_name = "Italy"
+ AND EXTRACT(YEAR FROM date) = <対象年, 例: 2020>
+ AND EXTRACT(MONTH FROM date) = <対象月の数字, 例: 4>
+ -
+
- EXTRACT +
- 日付や時刻の値から年・月・日などの一部だけを取り出す関数 +
しきい値を超えた特定の日を探す
+SELECT
+ date
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ country_name = "Italy"
+ AND cumulative_deceased > <Death Count>
+ORDER BY
+ date ASC
+LIMIT 1
+
+ なぜLIMIT 1だけで安全に「最初の日」が取れるのか。cumulative_deceasedは累積値なので、通常は日付が進むにつれて単調に増加(または横ばい)します。しきい値を超えた日付を昇順に並べて先頭を取れば、それが最初に超えた日になります。ただし実データでは、報告方法の見直しなどにより過去の値が下方修正され、累積値が一時的に前日を下回るケースもゼロではありません。
+
-
+
- 単調増加 +
- + 値が時間とともに減ることなく、増える・または変わらない、を繰り返す性質 + +
新規症例数がゼロだった日を数える(壊れたクエリの修正)
+
+ 壊れたクエリのどこが問題か。お題のSQLはindia_previous_day_comparisonというCTEを作るところまでしか書かれておらず、そのCTEを実際に読み出す外側のSELECT文がありません。CTEは定義しただけでは何も返さないため、このままではクエリが完結しません。加えてdate between '' and ''も空文字列のままなので、実際の日付に差し替える必要があります。
+
WITH india_cases_by_date AS (
+ SELECT
+ date,
+ SUM(cumulative_confirmed) AS cases
+ FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+ WHERE
+ country_name = "India"
+ AND date BETWEEN "<Start date>" AND "<Close date>"
+ GROUP BY
+ date
+ ORDER BY
+ date ASC
+)
+
+, india_previous_day_comparison AS (
+ SELECT
+ date,
+ cases,
+ LAG(cases) OVER (ORDER BY date) AS previous_day,
+ cases - LAG(cases) OVER (ORDER BY date) AS net_new_cases
+ FROM
+ india_cases_by_date
+)
+
+SELECT
+ COUNT(date) AS days_with_zero_net_new_cases
+FROM
+ india_previous_day_comparison
+WHERE
+ net_new_cases = 0
+ + このクエリは「CTEを段階的につないでいく」という、この後のタスク7・9でも繰り返し使うパターンの基本形です。下の図はそのパイプラインの流れを一般化したものです。 +
+
+ LAGは先頭の行(対象期間の一番古い日)では「1つ前の行」が存在しないためNULLを返します。NULL - 数値の計算結果もNULLになるため、net_new_cases = 0の判定には引っかからず、エラーにもならずに自然に除外されます。
+
-
+
- CTE(共通テーブル式) +
-
+
WITH 名前 AS (...)の形でクエリの中に一時的な名前付きテーブルを定義する仕組み。複雑な計算を段階に分けて書けるため可読性が上がる +
+
倍加速度(Doubling Rate)を調べる
+WITH us_cases_by_date AS (
+ SELECT
+ date,
+ SUM(cumulative_confirmed) AS cases
+ FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+ WHERE
+ country_name = "United States of America"
+ AND date BETWEEN "2020-03-22" AND "2020-04-20"
+ GROUP BY
+ date
+ ORDER BY
+ date ASC
+)
+
+, us_previous_day_comparison AS (
+ SELECT
+ date,
+ cases,
+ LAG(cases) OVER (ORDER BY date) AS previous_day,
+ SAFE_DIVIDE(
+ cases - LAG(cases) OVER (ORDER BY date),
+ LAG(cases) OVER (ORDER BY date)
+ ) * 100 AS percentage_increase
+ FROM
+ us_cases_by_date
+)
+
+SELECT
+ date AS Date,
+ cases AS Confirmed_Cases_On_Day,
+ previous_day AS Confirmed_Cases_Previous_Day,
+ percentage_increase AS Percentage_Increase_In_Cases
+FROM
+ us_previous_day_comparison
+WHERE
+ percentage_increase > <Limit Value>
+ + 処理の流れは前掲の「CTEパイプライン」の図とほぼ同じです。違いは2段目のCTEで、引き算だけでなく割り算してパーセントに換算する計算を加えている点です。 +
+ +
+ ここでSAFE_DIVIDEを使う理由:パンデミック初期の日付を対象期間に含めると、前日の累積症例数が実際に0件というケースがあり得ます。通常の/演算子だとゼロ除算でクエリがエラーになりますが、SAFE_DIVIDEならエラーにならず、その行の値がNULLになるだけで処理が続行されます。
+
回復率(Recovery Rate)ランキングを作る
+WITH cases_by_country AS (
+ SELECT
+ country_name AS country,
+ SUM(cumulative_confirmed) AS confirmed_cases,
+ SUM(cumulative_recovered) AS recovered_cases
+ FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+ WHERE
+ date = "2020-05-10"
+ GROUP BY
+ country_name
+)
+
+SELECT
+ country,
+ recovered_cases,
+ confirmed_cases,
+ SAFE_DIVIDE(recovered_cases, confirmed_cases) * 100 AS recovery_rate
+FROM
+ cases_by_country
+WHERE
+ confirmed_cases > 50000
+ORDER BY
+ recovery_rate DESC
+LIMIT <Limit Value>
+
+ ORDER BYの前にWHERE confirmed_cases > 50000を適用することで、症例数が少ないのに回復率だけ100%に近いような小規模な国がランキング上位に紛れ込むのを防いでいます。この順序(先に絞り込み、後で並べ替え)は、集計を伴うランキングクエリで繰り返し使えるパターンです。
+
CDGR(累積日次成長率)を計算する(壊れたクエリの修正)
+CDGR = (最終日の症例数 / 初日の症例数) ^ (1 / 経過日数) - 1
+ + 下の図は、お題のクエリに含まれる3つの不具合と、その修正内容を順番に示したものです。 +
+| 不具合 | +内容 | +修正 | +
|---|---|---|
| 1. 構文エラー | +LEAD(total_cases)にOVER句がない |
+ LEAD(total_cases) OVER (ORDER BY date)に修正 |
+
| 2. 未入力の値 | +date IN ('2020-01-24', '')の2番目が空文字 |
+ 最終日の日付リテラルを補完 | +
| 3. 関数の選び間違い | +SQRTは引数を1つしか取らない |
+ べき乗計算には2引数のPOWER(底, 指数)を使う |
+
+ 不具合1は、Step + 2で説明した「ウィンドウ関数は必ずOVER句とセットで書く」というルール違反です(ナビゲーション関数リファレンス )。不具合3は、平方根と累乗という別の演算を混同したものです(数学関数リファレンス )。 +
+ +WITH france_cases AS (
+ SELECT
+ date,
+ SUM(cumulative_confirmed) AS total_cases
+ FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+ WHERE
+ country_name = "France"
+ AND date IN ("2020-01-24", "<最終日, 例: 2020-05-10>")
+ GROUP BY
+ date
+ ORDER BY
+ date
+)
+
+, summary AS (
+ SELECT
+ total_cases AS first_day_cases,
+ LEAD(total_cases) OVER (ORDER BY date) AS last_day_cases,
+ DATE_DIFF(LEAD(date) OVER (ORDER BY date), date, DAY) AS days_diff
+ FROM
+ france_cases
+ LIMIT 1
+)
+
+SELECT
+ first_day_cases,
+ last_day_cases,
+ days_diff,
+ POWER(SAFE_DIVIDE(last_day_cases, first_day_cases), SAFE_DIVIDE(1, days_diff)) - 1 AS cdgr
+FROM
+ summary
+ 処理の流れ:
+-
+
france_casesCTEで、初日と最終日、2行だけを取り出す
+ -
+
summaryCTEで、LEADを使って「1行目に初日、2行目に最終日」という2行を1行にまとめ、DATE_DIFFで経過日数も同じ行に並べる +
+ LIMIT 1で、まとめ終わった1行だけを残す
+ - 最後の
SELECTでPOWERを使ってCDGRを計算する
+
-
+
- DATE_DIFF +
- + 2つの日付の間の日数を返す関数(日付関数リファレンス) + +
- POWER(底, 指数) +
- 底を指数乗した値を返す関数 +
Looker Studioでレポートを作成する
+SELECT
+ date,
+ SUM(cumulative_confirmed) AS country_cases,
+ SUM(cumulative_deceased) AS country_deaths
+FROM
+ `bigquery-public-data.covid19_open_data.covid19_open_data`
+WHERE
+ country_name = "United States of America"
+ AND date BETWEEN "<Date Rangeの開始日>" AND "<Date Rangeの終了日>"
+GROUP BY
+ date
+ORDER BY
+ date
+ | 手順 | +操作内容 | +
|---|---|
| 1 | ++ BigQueryのクエリエディタで上記クエリを実行し、正しく結果が返ることを確認する + | +
| 2 | ++ 結果画面から「Explore Data」→「Explore with Looker + Studio」(手順書では「Explore with Data Studio」)を選ぶ + | +
| 3 | +Looker StudioにBigQueryへのアクセスを許可(承認)する | +
| 4 | ++ 初回ログイン時に失敗する場合は「空のレポート」を作成して利用規約に同意したうえで、BigQuery側から改めて接続し直す + | +
| 5 | ++ レポート編集画面で「グラフを追加」から「時系列グラフ」を選ぶ + | +
| 6 | +
+ 指標(Metric)にcountry_casesとcountry_deathsの両方を追加する
+ |
+
| 7 | +「保存」をクリックして変更を確定する | +
+ ラボの指示では「BigQueryのExplore with Data + Studioオプションは使わないこと」となっている場合があります。必ず自分のラボの手順書の指示を優先してください(Looker StudioとBigQueryの接続 公式ドキュメント + )。 +
+提出前チェックリスト
+-
+
-
+ クエリ中のすべての
<プレースホルダー>を、自分のラボ画面に表示された実際の値に置き換えたか +
+ -
+
country_nameとcountry_codeのどちらで絞り込むべきタスクか、混同していないか +
+ -
+ 州・郡レベルの集計をするタスクで
subregion1_name IS NOT NULLの絞り込みを入れたか +
+ -
+ 集計後の値を条件にするときは
WHEREではなくHAVINGかサブクエリ/CTEを使っているか +
+ -
+
LAG/ +LEADにOVER (ORDER BY ...)を付け忘れていないか +
+ -
+ 割り算を含むタスクで、ゼロ除算対策(
SAFE_DIVIDE)を検討したか +
+ - + タスク10では、手順書の指示とこのガイドの説明のどちらを優先すべきか確認したか + +
参考文献・出典
+| 項目 | +用途 | +URL | +
|---|---|---|
| ラボ本体(Google Cloud Skills Boost) | +このガイドが解説する課題ラボ本体 | ++ skills.google/course_templates/623/labs/629091 + | +
| covid-19-open-data 公式リポジトリ | +集計レベル・subregionカラムの意味に関する一次情報 | ++ github.com/GoogleCloudPlatform/covid-19-open-data + | +
| BigQuery標準SQL: 関数一覧 | +GoogleSQLの名称に関する一次情報 | ++ docs.cloud.google.com/.../functions-all + | +
| BigQuery標準SQL: ウィンドウ関数の呼び出し方 | +OVER句・ウィンドウ関数全般の構文リファレンス | ++ cloud.google.com/.../analytic-function-concepts + | +
| BigQuery標準SQL: ナビゲーション関数 | +LAG / LEAD関数の引数・挙動 | ++ cloud.google.com/.../navigation_functions + | +
| BigQuery標準SQL: 日付関数 | +DATE_DIFFの構文と算出ロジック | ++ cloud.google.com/.../date_functions + | +
| BigQuery標準SQL: 数学関数 | +POWER / SAFE_DIVIDEなどの一次情報 | ++ cloud.google.com/.../mathematical_functions + | +
| BigQueryクエリプランと実行タイムライン | +自己結合よりウィンドウ関数を推奨する根拠 | ++ docs.cloud.google.com/.../query-plan-explanation + | +
| BigQuery関数のベストプラクティス | +エラー処理関数の利用指針 | ++ docs.cloud.google.com/.../best-practices-performance-functions + | +
| Looker StudioとBigQueryの接続 | +カスタムクエリでの接続手順(公式) | ++ cloud.google.com/looker/docs/studio/connect-to-google-bigquery + | +