データ分析現場の失敗事例集

# はじめに

この記事は以前に従事していたデータ分析基盤の業務で感じた問題点を整理したものです。

以前の現場では既に運用されている分析基盤を扱っていたため変更の仕様がない部分がありましたが、これから自分が１から分析基盤を構築していくにあたり、気を付けるべきところを出来る限りまとめて行こうと思います。


# 前提

## 業務体制

以下はこの記事で扱うデータ分析基盤の業務体制です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/04fb0e9f-6c66-9a36-0e1f-9de20a144858.png)

- 発注者（マーケティング会社）
    各種メディアから広告視聴データを購入してデータ分析基盤を構築・運用している。
    統計の専門家組織だがITの知見がないため基盤設計～運用までの業務を外注している。
    <br>
- １次請け（中規模SIer）
    発注者であるマーケティング会社から基盤設計～運用までの業務を請け負っている。
    プロパー社員が案件管理を行い、プレイヤー要員を弊社などの外部企業から調達。
    <br>
- ２次請け（弊社）
    私が所属していた会社。（※退職済みだが便宜的に「弊社」と記載）
    ２次請けの立場ではあったが、実質的には設計を含む全工程を担当。

なお、上図の組織体制図では分析基盤を「Redshift→BigQuery」と表記していますが、これは現場参画中にデータ分析基盤の移行というイベントがあったからです。

基盤移行によりAWSとGoogle Cloudの両方を経験できたので、この記事では後でAWS製品とGoogle Cloud製品の比較についても触れてみようと思います。


## 記事の構成

