地図で見る統計のインパクト
棒グラフやヒートマップは「数値の大小」を直感的に伝えるのが得意だが、「地理的な偏り」を見せるなら地図に勝るものはない。たとえば「西日本のほうが医療費が高いのか?」「首都圏は本当に医療費が低いのか?」といった問いは、47本の棒グラフよりも、47都道府県を塗り分けた1枚の地図のほうが数十倍速く答えてくれる。
本シリーズ Part 1〜4 では、棒グラフ・ヒートマップ・折れ線といった「直交座標系のチャート」を Claude Code で量産する方法を扱ってきた。今回の Part 5 で初めて、地理空間データ(GIS) に踏み込む。
地理空間データは、これまで「専門知識がないと触れない領域」だった。ESRI Shapefile、座標参照系(CRS)、投影法(projection)、トポロジー保持型の簡素化(topology-preserving simplification)…用語からして取っ付きづらい。だが、Claude Code のような AI コーディング支援ツールを横に置けば、「地理データの全体像を 1 時間で掴み、コロプレス地図を本番投入できるレベルまで持っていく」 ことが現実的になる。
本記事のゴールは次の 3 点だ。
- e-Stat API から都道府県別国民医療費を取得し、47都道府県のデータを手元に揃える
- dataofjapan/land の TopoJSON を入手し、必要に応じて Claude Code に頼んで簡素化する
- D3-geo + TopoJSON-client で、医療費を色の濃淡で塗り分けたコロプレス地図を SVG で描画する
完成イメージはこんな感じだ。日本地図がブラウザに表示され、医療費が高い県は濃い赤、低い県は淡い赤で塗られる。ホバーすれば県名と金額が出る。
今回のキモは「Claude Code に TopoJSON の整形を丸投げできるか」という一点に尽きる。先に結論を言うと、ほぼ丸投げできる。ただし「地域コードの結合キー」「投影法の選択」「離島の配置」の 3 つだけは、人間側が判断材料を持っておかないと AI に良い指示が出せない。この記事ではそこも丁寧に解説していく。
使うデータ: 都道府県別国民医療費
厚生労働省が毎年公表する「国民医療費」は、その年度に国民が医療機関等で治療を受けた費用を全国・都道府県別に推計したものだ。e-Stat 上では「国民医療費」という統計調査として整備されており、statsCode 30001 配下の各統計表に格納されている。
今回は分かりやすさを優先して、以下の指標を使う。
| 指標 | 単位 | 想定される傾向 |
|---|---|---|
| 人口一人当たり国民医療費(都道府県別) | 千円/人 | 高齢化率が高い県(高知・鹿児島・長崎など)で高い傾向 |
| 国民医療費総額(都道府県別) | 億円 | 人口比例で東京・大阪・神奈川が上位 |
| 後期高齢者医療費(都道府県別、参考) | 億円 | 高齢化率と相関 |
コロプレス地図で見せるなら 「人口一人当たり」のほうが圧倒的に面白い。総額だと「人口が多い県=医療費が多い」という当たり前の結論しか出てこないが、一人当たりにすると「西高東低」「島嶼部での突出」といった構造が浮き上がってくる。
統計表のメタ情報は事前に /inspect-estat-meta 系のスキルで眺めておくと良い。e-Stat の同じ統計調査でも統計表ごとに「都道府県別」「年齢階級別」「制度別」など切り口が異なるため、「人口一人当たり × 都道府県 × 直近年度」 が揃った表を 1 本選ぶのが地味に重要だ。
補足: 本記事のコード例では statsCode
30001配下の架空 ID0003411111を例示として使う。実際に試すときは/search-estatで「人口一人当たり 国民医療費 都道府県」のキーワードで検索し、最新のstatsDataIdを取得してから差し替えてほしい。
Step 1: TopoJSON を入手する
コロプレス地図を描くには、「都道府県の輪郭ポリゴン」 をどこかから持ってこないといけない。日本の都道府県境界データは、無料で入手できる代表的な選択肢が 3 つある。
| データソース | 形式 | サイズ感 | 特徴 |
|---|---|---|---|
| dataofjapan/land (GitHub) | TopoJSON | 約 250KB | 軽量、47都道府県 ID 付き、すぐ使える |
| 国土数値情報「行政区域」 | Shapefile | 約 200MB | 公式・最新、市区町村まで含む、巨大 |
| Natural Earth (Admin 1) | Shapefile/GeoJSON | 約 5MB | 国際標準、世界全体、日本部分のみ抽出可 |
Web フロントで使うなら、ほぼ確実に dataofjapan/land 一択だ。軽量で TopoJSON、しかも各都道府県に id プロパティが付いている。国土数値情報は精度が高いが、市区町村レベルで都道府県を塗り分けたい場合や、漁港・港湾レイヤと重ねたいときに改めて検討すれば良い。
ダウンロードはシンプルに curl で落とせる。
mkdir -p public/geo
curl -L -o public/geo/japan.topojson \
https://raw.githubusercontent.com/dataofjapan/land/master/japan.topojson
# 軽量化の確認
ls -lh public/geo/japan.topojson
# -rw-r--r-- 1 user staff 245K ... japan.topojson
このファイルがそのまま使えるなら 247KB は許容範囲だが、もう少し削りたい場合は TopoJSON の simplify をかける。ここで Claude Code の出番だ。
あなた:
public/geo/japan.topojson があります。これを mapshaper で
- 元のトポロジーを保ったまま頂点を 30% に削減
- Visvalingam (weighted area) アルゴリズムを使う
- 出力は public/geo/japan-simplified.topojson
というワンライナーで簡素化してください。Node.js の mapshaper を
npm script で叩く形にしたいです。
Claude Code:
package.json に以下のスクリプトを追加します:
"scripts": {
"geo:simplify": "mapshaper public/geo/japan.topojson -simplify visvalingam weighted 30% -o public/geo/japan-simplified.topojson format=topojson"
}
実行前に: npm i -D mapshaper
実行後のサイズ: 約 90KB(元の 1/3)になる想定です。
mapshaper は CLI と JS API の両対応で、-simplify オプションの引数(dp ダグラス・ピューカー法、visvalingam 加重面積法など)の使い分けは慣れが必要だ。Claude Code は 「Visvalingam の weighted を使え」「トポロジーを保て」 といった微妙なオプション選択も、目的を伝えれば適切に出してくれる。
簡素化後の TopoJSON の構造はこんな感じになる。
{
"type": "Topology",
"arcs": [
[[100, 200], [101, 201], [102, 203]],
[[200, 300], [201, 301]]
],
"transform": {
"scale": [0.0001, 0.0001],
"translate": [122.0, 24.0]
},
"objects": {
"japan": {
"type": "GeometryCollection",
"geometries": [
{
"type": "MultiPolygon",
"id": 1,
"properties": { "nam": "Hokkaido", "nam_ja": "北海道" },
"arcs": [[[0, 1, 2]]]
},
{
"type": "MultiPolygon",
"id": 13,
"properties": { "nam": "Tokyo", "nam_ja": "東京都" },
"arcs": [[[5, 6]]]
}
]
}
}
}
ここで超重要なのが id フィールドだ。dataofjapan/land の TopoJSON は id に JIS 都道府県コード(1〜47) が入っている。北海道が 1、東京都が 13、沖縄県が 47。後で医療費データと結合する際の結合キーになる。
Step 2: Claude Code に「医療費データ取得」を頼む
データ取得側は本シリーズ Part 2 でセットアップした e-Stat スキルがあれば、ワンライナーで終わる。スキルがない場合でも、Claude Code に以下のように頼めば 5 分でクライアントコードが出てくる。
あなた:
e-Stat API で「人口一人当たり国民医療費(都道府県別、最新年度)」を取得して、
[{ areaCode: "01000", areaName: "北海道", value: 425.3 }, ...] という
配列に整形して data/medical-cost-per-capita.json に保存するスクリプトを書いて。
仕様:
- statsDataId は 0003411111 (例)
- e-Stat API キーは process.env.ESTAT_API_KEY
- 都道府県コードは5桁(01000〜47000)で取得し、そのまま areaCode に入れる
- 値が "***" や "-" の場合は null にして除外
- TypeScript で書く、tsx で実行できる形
Claude Code:
scripts/fetch-medical-cost.ts を作成します。
(コード省略・約 60 行)
実行: npx tsx scripts/fetch-medical-cost.ts
出力: data/medical-cost-per-capita.json (47 件 + メタ情報)
ポイントは「areaCode を 5 桁文字列で持つ」と明示すること。e-Stat の API は地域コードを 2 桁・5 桁・6 桁などのバリエーションで返してくることがあり、後段の地図結合で必ずトラブルになる。社内ルール(.claude/rules/estat-api.md)でも 5 桁固定を推奨している。
出力 JSON はこんな構造になる。
{
"meta": {
"statsDataId": "0003411111",
"indicator": "人口一人当たり国民医療費",
"unit": "千円",
"year": "2023年度",
"fetchedAt": "2026-05-22T10:30:00+09:00"
},
"data": [
{ "areaCode": "01000", "areaName": "北海道", "value": 425.3 },
{ "areaCode": "02000", "areaName": "青森県", "value": 415.8 },
{ "areaCode": "13000", "areaName": "東京都", "value": 372.6 },
{ "areaCode": "47000", "areaName": "沖縄県", "value": 392.1 }
]
}
47 行きっちり揃っているか、jq '.data | length' で確認しておこう。1 件でも欠けると地図に「白い穴」が空く。
Step 3: 地域コード(5桁)と都道府県名のマッチング — id 結合の罠
ここが コロプレス地図でいちばん事故りやすいポイントだ。
| データ側 | 地図側 (dataofjapan/land) | 形式 | マッチ方法 |
|---|---|---|---|
areaCode: "01000" | id: 1 | 文字列 vs 数値 | parseInt して数値同士で比較 |
areaCode: "13000" | id: 13 | 5桁文字列 vs 1〜47 整数 | parseInt(areaCode.slice(0, 2), 10) |
areaName: "北海道" | properties.nam_ja: "北海道" | 文字列 vs 文字列 | 完全一致だが表記揺れ注意 |
結論: areaCode を parseInt(code.slice(0, 2), 10) で 1〜47 の整数に変換して、TopoJSON の id と結合するのがいちばん安全だ。都道府県名で結合すると「神奈川県」vs「神奈川」、「東京都」vs「東京」のような表記ゆれで詰む。
Claude Code に頼むときは、この変換ルールを明示しておく。
あなた:
data/medical-cost-per-capita.json を読み込んで、
{ [prefId: number]: { name: string, value: number } } という
Map に整形してください。prefId は areaCode の上2桁を数値化したもの。
Claude Code:
const raw = await Bun.file("data/medical-cost-per-capita.json").json();
const valueMap = new Map<number, { name: string; value: number }>();
for (const row of raw.data) {
const prefId = parseInt(row.areaCode.slice(0, 2), 10);
valueMap.set(prefId, { name: row.areaName, value: row.value });
}
return valueMap;
この Map を地図描画関数に渡せば、feature.id で値を引けるようになる。
Step 4: D3-geo で地図を描画する
ここから D3 の世界。インストールは以下の 3 パッケージで足りる。
npm i d3-geo d3-scale d3-array topojson-client
npm i -D @types/d3-geo @types/d3-scale @types/d3-array @types/topojson-client
d3-geo の核心は projection (投影法) だ。地球は球体だが画面は平面なので、必ず何らかの投影が必要になる。日本地図でよく使う候補を比較する。
| Projection | 関数名 | 特徴 | 日本地図での印象 |
|---|---|---|---|
| Mercator | geoMercator | Web 地図のデファクト。高緯度で面積が誇張される | 北海道がやや大きく見える |
| Conic Equal Area | geoConicEqualArea | 円錐図法。面積が正確 | 自然で、面積比較に向く |
| Equirectangular | geoEquirectangular | 緯度経度を直接 x/y にマップ | 単純だが歪みが目立つ |
| Mercator (center 調整) | geoMercator + center | 中心と縮尺を手動指定 | 日本列島に最適化可能 |
コロプレス地図で「西高東低」のような面積比較をしたいなら geoConicEqualArea が最適だ。Mercator は北に行くほど面積が拡大されるので、北海道が実態以上に「医療費総額の大きさ」を演出してしまう。一人当たり指標なら Mercator でも誤解は起きづらいが、無難に Conic で行くのが本記事の推奨だ。
完全実装はこんな具合になる。
// src/components/MedicalCostChoropleth.jsx
import { useEffect, useRef } from "react";
import { geoConicEqualArea, geoPath } from "d3-geo";
import { scaleQuantize } from "d3-scale";
import { interpolateReds } from "d3-scale-chromatic";
import { feature } from "topojson-client";
export function MedicalCostChoropleth({ topology, valueMap }) {
const svgRef = useRef(null);
const width = 800;
const height = 600;
useEffect(() => {
if (!svgRef.current || !topology) return;
// TopoJSON -> GeoJSON FeatureCollection
const japan = feature(topology, topology.objects.japan);
// 日本列島を中心に投影
const projection = geoConicEqualArea()
.parallels([34, 40]) // 日本の南北の代表緯度
.rotate([-135, 0]) // 中心経度 135度(明石)
.center([0, 36]) // 中心緯度 36度(関東〜中部)
.scale(2400)
.translate([width / 2, height / 2]);
const pathGenerator = geoPath(projection);
// 医療費の min/max からカラースケールを構築
const values = japan.features
.map((f) => valueMap.get(f.id)?.value)
.filter((v) => v != null);
const min = Math.min(...values);
const max = Math.max(...values);
const colorScale = scaleQuantize()
.domain([min, max])
.range([
"#fee5d9", "#fcbba1", "#fc9272",
"#fb6a4a", "#ef3b2c", "#cb181d", "#99000d",
]);
// SVG 描画(生 DOM 操作。React で書くなら map に置き換え)
const svg = svgRef.current;
svg.innerHTML = "";
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
for (const f of japan.features) {
const value = valueMap.get(f.id)?.value;
const fill = value != null ? colorScale(value) : "#eee";
const path = document.createElementNS(
"http://www.w3.org/2000/svg", "path"
);
path.setAttribute("d", pathGenerator(f));
path.setAttribute("fill", fill);
path.setAttribute("stroke", "#fff");
path.setAttribute("stroke-width", "0.5");
path.setAttribute("data-pref-id", String(f.id));
path.setAttribute("data-value", String(value ?? ""));
svg.appendChild(path);
}
}, [topology, valueMap]);
return <svg ref={svgRef} className="w-full h-auto" />;
}
ここでのデザイン判断ポイントは 3 つだ。
parallels([34, 40]): 日本列島の南北を覆う代表緯度を 2 本指定する。緯度に近いほど歪みが小さくなるrotate([-135, 0]): 中心経度 135 度(兵庫県明石市)に座標系を回転。負の値なので注意scale(2400): 縮尺。画面幅に応じて 1500〜3000 程度で調整する
このパラメータも Claude Code に「日本列島を 800x600 の SVG にきれいに収めて」と頼めば、上記のような値を提案してくれる。「日本地図のための魔法の数字」を覚える必要はない。
Step 5: 都道府県を医療費でカラー塗りする
カラースケールの設計は、コロプレス地図の「読みやすさ」を 80% 決める要素だ。やってしまいがちな失敗を 3 つ挙げる。
- 連続スケール(
scaleLinear+ interpolateReds)で塗る → 微妙な差が人間の目で識別できない - 均等区間(
scaleQuantize)で 5 段階に塗る → 外れ値(高知・東京)に引っ張られて中間が潰れる - 七色のレインボーで塗る → どっちが大きいのか直感的に分からない
おすすめは scaleQuantize で 7 段階に区切り、単色グラデーション(Reds, Blues, Greens)を使うこと。今回の医療費なら「赤=高い、白=低い」が自然だ。
外れ値対策としては、scaleQuantile(パーセンタイル区切り)を使う手もある。scaleQuantize が「値域を等分」するのに対し、scaleQuantile は「データの数を等分」するため、外れ値の影響を受けにくい。47都道府県のような少数データセットでは scaleQuantize で十分なケースが多いが、極端な突出県がある場合は scaleQuantile か手動の閾値配列を検討する。
// 外れ値に強いパーセンタイルベース
import { scaleQuantile } from "d3-scale";
const colorScale = scaleQuantile()
.domain(values) // 全データを渡す
.range(["#fee5d9", "#fcbba1", "#fc9272", "#fb6a4a", "#ef3b2c", "#cb181d", "#99000d"]);
凡例も必ず付ける。色だけでは「どの色が何千円か」が分からない。SVG 内に小さな矩形を 7 個並べて、colorScale.thresholds() で取得した閾値を下に書くだけだ。
// 凡例描画(抜粋)
const legend = svgRef.current.querySelector("g.legend") ?? createLegendGroup();
const thresholds = colorScale.thresholds?.() ?? [];
const swatchW = 30, swatchH = 12;
colorScale.range().forEach((color, i) => {
// 色矩形
const rect = createSvgEl("rect", {
x: 600 + i * swatchW, y: 540,
width: swatchW, height: swatchH, fill: color,
});
legend.appendChild(rect);
// 閾値ラベル(右側のみ)
if (i < thresholds.length) {
const t = createSvgEl("text", {
x: 600 + (i + 1) * swatchW, y: 540 + swatchH + 10,
"font-size": 9, "text-anchor": "middle",
});
t.textContent = Math.round(thresholds[i]).toString();
legend.appendChild(t);
}
});
Step 6: ツールチップで詳細表示
コロプレス地図で「だいたいの傾向」は色から伝わる。だが「東京は実際いくらなの?」「島根は?」といった具体値は、ホバーで出すツールチップで補う。
SVG 内に <title> 子要素を入れるだけでも、ブラウザネイティブのツールチップは出る。最小実装としてはこれで十分だ。
// path の中に <title> を入れる
const titleEl = document.createElementNS(
"http://www.w3.org/2000/svg", "title"
);
titleEl.textContent = value != null
? `${valueMap.get(f.id).name}: ${value.toLocaleString()} 千円`
: `${valueMap.get(f.id)?.name ?? "—"}: データなし`;
path.appendChild(titleEl);
リッチなツールチップ(カスタム HTML、グラフ画像、複数指標)を出したい場合は、mousemove イベントで絶対配置の <div> を表示する形に拡張する。pointer-events: none を忘れずに付けないと、ツールチップ自体がホバー判定を奪って点滅する。
つまずきポイント 3 選
ここまでがハッピーパス。実際にやると詰まる落とし穴を 3 つ挙げる。
1. TopoJSON のサイズ問題
dataofjapan/land の生 TopoJSON は約 250KB だが、本番では Brotli/Gzip 圧縮で 60-80KB になるので意外と許容範囲だ。ただし「市区町村レベル」「世界地図に日本を重ねたい」となるとサイズが数 MB に跳ね上がる。
| ケース | データ量 | 対応策 |
|---|---|---|
| 都道府県 47 のみ | 〜250KB | そのまま使える |
| 市区町村 1741 含む | 5〜20MB | mapshaper で 10% 程度に simplify、地域別に分割配信 |
| 世界地図 + 日本詳細 | 50MB+ | ベクタータイル化 (Mapbox / MapLibre) を検討 |
mapshaper の -simplify でほとんどの場合は解決する。Claude Code に「サイズを 50KB 以下にして」と頼むと、適切な percentage を試行錯誤してくれる。
2. Visvalingam vs ダグラス・ピューカー
簡素化アルゴリズムは大きく 2 系統ある。
- Visvalingam-Whyatt (weighted area): 「面積が小さい三角形の頂点を削る」発想。自然な見た目を保ちやすい
- Douglas-Peucker (dp): 「直線からの距離が小さい頂点を削る」発想。シャープに直線化される
コロプレス地図のように 見た目重視 なら Visvalingam 一択。地形図のように 直線性を保ちたい ならダグラス・ピューカー。迷ったら Visvalingam にしておけば良い。
3. Mercator は北海道を大きく見せる
geoMercator は緯度が高いほど面積が拡大される投影法だ。日本国内であっても、北海道(北緯 41〜45 度)と沖縄(北緯 24〜27 度)では地図上の見え方が変わる。
- Web 地図風の見た目で OK →
geoMercator - 面積比較を厳密に →
geoConicEqualArea(Albers 系) - 緯度経度をそのまま →
geoEquirectangular(あまり推奨しない)
医療費の「総額」を見せるなら面積感が誤解を生むので Conic、「一人当たり」なら色で語るので Mercator でも問題ない、というのが個人的な使い分けだ。
番外: 北海道・沖縄の配置
本格的な日本地図では、沖縄を本州の左下にインセット表示するレイアウトをよく見る。dataofjapan/land の TopoJSON は実際の経度通りに沖縄が右下に配置されているので、デフォルトでは横長の地図になる。
インセット表示が必要なら、沖縄だけ別の projection で描画して合成するか、TopoJSON 自体を編集して座標を移動する。本記事では「素直に表示」の方針で、本州の南西に沖縄が配置される横長レイアウトのまま採用している。インセット化は Claude Code に「沖縄を本州の左下に移動した形で SVG を組んで」と頼めば、レイヤー合成のコードを返してくれる。
次回予告: Part 6 散布図で多次元データ
ここまでで「1 つの指標を地理的に塗り分ける」までができた。次の Part 6 では、「2 つの指標の相関を散布図で見る」 ことに踏み込む。たとえば「高齢化率 × 医療費」を 47 都道府県の点で打てば、強い正の相関が一目で分かる。回帰直線も Claude Code に引かせる。
予定トピック:
- e-Stat から異なる統計表 2 つを取得して結合する
d3-scaleの linear / log の使い分け- ラベル衝突(point label overlap)の回避テクニック
- 相関係数を計算して表示する(Pearson / Spearman)
地図と散布図が揃えば、ブログ記事の 「探索 → 検証」のセット が組めるようになる。Part 5(地図)で「地理的傾向に気付く」、Part 6(散布図)で「相関で裏付ける」、という流れだ。
関連ランキング・記事
- 都道府県別人口一人当たり国民医療費ランキング(本記事のデータ元、socialsecurity カテゴリ)
- 後期高齢者医療費ランキング(高齢化率との相関を読み解く)
- 高齢化率ランキング(医療費との相関分析の入口)
- Part 4: 高齢化率ヒートマップ|時系列 × 都道府県の二次元データ
- Part 6: 県民所得と教育費の散布図|相関係数まで一気通貫 Claude Code
地理データは「専門家の領域」と思われがちだが、TopoJSON + d3-geo の組み合わせさえ理解できれば、コロプレス地図の制作コストは棒グラフと大差ない。むしろ「地図にすることで初めて見えてくる傾向」が多いため、統計コンテンツの引き出しを増やす意味で投資価値は高い。
Claude Code は projection の選択・simplify の調整・凡例レイアウト・ツールチップ実装といった「面倒だが定型的な作業」を引き受けてくれる。人間側は「どの指標を地図にすると面白いか」「どの投影法が誤解を生まないか」というメタな判断に集中できる。これが AI 時代のデータビジュアライゼーション制作だ。