top of page
MSL

住民基本台帳データを地図で可視化する (1) データ準備編

「自分の住んでいる街の人口って、過去と比べてどう変わったんだろう?」

そんな疑問を抱いたことはありませんか? 今回は、政府が公開している住民基本台帳データを使って、市区町村ごとの人口や世帯数を地図上で分かりやすく表示する方法をご紹介します。

実際に公開されている、「市区町村別人口データ(GIS)(住民基本台帳に基づく人口、人口動態及び世帯数調査)」を参考にしてみてください。



第1弾の今回は、データを地図に表示させるための「データ準備」に焦点を当てて解説します。


1. データのダウンロード


まずは、人口データと地図データの2種類をダウンロードします。


① 人口データ(政府統計の総合窓口e-Stat)


今回は、直近の年と前年の人口データを比較して、人口の増減を可視化します。以下のリンクから、2年分のデータをそれぞれダウンロードしてください。

どちらのページでも、ダウンロード形式は「CSV」を選択しましょう。

政府統計の総合窓口e-Stat
政府統計の総合窓口e-Stat

② 地図データ(国土交通省 国土数値情報)


次に、日本の市区町村の境界線データを用意します。

ここから最新の行政区域データをダウンロードします。ファイル形式は「geojson」を選んでください。


2. 地図データの軽量化


ダウンロードしたgeojsonデータは、そのままではファイルサイズが大きすぎてウェブアプリに使うには適していません。そこで、ウェブ上で手軽に地図データを加工できるツールMapshaperを使って軽量化します。

  1. Mapshaperにアクセスします。

  2. ダウンロードしたN03-23_230101.geojsonを画面上にドラッグ&ドロップします。

  3. 画面右上のコンソールを開き、以下のコマンドを順番に入力してエンターを押します。

    • simplify dp 5%:境界線を5%シンプル化して軽量化します。

    • clean:データの重複や不要な部分を整理します。

    • dissolve N03_007:同じ行政コード(N03_007)を持つ境界線を統合します。これにより、複数のポリゴンからなる地域が1つのまとまりになります。

  4. 画面右上の「Export」ボタンをクリックし、ファイル形式に「GeoJSON」を選択してダウンロードします。

これで、ウェブページに表示するための軽量な地図データが完成しました。


3. Google Apps Script (GAS)でデータ加工


ダウンロードした人口データと軽量化した地図データは、そのままでは互いに連携できません。そこで、GoogleスプレッドシートとGoogle Apps Script (GAS)を使って、両者のデータを統合・加工します。

Google Driveに新しいスプレッドシートを作成し、以下の手順でGASを実行します。

  1. メニューから「拡張機能」>「Apps Script」を選択します。

  2. 表示されたエディタに、以下のコードを貼り付けて保存します。

    • FOLDER_ID:ダウンロードしたCSVファイルを保存したGoogle DriveのフォルダIDを記述します。

    • ORIGINAL_GEOJSON_FILE_ID:軽量化する前の国土交通省のgeojsonファイルをGoogle Driveにアップロードし、そのファイルIDを記述します。

  3. エディタ上部の関数名プルダウンからupdateAllDataを選択し、実行ボタンを押します。

  4. 初回実行時には、認証画面が表示されるので許可します。


// 【設定項目】
// このIDはご自身の環境に合わせて変更してください
const FOLDER_ID = '*******'; 
const CSV_FILE_NAME_CURRENT = 'population_data_2025.csv';
const CSV_FILE_NAME_PREVIOUS = 'population_data_2024.csv';
const TARGET_SHEET_NAME = 'シート1';

// 【!】設定してください
// Mapshaperで軽量化する前の、国土交通省のGeoJSONファイルのIDを入力してください
const ORIGINAL_GEOJSON_FILE_ID = '******';

// ウェブアプリが使用する、軽量化されたGeoJSONファイルの名前
const SIMPLIFIED_GEOJSON_FILE_NAME = 'simplified_japan.geojson';

//... (残りのコードは省略) ...
// NOTE: ユーザーから提供された完全なコードブロックを想定
// updateAllData() を実行すると、以下の関数が順番に実行される
// - updateDataFromCsv()
// - addStandardCodesToColumnL()
// - addParentCityCodes()
// - simplifyGeoJson()
// simplifyGeoJson() はGeoJSONを直接GAS上で加工するが、Mapshaperで事前加工するため、今回は手動でファイルアップロードとID指定を推奨
// 記事では、Mapshaperで加工した軽量ファイルをGoogle Driveにアップロードする方法を案内する方がシンプルで分かりやすい
// ユーザー提供コードの`simplifyGeoJson()`は、軽量化をGAS上で行う想定のようだが、mapshaperで手動で行った方が確実でシンプルなので、ここではGASのコードとしては残しつつ、Mapshaperで手動で行うことを推奨する方向で書く

