/home/by-natures/dev*

データ界隈で働くエンジニアとしての技術的なメモと、たまに普通の日記。

2023/04/28 dbt + duckdb でオープンデータの集計

先日読んだ記事で dbt に DuckDB 用のプラグインがあると知り、dbt と DuckDB の学習も兼ねて試してみることにしました。とても簡単な実験ですが、何かの参考になれば幸いです。

今回実際に試してみて感じたのが、dbt と DuckDB の相性の良さです。手元で動く DWH に対して dbt のデータ変換処理の開発を手早く行うことができ、外部連携部分は DuckDB が吸収してくれるため、dbt は素直に SQL を利用したデータ変換に専念できそうです。以前読んだ記事は、データが巨大でなければ dbt + DuckDB で本番データに対してもデータ変換処理をしてしまおうという趣旨の記事で、面白い内容でした。併せてご覧ください:

bynatures.hatenadiary.jp

概要

アーキテクチャ

アーキテクチャというほどのものではないですが、S3 の CSV ファイルを dbt-duckdb プラグインで読み込んで集計し、結果を Parquet ファイルにして S3 へ転送してみます。非常に簡単な実験ですが、上の記事に倣って dbt + DuckDB を ETL 処理として使うなら、外部からデータを読み込み、集計結果をまたどこか別の場所に保存する似たようなアーキテクチャになるかなと思います。

①のオープンデータのS3への保存は手動で行なったのですが、次で理由を説明します。

使ったデータ

使うオープンデータはこちらにしました。CSV形式で提供されていたのと、市区町村別の人口総数という分かりやすい指標だったからです。

data.e-gov.go.jp

