1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python ヒートマップの上に動線グラフを重ねる

Last updated at Posted at 2025-07-06

概要

の続き

人流データをどうやって見せるか考えた結果、
会場のヒートマップを作って、その上に人の動きの線を重ねてみることにしました。

ただ重ねると分析しずらいので
image.png

条件を決めて作しました
 ・背景のヒートマップは全データで作成(人数が分母で比率)
 ・動線は人数を絞る(図は5人)
 ・動線の起点にid表示
 ・動線の進行方向に矢印
 ・id毎に動線の色を変える

image.png

環境

python3.x
jupyter lab

ソース

最初に、前回のおさらい


import pandas as pd
import numpy as np
import os
from scipy.spatial import distance

#=============================================
# Inputファイル情報
#=============================================
INPUT_DNAME = "人流データ.csv"
INPUT_folder = "2_data"        
#=============================================
# Outputファイル情報
#=============================================
OUTPUT_DNAME = "データ縦持ち整形.csv"
OUTPUT_DIS = "データ縦持ち整形_距離追加.csv"
#=============================================
# カレントパス
#=============================================
current_dpath = os.getcwd()
print("INFO:カレントパス:" + current_dpath)
#=============================================
# パレントパス
#=============================================
parent_dpath =os.path.sep.join(current_dpath.split(os.path.sep)[:-1])
print("INFO:パレントパス:" + parent_dpath)   
#=============================================
# Inputデータファイル Path
#=============================================
input_dpath =os.path.sep.join([parent_dpath + '\\' + INPUT_folder,INPUT_DNAME])
print("INFO:データファイルパス:" + input_dpath) 
#=============================================
# Outputデータファイル Path
#=============================================
output_dpath =parent_dpath + '\\' + OUTPUT_folder
print("INFO:出力先のフォルダパス:" + output_dpath)
#=============================================
# Inputデータ読み込む
#=============================================
df = pd.read_csv(input_dpath,encoding='shift-JIS')
#=============================================
# 検知数が0件の行は削除
#=============================================
df = df.query('0 < 検知数')

 #--index振りなおす
df_a = df.reset_index(drop=True)

#=============================================
# データを縦持ちにする
#=============================================
df_list = pd.DataFrame()

# 1行ずつ処理を行っていく
for index, row in df_a.iterrows():
    df_b = df_a.loc[[index]]  # 1行抜き出す
    
    if df_b.empty:
        continue  # もし何もなければスキップ

    # 検知数(人数)を取得
    Lop_cnt = df_b['検知数'].iloc[0]

    # 最初の idのカラム位置
    col = 2
    #検知数だけ処理を繰り返す
    for i in range(Lop_cnt):
        try:
            new_row = {
                '検知数': df_b.iloc[0, 0],
                'time_step': df_b.iloc[0, 1],
                'id': df_b.iloc[0, col],
                'x': df_b.iloc[0, col + 1],
                'y': df_b.iloc[0, col + 2]
            }
            # 記憶
            df_list = pd.concat([df_list, pd.DataFrame([new_row])], ignore_index=True)
            # 次のidカラム位置
            col += 3
        except IndexError:
            print(f"列不足エラー: index={index}, col={col}")
            continue
            
#=============================================
# csvファイルに出力
#=============================================            
df_list.to_csv(output_dpath + "\\" + OUTPUT_DNAME ,encoding='cp932',index =False)

#======================================================
# idのデータを取り出して、time_stepソート、移動距離追加
#======================================================  
df_c = df_list['id']
df_d = df_c.drop_duplicates()

result_rows = []

for current_id in df_d:
    df_e = df_list.query('id == @current_id').sort_values('time_step')
    df_f = df_e.reset_index(drop=True)

    for index in range(len(df_f)):
        row = df_f.iloc[index]
        
        if index == 0:
            oldx = row['x']
            oldy = row['y']
            dis_c = 0
            moji = 0
        else:
            newx = row['x']
            newy = row['y']
            dis_c = distance.euclidean((oldx, oldy), (newx, newy))
            idokaku = np.rad2deg(np.arctan2(newy - oldy, newx - oldx))
            oldx, oldy = newx, newy

        result_rows.append({
            'time_step': row['time_step'],
            'id': current_id,
            'x': row['x'],
            'y': row['y'],
            '移動距離': dis_c,
        })
        
df_result = pd.DataFrame(result_rows)
#=============================================
# csvファイルに出力
#=============================================    
df_result.to_csv(output_dpath + "\\" + OUTPUT_DIS ,encoding='cp932',index =False)

ここから今回分

import codecs
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
# =========================
# 分割するid数(人数)を指定
# =========================
group_size = 5

# =========================
# 動線色分け
# =========================  
clrs = [
    'red', 'green', 'blue', 
    'purple', 'orange', 'gold', 
    'lime', 'navy', 'pink', 
    'cyan', 'brown'
    ]
# =========================
# 背景ヒートマップデータ
# =========================
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

# =========================
# 全IDを指定数ずつに分割
# =========================
id_list = list(df_d)

groups = [id_list[i:i+group_size] for i in range(0, len(id_list), group_size)]

# =========================
# 各グループでグラフ生成
# =========================
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_list.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']

            # スタート地点にIDを描く
            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:
                    # 線
                    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  # 矢印のサイズ 10:小.20:中.30:大
                        )
                    )
                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()
    plt.savefig(f"{output_dpath}\\movement_group_{grp_idx+1}.png", dpi=130)
    plt.show()
    plt.close()

出力

3_outputフォルダの中に movement_group_連番.png が作成されます

image.png

サンプルデータのため動きがおかしい箇所がありますが、出発地点にはIDが表示され、進行方向に矢印が出ます
image.png

まとめ

お化けデータを削除するのに下記を使いましたが、詳しいやり方は別途

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?