aa-top-movers-watchlist について
Adobe Analytics で二つの期間の指標を比較し、ディメンション別に増加・減少が大きい項目を順位付けするスキルです。日付範囲を統一し、低ボリューム項目を除外するルールを備え、HTML レポートとテキスト要約を作成します。
主な機能
- 01二期間のディメンション項目を比較
- 02絶対変化量で増加・減少を順位付け
- 03新規・消失項目を表示
- 04重要度のしきい値を適用
- 05HTML レポートとテキスト要約を生成
活用シーン
- 01週ごとの変化が大きいページを特定
- 02期間別にキャンペーンやチャネルの実績を比較
- 03低ボリューム項目を除いて製品や地域の変化を確認
MCP 対応
Adobe Analytics の MCP ツールを使い、レポートスイートを選択し、指標とディメンションを特定して各期間のレポートを取得します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ adobe/ skills --skill aa-top-movers-watchlistインストール後、Claude Code で/aa-top-movers-watchlistを入力して読み込まれたか確認します。
同梱ファイルevals/evals.json
詳細
上昇・下降項目のウォッチリスト(Adobe Analytics)
2つの期間を比較して、主要な指標が最も増加または減少したディメンション項目を特定します。ページ、チャネル、キャンペーン、製品、その他のブレイクダウンディメンションで、上昇項目と下降項目の上位を明らかにします。
使用する AA MCP ツール
findReportSuites— レポートスイートを選択しますsetSessionDefaults— セッションコンテキストを設定します(reportSuiteId + globalCompanyId)describeAa(REPORT_SUITE_CONTEXT_GUIDE)— カレンダーとタイムゾーンのコンテキストを読み込みますfindMetrics— 主要な指標 ID を解決しますfindDimensions— 利用可能なブレイクダウンディメンションを確認しますrunReport— 各ディメンションについて期間 A と期間 B のレポートを実行しますsearchDimensionItems— 必要に応じて特定の項目を検証します
フェーズ 0 — セットアップ
-
findReportSuites/setSessionDefaultsでレポートスイートを確認します。 -
describeAa(REPORT_SUITE_CONTEXT_GUIDE)を呼び出して、レポートスイートの コンテキストを読み込みます。以下を記録します。WEEK_START_DOW— コンテキストガイドに記載された週の開始曜日。コンテキストガイドに 値がない場合は、明示的かつ決定論的なデフォルトとして月曜日(ISO 8601)を使用します。 これは実行のたびに変わるフォールバックではありません。同じレポートスイートでの実行では、 コンテキストガイドに記載された値、または固定の月曜日のデフォルトのいずれかにより、WEEK_START_DOWは常に同じ値になります。TIMEZONE— レポートスイートのタイムゾーン。WEEK_START_DOW_SOURCE— 値をdescribeAaから取得した場合は"context guide"、 コンテキストガイドに値がなく月曜日を使用した場合は"default"を指定します。 出所を監査できるよう、これを成果物のフッターに表示します。
これらの値は、フェーズ 1 で期間 A と期間 B を定義するときに使用します。
findReportSuites(globalCompanyId: "<gcid>")
setSessionDefaults(globalCompanyId: "<gcid>", reportSuiteId: "<rsid>")
describeAa(guideType: "REPORT_SUITE_CONTEXT_GUIDE")
フェーズ 1 — パラメーターの確認
ユーザーに以下を尋ねます。
-
指標 — 「どの指標で上昇・下降項目を順位付けしますか?(訪問数、売上、コンバージョン数、 ページビューなど)」 指定がない場合は、
metrics/visitsをデフォルトにします。 -
ディメンション — 「どのディメンションでブレイクダウンしますか?
- ページ
- マーケティングチャネル
- キャンペーン
- 製品
- トラフィックソース
- 地域
- エントリーページ」 指定がない場合は、上記のリストを提示します。
-
期間 — 「比較する2つの期間を指定してください。」 よく使われるデフォルト:
- 今週と先週
- 今月と先月
- 過去7日間とその前の7日間
- 過去30日間とその前の30日間
カレンダー規則(必須): 期間 A と期間 B には、フェーズ 0 の 同じ
WEEK_START_DOWを必ず使用します。両期間のstartDateは 同じ曜日になり、長さは完全に等しく、期間 B は期間 A の開始直前に 終了する必要があります。同じ実行内で規則を混在させてはいけません(例: 期間 A が月曜日~日曜日で、期間 B が日曜日~土曜日)。カスタムの日付範囲では、 期間 A と長さが等しく、期間 A の開始直前に終了するよう期間 B を計算します。runReport呼び出し前の整合性チェック:periodA.startDateとperiodB.startDateが同じ曜日であること、またperiodA.startDate - periodB.endDate == 1 dayであることを確認します。 そうでない場合は再計算します。 -
項目数の上限 — 表示する上昇項目と下降項目の数(デフォルト:各10件)。
-
重要度しきい値 — 含めるために必要な期間 A または B の絶対値の最小値 (ボリュームが少ない項目によるノイズを除外します)。デフォルトでは、両期間とも 訪問数(または同等の指標)が100未満の項目を除外します。
「[指標] を [ディメンション] でブレイクダウンし、[期間 A] と [期間 B] を比較して、 上昇項目と下降項目の上位10件を表示します。」と確認します。
フェーズ 2 — コンポーネントの解決
findMetrics(expansions: "componentType")
findDimensions(page: 1, limit: 100)
注:
findMetricsではexpansionsパラメーターが必要です("componentType"または"categories"を使用します)。findDimensionsではpageとlimitが必要です。
指標とディメンションの id を記録します。
フェーズ 3 — 期間別レポートの実行
選択したディメンションについて、両期間の指標を実行します。
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period A start>T00:00",
endDate: "<period A end>T23:59",
limit: 200
)
runReport(
metricIds: "<metricId>",
dimensionId: "<dimensionId>",
startDate: "<period B start>T00:00",
endDate: "<period B end>T23:59",
limit: 200
)
注:
runReportではmetricIdではなくmetricIdsを使用します。また、dateRangeパラメーターではなく、ISO 8601 形式(YYYY-MM-DDTHH:mm)のstartDate/endDateを使用します。
有意義な上昇・下降分析に十分な項目を取得するため、limit: 200 を使用します。
フェーズ 4 — 差分を計算して順位付けする
いずれかの期間に存在する各ディメンション項目について、以下を計算します。
- 期間 A の値(結果にない場合は 0)
- 期間 B の値(結果にない場合は 0)
- 絶対差分:期間 A - 期間 B
- 変化率:差分 / 期間 B × 100(期間 B = 0 の場合は「+100% 新規」)
- 重要度フィルターを適用:max(期間 A, 期間 B) が重要度のしきい値未満の項目を除外
増加項目は絶対差分の降順で並べ替えます(正の差分)。 減少項目は絶対差分の昇順で並べ替えます(負の差分)。
各リストから上位 N 件を取得します。
特殊なケース
- 新規項目(期間 A のみに存在):「新規 — 以前の期間のデータなし」としてフラグを付けます。重要度のしきい値を満たす場合は含めます。
- 消失した項目(期間 B のみに存在し、期間 A では 0): 「減少 — 現在の期間のデータなし」としてフラグを付けます。
- ほぼゼロの項目:指標の合計値の 1% 未満の項目は、非常に大きな変化率を示す場合を除き、ウォッチリストからの除外を検討します。
フェーズ 5 — 複数ディメンションのウォッチリスト(任意)
複数のディメンション(例:ページとチャネルの両方)にわたる変動項目をユーザーが求めている場合は、追加の各ディメンションについてフェーズ 3〜4 を繰り返します。ディメンションを 1 つ追加するごとに、runReport の呼び出しが 2 回増えます。
runReport(metricIds: "<metricId>", dimensionId: "variables/marketingchannel",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
runReport(metricIds: "<metricId>", dimensionId: "variables/page",
startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
HTML レポートでは、結果をディメンションごとに別々の表にまとめます。
フェーズ 6 — HTML レポートを生成する
変動項目レポートをインラインで作成し、
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html に書き込みます。
表示ルール — 実行ごとに一貫して適用
同じレポートスイート、指標/ディメンション、期間でこのスキルを 2 回実行した場合、(生成時刻を除いて)同一の表示にする必要があります。以下のルールにより、AI が選択を変えてしまう可能性のある書式を固定します。
数値の書式
- KPI 値(各サマリータイルの大きな数値と、変動項目の行ごとの値)— 桁区切りを付けた完全な数値を使用します(
8,160、77,584、1,250,000)。大きな値であっても、KやMなどの SI 接頭辞は使用しないでください。関係者が必要としているのは略記ではなく、正確な数値です。 - 変化率(ピルと説明文の箇条書き)— 常に小数点以下 1 桁とし、ゼロから遠ざかる方向に丸めます。たとえば、
−23.55%は−23.6%と表示し、−23.5%とは表示しません。完全な精度の値で計算し、表示時にのみ丸めます。 - パーセンテージポイントの変化(コンバージョン率や直帰率など、すでにパーセンテージで表される指標の場合)— 同じ丸め方を適用し、接尾辞
ppを付けます。例:+0.40 pp。 - 通貨 —
$を先頭に付け、3 桁区切りを使用し、$100 以上の値は小数なしで表示します($1,240,000)。$100 未満の値に限り、セントまで表示します($45.20)。
NULL/欠損データの処理
KPI タイルまたは変動項目の行には、レポートスイートから実際に返された内容を反映する必要があります。AI は、レポートを見栄えよくするために、別の指標へ黙って置き換えたり、タイルを非表示にしたりしてはいけません。
- サマリータイルで、追跡対象の指標について両期間とも
0またはNULLが返された場合:kpi-valueをData unavailable、ピルのクラスをflat、ピルのテキストを⚠ N/A、priorのテキストをBoth periods returned no data — validate instrumentationとしてタイルを表示します。タイルはグリッド内に残し、省略しないでください。 - 一方の期間で有効なデータが返され、もう一方が
0またはNULLの場合:有効な値をkpi-valueとして表示し、ピルのクラスをflat、ピルのテキストを⚠ N/A、priorのテキストをPrior {period_noun}: no dataとします。 - 別の指標で決して代用しないでください(例:Revenue が
$0で返されたからといって、Revenue から Orders に切り替える)。分析対象の指標を必ず表示してください。
HTML テンプレート
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers — {ORG_NAME} — {METRIC_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f4f1;
--surface: #ffffff;
--ink: #1a1a1a;
--ink-muted: #6b6b6b;
--border: #e5e2dc;
--header-bg: #0e0e10;
--header-warm: #3a1010;
--accent-red: #c8312f;
--accent-red-bright: #ff6b68;
--accent-red-soft: #fdecea;
--accent-green: #1f7a4d;
--accent-yellow: #d4a017;
}
body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg); color: var(--ink); line-height: 1.5;
-webkit-font-smoothing: antialiased; }
/* === Header === */
header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
header::after { content: ""; position: absolute; right: -140px; top: -140px;
width: 460px; height: 460px;
background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
pointer-events: none; }
.header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
.eyebrow { display: inline-flex; align-items: center; gap: 8px;
padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
border-radius: 999px; color: var(--accent-red-bright);
font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
text-transform: uppercase; margin-bottom: 24px;
background: rgba(200,49,47,.10); }
.eyebrow::before { content: ""; width: 6px; height: 6px;
background: var(--accent-red-bright); border-radius: 50%; }
header h1 { font-family: "Playfair Display", Georgia, serif;
font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
line-height: 1.05; margin-bottom: 14px; color: #fff; }
header .lede { font-size: 16px; max-width: 600px;
color: rgba(255,255,255,.80); margin-bottom: 24px;
line-height: 1.55; }
header .meta { display: flex; flex-wrap: wrap; gap: 22px;
font-size: 13px; color: rgba(255,255,255,.60); }
header .meta span { display: inline-flex; align-items: center; gap: 6px; }
header .meta .icon { opacity: .8; }
/* === Tabs === */
nav { background: var(--surface); border-bottom: 1px solid var(--border);
padding: 0 56px; display: flex; gap: 28px;
position: sticky; top: 0; z-index: 50; }
nav a { display: block; padding: 16px 0; font-size: 14px;
color: var(--ink); text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color .15s ease; }
nav a:hover { border-bottom-color: var(--accent-red); }
/* === Container === */
.container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }
/* === Period strip === */
.period-bar { background: var(--surface); border-radius: 8px;
padding: 14px 22px; margin-bottom: 28px;
font-size: 13px; color: var(--ink-muted);
box-shadow: 0 1px 3px rgba(0,0,0,.04);
border-left: 3px solid var(--accent-red); }
.period-bar strong { color: var(--ink); font-weight: 600; }
/* === Dimension section === */
.dim-section { margin-top: 32px; }
.dim-section > h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 22px; font-weight: 700; color: var(--ink);
margin-bottom: 16px; padding-bottom: 10px;
border-bottom: 1px solid var(--border); }
/* === Gainers / Losers dual panels === */
.movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
margin-bottom: 32px; }
.movers-panel { background: var(--surface); border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; }
.movers-panel-header { padding: 18px 24px; border-bottom: 1px solid var(--border); }
.movers-panel-header.gainers { border-top: 3px solid var(--accent-green); }
.movers-panel-header.losers { border-top: 3px solid var(--accent-red); }
.movers-panel-header h2 { font-family: "Playfair Display", Georgia, serif;
font-size: 17px; font-weight: 700; }
.movers-panel-header.gainers h2 { color: var(--accent-green); }
.movers-panel-header.losers h2 { color: var(--accent-red); }
/* === Tables === */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #faf8f4; padding: 10px 18px;
text-align: left; font-weight: 600;
text-transform: uppercase; letter-spacing: .6px;
font-size: 11px; color: var(--ink-muted);
border-bottom: 1px solid var(--border); }
tbody td { padding: 10px 18px; border-bottom: 1px solid #f4f1eb; }
tbody tr:last-child td { border-bottom: none; }
.delta-pos { color: var(--accent-green); font-weight: 700; }
.delta-neg { color: var(--accent-red); font-weight: 700; }
/* === Rank badges (circular) === */
.rank-badge { display: inline-flex; align-items: center; justify-content: center;
width: 24px; height: 24px;
border-radius: 50%;
background: var(--accent-red); color: #fff;
font-size: 11px; font-weight: 700;
margin-right: 8px; vertical-align: middle; }
.rank-badge.gain { background: var(--accent-green); }
.back-top { position: fixed; bottom: 24px; right: 24px; background: var(--accent-red);
color: #fff; width: 44px; height: 44px; border-radius: 50%; border: none;
font-size: 20px; cursor: pointer;
box-shadow: 0 4px 12px rgba(200,49,47,.35); }
footer { text-align: center; padding: 32px 24px;
font-size: 12px; color: var(--ink-muted); }
@media (max-width: 700px) { .movers-grid { grid-template-columns: 1fr; } }
/* === Print === */
@media print {
nav { display: none; position: static; }
header { padding: 36px 32px 28px; }
header h1 { font-size: 42px; }
.back-top { display: none; }
.movers-grid { page-break-inside: avoid; }
.movers-panel, .period-bar {
box-shadow: none; border: 1px solid var(--border);
}
}
</style>
</head>
<body>
<header>
<div class="header-inner">
<div class="eyebrow">Top Movers Watchlist</div>
<h1>{ORG_NAME} Top Movers</h1>
<p class="lede">Biggest gainers and decliners for {METRIC_NAME} between {PERIOD_A} and {PERIOD_B}.</p>
<div class="meta">
<span><span class="icon">📅</span> {PERIOD_A} vs {PERIOD_B}</span>
<span><span class="icon">📊</span> {REPORT_SUITE}</span>
<span><span class="icon">🕔</span> Prepared {GENERATED_DATE}</span>
</div>
</div>
</header>
<nav>
<a href="#summary">Summary</a>
</nav>
<div class="container">
<div id="summary" class="period-bar">
<strong>Metric:</strong> {METRIC_NAME} |
<strong>Period A:</strong> {PERIOD_A} |
<strong>Period B:</strong> {PERIOD_B} |
<strong>Materiality:</strong> ≥{THRESHOLD} {METRIC_UNIT}
</div>
<div id="dim-{DIM_ID}" class="dim-section">
<h2>{DIMENSION_NAME}</h2>
<div class="movers-grid">
<div class="movers-panel">
<div class="movers-panel-header gainers"><h2>▲ Top Gainers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
<div class="movers-panel">
<div class="movers-panel-header losers"><h2>▼ Top Losers</h2></div>
<table>
<thead><tr>
<th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
<th>Delta</th><th>% Change</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
⇧
</button>
<footer>Top Movers Watchlist — {ORG_NAME} — Generated {GENERATED_DATE}</footer>
</body>
</html>
セクションタイトル — フェーズ番号なし:HTML レポートのセクション見出しにフェーズ番号を含めないでください。セクション名のみを使用します(例:「フェーズ 3 — 変動上位」ではなく「変動上位」、「フェーズ 4 — ディメンションの内訳」ではなく「ディメンションの内訳」)。
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html に書き込み、開きます。
open /tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html
インラインサマリー(必ず提供)
HTML レポートの後には、必ず簡潔なテキストの要約を付けます。
Top Movers: [Metric] — [Period A] vs [Period B]
GAINERS (by [Dimension])
1. [Item A] +12,400 visits (+34%)
2. [Item B] +8,200 visits (+21%)
3. [Item C] +5,100 visits (+18%)
LOSERS
1. [Item X] -9,800 visits (-41%)
2. [Item Y] -4,200 visits (-18%)
3. [Item Z] -2,900 visits (-12%)
Key takeaway: [1 sentence explanation of the story]
ガードレール
- 量がごくわずかな項目によるノイズを抑えるため、常に重要度のしきい値を適用します。
- 新規項目(前の期間には存在しない)と消失した項目(現在の期間には存在しない)を明示します。これらは重要な状況を示していることがよくあります。
- 変化率による順位付けでは、量の少ない項目が誤解を招く大きな割合で上位に表示されるのを避けるため、主なソート基準として変化量の絶対値(割合ではなく)を使用します。
- 呼び出し効率のため、ディメンションのリストは6個までに制限します。必要に応じて、追加のディメンションも実行できることを案内します。
対話例
「先週、最も増減したページはどれですか?」
- レポートスイートを確認します。
- 指標:ページビュー。ディメンション:ページ名。期間:過去7日間とその前の7日間。件数:上位各10件を確認します。
- ページ × 訪問数のレポートを2件実行します。
- 変化量を計算し、重要度の基準を適用します(いずれかの期間でページビューが最低500件)。
- HTML レポートを作成し、インラインの要約を提供します。
- 「最も増加した項目:/products/new-feature(ページビューが18,200件増、312%増)。 最も減少した項目:/promo/expired-campaign(ページビューが9,400件減、88%減)。」