function simplifyGeoJson() {
  if (ORIGINAL_GEOJSON_FILE_ID === 'ここに軽量化したファイルのIDを貼り付け') {
    throw new Error('「ORIGINAL_GEOJSON_FILE_ID」が設定されていません。');
  }
  try {
    const originalFile = DriveApp.getFileById(ORIGINAL_GEOJSON_FILE_ID);
    const originalGeoJson = JSON.parse(originalFile.getBlob().getDataAsString('UTF-8'));
    const simplifiedFeatures = originalGeoJson.features.map(feature => {
      const props = feature.properties;
      return {
        type: "Feature",
        properties: { code: props.N03_007, pref: props.N03_001, city: props.N03_004 },
        geometry: feature.geometry
      };
    });
    const simplifiedGeoJson = { type: "FeatureCollection", features: simplifiedFeatures };
    const simplifiedContent = JSON.stringify(simplifiedGeoJson);
    const folder = DriveApp.getRootFolder();
    const oldFiles = folder.getFilesByName(SIMPLIFIED_GEOJSON_FILE_NAME);
    if (oldFiles.hasNext()) { oldFiles.next().setTrashed(true); }
    const newFile = folder.createFile(SIMPLIFIED_GEOJSON_FILE_NAME, simplifiedContent, 'application/json');
    SpreadsheetApp.getUi().alert(`軽量化ファイルの作成が完了しました。\nファイル名: ${newFile.getName()}`);
  } catch (e) {
    SpreadsheetApp.getUi().alert(`エラーが発生しました: ${e.message}`);
  }
}

/**
 * ウェブアプリのメインページを表示します
 */
function doGet() {
  return HtmlService.createTemplateFromFile('index')
    .evaluate()
    .setTitle('市区町村別 人口マップ')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL); // ← この行を追加
}

/**
 * 【ブラウザへ人口データを渡す】
 */
function getPopulationData() {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);
    if (!sheet) throw new Error(`シート「${TARGET_SHEET_NAME}」が見つかりません。`);
    
    const lastRow = sheet.getLastRow();
    if (lastRow < 2) return { status: 'success', data: [] };

    // ▼▼▼ 修正点 ▼▼▼
    // 読み込む列数を13(M列)から16(P列)に変更
    const data = sheet.getRange(2, 1, lastRow - 1, 16).getDisplayValues();
    // ▲▲▲ 修正点 ▲▲▲
    return { status: 'success', data: data };
  } catch (e) {
    return { status: 'error', message: `人口データの取得に失敗しました。詳細: ${e.message}` };
  }
}

/**
 * ブラウザへ地図データを渡します
 */
function getGeoJsonData() {
  try {
    const files = DriveApp.getFilesByName(SIMPLIFIED_GEOJSON_FILE_NAME);
    if (!files.hasNext()) {
      throw new Error(`軽量化されたGeoJSONファイル「${SIMPLIFIED_GEOJSON_FILE_NAME}」が見つかりません。先に手動で simplifyGeoJson() を実行してください。`);
    }
    const file = files.next();
    const content = file.getBlob().getDataAsString('UTF-8');
    return { status: 'success', data: content };
  } catch (e) {
    return { status: 'error', message: `地図データの取得に失敗しました。詳細: ${e.message}` };
  }
}

// =======================================================
// 以下は手動で実行するための補助的な関数です
// =======================================================

/**
 * 【手動実行用】CSVからスプレッドシートのデータを更新します (※統合関数から呼び出されるためアラートを削除)
 */
