概要
弊社の計測部には計測サービス企画部という部署が存在します。
計測サービス企画部ではサービスの利用者数等の KPI ダッシュボードを Looker Studio で管理していましたが、効率的な運用ができていない状況でした。
そこで、以下の方法で作業を自動化し、運用を効率化しました。
- 日次: KPI ダッシュボードを PDF 化し Slack に投下する
- 週次: Slack に投下された PDF を元に Dify を使って分析作業を自動化する
ここでは、特に「Slack に投下された PDF を元に Dify を使って分析作業を自動化する」という取り組みについて詳しくまとめます。
なお、日次バッチに関しては https://zenn.dev/thenagain/articles/55bd44cde85287 の構成を取っているため、必要な方はぜひ参考にしてみてください。
分析の内容
週次の KPI 分析では、以下の4サービスを対象としています。
| サービス名 | 分析項目 |
|---|---|
| ZOZOMAT | LP PV数(UU)、申込者数、計測者数、Shoes購入者数 など |
| ZOZOGLASS | LP PV数(UU)、申込者数、計測者数、購入者数 など |
| ZOZOMAT for Kids | LP PV数(UU)、申込者数、計測者数、Shoes購入者数 など |
| お試しメイク | メイク投稿数 など |
各項目について、以下の分析を自動で行います。
- 今週・先週の合計値 の算出
- WoW(Week over Week:先週比) の計算
- 目標値に対する達成率 の計算
- 傾向分析(増加傾向、減少傾向、横ばいなどをテキストで記述)
出力例
集計期間:3/20~3/26
ZOZOMAT:
LP PV数 (UU):25020件(WoW -2.14%)
目標:23331件(達成率107.2%)
申込者数:2353件(WoW -38.55%)
目標:3031件(達成率77.63%)
計測者数:2663件(WoW 53.97%)
目標:1981件(達成率134.43%)
傾向:
LP PV数(UU)は3月23日の5212をピークに、3月24日〜28日にかけて3000前後で推移しています。
先週比では微減(-2.14%)となっています。
申込者数は3月11日〜16日に高い数値(803〜1642)を記録していましたが、その後大幅に減少し、
直近2週間は300〜400台で安定推移しています。先週比では大幅減少(-38.55%)となっています。
...
(数値はダミーです)
構成
全体のアーキテクチャ
┌───────────────┐ ┌─────────────────────────────────────────┐ ┌─────────────┐
│ EventBridge │────▶│ Lambda │────▶ │ Dify │
│ (週次トリガー) │ │ 1. SlackからPDF取得 │ 2.PDF│ (ワークフロー │
│ │ │ 2. Dify API呼び出し │◀──── │ 実行) │
└───────────────┘ │ 3. 分析結果をSlackに通知 │分析 └─────────────┘
└──────┬──────────────────────────┬──────┘結果
│ │
│ 1.PDF取得 3.結果通知│
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Slack │ │ Slack │
│ (PDF保管) │ │ (通知) │
└─────────────┘ └─────────────┘
処理フロー
- EventBridge が週1回 Lambda を起動
- Lambda が Slack から最新の KPI PDF を取得
- PDF をリクエストに含めて Dify API を呼び出し、分析を実行
- Lambda が分析結果を Slack に通知
Dify ワークフローの構成
Dify では、PDFを入力として受け取り、LLMを活用して分析結果を出力するワークフローを構築しています。
┌───────┐ ┌────────────┐ ┌───────────────────────────────────────────────────────┐
│ Start │──▶│PDF Splitter│──▶│ イテレーション │
│(PDF入力)│ │(ページ分割) │ │ │
└───────┘ └────────────┘ │ ┌────────────┐ ┌────────────┐ ┌────────────────┐ │
│ │テキスト抽出 │──▶│必要情報抽出 │──▶│対象日データ抽出 │ │
│ │ │ │LLM │ │LLM │ │
│ └────────────┘ │(JSON構造化) │ │(今週/先週分類) │ │
│ └────────────┘ └───────┬────────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ ┌────────────────┐ │
│ │ 分析LLM │◀──│メトリクス計算 │ │
│ │ (傾向分析) │ │(コード) │ │
│ └────────────┘ └────────────────┘ │
└───────────────────────────┬───────────────────────────┘
│
▼
┌─────────────┐
│ End │
│ (結果出力) │
└─────────────┘
処理の流れ
- PDF Splitter: 入力PDFをページごとに分割(各ページが1サービスのKPIに対応)
-
イテレーション: 各ページに対して以下の処理を実行
- テキスト抽出: PDFページから全テキストを抽出
- 必要情報抽出LLM: KPI項目をサービスごとにJSON形式で構造化
- 対象日データ抽出LLM: 現在日時を基に「今週」「先週」「先週より前」にデータを分類
- メトリクス計算(コード): 合計値、WoW、達成率を計算
- 分析LLM: 傾向分析を出力
- 各イテレーションの結果を結合して最終出力
実装内容
前提として、Looker Studio から生成される PDF ではサービスごとにページを分けて KPI を表示し、それらをまとめて1つの PDF ファイルとして出力しています。

