/home/by-natures/dev*

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

2023/05/20 ChatGPT で CSV から様々なモデルの生成

blog.gopenai.com

CSV ファイルから ChatGPT で概念モデルを生成するブログ記事を見つけました。私も試してみて、ついでにデータボルトモデリングにも挑戦しました。

複雑なことをさせようとしたので試行錯誤しましたが面白いですね。プロンプトエンジニアリングという言葉が出てくるのも納得です。(余談ですが、個人的にはあまり「エンジニアリング」ぽくはない気がしていて、「ヒューリスティック(発見法)」が近いのではということを仰る方を見かけて、こちらの方がしっくりきます)。

同じような聞き方でも結構違うモデルが返ってくることがあって試行錯誤しました。ChatGPT の癖になれる必要がありそうです。地味な利点としてモデルの可視化に PlantUML のコードを出してもらっているのですが、実際に書くとなると面倒なのでとてもありがたいです。

もくじ

データ

HR Case Study | Kaggle

上記ブログ記事に倣い、こちらの Kaggle のデータを利用しました。

概念データモデル

冒頭のブログ記事に倣い、概念データモデルに挑戦してみます。

[Request] Based on this sample CSV data, please create a conceptual data model:

Name,Gender,Department,Annual Salary,Location,Rating,Distance to Office,Age,Tenure in Company
Aarti Panchal,Female,CEO,10000000,Mumbai,Very Good,25,31,10.4
...
[ChatGPT]
Table 1: Employee
  EmployeeID (Primary Key)
  Name
  Gender
  Age
  TenureInCompany
  DistanceToOffice
  Rating
  DepartmentID (Foreign Key)
  LocationID (Foreign Key)
  SalaryID (Foreign Key)

Table 2: Department
  DepartmentID (Primary Key)
  DepartmentName

Table 3: Location
  LocationID (Primary Key)
  LocationName

Table 4: Salary
  SalaryID (Primary Key)
  AnnualSalary

Now, let's define the relationships between these tables:

Employee to Department: One-to-Many from Department to Employee. A department can have many employees, but an employee belongs to exactly one department.
Employee to Location: One-to-Many from Location to Employee. A location can have many employees, but an employee works at exactly one location.
Employee to Salary: One-to-One. Each employee has one salary, and each salary is associated with one employee.

だいぶそれらしいモデルが返ってきました。Employee to Salary が 1対1なのが気になるので、そこだけ修正します。

[Request] Please create a plantuml code for a ER diagram with the following correction:
- The relationship between Employee and Salary should be One-to-Many.

ChatGPT が PlantUML コードを返してくれるので、可視化します:

ChatGPTによるER図(可視化は PlantUML Web Server にて)

型情報まで付けてくれました。いくつか気になる点はありますし、同じ文言で問い合わせてもかなり違うモデルが返ってくることも多々ありました。まだ ChatGPT を使うのには慣れが必要そうですが、CSV からここまで簡単に構造化できるなら十分そうです。

Data Vault Modeling

データボルトモデリングも挑戦してみました。試行錯誤しましたが、次の聞き方でうまくいきました:

[Request] Create a data model based on data vault modeling with the following sample CSV data:

Name,Gender,Department,Annual Salary,Location,Rating,Distance to Office,Age,Tenure in Company
Aarti Panchal,Female,CEO,10000000,Mumbai,Very Good,25,31,10.4
...
[ChatGPT]
Hubs:
  Hub_Employee: Attributes: Employee Key (hash key), Name
  Hub_Department: Attributes: Department Key (hash key), Department
  Hub_Location: Attributes: Location Key (hash key), Location

Links:
  Link_Employee_Department: Attributes: Link Key (hash key), Employee Key (hash key), Department Key (hash key)
  Link_Employee_Location: Attributes: Link Key (hash key), Employee Key (hash key), Location Key (hash key)

Satellites:
  Sat_Employee: Attributes: Employee Key (hash key), Gender, Age, Tenure in Company, Record Source
  Sat_Department: Attributes: Department Key (hash key), Annual Salary, Rating, Record Source
  Sat_Location: Attributes: Location Key (hash key), Distance to Office, Record Source

ついでに可視化します:

[Request] Create a plantuml code for a ER diagram for the data model above.

