GeoJSONファイルの複数データをクロス集計して、Google Mapの地図上に表示してみた

# 集計方法
今回は駅を中心に半径2kmの円を範囲に、町丁目ポリゴンと複数の属性データから成るGeoJSONを読み込み、地図上で表示してみました。

# 集計データについて
今回は仮の事例「高価格帯の幼児教室の集客キャンペーンの場合」を想定して、乳幼児（6歳未満と想定）のいる世帯比率と年収700万円以上の世帯比率、この二つのデータを集計します。

乳幼児（6歳未満と想定）のいる世帯データは2020年国勢調査データ、年収700万円以上の世帯データの計算は年収別世帯数推計データを使用しました。
以下で登場する町丁目ポリゴンはマップマーケティングの[TerraMap API](https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_gmap_crossdata_c9b08c38c3d303b0975a_01)の行政界ポリゴンを使用しました。


# クロス集計
乳幼児（6歳未満と想定）のいる世帯比率と年収700万円以上の世帯比率をクロス集計して、地図上で表示した画面はこのように。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2916958/01612e3c-b89b-b23e-6ee2-868090f8d60c.png)

画面右の凡例のように、濃い緑のエリアは今回は集計された地域の中で、乳幼児（6歳未満と想定）のいる世帯比率と年収700万円以上の世帯比率どちらも高い地域です。薄い黄色のエリアは乳幼児（6歳未満と想定）のいる世帯比率と年収700万円以上の世帯比率どちらも低い地域です。

このようにクロス集計によって販促を仕掛けるべきエリアを発見することができます。

# コードについて
Maps JavaScript APIを使用します。
Google Map上のポリゴン表示は以下の記事をベースにしました。

https://qiita.com/yan_x/items/c069c8fb7c5d2648fe50

## データの計算
6歳以下世帯数の割合は国勢調査2020人口等データから6歳未満のいる一般世帯数データを取得し、一般世帯数データを割ったものです。
```math
6歳以下世帯数の割合 = 6歳未満のいる一般世帯数 \div 一般世帯数
```
年収700万以上の世帯数の割合は年収別世帯推計2020データから年収700～800万円世帯数、年収800～900万円世帯数、年収900～1000万円世帯数、年収1000～1250万円世帯数、年収1250～1500万円世帯数、年収1500～2000万円世帯数、年収2000万円以上世帯数データを取得し、その合計値で年収階級別世帯合計データを割ったものです。
```math
年収700万以上の世帯数の割合 = (年収700～800万円世帯数+年収800～900万円世帯数+年収900～1000万円世帯数+年収1000～1250万円世帯数+年収1250～1500万円世帯数+年収1500～2000万円世帯数+年収2000万円以上世帯数) \div 年収階級別世帯合計
```
```js
// 6歳以下世帯数を取得
function getUnder6Households(data) {
  const under6Item = data.find(item => item.stat_item_id === 15918);
  return under6Item ? parseInt(under6Item.value, 10) : 0;
}

// 6歳以下世帯数の割合を取得
function getUnder6HouseholdsPercentage(data) {
  const under6Households = getUnder6Households(data);
  const totalHouseholds = data.find(item => item.stat_item_id === 15884);
  const totalHouseholdsValue = totalHouseholds ? parseInt(totalHouseholds.value, 10) : 1;

  const percentage = (under6Households / totalHouseholdsValue) * 100;
  return `${percentage.toFixed(2)}`;
}

// 年収700万以上の世帯数を取得
function getIncomeAbove700(data) {
  const incomeItems = data.filter(item =>
    [17992, 17993, 17994, 17995, 17996, 17997, 17998].includes(item.stat_item_id)
  );
  return incomeItems.reduce((sum, item) => sum + parseInt(item.value, 10), 0);
}

// 年収700万以上の世帯数の割合を取得
function getIncomeAbove700Percentage(data) {
  const incomeAbove700 = getIncomeAbove700(data);
  const totalIncomeHouseholds = data.find(item => item.stat_item_id === 17985);
  const totalIncomeHouseholdsValue = totalIncomeHouseholds ? parseInt(totalIncomeHouseholds.value, 10) : 1;

  const percentage = (incomeAbove700 / totalIncomeHouseholdsValue) * 100;
  return `${percentage.toFixed(2)}`;
}
```
## 色塗り分けするコード
6歳以下世帯数はを5%と10%を境として、年収700万以上の世帯数は15%と35%を境として色塗り分けしています。
```js
// 6歳以下世帯数の割合と年収700万以上の世帯数の割合に基づく9色カラーマトリックス
function getColor(under6, incomeAbove700) {
  if (under6 < 5 && incomeAbove700 < 15) {
    return '#FFEDA0';  // 低・低
  } else if (under6 < 5 && incomeAbove700 <= 35) {
    return '#FED976';  // 低・中
  } else if (under6 < 5 && incomeAbove700 > 35) {
    return '#E69F00';  // 低・高
  } else if (under6 <= 10 && incomeAbove700 < 15) {
    return '#FD8D3C';  // 中・低
  } else if (under6 <= 10 && incomeAbove700 <= 35) {
    return '#FC4E2A';  // 中・中
  } else if (under6 <= 10 && incomeAbove700 > 35) {
    return '#B22222';  // 中・高
  } else if (under6 > 10 && incomeAbove700 < 15) {
    return '#A1D99B';  // 高・低
  } else if (under6 > 10 && incomeAbove700 <= 35) {
    return '#41AB5D';  // 高・中
  } else if (under6 > 10 && incomeAbove700 > 35) {
    return '#00441B';  // 高・高
  }
}
```
## JavaScript全体のサンプルコード
<details><summary>index.js</summary>

