1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

国のオープンデータで「人口減少シミュレーター」を作った話 — e-Stat APIとIPSS推計の実践活用

1
Last updated at Posted at 2026-04-20

国のオープンデータで「人口減少シミュレーター」を作った話 — e-Stat APIとIPSS推計の実践活用

はじめに

「2050年、あなたの町の人口は何人になっているか?」——この疑問に答える静的サイトを、政府が公開しているオープンデータだけで作りました。対象は全国1,741市区町村すべて。サーバーは持たず、ビルド時に全データをJSONへ焼き固めてS3 + CloudFrontから配信しています。

👉 公開サイト: https://jinkou.keydrop.net/

本記事では、以下2つの日本の代表的な政府オープンデータをどう組み合わせて実用サイトに落とし込んだかを、コード付きで紹介します。

  • e-Stat API(政府統計の総合窓口)— 国勢調査の実績値
  • IPSS(国立社会保障・人口問題研究所)— 将来推計人口(令和5年推計)

なぜオープンデータか

人口推移は行政・不動産・教育・医療など幅広い領域で意思決定の基礎になる情報です。しかし総務省統計局の生データはExcelやAPIの形式で提供されており、一般ユーザーが「自分の町の未来」として触れられる形にはなっていません。

ここに**「生データ → 使えるUI」への変換レイヤー**という、エンジニアが提供できる価値があります。

データソース

1. e-Stat API(実績値)

政府統計ポータル e-Stat はREST APIを無料で提供しています。アプリケーションIDを取得すれば誰でも利用可能です。

今回使った統計表:

テーブルID 内容
0003433219 2020年国勢調査 男女別人口
0003445162 2020年国勢調査 年齢5歳階級 × 男女 × 市区町村

2. IPSS 日本の地域別将来推計人口(令和5年推計)

IPSSは5年ごとに市区町村別の将来推計を公表しています。APIは無く、Excelファイル (.xlsx) としてのみ公開されています。2050年までの7時点(2020〜2050、5年刻み)の総人口と年齢5歳階級別人口が得られます。

アーキテクチャ

ポイントは**「ランタイムでAPIを叩かない」**こと。オープンデータは更新頻度が低いので、ビルド時に全データを取得・変換してJSONへ固めてしまい、あとは静的配信するだけで十分です。

IPSS Excel  ──┐
              ├─► scripts/build-data.ts ─► data/generated/city/{code}.json (1,741ファイル)
e-Stat API  ──┘                        ─► ranking.json / search-index.json
                                       ─► next build (output: 'export')
                                       ─► S3 + CloudFront

技術スタック:Next.js 15 (App Router, Static Export) + TypeScript + Recharts + Tailwind CSS 4

実装のポイント

ポイント1: e-Stat APIのページネーション

e-StatのAPIは1リクエスト10万レコードが上限です。年齢5歳階級 × 男女 × 1,741市区町村だと軽く超えるので、NEXT_KEYを見ながらループする必要があります。

async function fetchAllData(statsDataId: string, extraParams = ""): Promise<EStatValue[]> {
  const allValues: EStatValue[] = [];
  let startPosition = 1;

  while (true) {
    const url = `${BASE_URL}/getStatsData?appId=${APP_ID}`
      + `&statsDataId=${statsDataId}${extraParams}`
      + `&startPosition=${startPosition}&limit=100000`;

    const data = await fetchWithRetry(url);
    const statRoot = data.GET_STATS_DATA?.STATISTICAL_DATA;
    const values = statRoot?.DATA_INF?.VALUE ?? [];
    if (values.length === 0) break;

    allValues.push(...values);

    // NEXT_KEY は RESULT_INF 内にある(ここハマりどころ)
    const nextKey = statRoot?.RESULT_INF?.NEXT_KEY;
    if (!nextKey) break;
    startPosition = nextKey;
    await sleep(500); // レート制限対策
  }
  return allValues;
}

ハマりポイント:NEXT_KEYDATA_INF ではなく RESULT_INF の中にあります。ドキュメントを注意深く読まないと、1ページ目で終わってしまいます。

