モバイルデータからの人の行動パターン発見: 時間の循環性を考慮したGMMによるアプローチ


## 1. はじめに

人々の移動パターンは、一見ランダムに見えますが、実際には規則的な「習慣」に基づいていることが多いです。例えば、平日の通勤時間は毎日ほぼ同じ時間帯であったり、週末の買い物は特定の時間帯に集中したりします。しかし、これらの習慣は完全に固定されているわけではありません。

## 2. 時間データの循環性問題

### 2.1 時間データの特殊性

時間データを分析する際の大きな課題の一つは、その「循環性」です。例えば、23:55と翌日の0:05は、数値として見ると大きく離れていますが、実際の時間としては10分しか違いません。以下の参考文献で、この問題を解決するために、時間データを円形の座標系に変換する手法を採用しています。今回はこの論文の内容に基づいて手法の解説を行います。

Andrade, T., Cancela, B. & Gama, J. Discovering locations and habits from human mobility data. Ann. Telecommun. 75, 505–521 (2020). https://doi.org/10.1007/s12243-020-00807-x



### 2.2 時間データの変換方法

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83464/d5f9d6f0-4735-ce67-51e1-05b236942ce5.png)
上記の論文にある図は、人々の移動開始時刻（start hour）のデータを2つの異なる方法で可視化したものです。この図を通じて、時間データの循環的な性質がどのように表現されるかを理解することができます。
左側のグラフから右側のグラフへの変換の公式は以下となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83464/035c1336-e6ef-e127-3a56-60c0bb53a709.png)


左側のグラフ：従来の時間表現

- X軸は軌跡の順序（trajectory order）を示しており、0から140までの観測順序を表しています
- Y軸は24時間表記での開始時刻（0時から24時）を示しています
- 赤い点は各移動の開始時刻を表しています
- このグラフでは、例えば23時と0時が離れて表示されてしまうという問題があります

右側のグラフ：循環的な時間表現

- X軸は余弦変換値（start_time_cos）を示しています
- Y軸は正弦変換値（start_time_sin）を示しています
- 青い点は各開始時刻を円周上の位置として表現しています
- この表現方法により、23時と0時が円周上で近接して表示されるようになります

この変換の重要な特徴：

- 時間の連続性：円周上の表現により、深夜0時をまたぐ時間の連続性が自然に表現されます
- 距離の意味：円周上の2点間の距離が、実際の時間的な近さを正確に反映します
- パターンの可視化：時間帯ごとの集中度合いが円周上の点の密度として視覚的に理解できます

この表現方法は、特に人々の日常的な移動パターンを分析する際に有用です。例えば、通勤時間帯の集中や、深夜から早朝にかけての移動など、24時間周期の中での人々の行動パターンを適切に分析することができます。


以下のPythonコードで、時間データを循環座標に変換します：

```python
def transform_hours_to_circular(df):
    """時間データを循環座標に変換する関数"""
    # 時間を角度（ラジアン）に変換
    df['hour_rad'] = df['start_hour'] * 2 * np.pi / 24
    
    # 正弦と余弦の値を計算
    df['start_time_sin'] = np.sin(df['hour_rad'])
    df['start_time_cos'] = np.cos(df['hour_rad'])
    return df
```

この変換により、例えば：
- 0時 → (cos(0), sin(0)) = (1, 0)
- 6時 → (cos(π/2), sin(π/2)) = (0, 1)
- 12時 → (cos(π), sin(π)) = (-1, 0)
のように、時間が円周上の点として表現されます。

## 3. GMMを用いた習慣パターンの検出

### 3.1 人々の移動習慣の特徴とGMMの親和性
まず、人々の移動習慣の特徴を考えてみましょう。例えば、ある人の通勤時間を観察すると：

- 「だいたい朝8時ごろに家を出る」
- 「時々7時半に早めに出ることもある」
- 「たまに8時半に遅めになることもある」

このような「おおよその時間」を中心とした「ばらつき」のある行動パターンが一般的です。
人々の移動習慣を検出するために、ガウス混合モデルを使用します。GMMは、データが複数の正規分布の組み合わせから生成されていると仮定するモデルです。

GMMが優れているのは、まさにこのような「中心的な時間帯」と「自然なばらつき」を同時にモデル化できる点です。各ガウス分布が：

- 平均値：習慣の中心となる時刻（例：8時）
- 標準偏差：その習慣の柔軟性（例：±30分程度のばらつき）
を表現できます。

さらに、GMMの「混合」という特徴が重要です。人々は通常、複数の異なる習慣パターンを持っています：

- 朝の通勤パターン
- 昼食時の外出パターン
- 夕方の帰宅パターン

GMMは、これらの複数のパターンを自動的に見つけ出し、それぞれのパターンの特徴（中心時刻とばらつき）を同時に学習することができます。
また、先ほど説明した時間の循環性（23時55分と0時05分は近い時間）についても、GMMは円周上の座標系で学習を行うことで、この問題を自然に解決できます。
### 3.2 習慣パターンの可視化

習慣パターンを視覚的に理解するために、3つの図を生成します：

1. 時系列での表示：各移動の開始時刻を時系列順にプロット
2. 時間分布のヒストグラム：各時間帯の移動頻度を表示
3. GMM密度推定：検出された習慣パターンの確率密度を表示


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83464/8a956fc2-3103-9148-0b81-f617e1567573.png)

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.mixture import GaussianMixture
from scipy.stats import norm
import seaborn as sns
import matplotlib