ChatGPTによるデータボルトモデルの可視化

データボルトらしいモデルが作れました。3つの Hub が Link を通じて連携されており、詳しい属性は Satellite で表現されていることが分かります。

Happy data modeling!

2023/05/19 読んだ記事まとめ(Shadow Data Teamsによるデータ負債の蓄積)

最近、中学受験の算数の図形問題がYoutube のオススメにどんどん出てくるので、その度に手を止めて考えてます。うまく解けると気持ちいいです。

図形問題は苦手だったのですが少しずつコツを掴み始めていて、補助線をうまく引いて角度や面積を求められると気持ちいいですね。代数だと複雑な関数の積分を求めるのに近いというか、ある程度の基本的なパターンは覚えておきながらどれが当てはまるかを試行錯誤する感覚です。

今日は組織がデータメッシュに到達するまでの流れを独自の観点で解説している記事を紹介します。

もくじ:

How Shadow Data Teams Are Creating Massive Data Debt

medium.com

この記事では"Shadow data teams" という独自の概念を提唱していて、いくつかの記事を経て最終的には Data Mesh の解説まで行うそうです。著者の Diogo Silva Santos 氏は現在、the Western Europe Leader for Data & AI at Fujitsu も勤めているようで、LinkedIn を見るとデータ領域において幅広く活動されているようでした。

Shadow data teamが生まれる経緯

データがそこまで大量ではなかったころ、以下の点について考えるだけでデータの民主化は達成できていました:

  • データプロフェッショナルの雇用。今のデータスタックに長けているだけでなく、将来現れる技術にも対応できる人材。
  • データスタック。データライフサイクルが取り込み、保存、提供、データ利用までうまく回っている状態。
  • Cスイート(経営層)の支援。

しかし技術革新が進み、データ活用プロセスにおける複雑性が増してきます:

  • ビッグデータ時代の到来
  • データソースの増加
  • 潜在的なユースケースの増加
  • 様々なデータ利用者の登場

一方で以下の項目については今も変わらず必要であり、重要性が増してきています:

  • データ関連人材の協力関係
    • ソフトウェアエンジニアがデータを生成するが、データ提供をするインセンティブが無く分析サイクルからは外れている
    • データサイエンティストはビジネス層に近く、急かされることが多いため信頼に欠くデータを利用せざるを得ないケースがある
    • データエンジニアは中間に位置し、板挟みにあう
  • ビジネスコンテキストの理解
  • より早くデータからインサイトを得なければならない

このような問題が徐々に蓄積され、"Shadow data team" が生まれる結果となってしまいます。

Shadow data team とは?

The relation between insights velocity and governance(記事より引用)

組織におけるデータチームは、中央集権型分散型(Data Meshへつながる)、そして "Shadow data team" の3つに分けることが出来ます。Shadow data team は簡潔に言えば「データ活用のサイクルが短いのにデータガバナンスが低い」チームです。上記記事ではこの "Shadow data team" が今最も発生しやすいチームであり、その理由を簡単な時系列に沿って説明しています:

  1. ビジネスによって新たなデータ機能あるいは製品のためのリクエストが発生する
  2. データサイエンティストはリクエストを精査。その結果、必要なデータが利用不可能であるか、もしくは予想されるスキーマで利用不可能であることを発見する
  3. データサイエンティストは、データエンジニアに新たなデータパイプラインを要求する
  4. データエンジニアたちは日頃から膨大なリクエストを抱えており、このリクエストを解決するのに数週間から数ヶ月を要してしまうことを伝える
  5. データサイエンティストはビジネス層からのプレッシャーを感じており、データエンジニアの作業を待たずにデータソース等に直接アクセスし、プロダクション基準やCI/CDのベストプラクティスに従わず、明確な所有権がないSQLあるいはdbtパイプラインを作成する
  6. データサイエンティストはこのプロセスに慣れてしまい、結果として組織内で大量のデータ負債を生み出すことになる

この5番目は dbt という具体的なツール名まで挙げて説明されているのが興味深いです。

dbt はこの5番目の内容の通り、データサイエンティストが SQL だけを利用して自らデータパイプライン構築ができることを強みにしています。これがデータサイエンティストの悩みを解決するけれども、組織全体で見ると新たなデータ負債を生んでしまうため、"Shadow data team" につながるという指摘です。ただ dbt は分かりやすさのために紹介されているだけで、データサイエンティストが時間に追われて既存のデータパイプラインを使わずにデータ生成しているケースはよくあることだと思います。