出典:e-Govポータル (https://www.e-gov.go.jp

ただこの CSV が SJIS で提供されており、DuckDB の read_csv メソッドがどうやら UTF-8 にしか対応していないようで、一度文字コード変換をする必要があります。今回は手元にCSVをダウンロードして、iconv コマンドで文字コード変換をして S3 へアップロードしましたが、日々追加されていくファイルに対して手動オペレーションは挟めないので、どこかで自動化する必要があります(要調査。。)

iconv -f sjis -t utf8 ~/Downloads/sjis_file.csv > ~/Downloads/utf8_file.csv

環境準備

DuckDB インストール

brew でインストールできます。

dbt インストール

Python のライブラリを独立させるため、virtualenv を使います:

dbt-duckdb のインストール・セットアップは dbtの公式HPに倣います。pip install dbt-duckdb で dbt Core と dbt-duckdb プラグインをまとめてインストールできます。

ディレクトリ構成

普段使っていないのが丸わかりですが…とりあえず動くことを目標にしたので dbt のディレクトリ構成は最小限に留めました。dbt init コマンドで population_census プロジェクトを生成します。

% dbt init population_census
17:02:29  Running with dbt=1.4.6
Which database would you like to use?
[1] duckdb

(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)

Enter a number: 1
...
Happy modeling!
~/.dbt/profiles.yml
population_census/dbt_project.yml
population_census/models/population_census/opendata.yml
                                          /population_by_city.sql
                                          /population_by_prefecture.sql

profiles.ymldbt-duckdb の Github README に倣い、AWS へのアクセスキーとシークレットキーの設定をしています。dbt-duckdb の Github README だけ読むと分からないですが、プロジェクト配下ではなくホーム配下の .dbt/ フォルダに設置する必要があります。

dbt_project.yml

このファイルも大幅に簡略化しています。models: の指定だけディレクトリ構成と合っていれば大丈夫かと思います。

name: 'population_census'
version: '1.0.0'
config-version: 2
profile: 'population_census'

model-paths: ["models"]
test-paths: ["tests"]

target-path: "target"  # directory which will store compiled SQL files
clean-targets:         # directories to be removed by `dbt clean`
  - "target"
  - "dbt_packages"

models:
  population_census:
    +materialized: table

opendata.yml - 外部ソース(S3) の指定

DuckDB の read_csv を利用し、S3 の CSV ファイルの設定をします。今回読み込むCSVには不要なヘッダが7行あるためこれをスキップし、型推論は auto_detect=True で自動にしました。

version: 2

sources:
  - name: external_source
    tables:
      - name: population_census_jp_2014
        meta:
          external_location: "read_csv('s3://<BUCKET_NAME>/{name}.csv', delim=',',  auto_detect=True, header=True, skip=7)"

population_by_city.sql - 外部ソースの読み込み

外部ソースからデータを読み込み、 population_by_city というテーブル名で DuckDB に保存します。データ内の数値が3桁区切りごとにカンマが入った文字列型だったため、クレンジング処理を加えています。

北海道 札幌市,11002,"1,010,177 ","1,240,613 ","1,401,757","1,542,979","1,671,742","1,757,025","1,822,368","1,880,863","1,913,545"
WITH original_data AS (
  SELECT "市区町村名"     AS CityName,
         "市区町村コード"   AS CityCode,
         "1970年"        AS Population1970,
         "1975年"        AS Population1975,
...
         "2010年"         AS Population2010
  FROM {{ source('external_source', 'population_census_jp_2014') }}
)
SELECT CityName,
       CityCode,
       CAST(REPLACE(Population1970, ',', '') AS INT) AS Population1970,
       CAST(REPLACE(Population1975, ',', '') AS INT) AS Population1975,
...
       CAST(REPLACE(Population2010, ',', '') AS INT) AS Population2010
FROM original_data

population_by_prefecture.sql - DuckDB 上での集計&S3へ保存

population_by_city テーブルを都道府県のレベルで集約し、これを Parquet 形式で S3に保存します。この指定はファイル先頭に config の指定をするだけで実現できます。

{{ config(materialized='external', location='s3://dbt-duckdb-test-hitoshi-tsuda/population_by_prefecture.parquet') }}
SELECT STRING_SPLIT(CityName, ' ')[1] AS Prefecture,
       SUM(Population1970) AS Population1970,
       SUM(Population1975) AS Population1975,
...
       SUM(Population2010) AS Population2010
FROM {{ref('population_by_city')}}
GROUP BY 1

実行

dbt run で実行します。エラーが出た場合は dbt -d run でデバッグ出力をしてエラー内容を確認します。

% dbt run  
17:03:36  Running with dbt=1.4.6
17:03:36  Found 2 models, 0 tests, 0 snapshots, 0 analyses, 297 macros, 0 operations, 0 seed files, 1 source, 0 exposures, 0 metrics
17:03:36  
17:03:36  Concurrency: 1 threads (target='dev')
17:03:36  
17:03:36  1 of 2 START sql table model main.population_by_city ........................... [RUN]
17:03:39  1 of 2 OK created sql table model main.population_by_city ...................... [OK in 2.80s]
17:03:39  2 of 2 START sql external model main.population_by_prefecture .................. [RUN]
17:03:43  2 of 2 OK created sql external model main.population_by_prefecture ............. [OK in 4.21s]
17:03:44  
17:03:44  Finished running 1 table model, 1 external model in 0 hours 0 minutes and 7.36 seconds (7.36s).
17:03:44  
17:03:44  Completed successfully
17:03:44  
17:03:44  Done. PASS=2 WARN=0 ERROR=0 SKIP=0 TOTAL=2

出力ファイルの確認

S3に生成された Parquet ファイルをローカルにダウンロードし、DuckDB で読み込んでみます:

% ./duckdb /tmp/dbt.duckdb
D SELECT Prefecture, Population1970, Population2010 FROM '~/Downloads/population_by_prefecture.parquet' LIMIT 10 ;
┌────────────┬────────────────┬────────────────┐
│ Prefecture │ Population1970 │ Population2010 │
│  varchar   │     double     │     double     │
├────────────┼────────────────┼────────────────┤
│ 北海道     │      5184287.0 │      5506419.0 │
│ 青森県     │      1427520.0 │      1373339.0 │
│ 岩手県     │      1371383.0 │      1330147.0 │
│ 宮城県     │      1819223.0 │      2348165.0 │
│ 秋田県     │      1241376.0 │      1085997.0 │
│ 山形県     │      1225618.0 │      1168924.0 │
│ 福島県     │      1946077.0 │      2029064.0 │
│ 茨城県     │      2143551.0 │      2969770.0 │
│ 栃木県     │      1580021.0 │      2007683.0 │
│ 群馬県     │      1658909.0 │      2008068.0 │
├────────────┴────────────────┴────────────────┤
│ 10 rows                            3 columns │
└──────────────────────────────────────────────┘

参考ページ

notrogue さんの DuckDB の解説がとても分かりやすいです: zenn.dev

DuckDB Installation - DuckDB

GitHub - jwills/dbt-duckdb: dbt (http://getdbt.com) adapter for DuckDB (http://duckdb.org)

DuckDB setup | dbt Developer Hub

pip と仮想環境を使ってパッケージをインストールする — Python Packaging User Guide

人口総数:総務省『国勢調査』 | 人口総数:総務省『国勢調査』(CSV形式:178KB) | e-Govデータポータル