# 再現性を確保するための乱数シードを設定
np.random.seed(42)
matplotlib.use('TkAgg')
def generate_habit_data(n_samples=40):
    """
    習慣データのシミュレーション生成、以下の3つの主要な時間帯を含む：
    - 朝 (6-7時頃)
    - 昼 (11-12時頃)
    - 夜 (21-22時頃)
    """
    # 3つの時間帯の基準時刻と標準偏差を定義
    habits = [
        (6.5, 0.5, 20),   # 朝の習慣
        (11.5, 0.3, 10),  # 昼の習慣
        (9.5, 0.4, 10)    # 夜の習慣
    ]
    
    hours = []
    labels = []
    
    # 各習慣パターンのデータを生成
    for idx, (mean_hour, std, count) in enumerate(habits):
        sample_hours = np.random.normal(mean_hour, std, count)
        # 時刻を0-24の範囲に収める
        sample_hours = np.mod(sample_hours, 24)
        hours.extend(sample_hours)
        labels.extend([idx] * count)
    
    # DataFrameを作成
    df = pd.DataFrame({
        'start_hour': hours,
        'true_label': labels
    })
    
    # 移動順序を追加
    df['trajectory_order'] = range(len(df))
    
    return df
def transform_hours_to_circular(df):
    """
    時刻データを円周座標に変換
    """
    # 時刻をラジアンに変換
    df['hour_rad'] = df['start_hour'] * 2 * np.pi / 24
    
    # 正弦と余弦の値を計算
    df['start_time_sin'] = np.sin(df['hour_rad'])
    df['start_time_cos'] = np.cos(df['hour_rad'])
    
    return df
def fit_gmm(X, n_components=3):
    """
    ガウス混合モデルを使用してクラスタリングを実行
    """
    gmm = GaussianMixture(n_components=n_components, random_state=42)
    labels = gmm.fit_predict(X)
    return labels, gmm
# データを生成
df = generate_habit_data()
df = transform_hours_to_circular(df)
# GMM用の入力データを準備
X = df[['start_time_sin', 'start_time_cos']].values
# GMMによるクラスタリングを実行
labels, gmm = fit_gmm(X)
df['cluster'] = labels
# Figure 3の可視化を作成
plt.style.use('seaborn-v0_8')
fig = plt.figure(figsize=(15, 5))
# サブプロット1：開始時刻とクラスタリング結果
ax1 = plt.subplot(131)
colors = plt.cm.rainbow(np.linspace(0, 1, len(np.unique(labels))))
for label, color in enumerate(colors):
    mask = df['cluster'] == label
    ax1.scatter(df[mask]['trajectory_order'], 
                df[mask]['start_hour'],
                color=color, 
                label=f'Habit {label+1}',
                alpha=0.6)
ax1.set_xlabel('Trajectory Order')
ax1.set_ylabel('Start Hour')
ax1.set_title('(a) Starting hours and\ntheir classes')
ax1.legend()
# サブプロット2：時間帯ごとの移動回数のヒストグラム
ax2 = plt.subplot(132)
plt.hist(df['start_hour'], bins=24, color='red', alpha=0.7)
ax2.set_xlabel('Start Hour')
ax2.set_ylabel('# of Trajectories')
ax2.set_title('(b) Histogram of the starting\nhours with three main peaks')
# サブプロット3：GMM密度推定
ax3 = plt.subplot(133)
x = np.linspace(0, 24, 200)
density = np.zeros_like(x)
# 各成分の密度を計算
for i in range(gmm.n_components):
    theta = x * 2 * np.pi / 24
    X_test = np.column_stack([np.sin(theta), np.cos(theta)])
    log_prob = gmm.score_samples(X_test)
    density += np.exp(log_prob) * gmm.weights_[i]
ax3.plot(x, density, 'r-', lw=2)
ax3.set_xlabel('Start Hour')
ax3.set_ylabel('Density')
ax3.set_title('(c) Densities of the mix of\nGaussians found over the\ndistribution')
plt.tight_layout()
plt.show()
# クラスタリング結果の統計を表示
print("\n各習慣パターンの移動回数:")
print(pd.Series(labels).value_counts().sort_index())
# 各クラスタの平均時刻を表示
print("\n各習慣パターンの平均時刻:")
for label in range(gmm.n_components):
    mask = df['cluster'] == label
    mean_hour = df[mask]['start_hour'].mean()
    print(f"習慣パターン {label+1}: {mean_hour:.2f} 時")
```

## 4. 分析結果の解釈

この手法により、例えば以下のような習慣パターンを検出することができます：
解釈は人次第だが、以下は私の解釈例です。
- 朝の通勤パターン（6-7時）
- オフィスから出張の移動パターン（9-10時）
- 出張先の帰宅パターン（11-12時）

各パターンは、中心となる時間帯を持ちながらも、ある程度の柔軟性（標準偏差）を持っており、現実の人々の行動をよく表現できています。

## 5. まとめ

この手法の主な利点は：
1. 時間の循環性を適切に処理できる
2. 複数の習慣パターンを自動的に検出できる
3. パターンの柔軟性（変動）を自然に表現できる

以上の特徴により、モバイルデータから人々の実際の移動習慣を効果的に抽出し、分析することが可能となっています。

この記事で説明した手法は、都市計画や交通システムの最適化、個人向けサービスの改善など、様々な応用可能性を持っています。