```js
// 6歳以下世帯数を取得
function getUnder6Households(data) {
  const under6Item = data.find(item => item.stat_item_id === 15918);
  return under6Item ? parseInt(under6Item.value, 10) : 0;
}

// 6歳以下世帯数の割合を取得
function getUnder6HouseholdsPercentage(data) {
  const under6Households = getUnder6Households(data);
  const totalHouseholds = data.find(item => item.stat_item_id === 15884);
  const totalHouseholdsValue = totalHouseholds ? parseInt(totalHouseholds.value, 10) : 1;

  const percentage = (under6Households / totalHouseholdsValue) * 100;
  return `${percentage.toFixed(2)}`;
}

// 年収700万以上の世帯数を取得
function getIncomeAbove700(data) {
  const incomeItems = data.filter(item =>
    [17992, 17993, 17994, 17995, 17996, 17997, 17998].includes(item.stat_item_id)
  );
  return incomeItems.reduce((sum, item) => sum + parseInt(item.value, 10), 0);
}

// 年収700万以上の世帯数の割合を取得
function getIncomeAbove700Percentage(data) {
  const incomeAbove700 = getIncomeAbove700(data);
  const totalIncomeHouseholds = data.find(item => item.stat_item_id === 17985);
  const totalIncomeHouseholdsValue = totalIncomeHouseholds ? parseInt(totalIncomeHouseholds.value, 10) : 1;

  const percentage = (incomeAbove700 / totalIncomeHouseholdsValue) * 100;
  return `${percentage.toFixed(2)}`;
}


// 6歳以下世帯数の割合と年収700万以上の世帯数の割合に基づく9色カラーマトリックス
function getColor(under6, incomeAbove700) {
  if (under6 < 5 && incomeAbove700 < 15) {
    return '#FFEDA0';  // 低・低
  } else if (under6 < 5 && incomeAbove700 <= 35) {
    return '#FED976';  // 低・中
  } else if (under6 < 5 && incomeAbove700 > 35) {
    return '#E69F00';  // 低・高
  } else if (under6 <= 10 && incomeAbove700 < 15) {
    return '#FD8D3C';  // 中・低
  } else if (under6 <= 10 && incomeAbove700 <= 35) {
    return '#FC4E2A';  // 中・中
  } else if (under6 <= 10 && incomeAbove700 > 35) {
    return '#B22222';  // 中・高
  } else if (under6 > 10 && incomeAbove700 < 15) {
    return '#A1D99B';  // 高・低
  } else if (under6 > 10 && incomeAbove700 <= 35) {
    return '#41AB5D';  // 高・中
  } else if (under6 > 10 && incomeAbove700 > 35) {
    return '#00441B';  // 高・高
  }
}

let map;
// マップの初期化
async function initMap() {
  // マップのインスタンスを非同期に作成
  const { Map } = await window.google.maps.importLibrary("maps");
  map = new Map(document.getElementById('map'), {
    zoom: 13,
    center: { lat: 35.651810175156164, lng: 139.63688794553596 }, 
  });

  // GeoJSON データを非同期にロード
  await map.data.loadGeoJson('area.json');

  const infoWindow = new google.maps.InfoWindow();

  // ポリゴンに色を塗る
  map.data.setStyle(function (feature) {
    const data = feature.getProperty('data');
    const under6 = getUnder6HouseholdsPercentage(data);
    const incomeAbove700 = getIncomeAbove700Percentage(data);

    const color = getColor(under6, incomeAbove700);
    
    return {
      fillColor: color,
      strokeWeight: 1
    };
  });

  map.data.addListener('mouseover', function(event) {
    const feature = event.feature;
    const data = feature.getProperty('data');

    // 6歳以下の世帯数を取得（stat_item_id: 15918）
    const under6Households = getUnder6Households(data);
    const under6HouseholdsPercentage = getUnder6HouseholdsPercentage(data);

    // 年収700万以上の世帯数を取得（stat_item_id: 17992 ~ 17998 の合計）
    const incomeAbove7M = getIncomeAbove700(data);
    const incomeAbove700Percentage = getIncomeAbove700Percentage(data);

    // 情報ウィンドウの内容を設定
    infoWindow.setContent(`
      <div>
        <strong>6歳以下の世帯数:</strong> ${under6Households} <br>
        <strong>6歳以下の世帯数割合:</strong> ${under6HouseholdsPercentage}% <br>
        <strong>年収700万以上の世帯数:</strong> ${incomeAbove7M} <br>
        <strong>年収700万以上の世帯数割合:</strong> ${incomeAbove700Percentage}%
      </div>
    `);

    // 情報ウィンドウを表示
    infoWindow.setPosition(event.latLng);
    infoWindow.open(map);
  });

  // マウスが外れたときに情報ウィンドウを閉じる
  map.data.addListener('mouseout', function() {
    infoWindow.close();
  });

  // 凡例を作成
  const legend = document.createElement('div');
  legend.innerHTML = `
  <div class="legend">
    <h3>Under 6 Households vs Income Above 7M</h3>
    <table class="legend-table">
      <tr>
        <th>High U6</th>
        <td><span class="color-box" style="background-color: #A1D99B;"></span></td> <!-- 高・低 (緑) -->
        <td><span class="color-box" style="background-color: #41AB5D;"></span></td> <!-- 高・中 (緑) -->
        <td><span class="color-box" style="background-color: #00441B;"></span></td> <!-- 高・高 (緑) -->
      </tr>
      <tr>
        <th>Mid U6</th>
        <td><span class="color-box" style="background-color: #FD8D3C;"></span></td> <!-- 中・低 -->
        <td><span class="color-box" style="background-color: #FC4E2A;"></span></td> <!-- 中・中 -->
        <td><span class="color-box" style="background-color: #B22222;"></span></td> <!-- 中・高 -->
      </tr>
      <tr>
        <th>Low U6</th>
        <td><span class="color-box" style="background-color: #FFEDA0;"></span></td> <!-- 低・低 -->
        <td><span class="color-box" style="background-color: #FED976;"></span></td> <!-- 低・中 -->
        <td><span class="color-box" style="background-color: #E69F00;"></span></td> <!-- 低・高 -->
      </tr>
      <tr>
        <th></th> <!-- 空のセル -->
        <th>Low Inc</th>
        <th>Mid Inc</th>
        <th>High Inc</th>
      </tr>
    </table>
  </div>