[『実践的データ基盤への処方箋』](https://gihyo.jp/book/2021/978-4-297-12445-8)という著書に記載されていたデータ分析基盤の全体像が分かりやすかったので、この記事では書籍で紹介されていた下図の分類に沿ってアンチパターンを紹介していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/fa746268-88f6-0fe6-fbb9-f6dc00b60b7c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/897c8f08-ca49-ec1c-0075-3be7343da436.png)
引用：[書籍『実践的データ基盤への処方箋』](https://gihyo.jp/book/2021/978-4-297-12445-8)


# パート別の失敗事例集

## データソース
データソースとは、オリジナルデータのことです。

データソースにはECサイトの注文履歴から紙に記載されている伝票まで様々なものが含まれます。

以下はデータソースの領域で実際に発生していた問題です。

- データ作成ルールが緩い

    私がいた現場では外部の各種メディアから広告視聴データを購入していたのですが、各社が担当者の判断でデータを作成していたため後続処理で次のようなトラブルが絶えませんでした。
    
    - 半角・全角の違いでJOINできず集計値がずれる
    - IDの文字列が長すぎてカラムに格納できない
    - 重複したメタデータが生成されて集計条件が適用されない

    データソースはデータがありのまま存在する領域です。また、誤りや欠損は後続の処理で修正可能なため、元データに誤りや欠損があること自体は許容すべきです。しかし、事前に予想していないデータが入ってくるとトラブルは避けられません。

    <details><summary>📝対応の考察</summary>

    データ作成のルールが曖昧だと後続の担当者が安心して集計などができなくなるため、データソース領域では可能な限り作成されるデータの品質を一定水準以上に保つ工夫が必要です。

    私の現場ではデータの生成元が社外で難しいところがあったかもしれませんが、次のような施策を進めれば状況は少しは改善していたかもしれません。

    - 関係者間でデータ作成のルールを取り決める
    - データ作成ルールを強制できるような入力補助機能を導入する
    
    このような施策は既存の業務を変更することになるので反発があるかもしれません。なので、例えば私の現場では「データ生成元＝納品先」だったので、再集計が機会損失になると説明したり再集計の原因がデータ作成元である場合は再集計費用を生成元に求めるなど、データ生成元が改善の必要性を感じるような働きかけも必要になるでしょう。
    
    </details>
    

## データ基盤

### データレイク層

データレイク層とは、様々なデータを溜め込むための領域です。

データレイク層にデータを蓄積するときにはデータを整形したくなるかもしれませんが、データレイク層にはありのままのデータを取り込みます。

データレイク層にはクラウドストレージが採用されることが多く、私がいた現場でも各種メディアから購入したファイルは Amazon S3 や Google Cloud Storage に置いていました。しかし、残念ながら私たちの現場ではデータレイク層とデータソースが混同されており、結果次のような問題が発生していたと思います。


- オペミスが発生しやすい

    データソースとデータレイク層が同じ場合、後にデータウェアハウス(DWH)に取り込むデータを各担当者が手動で配置する場合があり、人為的なエラーが発生するリスクを抱えることになります。
    
    機会と比べれば人は単純にミスをしやすいですし、実際私がいた現場でも経験の浅い担当者がコピー＆ペーストとカット＆ペーストを間違えて他の後続処理で参照エラーを起こすみたいなことがありました。
    <br>
    
- エラー検知が遅れる

    大規模なデータ分析では後続のDWHの処理に非常に時間がかかることがあるので、データ配置ミスなどのオペミスは何時間も気づかれずに放置されるやすいです。
    
    例えば、私たちの現場では3~4時間かかるデータ取り込みが完了間近に異常終了し、当日の納品ジョブまでにリカバリーが間に合わないという事例がありました。
    <br>

    <details><summary>📝対応の考察</summary>

    DWH製品は基本的にデータベース製品なので、一般的なプログラムと比較して処理時間が長く、事前処理に問題があってもそれに気付くのが遅くなりがちという点に注意が必要です。
    
    以前の現場の失敗例に関して言えば、データソースとデータレイク層を分離し、必要なデータを取り込む作業をシステム化していれば、DWHでの集計前に異常を検知して早めに対処できていたと思います。

    </details>


 ### データウェアハウス層

データウェアハウス層とは、集計しやすく加工したデータや共通となる指標を格納する領域です。

飽くまで私がいた現場での体験談ですが、データウェアハウス層で発生するトラブルには次のようなものがありました。

- クレンジング不足でパフォーマンスが低下

    これはシンプルにデータウェアハウス層生成のロジックが不十分だっただけですが、例えば次の表のように複数のキャンペーン期間がデータレイク層から入り込んだ場合、運用上一律に配信開始日の最小値と配信終了日の最大値をとって「配信開始日: 2024/12/01, 配信終了日: 2024/12/15」とし、各集計でマスタを作り直すことになっていました。

    |ID|キャンペーン名|配信開始日|配信終了日|
    |:--:|:--|:--:|:--:|
    |1|キャンペーンA|2024/12/05|2024/1210|
    |1|キャンペーンB|2024/12/01|2024/1215|

    一律の指標があるならクレンジング段階でマスタを一意にすべきだと思いますが、私たちの現場ではそもそも「マスタは一意である」という前提で動いていたので、そのようなクレンジング処理は実装されませんでした。

    そして、そのような不十分なクレンジング処理が長らく放置された結果、所定の時間にデータウェアハウス層の生成が完了していることを想定して後続集計がスケジュールされ安易に計算量を増やすことができなくなり、クレンジングの不足はいわゆる<strong>"運用でカバーする"</strong>ことになっていました。

    本来なら１度のクレンジングで済んだのに、潜在バグを放置した結果、個別集計で同じクレンジングを何度もやるなんて本当に非効率ですね...。
    <br>

- 命名ミス

    データマートは特定の用途向けに集計されたデータを格納する領域を指すのですが、驚くことに私の現場ではdatamartという名前がデータウェアハウス層のテーブルに使われていました。
    
    データウェアハウス層とデータマート層の混同はシンプルに誤解を与えるので良くないですね。

    また、その他にもデータウェアハウス層のテーブルには次のように明らか命名ミスと思われるカラムがありました。

    - id1, id2, ... のような内容の推測が困難なカラムが多数ある
    - 同じ内容のカラムが特定のテーブルで異なる名前にされている

    以上のような命名ミスがあると、集計担当者は毎回カラムの内容やりカラムの名前を確認しないといけませんし、発注者側との協議でもしばしば意思疎通に問題を生じることがありました。

    <details><summary>📝対応の考察</summary>

    クレンジング不足を運用でカバーしたり、datamartテーブルが実は共通指標だったり、私たちのデータ基盤の運用はかなり癖が強かったと思います。そして、そのような運用が長年続いた結果、現場では俗人化が進み、効率が悪く費用も掛かる集計が常態的に行われるようになっていました。
    
    このような問題を解消するには発注者や上流側の理解が必要です。つまり、クレンジング不足やid2などの命名にどのような問題があるか気付いてもらい、集計スケジュールの変更やスキーマの変更などの比較的大きな問題に取り組む同意を得ていかないといけません。
    
    </details>


### データマート層

データマート層とは、特定の用途向けに集計されたデータを格納する領域です。
用途ごとに作るのでデータマートとユースケースは1対1の関係になります。

私たちの現場に関しては datamartテーブル が実質データウェアハウス層働きをしていたことを除くと特段引っかかる所はなかったと思います。

### ユースケース

ユースケースとは、データ基盤の具体的な用途です。

具体的には、「前週のジャンル別売上を確認する」とか「条件を満たすユーザにクーポンを配布する」などの用途がユースケースに該当します。

私の現場では広告代理店などから依頼を受けてから集計が行われていたので問題ありませんでしたが、元請けの企業によると「これからはデータの時代」という言葉に踊らされて「とりあえずデータ集めたけどどう使えばいいかわからん」という企業は意外と多いらしいので、出口戦略がしっかり考えられているかは確認すべきでしょう。


### メタデータ

メタデータはデータを説明するためのデータです。

メタデータにはテーブルやカラムの説明はもちろん、データの利用状況などの情報も含まれます。

データウェアハウス層の問題で指摘したように、運用が複雑だったり命名ミスが合ったりする場合は特にテーブルやカラムの仕様について理解できるよう情報を残す価値が高くなります。

お察しの通り、私たちの現場ではメタデータの運用も進んでいなかったので次のような問題が発生していました。

- テーブルを削除できなくなる

    データウェアハウス(DWH)はチームや個人用に沢山のスキーマやテーブルを持つことができます。これは便利な反面管理が行き届かなくなる危険性があります。
    
    実際、ほぼ未経験のエンジニアが作成した集計処理では検証用と思われてた個人スキーマ内のテーブルが本番集計で使われており、その担当者が退場したときに集計処理が止まってしまうという事象がありました。

- ファイルも削除しにくくなる

    RedshiftやBigQueryなどのクラウド製品はS3やGCSのようなクラウドストレージのファイルをロードすることがあるため、テーブルの問題と同様に退職者が出たときにクラウドストレージ上のファイルも安易に削除することができなくなります。

- 共通指標を作成できない

    データウェアハウスは共通指標をもつことがありますが、共通指標を作るにはその指標が実際によく使われていることを示せないといけません。

    私がいた現場ではメタデータの整備が進められていなかったため、共通指標の作成が個人の経験と勘にゆだねられており、共通指標を作っても使われないことがありました。

    <details><summary>📝対応の考察</summary>

    テーブル定義のSQLを残すのはもちろんですが、カラムにはコメントも書いてカラムの意味をどこでも確認できるようにしたほうがいいでしょう。

    また、参照関係や納品物の出力先についても、担当者に聞くとかソースコードを解析しないと分からない状態にするのではなく、決められた場所にドキュメントを上げて関係者が確認できる状態にしておきましょう。

    私の現場ではカラムの意味を担当者が各々メモに書き残していましたが、個人の勘や努力に頼るというのは管理体制が甘いと言わざるを得ません。
    
    </details>


### ワークフローエンジン

ワークフローエンジンとは、データ収集やデータウェアハウス生成など、データ分析基盤で実行される一連の処理を管理する製品です。

私がいた現場では体系立てられたワークフローエンジンは導入されておらず、PythonスクリプトをEC2にアップしてcrontabでデータウェアハウス層の生成処理や納品物の作成処理を個別に実行していたため、次のような問題が発生していました。

- 他のジョブの実行状況が見えない

    各ジョブを個別に作るのは簡単だし楽ですが、他のジョブの実行状況が見えないということは問題がありました。
    
    例えば、現場ではデータウェアハウス層の生成に3~4時間くらいかけていたのですが、ある日DBに負荷がかかって完了が遅れたときに、先に納品系のジョブが動き出してしまうことがありました。

    ジョブを順番通りに実行するには十分な実行間隔を開ける選択肢もありますが、間隔を開けすぎると今度はDBを使ってない時間が増えて機会損失が発生するのでこの塩梅は難しいところがあります。
    <br>

- 後続ジョブを止めないといけない
    
    他のジョブの実行状況が見えない場合、前提のジョブが失敗してもcrontab設定を解除しない限り次のジョブは自動実行されます。そして実行されるべきでないジョブが意図せず実行されると状態を復元するのが極めて難しくなることがあります。これはある意味「ジョブの暴走」と言ってもいいでしょう。
    <br>

- ジョブの品質にばらつきが出やすい

    各ジョブをばらばらに作る場合、エンジニアの技量にもばらつきがあるので品質も安定しません。

    一応私の現場にはコーディング規約はあったのですが、複数社が参画していた事情もあり、結局ジョブの品質は担当者の所属や技量によるところがありました。
    <br>

- 独自モジュールがプロジェクトを汚染する

    いくつかのジョブを開発すると「S3にファイルをアップロードする」などよく実行される処理が見つかります。そして、そのような処理は楽に実行できるよう共通モジュールが作られることが多いと思います。
    
    共通モジュールは重複コードを排除したり、コードをある程度均一化できるなどのメリットがあります。
    
    しかし、共通モジュールの扱いは意外と難しく、関係者間の合意形成ができていないと使ってもらえず、期待する効果が得られないことがあります。そして、最悪なのは各々が独自のモジュールを作り始め、結局は元のライブラリをそのまま使った方が見やすいという状況になることです。
    <br>

- ジョブの連結が困難になる

    ジョブが個別に作られている問題は最終的にはお客さんにも認知され、ジョブの実行を制御したいと相談を受けるようにもなりました。しかし、ジョブ実行をプログラムで制御するには呼び出し方をある程度一本化する必要があり、各担当者がバラバラにジョブを作っていると共通点を見つけるのが難しく、結局ジョブ自体を作り直さないといけなくなったのでこれはとても困難な課題になったと思います。
    
    <details><summary>📝対応の考察</summary>

    他のジョブの実行状況が見えな問題はジョブの実行状況を外部のDBやファイルに書き込むとかジョブ管理用のプログラムを作成するなどで解消できるでしょう。また、Google Cloudなどのクラウド製品を使っていればPub/Subなどのメッセージングサービスを使ってジョブをリレーすることも可能です。

    ジョブの品質にばらつきが出る問題は人間が引き起こしてる問題なので対処は難しいですが、ひとつの解決方法はみんなが納得して使えるフレームワークを作ることです。そして、コードのばらつきを抑えるならヘルパーだけでなく処理の書き方をある程度強制できるフレームワークも作った方がいいと思います。

    また、独自モジュールは記述量を減らすことを重視して独自のヘルパーを作りこむのではなく、可読性を重視して元のライブラリを継承するようにした方がベターいいでしょう。

    具体的には、google.cloud.bigqueryのClientライブラリを次のように継承し、元のクライアントライブラリと同じ使い方ができるようにするわけです。

    ```python
    from google.cloud.bigquery import Client
    from google.cloud.bigquery import enums

    class BigQueryClient(Client):
        
        def query_by_file(self, 
            filepath, job_config = None, job_id = None,
            job_id_prefix = None, location = None, project = None,
            retry = ..., timeout = ..., job_retry = ...,
            api_method = enums.QueryApiMethod.INSERT
        ):
            with open(filepath, 'r', encoding='utf-8') as f:
                query = f.read()
            return super().query(
                query, job_config, job_id,
                job_id_prefix, location, project,
                retry, timeout, job_retry, api_method
            )
    ```

    なお、この方法は下図のようにテストコードを実装する範囲を明確にできるという点でも便利です。

    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3939665/0e7450ed-7d9d-8aea-0987-8b1dd69f8147.png)
    
    </details>


<br>
        
# おわりに

成功体験は一見とても参考になりますが、そこには色々なバイアスがかかっている可能性があります。だから私は成功からだけでなく失敗からも多くを学ぶべきだと考えています。

発明家のエジソンは自身の失敗について問われたときに「私は失敗したのではなく上手くいかない方法を発見したのだ」と答えたそうですが、エジソンの発言はとても示唆に富んでおり、私たちエンジニアも失敗から知見を得て同じ轍を踏まないようにしていきたいですね😅

今回は文章が長めになってしまい恐縮でしたが、最後までお付き合いいただきありがとうございました！