ポイント2: 開発中のキャッシュ戦略

APIを叩くたびに数分待たされると開発効率が落ちるので、取得結果をローカルファイルにキャッシュします。

function cacheGet(key: string): unknown | null {
  const p = path.join(CACHE_DIR, `${key}.json`);
  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf-8")) : null;
}

これで2回目以降のnpm run build:dataは数秒で終わります。キャッシュディレクトリは.gitignoreに入れてリポジトリを太らせないようにします。

ポイント3: 市区町村コードの正規化

市区町村コードは5桁ゼロパディングの文字列として統一します。数値で持つとリーディングゼロが落ちる(北海道札幌市中央区の011011101になる)ので要注意。

また、e-Statの返却データには以下が混ざってきます:

  • 00000(全国合計)
  • 01000(都道府県合計)
  • 0120Bのような旧市区町村コード(合併前の町村)

これらを除外するバリデーションが必要です:

function isValidMunicipalityCode(code: string): boolean {
  if (code.length !== 5) return false;
  if (code === "00000") return false;
  if (code.endsWith("000")) return false;     // 都道府県合計
  if (/[A-Z]/.test(code)) return false;        // 旧コード
  return true;
}

ポイント4: IPSSのExcelパース

IPSSのkekkahyo1.xlsxは、ヘッダー4行+データ領域という構造。SheetJSで配列として読み、オフセットを手で指定します。

import * as XLSX from "xlsx";

const wb = XLSX.readFile("data/raw/ipss/kekkahyo1.xlsx");
const ws = wb.Sheets[wb.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json<(string | number | null)[]>(ws, {
  header: 1,
  defval: null,
});

// Row 5以降がデータ。Col0=コード, Col4-10=2020-2050年総人口
for (let i = 5; i < rows.length; i++) {
  const code = normalizeCode(rows[i][0]);
  if (!code || code.endsWith("000")) continue;
  // ... 年次データの取り込み
}

政府のExcelはマージセル・結合ヘッダー・小計行が多用されていて機械処理に向きません。シートの生構造を一度画面で確認してからオフセットを決めるのが確実です。

ポイント5: 年齢階級の粒度調整

e-Statは100歳以上まで5歳刻みで取れますが、UI上で21区分を並べるとグラフが潰れます。90歳以上は1グループに集約しました:

if (label === "90-94" || label === "95-99" || label === "100+") {
  acc90.male += g.male;
  acc90.female += g.female;
}

IPSS側は最初から「95+」区分なので、そちらに合わせる判断もあります。データソースごとの粒度の違いを事前に揃えておくのが、あとで結合で苦しまないコツです。

法的・運用上の注意

政府オープンデータは基本的に再利用可能ですが、出典表示は必須です。各ページのフッターに以下を明記しています:

  • e-Stat: 「このサービスは、政府統計の総合窓口(e-Stat)のAPI機能を使用していますが、サービスの内容は国によって保証されたものではありません。」
  • IPSS: 「出典:国立社会保障・人口問題研究所「日本の地域別将来推計人口(令和5年推計)」」

また、IPSSの推計は2050年までしか公表されていません。それ以降を外挿で埋める場合は「参考値・不確実性あり」と明示することが、誠実さとしても、将来の更新作業を忘れないためにも重要です。

まとめ

論点 採用した判断
ランタイムAPI呼び出し しない(ビルド時に全変換→静的JSON)
ホスティング S3 + CloudFront(月数十円)
データ更新 GitHub Actionsで定期リビルド
コスト ドメイン代+AWSの小銭のみ

政府オープンデータは「形式が古い・量が多い・前処理が面倒」という三重苦がありますが、一度パイプラインを組めば静的サイトで無限にスケールします。e-StatとIPSSは地域課題の可視化にきわめて相性が良いので、自治体ダッシュボードや不動産分析など、応用の余地は広いと感じます。

次は住民基本台帳の移動報告(転入転出)と組み合わせて、将来推計の精度を補正するプロトタイプに挑戦予定です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?