`;
  map.controls[google.maps.ControlPosition.RIGHT_BOTTOM].push(legend);
}

// ドキュメントの読み込み後にマップを初期化
window.onload = initMap;

```

</details>

<details><summary>style.css</summary>

```css
/* マップ全体を画面にフィットさせる */
#map {
    height: 100%;
}

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
}

.legend {
    background: white;
    padding: 10px;
    font-family: Arial, sans-serif;
    position: absolute;
    bottom: 20px;
    right: 20px;
    z-index: 5;
    border: 1px solid #ccc;
}

.legend-table {
    border-collapse: collapse;
    margin-top: 10px;
}

.legend-table th, .legend-table td {
    border: 1px solid #000;
    padding: 5px;
    text-align: center;
}

.color-box {
    display: inline-block;
    width: 20px;
    height: 20px;
}
```

</details>


index.jsコードで使用したJSONファイルの抜粋は以下になります。

<details><summary>area.json</summary>

```json
{
    "type": "FeatureCollection",
    "features": [
        {
            "type": "Feature",
            "properties": {
                "area": {
                    "area": 0.1715762426534826,
                    "ratio_area": [
                        0.02544194213949423
                    ],
                    "ratio": [
                        0.14828359536277455
                    ]
                },
                "data": [
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "320.44084957895580255"
                        ],
                        "stat_item_id": 15884,
                        "stat_id": "001012000",
                        "value": "2161"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "18.0905986342584951"
                        ],
                        "stat_item_id": 15918,
                        "stat_id": "001012000",
                        "value": "122"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "309.17129633138493675"
                        ],
                        "stat_item_id": 17985,
                        "stat_id": "007002000",
                        "value": "2085"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "14.828359536277455"
                        ],
                        "stat_item_id": 17992,
                        "stat_id": "007002000",
                        "value": "100"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "14.38350875018913135"
                        ],
                        "stat_item_id": 17993,
                        "stat_id": "007002000",
                        "value": "97"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "10.3798516753942185"
                        ],
                        "stat_item_id": 17994,
                        "stat_id": "007002000",
                        "value": "70"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "19.2768673971606915"
                        ],
                        "stat_item_id": 17995,
                        "stat_id": "007002000",
                        "value": "130"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "10.82470246148254215"
                        ],
                        "stat_item_id": 17996,
                        "stat_id": "007002000",
                        "value": "73"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "8.3038813403153748"
                        ],
                        "stat_item_id": 17997,
                        "stat_id": "007002000",
                        "value": "56"
                    },
                    {
                        "is_authorized": true,
                        "ratio_value": [
                            "3.2622390979810401"
                        ],
                        "stat_item_id": 17998,
                        "stat_id": "007002000",
                        "value": "22"
                    }
                ],
                "point_coordinates": [
                    139.65836,
                    35.64367
                ],
                "geocode": "13112000503",
                "points": [
                    [
                        "東京都",
                        "世田谷区",
                        "若林３丁目"
                    ]
                ]
            },
            "geometry": {
                "type": "MultiPolygon",
                "coordinates": [
                    [
                        [
                            [
                                139.660719195,
                                35.646073264
                            ],
                            [
                                139.660920669,
                                35.642591153
                            ],
                            [
                                139.660400811,
                                35.642397854
                            ],
```

</details>