TerraMap APIの学校区データや人口データを用いて、QGISで学校区内の小学生5年生の人口を作成し、学習塾でチラシをポスティング配布する際の販売促進の施策をシミュレーションしてみました。この記事を書くにあたって使用したのは QGIS 3.40 になります。
シミュレーション結果から紹介し、その後に学校区内の小学生人口や地図の作成方法などを記載しております。
学校区を活用した販促施策の地図イメージ
学校区を使った販促施策の地図画像は以下のようになります。
中央の⭐が塾の位置で、🟢が生徒のポイント、青いラインが塾周辺の小学校区ポリゴンです。小学校区のラベルには推計した5年生人口を表示し、円グラフの面積もその数に応じさせています。円グラフの緑部分が塾の生徒数であり、シェア率の可視化を行っています。
塾や生徒は架空データではありますが、作図結果からどんなチラシ配布の戦略が練れるか?考えてみました。
半径1kmの範囲に限れば、東側エリアに生徒を増やすポテンシャルがあるように見て取れます。
©OpenStreetMap contributors
実際にポスティングでチラシを配布する際には、町丁目と重ねあわせることも効果的です。町丁目は学区より細かく、また、配布エリアに町丁目を活用しているポスティングの事業者も多くあります。下図は町丁目のポリゴンに5年生人口の多い部分を濃い赤で色塗りしたコロプレスマップ1を重ねたものです。
©OpenStreetMap contributors
学校区内の小学生人口や地図の作成方法について
ここからは学校区内の小学生人口や地図の作成方法について説明していきます。
対象とする人口データは、小学生高学年のスタートである「5年生人口」(推計人口)としました。※ 記事に収める手順を簡略化するためにも1学年で限定させてもらいました。
また、学校ごとの各学年の生徒数データも存在しますが、この記事では5年生人口を作成することの応用性に重きを置きました。生徒数データを使ったケースは別の記事でご紹介したいと思います。
データソースについて
塾の位置、生徒データ
塾の位置および5年生の生徒データは、すべて架空の内容で用意しました。塾から半径2km円範囲内に115名の生徒、結果を分かりやすくするため規模の大きい塾を想定しています。
TerraMap API データ
TerraMap API は、行政界や学校区等のポリゴンや、その属性として各種統計データを提供するAPIです。中心位置(塾の位置)と円半径を指定して、APIから以下2種類のデータを取得し利用しました。
※ TerraMap APIのレスポンスデータはQGIS向けに加工しましたが、そのスクリプトは記事の最後に記載しております。
小学校区ポリゴン
塾周辺の小学校区ポリゴンです。この記事では、小学校区ポリゴンごとの生徒のシェア率を算出していきます。
TerraMap APIの学校区データの詳細に関しましては、以下をご参照下さい。
小学校区・中学校区データについて
町丁目ポリゴン + 5年生人口データ
塾周辺の町丁目ポリゴンで、属性として「5年生人口」を持たせています。小学校区ポリゴンよりも細かいエリアに分かれ、広い範囲で取得しています。主に計算に利用します。
「5年生人口」について
厳密には、TerraMap APIの「国勢調査2020高精度推計 / 5歳人口20S」を2025年現在の「10歳人口」、つまり「5年生人口」として扱っています。5年間の流入や流出などは加味せずに、簡易的に推計しました。
2025年現在のほとんどの人口データは、国勢調査2020の結果に基づいています。その中でも「国勢調査2020高精度推計」データは、弊社独自の推計を行い、0~19歳まで1歳ごとの人口データも提供しています。
QGISで使用した機能について
ここから先は、QGISの以下機能をそれぞれの目的とともにご紹介したいと思います。
| 機能名 | 目的 |
|---|---|
| ポリゴン内の点の数 | 学校区データに対して、塾の生徒数を追加する |
| フィールド計算機 | ポリゴンの面積フィールド等を追加する |
| 和集合 (union) と 集計 (aggregate) |
町丁目の5年生人口を、学校区データに変換する |
| ダイアグラム / 円グラフ | 学校区内の生徒数と非生徒数で、シェア率の円グラフを表示する |
小学校区ごとの人口データ作成
小学校区内の生徒数を追加
シェア率を求めるには、小学校区内の生徒数が必要になるので追加します。
ベクタ → Analysis Tools → ポリゴン内の点の数 ...
町丁目の面積を追加
小学校区の5年生人口は面積按分で算出するので、まずは町丁目の面積を追加します。
町丁目レイヤのプロパティ → フィールド 2 → フィールド計算機のボタン
元面積 = $area 3
町丁目データを小学校区データに変換
和集合(union)
和集合(union)を行うと、異なるポリゴンに関して、ポリゴンの重なっている部分を分解し、それぞれの属性を継承することができます。
「小学校区+生徒数」と「町丁目・5年生人口」の和集合レイヤを作成します。
ベクタ → Geoprocessing Tools → 和集合(union) ...
5年生人口の面積按分
分解されたポリゴンの面積を用いて、5年生人口の面積按分を行います。
和集合レイヤのプロパティ → フィールド → フィールド計算機のボタン
5年生人口・按分値 = 5年生人口 * ( $area / 元面積 )
小学校区での5年生人口の集計
和集合レイヤに関して、小学校区のジオコードでグループ化した5年生人口の集計を行います。また、この段階で町丁目の余分なフィールドは削除しました。
プロセシングツールボックス → aggregateで検索 → 集計
※ 集計の結果、学校区ではないポリゴン(ジオコードや学校名を持たない)も生成されましたが、それらは削除しました。
小学校区内の非生徒数を追加
最後は、円グラフに用いる小学校区内の非生徒数を追加します。
集計レイヤのプロパティ → フィールド → フィールド計算機のボタン
非生徒数 = 5年生人口・按分値 - 生徒数
小学校区ポリゴンと小学生人口を地図で可視化
加工されたデータの可視化については、ポイントとなる部分の設定例を提示させていただきます。
円グラフの表示
集計レイヤのプロパティ → ダイアグラム → 円グラフの選択
- 属性データの出力
- 大きさ
- 配置
※ 円グラフの配置設定によっては、ラベル表示が打ち消されてしまいます。ラベルの配置設定(デフォルト)と同一にすることで、ラベルが表示されないことを防いでいます。