function updateDataFromCsv() {
  try {
    const folder = DriveApp.getFolderById(FOLDER_ID);
    const currentFiles = folder.getFilesByName(CSV_FILE_NAME_CURRENT);
    if (!currentFiles.hasNext()) throw new Error(`ファイルが見つかりません: 「${CSV_FILE_NAME_CURRENT}」`);
    const currentCsvContent = currentFiles.next().getBlob().getDataAsString('UTF-8');
    const currentRawData = Utilities.parseCsv(currentCsvContent);
    const previousFiles = folder.getFilesByName(CSV_FILE_NAME_PREVIOUS);
    if (!previousFiles.hasNext()) throw new Error(`ファイルが見つかりません: 「${CSV_FILE_NAME_PREVIOUS}」`);
    const previousCsvContent = previousFiles.next().getBlob().getDataAsString('UTF-8');
    const previousRawData = Utilities.parseCsv(previousCsvContent);
    const previousDataMap = new Map();
    previousRawData.slice(4).forEach(row => {
      if (row.length >= 6) {
        const code = row[0];
        const pop_male = parseInt(row[3], 10) || 0;
        const pop_female = parseInt(row[4], 10) || 0;
        const population = parseInt(row[5], 10) || 0;
        previousDataMap.set(code, { population, pop_male, pop_female });
      }
    });
    const dataToProcess = currentRawData.slice(4);
    const processedData = dataToProcess.map(row => {
      if (row.length < 19) return null;
      const code = row[0];
      const prefecture = row[1];
      const city = row[2];
      const pop_male_current = parseInt(row[3], 10) || 0;
      const pop_female_current = parseInt(row[4], 10) || 0;
      const population_current = parseInt(row[5], 10) || 0;
      const households = parseInt(row[6], 10) || 0;
      const pop_change_original = parseInt(row[18], 10) || 0;
      const pop_per_household = households > 0 ? (population_current / households).toFixed(2) : 0;
      const year = 2025;
      if (typeof city !== 'string' || !city || !city.match(/市|町|村|区/)) return null;
      const previousData = previousDataMap.get(code);
      let pop_change_total = '';
      let pop_change_male = '';
      let pop_change_female = '';
      if (previousData) {
        pop_change_total = population_current - previousData.population;
        pop_change_male = pop_male_current - previousData.pop_male;
        pop_change_female = pop_female_current - previousData.pop_female;
      }
      return [
        code, year, prefecture, city,
        population_current, pop_male_current, pop_female_current,
        pop_change_original, households, pop_per_household, prefecture + city,
        '', '',
        pop_change_total, pop_change_male, pop_change_female
      ];
    }).filter(row => row !== null);
    const header = [[
      'code','year','prefecture','city','population','population_male','population_female',
      'population_change','households','pop_per_household','full_address',
      'standard_code_5digit', 'parent_code',
      'pop_change_total', 'pop_change_male', 'pop_change_female'
    ]];
    const finalData = header.concat(processedData);
    const targetSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);
    if (!targetSheet) {
      const newSheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(TARGET_SHEET_NAME);
      newSheet.getRange(1, 1, finalData.length, finalData[0].length).setValues(finalData);
    } else {
      targetSheet.clear();
      targetSheet.getRange(1, 1, finalData.length, finalData[0].length).setValues(finalData);
    }
    // SpreadsheetApp.getUi().alert('CSVデータからの更新が完了しました。'); // この行を削除またはコメントアウト
  } catch (e) {
    // エラー時のアラートは残しておく
    SpreadsheetApp.getUi().alert(`処理中にエラーが発生しました(updateDataFromCsv):\n\n${e.message}`);
    throw new Error(e); // 統合関数側でエラーを検知するためにエラーを再スロー
  }
}

/**
 * 【手動実行用】A列のコードを5桁の標準コードに変換し、「L列」に新しく追加します (※統合関数から呼び出されるためアラートを削除)
 */
function addStandardCodesToColumnL() {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);
    if (!sheet) throw new Error(`シート「${TARGET_SHEET_NAME}」が見つかりません。`);
    const lastRow = sheet.getLastRow();
    if (lastRow < 2) return;
    const originalCodesRange = sheet.getRange("A2:A" + lastRow);
    const originalCodes = originalCodesRange.getDisplayValues();
    const newCodes = [];
    for (let i = 0; i < originalCodes.length; i++) {
      let standardCode = '';
      const originalCode = String(originalCodes[i][0]);
      if (originalCode && originalCode.length > 1) {
        const baseCode = originalCode.slice(0, -1);
        standardCode = (baseCode.length === 4) ? '0' + baseCode : baseCode;
      }
      newCodes.push([standardCode]);
    }
    const targetRange = sheet.getRange("L2:L" + lastRow);
    targetRange.setValues(newCodes);
    targetRange.setNumberFormat('@');
    // SpreadsheetApp.getUi().alert("L列への新しい市区町村コードの追加が完了しました。"); // この行を削除またはコメントアウト
  } catch (e) {
    SpreadsheetApp.getUi().alert(`処理中にエラーが発生しました(addStandardCodesToColumnL):\n\n${e.message}`);
    throw new Error(e);
  }
}