(数値や文言をモザイク加工しており分かりづらいですが、横軸が時間で縦軸が数値になっており、紫で目標値が記載されています。)
PDF から情報を抽出するために、まずは PDF Process を使って PDF を1ページずつ分割し、1サービスごとでイテレーションを回すようにしています。
以下、イテレーション内の処理のうち主な要素についてまとめます。
1. PDF からの情報抽出
KPI 情報を処理しやすくするため、Doc Extractor で PDFから抽出した KPI 情報を構造化しています。
以下が構造化のための LLM ノードに設定しているプロンプトで、コンテキストとして Doc Extractor による抽出内容を渡すようにしています。(数値はダミーです)
プロンプト全文
あなたは「与えられた情報を構造化し、json で出力するエキスパート」です。
以下の <context> xml タグ内が与えられた情報です。
なお、与えられる情報は「ZOZOMAT」, 「ZOZOGLASS」, 「ZOZOMAT for Kids」, 「お試しメイク」の4サービスのうちいずれかに関する情報になっています。
与えられた情報がどのサービスに該当するか判断した上で、該当サービスにおける以下の情報を抽出し、json に含めてください。
- 「ZOZOMAT」の場合
- 「LP PV数 (UU)」
- 「申込者数」
- 「計測者数」
- 「Shoes 購入者数」
- 「申込者数/LP PV (UU) 数」
- 「申込後計測者数/申込者数」(与えられる情報には「申込後計測者数/申込後」と記載されているので注意)
- 「ZOZOGLASS」の場合
- 「LP PV数 (UU)」
- 「申込者数」
- 「計測者数」
- 「コスメ購入者数」
- 「申込者数/LP PV (UU) 数」
- 「申込後計測者数/申込者数」(与えられる情報には「申込後計測者数/申込」と記載されているので注意)
- 「ZOZOMAT for Kids」 の場合
- 「LP PV数 (UU)」
- 「申込者数」
- 「計測者数」
- 「Kids Shoes 購入者数」
- 「申込者数/LP PV (UU) 数」
- 「申込後計測者数/申込者数」
- 「お試しメイク」の場合
- 「メイク投稿数」
- 「お試しメイク投稿数」
- 「お試しメイク投稿数/メイク投稿数」
<output_example> xml タグ内の内容が、与えられた情報が「ZOZOMAT」だった場合に出力すべき json の構造を示しているので、これを参考に json を出力してください。
<context>
{{#context#}}
</context>
<output_example>
{
"サービス名": "ZOZOMAT",
"LP PV数(UU)": {
"Mar 11, 2025": "10047",
"Mar 12, 2025": "21055",
...,
"目標値": "30333"
},
"申込者数": {
"Mar 11, 2025": "8642",
"Mar 12, 2025": "8178",
...,
"目標値": "5433"
},
"計測者数": {
"Mar 11, 2025": "1279",
"Mar 12, 2025": "1320",
...,
"目標値": "1283"
},
"Shoes 購入者数": {
"Mar 11, 2025": "857",
"Mar 12, 2025": "872",
...,
"目標値": "883"
},
"申込者数/LP PV (UU) 数": {
"Mar 11, 2025": "35.33",
"Mar 12, 2025": "28.35",
...,
"目標値": "13.00"
},
"申込後計測者数/申込者数": {
"Jan 2024": "54.96",
"Feb 2024": "56.55",
...,
"目標値": "60.00"
}
}
</output_example>
LP UU数\r\nNov 4, 2025\r\nNov 5, 2025\r\nNov 6, 2025\r\nNov 7, 2025\r\nNov 8, 2025\r\nNov 9, 2025\r\nNov 10, 20…\r\nNov 11, 20…\r\nNov 12, 20…\r\nNov 13, 20…\r..., 2025\r\n0\r\n1K\r\n2K\r\n3K\r...\n2,772\r\n2,772\r\n2,772\r\n2,772\r\n2,772\r\n...n(3,000)\r....
Doc Extractor から抽出されるテキストは上記のように「日付」「縦軸の値」「数値」「目標値」がズラッと取得される形になっており全く構造化されていませんが、出力例を事前にプロンプトへ記載しておく(いわゆる few-shot prompting)ことで LLM がいい感じに日付ごとのデータとして構造化してくれます。
適宜グラフによって不要な情報が紛れ込んで出力が安定しない場合は、プロンプトを調整して対応するようにしています。
2. 日付に基づくデータ分類
次は、現在日時を基準にデータを「今週」「先週」「先週より前」に自動分類します。
以下が期間ごとでの分類のための LLM ノードに設定しているプロンプトで、context には 1 で得られた出力を渡しています。(数値はダミーです)
プロンプト全文
あなたは「与えられた情報から必要な情報だけを抽出し、構造化した上で json で出力するエキスパート」です。
以下の <context> xml タグ内が与えられた情報です。
与えられる情報は「あるサービスにおける日時ごとの KPI データ」です。
このデータのうち、「LP PV数 (UU)」, 「申込者数」, 「計測者数」, 「Shoes 購入者数」, 「申込者数/LP PV (UU) 数」については「目標値」・「先週より前のデータ」・「先週(17日前〜11日前)のデータ」・「今週(10日前〜4日前)のデータ」を抜き出し、「申込後計測者数/申込者数」については「目標値」・「各月毎のデータ」を抜き出して、json 化して出力してください。
例えば現在の日時が「3月30日」であれば、本日から4日前(3月26日)から10日前(3月20日)の間である「3月20日~26日」を「今週」のデータとして抜き出し、そのもう1週前の「3月13日~19日」を「先週」のデータとして抜き出してください。また、これらの集計期間もjsonに出力してください。
なお、現在の日時は「〇〇」です。 // ここは事前に CURRENT TIME プラグインを利用して埋めるようにする
<input_example> xml タグ内の内容が与えられる情報の例で、<output_example> xml タグ内の内容が「処理時点の日時が3月30日である」前提での出力すべき json の構造を示しているので、これを参考に json を出力してください。
<context>
{{#context#}}
</context>
<input_example>
"\n{\n \"サービス名\": \"ZOZOMAT\",\n \"LP PV数(UU)\": {\n \"Mar 11, 2025\": \"2000\",\n \"Mar 12, 2025\": \"2000\",\n \"Mar 13, 2025\": \"2000\",\n \"Mar 14, 2025\": \"2000\",\n \"Mar 15, 2025\": \"2000\",\n \"Mar 16, 2025\": \"2000\",\n \"Mar 17, 2025\": \"2000\",\n \"Mar 18, 2025\": \"2000\",\n \"Mar 19, 2025\": \"2000\",\n \"Mar 20, 2025\": \"2000\",\n \"Mar 21, 2025\": \"2000\",\n \"Mar 22, 2025\": \"2000\",\n \"Mar 23, 2025\": \"2000\",\n \"Mar 24, 2025\": \"2000\",\n \"Mar 25, 2025\": \"2000\",\n \"Mar 26, 2025\": \"2000\",\n \"Mar 27, 2025\": \"2000\",\n \"Mar 28, 2025\": \"2000\",\n \"Mar 29, 2025\": \"2000\",\n \"Mar 30, 2025\": \"2000\",\n \"Mar 31, 2025\": \"2000\",\n \"目標値\": \"3000\"\n },...."
</input_example>
<output_example>
{
"サービス名": "ZOZOMAT",
"集計期間": {
"今週": {
"開始": "2025-03-20",
"終了": "2025-03-26"
},
"先週": {
"開始": "2025-03-13",
"終了": "2025-03-19"
}
},
"LP PV数(UU)": {
"目標値": "3000",
"今週": {
"2025-3-20": "2000",
"2025-3-21": "2000",
...,
"2025-3-26": "2000"
},
"先週": {
"2025-3-13": "2000",
"2025-3-14": "2000",
...,
"2025-3-19": "2000"
},
"先週より前": {
"2025-03-11": "2000",
"2025-03-12": "2000"
}
},
...
}
</output_example>
この分類により、後続の処理で WoW(先週比)の計算や傾向分析をしやすくなります。
3. メトリクス計算
LLM は計算が弱いので、計算部分だけはコードノードに切り出して計算するようにしています。
import json
def calculate_metrics(section_data: dict, label: str):
metrics = {}
def safe_sum(values):
return sum(int(v) for v in values if v is not None and str(v).isdigit())
try:
weekly_data = section_data.get("今週", {})
last_week_data = section_data.get("先週", {})
goal_raw = section_data.get("目標値", None)
# 合計計算
weekly_sum = safe_sum(weekly_data.values()) if weekly_data else None
last_week_sum = safe_sum(last_week_data.values()) if last_week_data else None
# 目標値(週合計)計算
if goal_raw is not None:
try:
goal_value = float(goal_raw)
goal_total = goal_value * 7
goal_total_str = f"{goal_total:.2f}"
except:
goal_total_str = "目標値なし"
else:
goal_total_str = "目標値なし"
# WoW 計算
if weekly_sum is not None and last_week_sum:
try:
wow = ((weekly_sum - last_week_sum) / last_week_sum) * 100
wow_str = f"{wow:.2f}%"
except ZeroDivisionError:
wow_str = "データなし"
else:
wow_str = "データなし"
# 達成率 計算
if weekly_sum is not None and goal_total_str not in ["目標値なし"]:
try:
achievement = (weekly_sum / float(goal_total_str)) * 100
achievement_str = f"{achievement:.2f}%"
except:
achievement_str = "データなし"
elif goal_total_str == "目標値なし":
achievement_str = "目標値なし"
else:
achievement_str = "データなし"
# フラットなキー形式で返却(すべて str に変換)
metrics[f"{label}_weekly_sum"] = str(weekly_sum) if weekly_sum is not None else "データなし"
metrics[f"{label}_last_week_sum"] = str(last_week_sum) if last_week_sum is not None else "データなし"
metrics[f"{label}_goal_total"] = goal_total_str
metrics[f"{label}_wow"] = wow_str
metrics[f"{label}_achievement"] = achievement_str
except Exception as e:
metrics[f"{label}_error"] = str(e)
return metrics
def main(arg1: str) -> dict:
# LLM ノードから渡される出力を json として扱えるように加工
cleaned = arg1.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
if cleaned.endswith("```"):
cleaned = cleaned[:-3]
cleaned = cleaned.strip()
try:
data = json.loads(cleaned)
except json.JSONDecodeError as e:
return {"error": f"JSON decode error: {e}"}
result = {}
result.update(calculate_metrics(data["LP PV数(UU)"], "pv"))
# 他 KPI に関しても同様に処理
return result
4. 傾向分析の自動生成
分析LLMでは、計算されたメトリクスと時系列データを基に、自然言語で傾向を記述します。
以下が分析 LLM ノードに設定しているプロンプトで、context には 2 で得られた出力を渡しており、プロンプト内の変数部分は 3 のメトリクス計算による出力を示しています。(数値はダミーです)
プロンプト全文
あなたは「与えられた情報をもとに分析を行い、その結果を出力する」エキスパートです。
以下の <context> xml タグ内の情報が与えられた情報です。
また、分析は以下の <analysis> xml タグ内の手順でステップバイステップで行なってください。
<analysis>
1. 与えられた情報のうち「LP PV数 (UU)」, 「申込者数」, 「計測者数」, 「Shoes 購入者数」のデータを取得します。
2. 数値取得
* 「今週」の「LP PV数 (UU)」, 「申込者数」, 「計測者数」の合計をそれぞれ確認します。これらは事前に計算されており、「LP PV数 (UU)」の今週合計は{{#1745304962072.pv_weekly_sum#}}, 「申込者数」の今週合計は {{#1745304962072.applicant_weekly_sum#}}, 計測者数」今週合計は {{#1745304962072.measurer_weekly_sum#}}で与えられます。
* 「先週」の「LP PV数 (UU)」, 「申込者数」, 「計測者数」の合計をそれぞれ確認します。これらは事前に計算されており、「LP PV数 (UU)」の先週合計は {{#1745304962072.pv_last_week_sum#}}, 「申込者数」の先週合計は {{#1745304962072.applicant_last_week_sum#}}, 計測者数」先週合計は {{#1745304962072.measurer_last_week_sum#}}で与えられます。
* 先週比(WoW)を確認します。WoWは、(今週の合計値 - 先週の合計値) / 先週の合計値 * 100 で計算されます。これらは事前に計算されており、「LP PV数 (UU)」の WoW は {{#1745304962072.pv_wow#}}, 「申込者数」の WoW は {{#1745304962072.applicant_wow#}}, 「計測者数」の WoW は {{#1745304962072.applicant_wow#}}で与えられます。今週の合計値または先週の合計値が「データなし」の場合は、WoWも「データなし」として与えられます。
3. 目標値と達成率の取得
* 「LP PV数 (UU)」, 「申込者数」, 「計測者数」に「目標値」として日次の目標値が含まれているので、そのデータを使って、週の目標値は(日次の目標値 x 7)で計算されます。これらは事前に計算されており、「LP PV数 (UU)」の 週の目標値 は {{#1745304962072.pv_goal_total#}}, 「申込者数」の 週の目標値 は {{#1745304962072.applicant_goal_total#}}, 「計測者数」の 週の目標値 は {{#1745304962072.measurer_goal_total#}}で与えられます。
* 次に、(今週の合計値 / 週の目標値)* 100 で計算される達成率を取得します。これらは事前に計算されており、「LP PV数 (UU)」の 達成率は {{#1745304962072.pv_achievement#}}, 「申込者数」の 達成率 は {{#1745304962072.applicant_achievement#}}, 「計測者数」の 達成率 は {{#1745304962072.measurer_achievement#}}で与えられます。今週の合計値が「データなし」の場合、または目標値が提供されていない場合は、達成率は「データなし」または「目標値なし」で与えられます。
4. 傾向分析
* 「LP PV数 (UU)」, 「申込者数」, 「計測者数」, 「Shoes 購入者数」の推移について、「今週」のデータを「先週より前」・「先週」のデータと比較しつつ、増加傾向、減少傾向、横ばいなどの傾向を記述します。具体的な数値の変化を伴って記述すると、より詳細な分析になります。例えば、「LP PV(UU)数は3月中旬に急増し、3月12日に26,044を記録しています。」や「申込者数は2月中旬から3月初旬まで100-170人程度で推移していましたが、3月11日以降急増し、3月12日に215人を記録しています。」のように記述します。数値が大きく変動した場合(例えば、±20%以上)、その旨を明記してください。データが「データなし」の場合は、傾向も「データなし」と記述してください。
</analysis>
なお、現在の日時は「〇〇」なので、これを分析の参考にしてください。
例えば現在の日時が「3月30日」であれば、4日前(3/26)から10日前(3/20)の間である「3月20日~26日」を今週とし、そのもう1週前の「3月13日~19日」の週を比較して分析を行なってください。
今週の集計期間を初めに出力してください。
また、以下の <output_template> xml タグ内は出力のテンプレートなので、これに沿って出力してください。
<output_example>のフォーマットに従って出力してください。
以下の <input_example> は与えられる情報の例、<output_example1>, <output_example2> は出力の例です。
<output_template>
集計期間:
ZOZOMAT:
LP PV数 (UU):**件(WoW **%)
目標:**件(達成率**%)
申込者数:**件(WoW **%)
目標:**件(達成率**%)
計測者数:**件(WoW **%)
目標:**件(達成率**%)
傾向:**
</output_template>
<input_example>
"\n```json\n{\n \"サービス名\": \"ZOZOMAT\",\n \"LP PV数(UU)\": {\n \"目標値\": \"3000\",\n \"今週\": {\n \"2025-03-24\": \"2000\",\n \"2025-03-25\": \"2000\",\n \"2025-03-26\": \"2000\",\n \"2025-03-27\": \"2000\",\n \"2025-03-28\": \"2000\",\n \"2025-03-29\": \"2000\",\n \"2025-03-30\": \"2000\"\n },\n \"先週\": {\n \"2025-03-17\": \"2000\",\n \"2025-03-18\": \"2000\",\n \"2025-03-19\": \"2000\",\n \"2025-03-20\": \"2000\",\n \"2025-03-21\": \"2000\",\n \"2025-03-22\": \"2000\",\n \"2025-03-23\": \"2000\"\n },\n \"先週より前\": {\n \"2025-03-11\": \"2000\",\n \"2025-03-12\": \"2000\",\n \"2025-03-13\": \"2000\",\n \"2025-03-14\": \"2000\",\n \"2025-03-15\": \"2000\",\n \"2025-03-16\": \"2000\"\n }\n },...}\n```"
</input_example>
<output_example>
集計期間:3/20~3/26
ZOZOMAT:
LP PV数 (UU):25020件(WoW -2.14%)
目標:23331件(達成率107.2%)
申込者数:2353件(WoW -38.55%)
目標:3031件(達成率77.63%)
計測者数:2663件(WoW 53.97%)
目標:1981件(達成率134.43%)
傾向:
LP PV数(UU)は3月23日の5212をピークに、3月24日〜28日にかけて3000前後で推移しています。先週比では微減(-2.14%)となっています。
申込者数は3月11日〜16日に高い数値(803〜1642)を記録していましたが、その後大幅に減少し、直近2週間は300〜400台で安定推移しています。先週比では大幅減少(-38.55%)となっています。
計測者数は大きく変動しており、3月25日に651と直近のピークを記録しました。先週比では大幅増加(+53.97%)となっており、目標達成率も134.43%と好調です。
Shoes購入者数は週末に高い傾向があり、3月23日に921、3月30日に767を記録しています。先週比ではわずかな増加(+2.62%)となっています。
</output_example>
<context>
{{#context#}}
</context>
すると、結果として以下のような出力が得られるようになります。
傾向:
LP PV数(UU)は3月23日の5212をピークに、3月24日〜28日にかけて3000前後で推移しています。
先週比では微減(-2.14%)となっています。
申込者数は3月11日〜16日に高い数値(803〜1642)を記録していましたが、その後大幅に減少し、
直近2週間は300〜400台で安定推移しています。先週比では大幅減少(-38.55%)となっています。
まとめ
本記事では、DifyとLLMを活用してKPI分析作業を自動化した事例を紹介しました。
得られた効果としては以下が挙げられるかと思います。
- 工数削減: 毎週行っていた分析作業が自動化され、担当者の負担が軽減
- レポートの標準化: 分析フォーマットが統一され、品質のばらつきが解消
- 迅速な情報共有: 週次定例前に自動で分析結果がSlackに投稿されるため、メンバー全員が事前に状況を把握可能
本稿が、Dify を始めた方々や何かで困っている方々の参考になれば幸いです。
ご質問やフィードバックがあれば、ぜひコメントでお知らせください。
一緒に働きませんか?
私たち計測システム部SREチームでは、ZOZOの計測システムを支えるSREを募集しています。
カジュアル面談も歓迎です。お気軽にご応募ください!