1. はじめに
foliumを使って新潟県県央地域の路線バスの1日の動きを可視化してみたでは、foliumを用いてGTFSデータとGTFS-RT(リアルタイム)データを組み合わせ、越後交通および新潟県燕市・三条市のコミュニティバスの1日の運行状況を可視化しました。
今回はその延長として、せっかくなので他のオープンデータも活用し、燕市に関する別の側面を可視化してみたいと考えました。そこで取り組んだのが、燕市のこれまでの人口推移の可視化です。
バスに関連して関心を持った燕市ですが、同市は2006年に3つの市町村が合併して誕生しました。人口推移に関しては、当時の旧市町村ごとの区域別データが公開されていたため、それを活用し、合併前後を通じて旧市町村単位での人口の変化を可視化できるのではと考えました。
また、人口統計データは1920年までさかのぼることができますが、その当時は現在の合併前の3市町村もまだ存在せず、さらに小さな市町村に分かれていました。本記事では2006年の合併時の旧3市町村の区域を基準とし、1920年から現在までの人口推移を可視化することを目指しています。
2. 使用データと使用ツール
-
1920年~2006年までの人口データ
-
2016年~の人口データ
上記2つのデータをもとに合併時の旧3市町村の区域を基準とし、1920年から現在までの人口推移をpopulation.csvファイルにまとめました。
- 地図データ
3. アニメーションした結果
4. アニメーション化に用いたプログラム
👇地図データの生成
import folium, geopandas as gpd, pandas as pd, time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from pathlib import Path
BASE = Path(__file__).resolve().parent
SHAPE = BASE/"N03-05_15-g_AdministrativeBoundary.shp"
CSV = BASE/"population.csv"
HTML_DIR = BASE/"map_html"; HTML_DIR.mkdir(exist_ok=True)
PNG_DIR = BASE/"map_png"; PNG_DIR.mkdir(exist_ok=True)
pop = pd.read_csv(CSV).astype({"year":int,"population":int})
gdf = gpd.read_file(SHAPE, encoding="cp932")\
.query('N03_004 in ["燕市","吉田町","分水町"]')\
.rename(columns={"N03_004":"city"})
if gdf.crs is None: gdf = gdf.set_crs(epsg=4612)
gdf = gdf.to_crs(epsg=4326)
center = list(gdf.geometry.union_all().centroid.coords)[0][::-1]
colors = {"燕市":"#F15A22","吉田町":"#0A00CC","分水町":"#4AB905"}
html_files=[]
for yr in sorted(pop.year.unique()):
m = folium.Map(location=center, zoom_start=12, tiles="cartodbpositron")
folium.GeoJson(
gdf,
style_function=lambda f: {
"fillColor": colors[f["properties"]["city"]],
"color": "black","weight":1,"fillOpacity":0.6},
tooltip="city"
).add_to(m)
outfile = HTML_DIR/f"map_{yr}.html"
m.save(outfile); html_files.append(outfile)
# headless Chrome → PNG
opts = Options(); opts.add_argument("--headless"); opts.add_argument("--window-size=800,600")
driver = webdriver.Chrome(options=opts)
for html in html_files:
driver.get(html.resolve().as_uri()); time.sleep(1)
png = PNG_DIR/f"{html.stem}.png"
driver.save_screenshot(str(png)); print("map:", png.name)
driver.quit()
👇人口グラフの生成
import pandas as pd, matplotlib.pyplot as plt, matplotlib
from pathlib import Path
matplotlib.rcParams["font.family"] = "MS Gothic" # ← 環境に合わせて
BASE = Path(__file__).resolve().parent
CSV = BASE/"population.csv"
BAR_DIR = BASE/"bar_png"; BAR_DIR.mkdir(exist_ok=True)
df = pd.read_csv(CSV).astype({"year":int,"population":int})
city_order = ["燕市","吉田町","分水町"]
colors = {"燕市":"#F15A22","吉田町":"#0A00CC","分水町":"#4AB905"}
global_max = df.population.max()
for yr, grp in df.groupby("year"):
pops = grp.set_index("city").reindex(city_order)["population"].fillna(0)
plt.figure(figsize=(3,3))
plt.bar(city_order, pops, color=[colors[c] for c in city_order])
plt.ylim(0, global_max*1.05); plt.title(f"{yr} 年人口"); plt.ylabel("人口")
plt.xticks(rotation=30); plt.tight_layout()
out = BAR_DIR/f"bar_{yr}.png"; plt.savefig(out,dpi=150); plt.close()
print("bar:", out.name)
👇地図と人口グラフのマージ
from PIL import Image
from pathlib import Path
BASE = Path(__file__).resolve().parent
MAP_DIR, BAR_DIR = BASE/"map_png", BASE/"bar_png"
OUT_DIR = BASE/"frame"; OUT_DIR.mkdir(exist_ok=True)
for map_png in MAP_DIR.glob("map_*.png"):
yr = map_png.stem.split("_")[1]
bar_png = BAR_DIR/f"bar_{yr}.png"
if not bar_png.exists(): continue
map_img = Image.open(map_png).resize((800,600))
bar_img = Image.open(bar_png).resize((300,600))
combo = Image.new("RGB",(1100,600),(255,255,255))
combo.paste(map_img,(0,0)); combo.paste(bar_img,(800,0))
combo.save(OUT_DIR/f"frame_{yr}.png"); print("frame:", f"frame_{yr}.png")
👇上記2つのプログラムで作成したpngファイルをアニメーション化するプログラム
import os
import re
import shutil
import subprocess
def create_video_from_frames():
# 元のフォルダ(frameフォルダ内にpngファイルがある想定)
src_folder = "frame"
temp_folder = "temp_frames"
# frameフォルダの存在確認
if not os.path.exists(src_folder):
print(f"❌ エラー: {src_folder} フォルダが見つかりません。")
return
# 作業用フォルダを作成(既存なら削除して新規作成)
if os.path.exists(temp_folder):
shutil.rmtree(temp_folder)
os.makedirs(temp_folder)
# frameフォルダ内の frame_数字.png のファイルを抽出
files = [f for f in os.listdir(src_folder) if re.match(r'frame_\d+\.png', f)]
if not files:
print("❌ frameフォルダ内に frame_数字.png 形式のファイルが見つかりません。")
return
# 年代(数字)順にソート
files.sort(key=lambda x: int(re.findall(r'\d+', x)[0]))
print("年代順のファイルリスト:")
for i, filename in enumerate(files):
print(f" {i}: {filename}")
# 連番で一時フォルダにコピー
for i, filename in enumerate(files):
src_path = os.path.join(src_folder, filename)
dst_path = os.path.join(temp_folder, f"frame_{i:04d}.png")
shutil.copy2(src_path, dst_path)
print(f"\n{len(files)}枚のファイルを連番に変換しました。")
# ffmpegでMP4を作成
try:
cmd = [
"ffmpeg",
"-y", # 上書き許可
"-framerate", "1", # 1fps
"-i", f"{temp_folder}/frame_%04d.png",
"-c:v", "libx264", # H.264コーデック
"-pix_fmt", "yuv420p", # 互換性のあるピクセルフォーマット
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", # 偶数サイズに調整(歪み防止)
"population.mp4"
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print("\n🎉 MP4作成成功!")
print("population.mp4 が作成されました。")
else:
print("\n❌ MP4作成失敗:")
print(result.stderr)
except FileNotFoundError:
print("\n❌ ffmpegが見つかりません。")
print(f"\n作業用フォルダ '{temp_folder}' は残っています。")
if __name__ == "__main__":
create_video_from_frames()
5. まとめ
人口に関しては、グラフ化することで全体的な傾向を把握できるため、今回の動画では詳細な考察は行うつもりはありません。ただし、近年はやはり人口が徐々に減少してきていることが、動画を通しても確認できました。また1920年代からの人口推移を可視化しましたが、この地域では人口が劇的に増減することは少なく、比較的安定していたと考えられます。
また本来であれば、地図上に人口推移のグラフを重ねて表示したかったのですが、foliumの機能をうまく活用できず、最終的には地図とグラフを並列に示す形式の動画となりました。今後の課題としては、地図上にグラフを重ねて表示することで、より直感的に情報が伝わる動画を目指したいと考えています。また、他のオープンデータについても可視化を試みていく予定です。
注:
この記事は、以下の著作物を改変して利用しています。
- 新潟県人口時系列データ(市町村別): https://www.pref.niigata.lg.jp/site/tokei/1282075307357.html
- 燕市人口データデータ: https://www.city.tsubame.niigata.jp/soshiki/shimin_seikatsu/1/5/6/17143.html
CC BY 4.0 https://creativecommons.org/licenses/by/4.0/legalcode.ja
- 国土数値情報ダウンロードサイトより行政区域データ: https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-v2_4.html