観光客数の積み上げ棒|国内 / 訪日を一枚に Claude Code で

ClaudeCode
e-Stat
観光
積み上げ棒
D3

「観光客数」と一口に言っても、宿泊しているのが日本人なのか外国人なのかで、その県の観光産業の性格はガラッと変わる。京都や大阪のように訪日比率が 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 つ。

  1. d3.stack() の戻り値 series3 次元配列 に近い構造で、series[seriesIndex][rowIndex] = [y0, y1] という形になる。y0 が下端、y1 が上端で、ここをそのまま y(seg[0]) - y(seg[1]) で高さに変換する。
  2. d3.select には触らない。React で d3 を扱うときの鉄則。レイアウト計算は d3、描画は React の流儀で完結させる。
  3. 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-estatstatsDataId を引いたとき、両方ヒットする月があり、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 のページを置いておく。記事内チャートの「裏付け」として参照してほしい。

シリーズ過去回も再掲。

ここまで読んで「自分でも積み上げ棒を組んでみたい」と思ったら、data/tourism/overnight-2024.json のスキーマだけ覚えて手元の好きなフレームワーク(Recharts でも Chart.js でも)で組んでみてほしい。d3.stack の威力は、一度自前で書いた人ほど感じやすい。


まとめ

  • 観光統計は「国内」と「訪日」を分けないと県別の本性が見えない
  • e-Stat API は cat02 軸で日本人 / 外国人を分離取得できる
  • 多系列処理は stack 入力に整える整形ステップ を 1 段挟むだけで Claude Code に投げやすくなる
  • 並び順、凡例配置、系列順は「読み手の理解速度」を直接決める
  • 対数スケールは積み上げ棒では使わない(足し算が壊れる)

次回 Part 12 のツリーマップで会いましょう。