データ負債が増大していく

この結果生まれるデータ負債には、次のようなものが挙げられます:

  • 一貫性がなく信頼性の低いデータ
  • 高いメンテナンスコスト
  • データに関する問題のトラブルシューティングが難しくなる
  • 生成されたデータに対して誰が責任を負うのかを決めるのが難しい
  • 信頼性の低い機械学習モデル
  • 保守されていないパイプライン、テーブル、重複したデータに関連する追加費用
  • レイクとデータウェアハウスの操作がより複雑になる

一方データエンジニアチームはというと、データサイエンティストが独自のプロセスでデータ活用をし始めるとデータエンジニアの必要性が薄れてきて、チームとしての信頼性が失われていきます。dbt はアナリティクスエンジニアという言葉を提唱していますが、データサイエンティストの職責がデータエンジニア側に拡大し、その拡大部分がアナリティクスエンジニアと呼ばれているのだろうと理解しています。

このまま月日が経つとデータ負債はどんどん増えていきます。もし経営層や責任者が組織体制を立て直そうとしても、ビジネス層のメンバーはデータサイエンティストによる素早いデータ活用サイクルに慣れてしまっているため、立て直しは非常に困難です。

この続きは著者の Diogo Silva Santos の新たな投稿待ちとなりますが、「データ負債についてのさらなる解説」、「データウェアハウスが最も重要なデータ資産であること」、そして「データメッシュの紹介」と続くようです。

感想

この記事で紹介されている "Shadow data team" へつながる流れは私も感じたことがあります。標準プロセスを重視したいデータエンジニアと、ビジネス層に急かされ自分たちだけでデータ活用を進めてしまうデータサイエンティストという構図は多くの組織で見られるのではないでしょうか。

営業担当者と開発者に置き換えても似たような話はできますが、営業担当者だけで製品開発を進めることができません。一方でデータエンジニア/データサイエンティストのケースだとデータサイエンティストだけでデータ活用サイクルを進めてしまえる点が状況をより複雑にしている気がしました。

続く記事が出たら、またまとめてみます。

2023/05/09 DuckDB のための AWS Lambda での文字コード変換

前回話した DuckDB の文字コード問題ですが、やはり UTF-8 しか想定されていないようです(DuckDB Foundation の方からリプを貰えました)。DuckDB は S3 や HTTP(S) などの外部データを直接読み込むことができる便利な機能がありますが、処理したいファイルの文字コードが UTF-8 以外だと DuckDB 以外のツールで変換する必要があります。

AWS Lambda でファイル取得&文字コード変換

Lambda で文字コード変換

DuckDB で直接読み込むことは諦めて、自動化を見据えたユースケースとして AWS Lambda で文字コード変換とヘッダ処理だけ行いました。Pandas の read_csv が直接 URL を引数に受け取れるのが強力でした。ここに文字コード sjis を指定して、出力時に utf8 を指定することで文字コード変換をします。

import boto3
import pandas as pd
from io import StringIO

def lambda_handler(event, context):
    # Read the target data through 'read_csv function
    url = 'https://path/to/file'
    df = pd.read_csv(url, header=7, encoding='sjis')

    # Write the CSV data into a dataframe
    csv_buffer = StringIO()
    df.to_csv(csv_buffer, encoding='utf8', header=True, index=False)
    
    # Store the result into S3
    s3_resource = boto3.resource('s3')
    s3_resource.Object('bucket-name', 'output-path.csv').put(Body=csv_buffer.getvalue())
    return "OK"

Pandas を読み込むためのレイヤー追加

AWS Lambda のランタイムは Python 3.10 を選択しましたが、このランタイムには標準で Pandas が含まれていません。レイヤー追加して Pandas を読み込めるようにします:

qiita.com

ワークフローまで考えたい

以前読んだ Medium の記事では、dbt を使う際に Astronomer や Google Cloud Composer を使っているものがありましたが、今回の私の簡単な例だけでもワークフロー管理まで考えると色々と疑問が出てきました:

  • AWS における dbt + Lambda ワークフロー管理(Glue?)
  • DuckDB の本番環境をどこで動かすか
  • アドホック分析環境をどこに用意するか?S3上にデータが揃うので Athena でもいいし、SnowPipe で Snowflake に流してもいい?

