「観光客数」と一口に言っても、宿泊しているのが日本人なのか外国人なのかで、その県の観光産業の性格はガラッと変わる。京都や大阪のように訪日比率が 30% を超える県もあれば、ほぼ 100% 国内客で回っている県もある。1 つの数字に丸めてしまうと、この差は完全に消える。
そこで本記事では、観光庁「宿泊旅行統計調査」から 国内宿泊客数 と 訪日外国人宿泊客数 の 2 系列を e-Stat API 経由で取得し、47 都道府県別の 積み上げ棒グラフ を Claude Code に作らせる手順をまとめる。シリーズ Part 11、扱うチャート種は積み上げ棒、論点は「多系列処理」「d3.stack」「凡例配置」の 3 点。
このシリーズの過去回は単系列のチャート(棒・ヒートマップ・散布図・レーダー)が中心だったが、今回からは複数系列が絡む。データ整形のレシピと、Claude Code への伝え方のコツが少し変わるので、その差分を意識しながら読んでほしい。
1. 導入: 観光統計を「一枚」で見る価値
観光統計をニュースで見るとき、よくあるのは「訪日客が過去最高」「国内旅行需要が回復」といった全国合計の話だ。だが現場でデータを使う側からすると、知りたいのは大抵こうだ。
- うちの県の観光産業は 国内 で食べているのか、訪日 で食べているのか
- 隣県と比べて、訪日依存度 はどのくらい違うのか
- 訪日が落ち込んだとき、ベースとなる国内客 がどれだけクッションになるか
これは折れ線や単独の棒では一目で読めない。積み上げ棒 が一番素直で、47 県を横に並べた瞬間に「赤い帯(訪日)の割合」が地図のように浮かび上がる。京都・大阪・東京の赤帯が他県を圧倒し、その隣に「ほぼ青一色(国内)」の地方県が並ぶ、あの絵だ。
データ可視化の世界では「stacked bar は比較に向かない」と言われがちで、確かに「2 番目以降のセグメントの絶対値比較」は読みづらい。だが今回のように 合計値の県別比較 + 内訳の割合提示 を同時にやりたいケースでは、積み上げ棒は今もって最強クラスの選択肢である。
| 表現方法 | 合計の比較 | 内訳の比較 | 47 県を並べたとき |
|---|---|---|---|
| 単純棒(合計のみ) | ◎ | × | 読みやすい |
| グループ棒(横並び 2 本) | △ | ○ | 棒が細くなり崩壊 |
| 100% 積み上げ棒 | × | ◎ | 比率は読めるが絶対値消える |
| 絶対値の積み上げ棒 | ◎ | ○ | 本記事の選択 |
| 折れ線(年次) | × | × | 単年の比較には不向き |
100% 積み上げ棒(normalized stacked bar)も魅力的なオプションだが、最初の 1 枚としては絶対値の積み上げ棒を推す。観光地としての規模感が消えてしまうと「沖縄と鳥取が同じ高さ」のような見た目になり、誤読の温床になるからだ。
2. 使うデータ: 宿泊旅行統計の都道府県別宿泊客数
観光庁の 宿泊旅行統計調査 は、ホテル・旅館・簡易宿所・その他の宿泊施設を対象に、月次で宿泊者数を集計している調査だ。e-Stat 上では「観光庁」の調査として登録されており、statsDataId を 1 つ叩けば全国 + 47 県 × 国籍区分(日本人 / 外国人)のクロス集計が一気に取れる。
データの粒度は概ねこうなっている。
| 項目 | 値 |
|---|---|
| 公表元 | 観光庁 |
| 調査名 | 宿泊旅行統計調査 |
| 公表頻度 | 月次(速報 → 確報) |
| 集計単位 | 都道府県、宿泊施設タイプ、国籍(日本人 / 外国人) |
| 単位 | 延べ宿泊者数(人泊) |
| 注意点 | 「人泊」= 宿泊者数 × 宿泊日数。例えば 1 人が 3 連泊すると 3 人泊 |
「人泊」ベースである点は重要だ。観光統計で「観光客数」と言うとき、来訪者の頭数を指す場合と、宿泊数を指す場合がある。本データは後者で、リピーターや連泊客の効果がそのまま積み上がる。
2-1. 国籍区分の取り方
宿泊旅行統計の cat02 軸には「総数 / 日本人 / 外国人」の区分が含まれている。今回欲しいのは以下の 2 系列。
- 国内宿泊客数 =
cat02 == 日本人 - 訪日宿泊客数 =
cat02 == 外国人
総数は「日本人 + 外国人」とほぼ一致するが、不詳が少し混じるので、積み上げ棒では「日本人」と「外国人」だけを足し合わせて見せるほうがクリーンだ。総数を別途引いてラベルだけ表示するのもあり。
2-2. 施設タイプの取り方
cat01 軸には「全宿泊施設 / ホテル / 旅館 / 簡易宿所 / その他」のような施設タイプが入る。今回の積み上げ棒は 施設タイプは「全宿泊施設」固定 で、国籍だけを積み上げる。施設タイプも積み上げると 3 階建ての積み上げになり、Part 11 のスコープを超えるのでやらない。
3. Step 1: 2 つの系列を Claude Code に取らせる
ここから手を動かす。最終的にやりたいのは「指定年の 12 か月合計を、47 県 × 2 系列で配列に整形してファイル出力」だ。
まず Claude Code に渡す前提を共有する。プロジェクトのセットアップは Part 01 で済んでいるものとして、packages/estat-api 経由で API キー読み込みと R2 キャッシュが動く想定。Part 02 の /search-estat スキルで statsDataId の候補当たりをつけたあとに、本番取得に進む。
Claude Code への最初の依頼はこう書く。
@.claude/skills/estat/fetch-estat-data/SKILL.md
宿泊旅行統計調査(観光庁)から、2024 年(年間集計)の都道府県別
「全宿泊施設 × 日本人」「全宿泊施設 × 外国人」の延べ宿泊者数を取ってきて、
scripts/tourism/fetch-overnight-2024.ts として書き出して。
要件:
- e-Stat API は packages/estat-api 経由(cdArea / cdTime は使わない)
- 全年・全県を一括取得してメモリでフィルタ
- 出力は data/tourism/overnight-2024.json
- スキーマ:
{ year: 2024, items: [
{ areaCode: "01000", areaName: "北海道", domestic: 12345678, inbound: 234567 },
...
] }
- 1 県でも欠損があれば warn ログ + nullable で詰める
- 既存の指標登録テーブル indicators を汚さない(読み取りオンリーで OK)
@ でスキルを直接食わせるのが Part 02 で習った癖。Claude Code は SKILL.md を読み、e-Stat API の規約(cdArea / cdTimeFrom を使わない・全件取得してメモリでフィルタ)を勝手に守った形で吐いてくれる。
実行すると、こんな感じのスクリプトが返ってくる。
// scripts/tourism/fetch-overnight-2024.ts
import { fetchEstatStats } from "@stats47/estat-api";
import { writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";
const STATS_DATA_ID = "0003445XXX"; // 宿泊旅行統計の確報テーブル
const TARGET_YEAR = "2024";
type OvernightRow = {
areaCode: string;
areaName: string;
domestic: number | null;
inbound: number | null;
};
async function main() {
// 全件取得(cdArea / cdTime 指定なし)
const raw = await fetchEstatStats({
statsDataId: STATS_DATA_ID,
cdCat01: "ALL_FACILITIES", // 全宿泊施設
});
// 軸を解釈: AREA / TIME / CAT02(日本人・外国人)
const byPref = new Map<string, OvernightRow>();
for (const row of raw.values) {
if (!row.timeCode.startsWith(TARGET_YEAR)) continue;
const code = row.areaCode;
if (!byPref.has(code)) {
byPref.set(code, {
areaCode: code,
areaName: row.areaName,
domestic: 0,
inbound: 0,
});
}
const target = byPref.get(code)!;
if (row.cat02Code === "JAPANESE") {
target.domestic = (target.domestic ?? 0) + row.value;
} else if (row.cat02Code === "FOREIGN") {
target.inbound = (target.inbound ?? 0) + row.value;
}
}
// 47 県だけに絞る(00000 全国 / 99000 不詳を除く)
const items = [...byPref.values()]
.filter((r) => r.areaCode.endsWith("000") && r.areaCode !== "00000")
.sort((a, b) => a.areaCode.localeCompare(b.areaCode));
for (const r of items) {
if (r.domestic == null || r.inbound == null) {
console.warn(`[warn] missing series at ${r.areaCode} ${r.areaName}`);
}
}
const outPath = "data/tourism/overnight-2024.json";
await mkdir(dirname(outPath), { recursive: true });
await writeFile(outPath, JSON.stringify({ year: 2024, items }, null, 2));
console.log(`[done] wrote ${items.length} rows to ${outPath}`);
}
main().catch((e) => {
console.error(e);
process.exit(1);
});
実行コマンドはお馴染みの 1 行。
npx tsx scripts/tourism/fetch-overnight-2024.ts
API キーは .env から読まれる前提で、初回だけ e-Stat にリクエストが走り、2 回目以降は R2 キャッシュにヒットしてミリ秒単位で返ってくる。
ここまでで「データを取る」フェーズはおしまい。e-Stat API そのものの細かい話は Part 01〜03 で散々書いたので深入りしない。
4. Step 2: 47 県 × 2 系列 の行列に整形
API レスポンスを保存した overnight-2024.json はこんな構造になる。
{
"year": 2024,
"items": [
{ "areaCode": "01000", "areaName": "北海道", "domestic": 36456000, "inbound": 9120000 },
{ "areaCode": "02000", "areaName": "青森県", "domestic": 4980000, "inbound": 360000 },
{ "areaCode": "03000", "areaName": "岩手県", "domestic": 5210000, "inbound": 290000 },
{ "areaCode": "13000", "areaName": "東京都", "domestic": 51230000, "inbound": 28760000 },
{ "areaCode": "26000", "areaName": "京都府", "domestic": 21560000, "inbound": 14210000 },
{ "areaCode": "27000", "areaName": "大阪府", "domestic": 31870000, "inbound": 19340000 },
{ "areaCode": "47000", "areaName": "沖縄県", "domestic": 18920000, "inbound": 4530000 }
]
}
数値はサンプル。実値は e-Stat 側の確報を見てほしい。
積み上げ棒に食わせるには、これを 「stack の対象キー(series)」と「行レコード(areaCode 単位)」 の 2 階層に整える。d3-shape の d3.stack() が受け取れる形にしてやるイメージだ。
// scripts/tourism/build-stack-input.ts
import { readFile, writeFile } from "node:fs/promises";
type Row = {
areaCode: string;
areaName: string;
domestic: number | null;
inbound: number | null;
};
const SERIES = ["domestic", "inbound"] as const;
type Series = (typeof SERIES)[number];
type StackInput = {
series: typeof SERIES;
rows: Array<{
areaCode: string;
areaName: string;
total: number;
domestic: number;
inbound: number;
}>;
};
async function main() {
const raw = JSON.parse(await readFile("data/tourism/overnight-2024.json", "utf-8")) as {
items: Row[];
};
const rows = raw.items.map((r) => {
const domestic = r.domestic ?? 0;
const inbound = r.inbound ?? 0;
return {
areaCode: r.areaCode,
areaName: r.areaName,
domestic,
inbound,
total: domestic + inbound,
};
});
// 合計値降順で並び替え(積み上げ棒の常套手段)
rows.sort((a, b) => b.total - a.total);
const out: StackInput = { series: SERIES, rows };
await writeFile(
"data/tourism/overnight-2024.stack.json",
JSON.stringify(out, null, 2)
);
console.log(`[done] ${rows.length} rows ready for d3.stack`);
}
main();
ここで効いているのが「合計値降順ソート」。47 県のような多数カテゴリを並べると、五十音順や areaCode 順で並べたグラフは「読み手の脳に負荷を強いるだけ」になる。「視覚的に山なりに減っていく」絵にするだけで、読みやすさが体感 2 倍は変わる。
実は Claude Code に「Step 2 の整形スクリプトを書いて。stack 用の series キーと rows 配列。並びは合計値降順」とだけ伝えれば、上記スクリプトはほぼそのまま出てくる。意図(並び順、key 名)を文字で伝える のがコツで、Claude Code は意図を取り違えるよりも「意図がないところを勝手に埋める」ほうがエラー率が高いので、書きすぎても損はしない。
5. Step 3: d3.stack で積み上げレイアウト
ここからフロント側。packages/visualization/d3 配下に積み上げ棒コンポーネントが既にあれば再利用、なければ Claude Code に新規生成させる。今回は新規前提で進める。
Claude Code に投げるプロンプト。
packages/visualization/d3/StackedBarChart.tsx を作成。
要件:
- props: { data: StackInput; width: number; height: number; colors?: Record<Series, string> }
- d3-scale, d3-shape のみ使用(React は SVG をそのまま描画、d3 セレクションは触らない)
- x: scaleBand(areaName), y: scaleLinear(0 〜 total max)
- d3.stack().keys(series) で積み上げ
- 描画は SVG 直書き、d3.select はしない
- アクセシビリティ: <title> で areaName + 各系列の値、role="img"
- ホバー時の tooltip は別 PR でやるので、今は描画のみ
- 単位は「万人泊」に丸める(生値 / 10000、小数 1 桁)
返ってくる中身がこうなる。
// packages/visualization/d3/StackedBarChart.tsx
import { scaleBand, scaleLinear } from "d3-scale";
import { stack } from "d3-shape";
import { max } from "d3-array";
type Series = "domestic" | "inbound";
type Row = {
areaCode: string;
areaName: string;
domestic: number;
inbound: number;
total: number;
};
type StackInput = {
series: readonly Series[];
rows: Row[];
};
const DEFAULT_COLORS: Record<Series, string> = {
domestic: "#3b82f6", // blue-500
inbound: "#ef4444", // red-500
};
const SERIES_LABEL: Record<Series, string> = {
domestic: "国内(日本人)",
inbound: "訪日(外国人)",
};
const MARGIN = { top: 24, right: 24, bottom: 80, left: 56 };
export function StackedBarChart({
data,
width,
height,
colors = DEFAULT_COLORS,
}: {
data: StackInput;
width: number;
height: number;
colors?: Record<Series, string>;
}) {
const innerW = width - MARGIN.left - MARGIN.right;
const innerH = height - MARGIN.top - MARGIN.bottom;
const x = scaleBand<string>()
.domain(data.rows.map((r) => r.areaName))
.range([0, innerW])
.padding(0.15);
const yMax = max(data.rows, (r) => r.total) ?? 0;
const y = scaleLinear()
.domain([0, yMax])
.nice()
.range([innerH, 0]);
const series = stack<Row, Series>()
.keys([...data.series])
.value((row, key) => row[key])(data.rows);
return (
<svg
role="img"
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
>
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
{/* Y 軸グリッド + ラベル */}
{y.ticks(5).map((t) => (
<g key={t} transform={`translate(0,${y(t)})`}>
<line x1={0} x2={innerW} stroke="#e5e7eb" />
<text x={-8} y={4} fontSize={11} textAnchor="end" fill="#64748b">
{(t / 10000).toFixed(0)}万
</text>
</g>
))}
{/* 各系列の rect */}
{series.map((s) => (
<g key={s.key} fill={colors[s.key as Series]}>
{s.map((seg, i) => {
const row = data.rows[i];
const xPos = x(row.areaName) ?? 0;
const yTop = y(seg[1]);
const yBot = y(seg[0]);
return (
<rect
key={row.areaCode}
x={xPos}
y={yTop}
width={x.bandwidth()}
height={Math.max(0, yBot - yTop)}
>
<title>
{row.areaName} / {SERIES_LABEL[s.key as Series]}:{" "}
{((seg[1] - seg[0]) / 10000).toFixed(1)} 万人泊
</title>
</rect>
);
})}
</g>
))}
{/* X 軸ラベル(縦書きで 47 件さばく) */}
{data.rows.map((r) => {
const xPos = (x(r.areaName) ?? 0) + x.bandwidth() / 2;
return (
<text
key={r.areaCode}
x={xPos}
y={innerH + 12}
fontSize={10}
textAnchor="end"
transform={`rotate(-60, ${xPos}, ${innerH + 12})`}
fill="#334155"
>
{r.areaName}
</text>
);
})}
</g>
</svg>
);
}
ポイントは 3 つ。
d3.stack()の戻り値seriesは 3 次元配列 に近い構造で、series[seriesIndex][rowIndex] = [y0, y1]という形になる。y0が下端、y1が上端で、ここをそのままy(seg[0]) - y(seg[1])で高さに変換する。d3.selectには触らない。React で d3 を扱うときの鉄則。レイアウト計算は d3、描画は React の流儀で完結させる。- X 軸ラベルは 47 件入る都合、
rotate(-60)で縦傾けが現実解。フォントを 10px まで落とすと収まる。
このコンポーネントを apps/web/src/app/blog/cc-estat-11-tourism-stacked/Chart.tsx あたりから呼び出せば、記事内チャートとして配信できる。
6. Step 4: 凡例配置(縦/横、色選び)
ここまでで「正しく積み上がる絵」は出る。だが凡例なしの積み上げ棒は ただの 2 色の塔 で意味不明だ。凡例を打つ場所が、地味だが効く。
6-1. 縦並び vs 横並びの判断軸
| シナリオ | 縦並び凡例(右側 or 左側) | 横並び凡例(上部 or 下部) |
|---|---|---|
| モバイル幅(〜768px) | 棒のスペースを圧迫する | こちらを採用 |
| ワイド PC(1280px+) | こちらを採用 | 横幅が間延びする |
| 系列数 5 以上 | スクロール可能で吉 | 折り返しが汚い |
| 系列数 2-3 | どちらでも | どちらでも |
47 県 × 2 系列という今回の構成では、横並び凡例を 棒グラフの上部 に置くのが鉄板。凡例を見てから棒に視線を落とす、という自然な視線移動になる。
6-2. 色の決め方
「国内 = 青、訪日 = 赤」をデフォルトにしたが、これは深い意味があるわけではない。注意したい原則だけ列挙する。
- 赤を「悪い」と読まれない文脈 か確認する。本記事の訪日客は明らかにポジティブな指標なので赤でも OK。コロナ禍の文脈なら緑系に振る方が無難。
- 色覚多様性対応。Okabe-Ito パレットや Tableau 10 など、テストされた組合せから選ぶ。
#0072B2と#D55E00の組合せは色覚多様性下でもコントラストが取れる。 - 背景とのコントラスト 4.5:1 以上(WCAG AA)。淡すぎる青は要注意。
Claude Code に色見直しを依頼するときは、こう書くだけで Okabe-Ito を採用してくれる。
StackedBarChart のデフォルトカラーを Okabe-Ito パレットに合わせて。
国内 = '#0072B2'(青)、訪日 = '#D55E00'(オレンジ)。
WCAG AA を満たすか自己チェックも吐いて。
凡例 UI 自体は SVG でもいいし、HTML の <ul> でも OK。アクセシビリティを考えると HTML のほうが楽だ。
function Legend({ colors }: { colors: Record<Series, string> }) {
return (
<ul className="flex flex-wrap gap-4 text-sm text-slate-700 mb-2">
{(Object.keys(colors) as Series[]).map((key) => (
<li key={key} className="flex items-center gap-2">
<span
aria-hidden
className="inline-block w-3 h-3 rounded-sm"
style={{ backgroundColor: colors[key] }}
/>
<span>{SERIES_LABEL[key]}</span>
</li>
))}
</ul>
);
}
aria-hidden を色見本に振り、ラベルだけスクリーンリーダーに読ませるのも地味に効くテクニック。
7. Step 5: 並び順(合計値降順)と注釈
「並び順」は Step 2 で合計値降順にしたが、それで終わりではない。並び順を変えると別のメッセージが読める のが積み上げ棒の面白いところだ。
7-1. 並び替え軸の選択肢
| 並び順 | 読めるメッセージ | おすすめ用途 |
|---|---|---|
| 合計人泊の降順 | 観光地としての総量ランキング | 一般読者向けの最初の 1 枚 |
| 訪日比率の降順 | 国際観光に依存している県 | インバウンド戦略の議論 |
| 訪日絶対値の降順 | 訪日客が集まっている県 | 自治体プロモ系記事 |
| 国内比率の降順 | 国内市場が太い県 | 内需を語る記事 |
| 五十音順 / areaCode 順 | 並び順に意味を持たせない | リファレンス用途のみ |
Claude Code に並び順切替の UI を頼むと、<select> で 4 種類のソートキーを切り替えられるコンポーネントを作ってくれる。本記事のスコープ外なので深追いしないが、ソート関数だけ別ファイルに切り出しておくと別チャートで使い回せる。
type SortKey = "total_desc" | "inbound_ratio_desc" | "inbound_desc" | "domestic_desc";
const COMPARATORS: Record<SortKey, (a: Row, b: Row) => number> = {
total_desc: (a, b) => b.total - a.total,
inbound_ratio_desc: (a, b) =>
b.inbound / Math.max(b.total, 1) - a.inbound / Math.max(a.total, 1),
inbound_desc: (a, b) => b.inbound - a.inbound,
domestic_desc: (a, b) => b.domestic - a.domestic,
};
export function sortRows(rows: Row[], key: SortKey): Row[] {
return [...rows].sort(COMPARATORS[key]);
}
7-2. 注釈をどう載せるか
47 県積み上げ棒は情報量が多いので、3〜5 個の注釈を直接 SVG に焼き付けると一気に読みやすくなる。例えば。
- 東京 に「全国一の合計、訪日比率 35%」
- 京都 に「訪日比率 39%、最高水準」
- 沖縄 に「訪日比率 19%、九州最高」
注釈の出し方には流派が 2 つ。SVG の <g> に <text> + <line> を生で書くか、d3-annotation ライブラリを噛ませるか。47 件中 3〜5 件くらいなら手書きで十分。
const ANNOTATIONS = [
{ areaName: "東京都", text: "全国一の合計\n訪日比率 35%", dx: -40, dy: -30 },
{ areaName: "京都府", text: "訪日比率 39%(最高)", dx: 40, dy: -50 },
{ areaName: "沖縄県", text: "九州最高の合計", dx: 60, dy: -20 },
];
データ駆動で生成するなら、inbound / total > 0.35 のような閾値で自動抽出するロジックを書いてもいい。手で選ぶか自動かは、記事の編集方針次第。
8. つまずきポイント(系列順、対数スケール、業態差)
ここからは「やってみるとハマる」典型例 3 つ。本記事の最重要セクションだ。Claude Code が綺麗な絵を返してきても、これらを知らないと誤読を量産する。
8-1. 系列順 = 視覚的重要度
積み上げ棒では 下に置いた系列のほうが視覚的に基準化されやすい。今回「下: 国内、上: 訪日」にしたのは、
- 国内のほうが圧倒的に量が多く、土台として安定している
- 訪日が「上に積まれる差分」として読みやすい
という理由から。これを逆にすると、「訪日の絶対値(下段)」と「国内の絶対値」が両方読みづらくなる(上段の値は y1 - y0 を脳内で計算する必要がある)。
ルール of thumb は 「量が多くて変動が少ない系列を下に置く」。テンプレ的にはこれで困らない。
Claude Code に伝えるときは、こう。
d3.stack のキー順は ["domestic", "inbound"] 固定で。
domestic を下、inbound を上に積む。順序を逆にしてはいけない。
「順序を逆にしてはいけない」と禁止形まで書くのは大事。Claude Code は「より一般的な順序」へ勝手に直してしまうことがある(特に alphabetical 系の癖)。
8-2. 対数スケールに逃げない
「東京・京都が大きすぎて他の県の差が読めない」と感じると、つい scaleLog に逃げたくなる。積み上げ棒で対数スケールはやってはいけない。
理由はシンプルで、対数スケールでは 足し算が成立しない から。log(a) + log(b) != log(a + b)。下段と上段の高さを合計しても全体の値にならず、視覚的に意味のない図になる。
差を強調したいときは、
- 横軸を絞る(上位 20 県だけ表示する、地方別に分割する)
- 2 軸表示 ではなく 小倍数(small multiples) に切り替える
- チャートを差し替える(散布図 / ドットプロット)
のいずれかが正攻法。Part 6 でやった散布図と組合せると、「合計 × 訪日比率」の 2 軸散布図でハイライトを変えるアプローチも有効。
8-3. ホテル / 旅館の業態差
宿泊旅行統計の「全宿泊施設」には、ホテル・旅館・簡易宿所・その他(民泊含む)が混ざる。県別の数字を比べるときに知っておきたい癖がある。
| 業態 | 特徴 | 偏在県 |
|---|---|---|
| シティホテル | 訪日比率が高い | 東京・大阪・名古屋 |
| リゾートホテル | 国内・訪日両方厚い | 沖縄・北海道・京都 |
| 旅館 | 国内比率が非常に高い | 静岡(熱海)・群馬(草津)・大分(湯布院) |
| 簡易宿所 | 訪日比率がムラがある | 京都(町家民泊が多い) |
積み上げ棒で「東京と京都の訪日比率が同じくらい」と読めても、中身は 東京がシティホテル、京都が町家民泊と旅館の和ホテル化 だったりして、観光産業構造はかなり違う。読者が誤読しないよう、本文側で 1 行注釈を入れておくと親切。
「業態別の積み上げ」を本気でやるなら、業態 × 国籍 の二重積み上げになる。これは Part 14 あたりで扱う予定だ。
8-4. (おまけ)速報値と確報値の取り違え
宿泊旅行統計は 速報 と 確報 が別 statsDataId で公開される。年次集計を出すなら確報を待つのが王道。/search-estat で statsDataId を引いたとき、両方ヒットする月があり、Claude Code が速報を選んでしまうことがある。
搜索結果から速報(preliminary)でなく確報(confirmed)の statsDataId を選んで。
公表時期が遅い方が確報。
と添えておくのが安全。
9. 次回予告(Part 12: ツリーマップ)
次回は ツリーマップ をやる。同じ観光データを別の角度から見るのに便利で、「県 → 業態」のように 2 階層を一望できる。今回の積み上げ棒で「東京の訪日比率が高い」とわかった次は、「東京の訪日のうちホテルと民泊と旅館の比率は?」を 1 枚で見せる、というストーリー設計。
Claude Code で d3.treemap() を扱うコツと、ツリーマップ特有の「ラベル配置が地獄」問題への対処を書く予定。
| Part | チャート | テーマ |
|---|---|---|
| 11(本記事) | 積み上げ棒 | 観光客の国内 / 訪日 |
| 12 | ツリーマップ | 業態 × 国籍の階層 |
| 13 | サンキー | 出発地県 → 到着地県の流動 |
| 14 | 二重積み上げ棒 | 業態 × 国籍の同時提示 |
Part 11〜14 で「観光データ可視化 4 連発」になる構成。観光統計は系列数が多いので、可視化の練習材料として極めて優秀だ。
10. 関連ランキング・記事
本記事で扱った宿泊旅行統計に関連する stats47.jp のページを置いておく。記事内チャートの「裏付け」として参照してほしい。
シリーズ過去回も再掲。
- Part 01: Claude Code × e-Stat API のセットアップ
- Part 02:
/search-estatスキルで statsDataId を当てる - Part 03: 人口の都道府県別棒グラフを 30 分で作る
- Part 06: 所得と物価の散布図、回帰直線まで
- Part 09: 都道府県プロフィールをレーダーで一覧化
ここまで読んで「自分でも積み上げ棒を組んでみたい」と思ったら、data/tourism/overnight-2024.json のスキーマだけ覚えて手元の好きなフレームワーク(Recharts でも Chart.js でも)で組んでみてほしい。d3.stack の威力は、一度自前で書いた人ほど感じやすい。
まとめ
- 観光統計は「国内」と「訪日」を分けないと県別の本性が見えない
- e-Stat API は
cat02軸で日本人 / 外国人を分離取得できる - 多系列処理は stack 入力に整える整形ステップ を 1 段挟むだけで Claude Code に投げやすくなる
- 並び順、凡例配置、系列順は「読み手の理解速度」を直接決める
- 対数スケールは積み上げ棒では使わない(足し算が壊れる)
次回 Part 12 のツリーマップで会いましょう。