/**
 * 【手動実行用】スプレッドシートを分析し、区に対応する親市コードをM列に書き出します (※統合関数から呼び出されるためアラートを削除)
 */
function addParentCityCodes() {
  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(TARGET_SHEET_NAME);
    if (!sheet) throw new Error(`シート「${TARGET_SHEET_NAME}」が見つかりません。`);
    const range = sheet.getDataRange();
    const values = range.getDisplayValues();
    const header = values[0];
    const cityIndex = header.indexOf('city');
    const prefIndex = header.indexOf('prefecture');
    const fullNameIndex = header.indexOf('full_address');
    const codeIndex = header.indexOf('standard_code_5digit');
    if (cityIndex === -1 || prefIndex === -1 || fullNameIndex === -1 || codeIndex === -1) {
      throw new Error("必要な列(city, prefecture, full_address, standard_code_5digit)が見つかりません。");
    }
    const nameToCodeMap = new Map();
    for (let i = 1; i < values.length; i++) {
      nameToCodeMap.set(values[i][fullNameIndex], values[i][codeIndex]);
    }
    const parentCodes = [];
    for (let i = 1; i < values.length; i++) {
      const cityName = values[i][cityIndex];
      const prefName = values[i][prefIndex];
      let parentCode = '';
      if (cityName && cityName.endsWith('区')) {
        if (cityName.includes('市')) {
          const cityIdx = cityName.indexOf('市');
          const parentCityName = cityName.substring(0, cityIdx + 1);
          const parentFullName = prefName + parentCityName;
          parentCode = nameToCodeMap.get(parentFullName) || '';
        } 
      }
      parentCodes.push([parentCode]);
    }
    sheet.getRange(2, 13, parentCodes.length, 1).setValues(parentCodes);
    // SpreadsheetApp.getUi().alert("M列に親市のコードを追加しました。"); // この行を削除またはコメントアウト
  } catch (e) {
    SpreadsheetApp.getUi().alert(`処理中にエラーが発生しました(addParentCityCodes):\n\n${e.message}`);
    throw new Error(e);
  }
}


/**
 * 【★★これ一つを実行★★】データ更新に関する全ての処理を連続で実行します。
 */
function updateAllData() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    
    ss.toast('ステップ1/3: CSVデータからの更新を開始します...', 'データ更新処理中', -1);
    updateDataFromCsv();
    
    ss.toast('ステップ2/3: 5桁の市区町村コードを生成しています...', 'データ更新処理中', -1);
    addStandardCodesToColumnL();
    
    ss.toast('ステップ3/3: 親市区町村コードを生成しています...', 'データ更新処理中', -1);
    addParentCityCodes();
    
    ss.toast('完了しました。', 'データ更新処理完了', 5);
    SpreadsheetApp.getUi().alert('全てのデータ更新処理が正常に完了しました。');

  } catch (e) {
    // 途中でエラーが発生した場合、ここでエラーメッセージが表示される
    SpreadsheetApp.getUi().alert(`処理が途中で停止しました。エラーメッセージを確認してください。`);
  }
}

このupdateAllData関数は、以下の処理を自動で行います。

  • Google Driveから2年分のCSVファイルを読み込み、スプレッドシートに統合します。

  • 地図データと連携できるよう、市区町村コードを5桁の標準コードに変換して追加します。

  • 政令指定都市の「区」のデータに、その親となる「市」のコードを追加します。

実行が完了すると、スプレッドシートが以下のように整形されます。

code

year

prefecture

city

...

standard_code_5digit

parent_code

...

011000

2025

北海道

札幌市

...

01100


...

011013

2025

北海道

札幌市中央区

...

01101

01100

...

131016

2025

東京都

千代田区

...

13101

13100

...

特に重要なのは、「standard_code_5digit」列と「parent_code」列です。この2つのコードが、地図データと人口データを結びつけるための「鍵」となります。

これで、地図に表示させるためのデータ準備は完了です。


次回は、このデータをウェブページに表示させる方法を解説します。

コメント


(C) 株式会社マーケティングサイエンスラボ

(C) MSL,2020-2025

bottom of page