DuckDB はクラスタを必要としないデータ処理を行えることが強みなので、Snowflake を出してしまうと最初から Snowflake+dbt で良いのではと思ってしまいますね… アドホック分析も各自のローカル環境で DuckDB を直接 S3 Parquet ファイルを参照させる形になるんでしょうか。

2023/05/02 dbt テンプレート・マクロ・テスト導入, 文字コード問題(未解決)

先日書いた記事では、 CSV(UTF8) 形式のファイルを dbt-duckdb プラグインで読み込み、集計した結果を Parquet ファイルとして S3 へ保存する処理を dbt で実装しました:

bynatures.hatenadiary.jp

この時はひとまず動くことを目標にしていたので、今日は dbt の設定見直しをしていました。dbt で更に外部の DWH が必要ない dbt + DuckDB は手元でさくさく試すことが出来て楽しいです。

もくじ:

冗長な宣言のマクロ&テンプレート化

元データでは年代別の人口がカラムに分かれており、同じ処理を複数行に渡って記述していました。前回は途中諸略しましたが、1970年〜2010年で5年ごとに9ヵ年のカラムが存在します:

WITH original_data AS (
  SELECT "市区町村名"     AS CityName,
         "市区町村コード"   AS CityCode,
         "1970年"        AS Population1970,
         "1975年"        AS Population1975,
         "1980年"        AS Population1980,
         "1985年"        AS Population1985,
         "1990年"        AS Population1990,
         "1995年"        AS Population1995,
         "2000年"        AS Population2000,
         "2005年"        AS Population2005,
         "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(Population1980, ',', '') AS INT) AS Population1980,
       CAST(REPLACE(Population1985, ',', '') AS INT) AS Population1985,
       CAST(REPLACE(Population1990, ',', '') AS INT) AS Population1990,
       CAST(REPLACE(Population1995, ',', '') AS INT) AS Population1995,
       CAST(REPLACE(Population2000, ',', '') AS INT) AS Population2000,
       CAST(REPLACE(Population2005, ',', '') AS INT) AS Population2005,
       CAST(REPLACE(Population2010, ',', '') AS INT) AS Population2010
FROM original_data

これを Jinja テンプレートを使ってループを組んでみました(カンマは末尾から先頭に持ってくることで、ループ終了処理を省いています):

{% set years = ["1970", "1975", "1980", "1985", "1990", "1995", "2000", "2005", "2010"] %}

WITH original_data AS (
  SELECT  "市区町村名"   AS CityName
         ,"市区町村コード" AS CityCode
         {% for year in years %}
           ,"{{year}}年" AS Population{{year}}
         {% endfor %}
  FROM {{ source('external_source', 'population_census_jp_2014') }}
)
SELECT  STRING_SPLIT(CityName, ' ')[1] AS PrefectureName
       ,STRING_SPLIT(CityName, ' ')[2] AS CityName
       ,CityCode
       {% for year in years %}
         ,{{ convert_str2int("Population"+year) }} AS Population{{year}}
       {% endfor %}
FROM original_data

プログラムとして見ると冗長性が省かれて良いはずなのですが、SQLで簡単にデータ変換処理をしようという dbt の思想からすると、本来実行される SQL とかなりかけ離れていて複雑な感じもします。業務でマクロやテンプレートを使う場合は不必要に複雑にしない&冗長性を省くバランスを取るために、ポリシー策定も必要になるかもしれません。

convert_str2intCAST(REPLACE(Population1970, ',', '') AS INT) AS Population1970 を定義にもつマクロです。Jinja のループで冗長性は十分に省けていますが、これは dbt のマクロを使ってみたかったので分離させました:

% cat macros/convert_str2int.sql
{% macro convert_str2int(column_name) %}
    CAST(REPLACE({{ column_name }}, ',', '') AS INT)
{% endmacro %}

テスト追加

テストも models フォルダ内に追加してみます。組み込みの Null チェックと、CityCode のユニーク制約チェックを設定しました:

% cat models/population_census/population_by_city.yml 
version: 2

models:
  - name: population_by_city
    columns:
      - name: PrefectureName
        tests:
          - not_null
      - name: CityName
        tests:
          - not_null
      - name: CityCode
        tests:
          - not_null
          - unique

dbt test でテスト実行をします。CI/CD を組む場合は dbt run の前に実行する感じになるでしょうか:

% dbt test
11:03:56  Running with dbt=1.4.6
11:03:56  Found 3 models, 4 tests, 0 snapshots, 0 analyses, 298 macros, 0 operations, 0 seed files, 2 sources, 0 exposures, 0 metrics
11:03:56  
11:03:56  Concurrency: 1 threads (target='dev')
11:03:56  
11:03:56  1 of 4 START test not_null_population_by_city_CityCode ......................... [RUN]
11:03:56  1 of 4 PASS not_null_population_by_city_CityCode ............................... [PASS in 0.10s]
11:03:56  2 of 4 START test not_null_population_by_city_CityName ......................... [RUN]
11:03:56  2 of 4 PASS not_null_population_by_city_CityName ............................... [PASS in 0.08s]
11:03:56  3 of 4 START test not_null_population_by_city_PrefectureName ................... [RUN]
11:03:56  3 of 4 PASS not_null_population_by_city_PrefectureName ......................... [PASS in 0.08s]
11:03:56  4 of 4 START test unique_population_by_city_CityCode ........................... [RUN]
11:03:56  4 of 4 PASS unique_population_by_city_CityCode ................................. [PASS in 0.08s]
11:03:56  
11:03:56  Finished running 4 tests in 0 hours 0 minutes and 0.60 seconds (0.60s).
11:03:56  
11:03:56  Completed successfully
11:03:56  
11:03:56  Done. PASS=4 WARN=0 ERROR=0 SKIP=0 TOTAL=4

(未解決)Shift JIS 形式 CSV ファイルの読み込み

一番解決したい問題は文字コード問題です。

DuckDB では CSV ファイルの読み込みに ENCODING オプションをつけられるのですが、これが現在は UTF-8 しか対応していません。今回利用したオープンデータは Shift JIS 形式で提供されており、これをそのまま CSV で読み込もうとするとエラーとなります:

11:09:39  Runtime Error in model population_by_city_sjis (models/population_census/population_by_city_sjis.sql)
11:09:39    Invalid Input Error: Error in file "s3://<S3_BUCKET>/file01.csv" at line 8 in column "0": Invalid unicode (byte sequence mismatch) detected in CSV file. Parser options:
11:09:39      file=s3://dbt-duckdb-test-hitoshi-tsuda/file01.csv
11:09:39      delimiter=','
11:09:39      quote='"' (auto detected)
11:09:39      escape='' (auto detected)
11:09:39      header=1
11:09:39      sample_size=20480
11:09:39      ignore_errors=0
11:09:39      all_varchar=0

公式ドキュメントでも明確に UTF8 のみを受け付けるとあり、テストコードまで存在しているので意図的にそうしていると読み取れます:

ENCODING If this option is used, its value must be UTF8. With any other encoding an error will be thrown.

CSV Import/Export - DuckDB

   def test_encoding_wrong(self, duckdb_cursor):
        with pytest.raises(duckdb.BinderException, match="Copy is only supported for UTF-8 encoded files, ENCODING 'UTF-8'"):
            rel = duckdb_cursor.read_csv(TestFile('quote_escape.csv'), encoding=";")

duckdb/test_read_csv.py at master · duckdb/duckdb · GitHub

単発での取り込みなら手元で文字コード変換をして取り込むCSVファイルを生成すればよいのですが、実業務で外部システムの出力するファイルが UTF-8 以外であるケースはあると思うので、dbt で CSVダウンロード&文字コード変換処理を入れたりできるのか、引き続き調べます。

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データポータル

2023/04/25 読んだ記事まとめ(dbt + Duckdb による Spark の置き換えについて)

先日 dbt + Duckdb についての Medium の投稿を読んで面白かったので Twitter に投稿したのですが、かなりの反響がありました。dbt と Duckdb というキーワードがキャッチーだからかな、と思ったのですが、改めてここでもブログの内容を少し要約してみます。

Use dbt and Duckdb instead of Spark in data pipelines

medium.com

この記事は Data Minded 社というベルギーにあるエンジニアからの投稿で、データエンジニアリングやデータサイエンスのコンサルティングサービスを提供している会社のようです。dbt + Duckdb が Spark を置き換えるという内容で、タイトルからして目を惹きます。Duckdb は以前にも "Big Data Is Dead" という記事も投稿されていますが、そこでも似たような主張がされています:

bynatures.hatenadiary.jp

dbt と Spark を一概に比較するのも難しい気がしますが、記事ではざっくりと「データ変換するツール」として捉えた場合の dbt のメリットを紹介しています:

Spark に対する dbt の優位性

Broader target audience

SQL を中心に据えることで、多くの人が dbt を使うことができます。

Simple execution

Spark のパラメータは複雑で、Sparkの仕組みを理解して利用する必要がある。dbt ではこの部分を DWH に切り離しているため、多くのユーザは低レベルのクエリ実行機構を意識する必要がない。ただ一方でこういったクラウドDWHはコストが高くなる場合もある。

Most companies don’t have big data

これは、上で紹介した "Big Data Is Dead" と同じ主張です。Spark は大規模なデータを処理できますが、あなたのデータは本当に Spark を使ってまで処理する必要があるのか?というアンチテーゼを提唱しています。

dbt + Duckdb を組み合わせるメリット

dbt には dbt-duckdb というプラグインがあり、Duckdb をシングルノードで動く DWH として使うことができます。dbt と Duckdb を組み合わせるメリットが3つ紹介されています:

No need to rely on your DWH for all processing

クラウド DWH をすべてのデータ変換処理で使うとコストが高くなりがちです。ローカルでの dbt テストや、パイプライン中のデータ変換に Duckdb を組み合わせることも可能です。

Integrate seamlessly with external storage

Duckdb は処理結果を外部システムに書き込んだり、主要なデータフォーマット(CVS, Parquetなど)に書き出すこともできます。この機能により、インプット/アウトプット形式は変更せず、既存のSparkジョブを置き換えることが可能です。

No need to change your data platform

Spark を利用したデータ変換処理ではコモディティハードウェアを利用してデータ変換処理を行い、結果をデータレイクもしくはDWHに書き出します。大きなデータアーキテクチャには手を加えずに この処理部分のみ、例えばデータサイズが小さかったり変換処理がシンプルであれば、dbt + Duckdb に置き換えることが可能です。

Dbt with Duckdb is faster than Spark

この記事では TPC-DS ベンチマークを用いた dbt + Duckdb / Spark の速度比較が行われています。詳しくは記事を参照いただきたいのですが、1台のマシンメモリ上で処理が完結できる場合には dbt + Duckdb の処理速度が速く、1台に収まりきらない場合はそもそも Duckdb で処理すべきではなく Spark で分散させるべき、という結論に至っています。

2023/04/16 IPA NW を受けてきました

今年頭から勉強してきた IPA ネットワークスペシャリストの試験がようやく今日終わりました。

午前Ⅰ、Ⅱは80点以上でパスできましたが、今年は午後Ⅰが非常に難しくて、基準の60点を超えていないかもしれません。。数日前から Wi-Fi 技術についての対策をしていたので午後Ⅰは問3を選んだのですが、問1の HTTP/2 は対策本にも過去問にも登場していなかったので無警戒でした。問2の IGMP, マルチキャストの問題は解ける気がしなかったので問1, 3 の選択でしたが、問1 が6割達していない感触なので、問3でどれだけ点数が取れているかに掛かっています。

結果は6月末なので忘れた頃に来るようです。ちょっと合格は難しいかな…(午後Ⅰで下駄履かせがあればもしかしたら、ぐらい)という感触ですが、いい機会なので HTTP/2 や Wi-Fi 技術について改めて学習しつつ、秋のデータベーススペシャリストが本命なのでそちらの勉強も5月以降から始めようと思います。

それにしてもこの3ヶ月、特に直前1ヶ月は受験勉強以来の勉強ぶりでした。4月の土日は1日コワーキングスペースにこもって勉強していました。直前1週間は不安が大きかったですが、知らないことをたくさん学べて楽しかったです。

IPA の試験を受けられた方、朝から夕方までお疲れ様でした。