月末になると、AdMob と App Store Connect と Firebase の管理画面を順番に開いて、CSV を書き出しております。個人開発で壁紙アプリを複数抱えていると、次の更新をどれに投じるかは勘ではなく数字で決めたくなります。
以前は、書き出した CSV をそのまま Claude に貼り付けていました。数秒でそれらしい折れ線が描かれます。手応えのある体験でした。
つまずいたのは、描かれた週次の山が、手元の表計算で確認した山と一日ずれていたときです。
原因を追いかけた結果は、拍子抜けするほど単純でした。貼ったデータが、最後まで読まれていなかったのです。
artifact は React コンポーネントとして描かれます。渡したデータは、どこかの時点でコード内の配列リテラルに書き写されます。4,300 行の CSV を貼るということは、4,300 個のオブジェクトを一つずつ書き写してもらう依頼に等しい。実際には、そこまで書き写されません。
以下は、この前提を受け入れた上で、個人開発の数値を意思決定に使える形にするまでの構成です。実際に手元で回しているコードを添えて整理します。
貼ったCSVは、そのままの姿では届いていない
まず起きていたことを、数字で確かめました。
AdMob から書き出した 90 日分の日次レポートは、4,312 行・178 KB でした。これを貼って生成された artifact のソースを開き、データ配列の要素数を数えたところ、117 件でした。
明示的な断りはありませんでした。「代表的な値を抜粋しました」といった注記もなく、素直に折れ線が描かれていました。週次の山が一日ずれていたのは、間引かれた結果として、たまたま隣の日がピークに見えていたからです。
これは不具合ではなく、当然の帰結だと考えています。出力トークンには上限があり、4,312 行を書き写せば、それだけで応答の大半を使い果たします。書ける範囲に収める判断は、むしろ妥当です。
問題は、こちらがその判断を依頼していないことでした。
検証のしかた
同じことを疑ったときは、artifact のコードを開いて数えるのが最短です。
- artifact のコードビューを開きます
- データ配列の宣言(
const data = [...] に相当する箇所)を探します
- 要素数を数え、元データの行数と突き合わせます
- 差があれば、そのチャートは意思決定には使いません
3 行の差なら丸め、4,000 行の差なら別物です。この確認を挟むようになってから、グラフを疑う時間がなくなりました。
集計は手元で終わらせる — 178 KB を 3.1 KB にする前処理
結論として私が採ったのは、集計を Claude に渡さない、という方針でした。
日次 4,312 行のうち、意思決定に効くのは「アプリ別・週次の推移」だけです。であれば、その形まで手元で畳んでから渡せばよい。以下は実際に使っているスクリプトです。AdMob の日次エクスポートを読み、週次に畳んで JSON を吐きます。
# aggregate_admob.py — AdMob 日次CSVを週次サマリJSONへ畳む
# 使い方: python3 aggregate_admob.py admob_daily.csv > weekly.json
import sys
import json
import pandas as pd
REVENUE = "Estimated earnings (USD)"
IMPRESSIONS = "Impressions"
def main(path: str) -> None:
df = pd.read_csv(path)
df["Date"] = pd.to_datetime(df["Date"])
# 週の区切りを月曜始まりに固定する。ここを揃えないと
# 表計算側の集計と一日ずれる(実際にずれました)
df["week"] = df["Date"].dt.to_period("W-SUN").dt.start_time
grouped = (
df.groupby(["App", "week"])
.agg(revenue=(REVENUE, "sum"), impressions=(IMPRESSIONS, "sum"))
.reset_index()
)
# eCPM は「合計収益 ÷ 合計表示回数 × 1000」で出します。
# 日次eCPMの平均を取ると表示回数の少ない日に引きずられて過大になります
grouped["ecpm"] = (grouped["revenue"] / grouped["impressions"] * 1000).round(2)
grouped["revenue"] = grouped["revenue"].round(2)
grouped["week"] = grouped["week"].dt.strftime("%Y-%m-%d")
records = grouped.to_dict(orient="records")
print(json.dumps(records, ensure_ascii=False, separators=(",", ":")))
if __name__ == "__main__":
main(sys.argv[1])
手元での実測は次のとおりです。
| 項目 | 生CSVをそのまま貼る | 週次に畳んでから貼る |
| データ量 | 178 KB / 4,312 行 | 3.1 KB / 26 行 |
| 入力トークン(概算) | 約 47,000 | 約 900 |
| artifact に載った要素数 | 117 件(間引かれた) | 26 件(全件) |
| 数値の一致 | 週次の山が1日ずれる | 表計算と一致 |
| 作り直しの所要 | 都度、長い貼り付けから | JSON を差し替えるだけ |
トークンにして約 98% の削減ですが、効いたのは費用ではありません。全件が載るようになったこと、そして作り直しが軽くなったことです。
W-SUN の指定は、つまずいた箇所そのものです。pandas の週次は既定で日曜終わりに寄り、管理画面側の週の区切りと一日ずれます。ここを固定するだけで、最初に気づいた「一日のずれ」は消えました。
集計そのものを Claude 側で完結させたい場合は、Claude API の Code Execution ツールに月次の売上CSV集計を任せる の構成が近いです。手元に Python を持ちたくないときは、そちらが素直です。
データ契約を先に宣言する
畳んだ JSON を渡しても、作り直すたびにキー名や軸の取り方が揺れることがありました。revenue が earnings になったり、Y軸が二本になったり一本に戻ったり。
止め方は単純で、先に契約を宣言しておくことです。
- 配列の各要素が持つキーと型を、先に文章で固定します
- 軸の割り当て(左軸・右軸)を明示します
- 「キーを増やさない・改名しない」と一言添えます
- 変更したいときは、契約の側を書き換えてから依頼します
実際に渡している宣言は、この程度の短さです。
データ契約(この形から外れないでください):
week: string "YYYY-MM-DD"(月曜始まり)
app: string
revenue: number(USD・小数2桁)
impressions: number(整数)
ecpm: number(USD・小数2桁)
描画の指定:
左軸 = revenue(棒)
右軸 = ecpm(線)
app ごとに系列を分ける
キーの追加・改名はしないでください
契約を先に置くと、依頼の文面が短くなります。「さっきのグラフの右軸を ARPDAU にしてください」で通るようになり、そのたびに全体を説明し直す必要がなくなりました。
実際に回している artifact のコード
以下は、上の契約を前提に落ち着いた React コンポーネントです。Recharts の複合チャートで、棒に収益、線に eCPM を載せています。artifact の中でそのまま動きます。
import { useMemo, useState } from "react";
import {
ComposedChart, Bar, Line, XAxis, YAxis,
CartesianGrid, Tooltip, Legend, ResponsiveContainer, ReferenceLine,
} from "recharts";
// 前処理スクリプトが吐いた JSON をそのまま貼る場所
const RAW = [
{ week: "2026-01-05", app: "Wallpapers", revenue: 412.8, impressions: 318400, ecpm: 1.3 },
{ week: "2026-01-12", app: "Wallpapers", revenue: 455.1, impressions: 331200, ecpm: 1.37 },
{ week: "2026-01-19", app: "Wallpapers", revenue: 398.4, impressions: 349800, ecpm: 1.14 },
{ week: "2026-01-26", app: "Wallpapers", revenue: 501.6, impressions: 356100, ecpm: 1.41 },
];
// 色覚特性に配慮した並び。既定のパレットに任せると赤と緑が隣り合います
const PALETTE = { revenue: "#0072B2", ecpm: "#E69F00", target: "#999999" };
const ECPM_TARGET = 1.3;
export default function AdRevenueDashboard() {
const [showTarget, setShowTarget] = useState(true);
const { data, delta } = useMemo(() => {
const sorted = [...RAW].sort((a, b) => a.week.localeCompare(b.week));
const head = sorted[0]?.revenue ?? 0;
const tail = sorted[sorted.length - 1]?.revenue ?? 0;
// ゼロ除算をここで潰しておかないと、初週のデータが空の月に NaN が出ます
const pct = head === 0 ? null : ((tail - head) / head) * 100;
return { data: sorted, delta: pct };
}, []);
return (
<div className="w-full p-4 space-y-3">
<div className="flex items-baseline justify-between">
<h2 className="text-lg font-semibold">週次 収益と eCPM</h2>
<span className="text-sm text-gray-600">
{delta === null ? "期首データなし" : `期首比 ${delta.toFixed(1)}%`}
</span>
</div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={showTarget}
onChange={(e) => setShowTarget(e.target.checked)}
/>
eCPM の目標線を表示する
</label>
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={data} margin={{ top: 8, right: 16, bottom: 8, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="week" tick={{ fontSize: 12 }} />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} />
<YAxis yAxisId="right" orientation="right" tick={{ fontSize: 12 }} />
<Tooltip />
<Legend />
<Bar yAxisId="left" dataKey="revenue" name="収益 (USD)" fill={PALETTE.revenue} />
<Line
yAxisId="right"
type="monotone"
dataKey="ecpm"
name="eCPM (USD)"
stroke={PALETTE.ecpm}
strokeWidth={2}
dot={{ r: 3 }}
/>
{showTarget && (
<ReferenceLine
yAxisId="right"
y={ECPM_TARGET}
stroke={PALETTE.target}
strokeDasharray="4 4"
label={{ value: `目標 ${ECPM_TARGET}`, position: "right", fontSize: 11 }}
/>
)}
</ComposedChart>
</ResponsiveContainer>
</div>
);
}
RAW の中身を差し替えるだけで、翌月も同じ形で見られます。作り直しを頼まずに済むことが、思っていた以上に効きました。
useMemo の中でゼロ除算を潰しているのは、実際に NaN を表示させた経験からです。新しいアプリを追加した月は初週のデータが空になり、期首比の計算が壊れます。データの端は、いつも想定より汚れています。
「リアルタイム更新」は artifact の外に置く
ここが、はっきりさせておきたい箇所です。
artifact のサンドボックスからは、外部 URL への fetch ができません。localStorage も使えません。つまり「30秒ごとに API を叩いて更新するダッシュボード」は、artifact 単体では成立しません。
この点を知らずに、ポーリング付きのダッシュボードを依頼して、動かない理由をしばらく探しておりました。原因はコードではなく、置き場所の選択でした。
そこで、更新の責務を外に出す構成に変えました。
- Claude Code に取得と集計を任せ、
weekly.json を吐かせます
- その JSON を、静的サイトと同じディレクトリに置きます
- 描画は artifact ではなく、自前ホスティングの HTML に持たせます
- cron で日次に回します
artifact は「その場で形を決める場所」、自前の HTML は「毎日見る場所」。この線を引いてから、どちらも素直に動くようになりました。
#!/usr/bin/env bash
# daily_dashboard.sh — 日次で集計してダッシュボード用JSONを差し替える
set -euo pipefail
WORK="$HOME/metrics"
OUT="$WORK/public/data/weekly.json"
cd "$WORK"
# エクスポート済みCSVを畳む。失敗したら既存JSONを残したまま終了する
# (壊れたJSONで上書きするより、昨日の数字が残っている方がましです)
if python3 aggregate_admob.py exports/admob_daily.csv > /tmp/weekly.json.new; then
mv /tmp/weekly.json.new "$OUT"
echo "[$(date +%F)] updated: $(wc -c < "$OUT") bytes"
else
echo "[$(date +%F)] aggregation failed, keeping previous JSON" >&2
exit 1
fi
失敗時に上書きしないのは、一度やらかしたためです。空の JSON で上書きしてしまい、翌朝ダッシュボードが真っ白になっていました。壊れた新しさより、古い正しさを残す。運用ではこちらを推奨します。
チーム規模で同じことをするなら、Claude Code Analytics API:チームの生産性ダッシュボードを構築する の設計が参考になります。個人開発の一人運用なら、上の cron 一本で足ります。
色は指定しないと必ず赤緑になる
もう一つ、毎回同じ手当てをしている箇所があります。
系列の色を指定せずに任せると、対比として赤と緑が選ばれがちです。増減を表す場面では自然な選択ですが、第一色覚・第二色覚の方には区別が難しい組み合わせになります。
私は青(#0072B2)と橙(#E69F00)を既定にしております。上のコードで PALETTE を定数に切り出しているのは、そのためです。色を毎回言葉で頼むより、コードの一箇所に置いた方が揺れません。
線種でも差をつけておくと、印刷やスクリーンショットでも読めます。実測値は実線、目標線は破線。この二つを守るだけで、色に頼らずに済みます。
つまずいた箇所と、その対処
| 症状 | 実際の原因 | 対処 |
| グラフの山が表計算と1日ずれる | 週の区切りが日曜終わりで揃っていない | 前処理側で W-SUN に固定する |
| データ点が思ったより少ない | 出力トークンの都合で書き写しが打ち切られている | artifact のコードを開いて要素数を数え、集計してから渡す |
| ポーリングが動かない | サンドボックスから外部 fetch ができない | 更新を cron + 静的JSON に外出しする |
| 期首比が NaN になる | 新規アプリの初週データが空 | ゼロ除算を明示的に潰し、null を返す |
| 作り直すたびにキー名が変わる | データ契約を宣言していない | キーと型と軸を先に固定する |
| eCPM が実感より高く出る | 日次eCPMの平均を取っている | 合計収益 ÷ 合計表示回数 で計算する |
| 朝ダッシュボードが真っ白 | 集計失敗時に空JSONで上書きした | 失敗時は既存ファイルを残して終了する |
私が守っている4つのルール
半年ほど回してきて、手元に残ったのはこれだけです。
1. 数えてから信じる。 チャートを意思決定に使う前に、要素数を元データと突き合わせます。この確認に 10 秒もかかりません。
2. 集計は手元でやる。 渡すのは畳んだ結果だけにします。判断の中身を委ねないという意味でもあります。
3. 契約を先に置く。 キー・型・軸を宣言してから頼みます。会話が短くなり、揺れが止まります。
4. 更新は外に出す。 artifact は形を決める場所、自前の HTML は毎日見る場所。役割を分けます。
逆に、やめたことも一つあります。「いい感じのダッシュボードを作ってください」と頼むことです。出てくるものは毎回きれいですが、毎回違います。毎日見る道具に、毎回違う形は要りません。
より広い範囲の数値を継続的に見たい場合は、Claude API のテレメトリを ClickHouse に流して分析する のように、蓄積側を先に設計する道もあります。個人開発の規模なら、CSV と cron でしばらく足ります。
次の一歩
手元にある CSV を一つ選んで、artifact に貼ってみてください。そして、描かれたグラフを眺める前に、コードビューを開いて配列の要素数を数えてみてください。
元データの行数と合っていれば、そのまま使えます。合っていなければ、この記事の前処理スクリプトを 1 本挟むところから始められます。
私自身、数字を見ているつもりで、数字の面影を見ていた時期がありました。道具の性質を知ることは、道具を疑うことではありません。どこまでを任せ、どこからを自分で持つのか。その線を引き直す作業なのだと思っております。
グラフを疑うために使っていた時間を、今は次の更新に回せております。ここまでお読みくださり、ありがとうございます。