ラベルの表示
集計レイヤのプロパティ → ラベル → 単一定義(single)の選択
補足
TerraMap API のレスポンスデータ加工について
TerraMap APIから取得される GeoJSON ファイルでは、属性値が深い階層にネストされているため、QGISでの取り扱いにはレイヤー単位での加工が必要です。
以下は今回使用したスクリプトです。FIELD_MAPPING や target_layer_name は、用途に応じて適宜変更してください。
from qgis.PyQt.QtCore import QVariant
from qgis.core import QgsField, QgsProject
# TerraMap APIのレスポンスデータをQGIS用に整形・更新するスクリプト
# stat_item_idと統計データ名称の対応テーブル
FIELD_MAPPING = {
"16399": "5年生人口", # 元の名称は「国勢調査2020高精度推計.5歳人口20S」
"16400": "6年生人口" # 元の名称は「国勢調査2020高精度推計.6歳人口20S」
}
def flat_list(lst):
result = []
for item in lst:
if isinstance(item, list):
result.extend(flat_list(item))
else:
result.append(str(item))
return result
target_layer_name = "町丁目・5年生人口" # 随時変更して下さい
layer = QgsProject.instance().mapLayersByName(target_layer_name)[0]
layer.startEditing()
try:
# === フィールドIDの決定 ===
all_field_ids = set()
for feature in layer.getFeatures():
if "data" in feature.fields().names():
data_list = feature["data"]
for item in data_list:
stat_item_id = str(item.get("stat_item_id"))
if stat_item_id:
field_name = FIELD_MAPPING.get(stat_item_id, stat_item_id) # マッピングがない場合はIDをそのまま使用
all_field_ids.add(field_name)
all_field_ids.add("address")
# === フィールド追加 ===
existing_fields = [field.name() for field in layer.fields()]
new_fields = []
for field_id in all_field_ids:
if field_id == "address":
if field_id not in existing_fields:
new_fields.append(QgsField(field_id, QVariant.String))
else:
if field_id not in existing_fields:
new_fields.append(QgsField(field_id, QVariant.Int))
if new_fields:
layer.dataProvider().addAttributes(new_fields)
layer.updateFields()
# === 属性設定 ===
for feature in layer.getFeatures():
fid = feature.id()
# data → stat_item_id:value
if "data" in feature.fields().names():
data_list = feature["data"]
for item in data_list:
stat_item_id = str(item.get("stat_item_id"))
value = item.get("value")
if stat_item_id and value is not None:
try:
int_value = int(value)
field_name = FIELD_MAPPING.get(stat_item_id, stat_item_id)
layer.changeAttributeValue(fid, layer.fields().indexOf(field_name), int_value)
except ValueError:
print(f"数値変換エラー: stat_item_id={stat_item_id}, value={value}")
# points → address
if "points" in feature.fields().names():
points = feature["points"]
if isinstance(points, list):
flat_points = flat_list(points)
address = "".join(flat_points)
layer.changeAttributeValue(fid, layer.fields().indexOf("address"), address)
layer.commitChanges()
except Exception as e:
print(f"エラーが発生したため変更を破棄します: {e}")
layer.rollBack()
- Pythonコンソールからの実行
プラグイン → Pythonコンソール















