Python×Matplotlibで作る人の動きのヒートマップ&トラジェクトリー可視化
📌はじめに
前回の記事では、
人流トラジェクトリーデータを縦持ち形式に変換し、分析しやすい形に整えました。
今回は、縦持ちデータを使って、
・会場全体の混雑傾向(ヒートマップ)
・特定個人の動き(動線グラフ)
を組み合わせた画像を作ってみます。
この方法を使うと、次のことが直感的にわかります。
・人の滞留しやすいエリア
・参加者の移動パターンの傾向
動線の表示方法の比較
1. 背景なし(動線のみ)
背景なしで動線だけを表示した例です。
動線だけでも移動パターンは見えますが、どのエリアで人が多く滞留しているかまでは分かりません。
2. 背景あり(ヒートマップ+動線)
そこで、全データを基に作成したヒートマップを背景に重ねてみます。
これにより、混雑傾向と個々の移動パターンを同時に確認できます。
背景あり表示の条件
ヒートマップ:全データを基に人数の割合で表示
・ 動線:表示人数を絞る(例では 1画像に5人分をプロット)
・ 起点:参加者IDを表示(任意)
・ 矢印:進行方向を明示
・ 色分け:IDごとに異なる色
この組み合わせで、次のことがひと目でわかります。
・どのルートを通ったか
・どのエリアが混雑していたか
・複数人の移動パターンの違い
📌人流データ
回のサンプルデータは、新たに作成したデータを使用します(前回のものは使用しません)。
sample.csv
カラム名 | 説明 |
---|---|
検知数 | 0.1秒間にセンサーが検知した人数 |
time_step | 時間を表すタイムスタンプ |
id | センサーが認識した個々のユニークID |
x | 座標のX値 |
y | 座標のY値 |
📌環境
python3.x
📌フォルダ構成
├── 1_flow/
│ └── overlap_graphs.py # ヒートマップの上に動線を重ねるスクリプト
├── 2_data/
│ └── sample.csv # 縦持ち形式のデータ(今回用に作成)
├── 3_output/
│ └── # 画像の出力先フォルダ
📌コード解説(overlap_graphs.py)
#============================================
# 0. ライブラリと変数
#============================================
import pandas as pd
import numpy as np
import os
import matplotlib.pyplot as plt
import math
# ファイル・フォルダの設定
INPUT_FOLDER = "2_data"
INPUT_FILENAME= "sample.csv"
OUTPUT_FOLDER = "3_output"
# プロットするID数の設定
group_size = 5
0. ライブラリと変数
-
pandas, numpy
:データ処理用 -
matplotlib
:グラフ作成用 -
os, math
:ファイル操作や数学関数用 -
2_data/sample.csv
:入力データ(縦持ち形式のトラジェクトリーデータ) -
3_output
:生成画像の出力先フォルダ -
group_size
:1枚の画像に重ねる参加者の数を制限するための設定
#============================================
# 1. ファイル読み込み
#============================================
# パスの取得
current_dir = os.getcwd()
parent_dir = os.path.dirname(current_dir)
input_path = os.path.join(parent_dir, INPUT_FOLDER, INPUT_FILENAME)
print("INFO: 入力データファイルパス:", input_path)
output_fol_path = os.path.join(parent_dir, OUTPUT_FOLDER)
os.makedirs(output_fol_path, exist_ok=True)
# CSV読み込み(UTF-8 BOM付き想定)
df_result = pd.read_csv(input_path, encoding="utf-8-sig") # ここをutf-8-sigに
#============================================
# 2. 背景になるヒートマップの設定
#============================================
L_Len = len(df_result)
xedges = np.linspace(0, 100, 51)
yedges = np.linspace(0, 100, 51)
position_x = df_result['x']
position_y = df_result['y']
H, xedges, yedges = np.histogram2d(position_x, position_y, bins=(xedges, yedges))
H = (H / L_Len) * 100
1. ファイル読み込み
-
current_dir / parent_dir
:カレントディレクトリと親ディレクトリを取得 -
INPUT_FOLDER
とINPUT_FILENAME
:入力データのパスを生成 -
OUTPUT_FOLDER
:出力フォルダを自動生成(存在しない場合は作成)
2. 背景になるヒートマップの設定
-
np.linspace(0, 100, 51)
: x軸とy軸の区間(ビンの境界)をそれぞれ50等分した配列(xedges
とyedges
)を作成。 -
np.histogram2d
: 2Dヒストグラムを作成
データ点の x, y 座標がどのマスに入るかをカウント -
(H / L_Len) * 100
: マス目ごとのカウントを全データ数で割り、百分率に変換
ヒートマップの基礎データとして使用
❓なぜ割合(百分率)に変換するの❓
-
全体に対する相対的な頻度をわかりやすく示すため
単純なカウントだとデータ量が違うと値の比較が難しいが、全体の○%という形にすれば他のデータセットとも比較しやすい -
ヒートマップの色の強弱を意味づけやすくするため
「20件」より「10%」の方が、色の濃さで直感的に理解しやすい -
異なるサイズのデータセット間で比較しやすくするため
データ総数が異なる場合でも、割合なら規模の違いを吸収して比較できる
つまり、データの分布を全体に対する割合で示すことで、視覚化や比較をしやすくするためです
#============================================
# 3. IDごとのグループ分け
#============================================
id_list = df_result['id'].drop_duplicates().tolist() # リスト化
groups = [id_list[i:i + group_size] for i in range(0, len(id_list), group_size)]
#============================================
# 4. 動線の色設定
#============================================
clrs = [
'red', 'green', 'blue',
'purple', 'orange', 'gold',
'lime', 'navy', 'pink',
'cyan', 'brown'
]
3. IDごとのグループ分け
-
df_result['id'].drop_duplicates().tolist()
:ユニークなIDを抽出 -
groups = [id_list[i:i + group_size] ...]
:指定した人数ごとにIDをグループ化
1枚の画像に複数IDの動線をまとめて表示するため
4. 動線の色設定
-
clrs
:IDごとに色を変えるための色リスト
複数IDの動線を描く際に、色をループして使い回す
#=========================================
# 5.ヒートマップの上に動線をプロット
#=========================================
for grp_idx, id_group in enumerate(groups):
fig, ax = plt.subplots(figsize=(14, 12), dpi=130)
# 背景ヒートマップ
img = ax.imshow(
H.T,
origin='lower',
cmap='PuRd',
extent=[0, 100, 0, 100],
alpha=0.7,
vmin=0.05
)
ax.set_xlim(0, 100)
ax.set_ylim(0, 100)
cbar = plt.colorbar(img, ax=ax)
cbar.set_label('Presence Percentage (%)')
for color_idx, current_id in enumerate(id_group):
data_a = df_result.query('id == @current_id').sort_values('time_step')
data_b = data_a.reset_index(drop=True)
if len(data_b) > 0:
line_color = clrs[color_idx % len(clrs)]
oldx, oldy = None, None
# スタート地点の座標
startx = data_b.loc[0, 'x']
starty = data_b.loc[0, 'y']
ax.text(
startx,
starty,
str(current_id),
fontsize=12,
color=line_color,
weight='bold',
ha='center',
va='center',
bbox=dict(facecolor='white', alpha=0.5, edgecolor='none', boxstyle='round,pad=0.3')
)
for idx, row in data_b.iterrows():
newx = row['x']
newy = row['y']
if oldx is not None:
if (not any(math.isnan(v) for v in [oldx, oldy, newx, newy])
and (oldx != newx or oldy != newy)):
ax.plot(
[oldx, newx],
[oldy, newy],
color=line_color,
linewidth=2,
alpha=0.8
)
ax.annotate(
'',
xy=(newx, newy),
xytext=(oldx, oldy),
arrowprops=dict(
arrowstyle="->",
color=line_color,
lw=1,
mutation_scale=20
)
)
oldx, oldy = newx, newy
ax.set_title(f"ID Group {grp_idx + 1} Movement Paths", fontsize=16)
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.set_xticks(np.arange(0, 110, 5))
ax.set_yticks(np.arange(0, 110, 5))
plt.tight_layout()
out_path = os.path.join(output_fol_path, f"movement_group_{grp_idx + 1}.png")
plt.savefig(out_path, dpi=130)
plt.close(fig) # 明示的に閉じる
# 画像のフォルダを開く
os.startfile(os.path.realpath(output_fol_path))
5. ヒートマップの上に動線をプロット
-
ax.imshow()
:ヒートマップを背景として表示-
cmap='PuRd'
:カラーマップの指定 -
alpha=0.7
:透明度を調整して、動線を重ねても背景が見えるように -
vmin=0.05
:色の最小値を設定
-
-
ax.set_xlim
/ax.set_ylim
:表示範囲を 0〜100 に設定 -
plt.colorbar
:ヒートマップの色のスケールを表示(単位:%) -
query('id == @current_id').sort_values('time_step')
:- 指定したIDのデータを抽出し、
time_step
で昇順に並べ替え - 時系列に沿った動きのプロットが可能
- 指定したIDのデータを抽出し、
-
math.isnan(v)
:oldx, oldy, newx, newy
のいずれかが NaN(無効な値)か判定 -
(oldx != newx or oldy != newy)
:前の座標と異なるか(移動があるか)をチェック
→ どちらかの条件を満たさない場合、その区間の線は描画せずスキップ -
ax.text(startx, starty, str(current_id), ...)
:動線の始点にIDを表示(コメントアウト可能) -
ax.plot([oldx, newx], [oldy, newy])
:前の座標と現在の座標を結ぶ線を描画
→ 同じ座標やNaNを除外して可視化 -
ax.annotate
:動線の進行方向を矢印で明示 -
clrs
:色リストをループして使用(複数IDがある場合でも色を使い回して区別可能) -
ax.set_title(...)
:グラフのタイトルを設定(例:「ID Group 1 Movement Paths」など) -
ax.set_xlabel("X")
/ax.set_ylabel("Y")
:軸ラベルを設定 -
ax.set_xticks(np.arange(0, 110, 5))
/ax.set_yticks(np.arange(0, 110, 5))
:軸目盛りを 0〜105 まで 5刻みで設定
→ 表示範囲に余裕を持たせる -
plt.tight_layout()
:レイアウトを調整 -
plt.savefig()
/plt.close(fig)
:画像として保存し、図を閉じる -
os.startfile()
:生成画像フォルダを自動で開く(Windows環境)
📌出力
3_output
フォルダの中にmovement_group_連番.png
のファイル名で画像が保存されます。
各画像には、指定した人数分のIDの動線がプロットされており、
動きの進行方向を示す矢印も付いています。
📌 まとめ
いかがでしたか?
あまり汎用的ではないかもしれませんが、参考になれば嬉しいです。
参考リンク
GitHubリポジトリ
本記事で紹介したコードやサンプルデータはこちらのリポジトリで公開しています。
https://github.com/iwakazusuwa/trajectory-heatmap-tool-mit
Discussion