アクセスログを可視化してみよう!ユーザー行動のデータビジュアライゼーション
本記事は、Lancers(ランサーズ) Advent Calendar 2024 の4日目の記事です。
はじめに
私は普段、アプリ開発やデータ分析をやっています。
最近では、社内にデータ分析や統計を普及させるために輪読会などを開催しています。
本記事では、アクセスログを分析して施策立案や機能改善に活かしたいという時に「行動時系列」という以下のような図示の仕方がとても良かったので紹介をします!
行動時系列とは?
行動時系列は、ユーザーの行動を時系列で表したデータの可視化手法です。
どのイベントがどの時間帯にどのような頻度で発生したかを把握できるようにし、全体のパターンを見て特徴を見つけるのに役立ちます。
探索型可視化にも説明型可視化にも用いることができる優れものです。
行動パターンの例:
- ユーザーごとのアクセス時間帯の違い ex. Aさんは朝にアクセスが多く、Bさんは夕方に多い
- 購入前に特定の行動パターンが現れる ex. 3回以上お気に入りをしていると購入率が高い
アクセスログや個々の観測データより、各イベントに色を割り当てたヒートマップに図示します。
図示することで、視覚的にデータを捉えることができ、これまで見えなかったインサイトも得ることができるはずです。
行動時系列を可視化
ここからはPythonのコードによる可視化を解説していきます。
紹介するコードはGithub上にGoogle Colabのノートブック形式で公開しているので、手元で実行してみてください。
概要
今回は以下の条件で行動時系列を作ります。
- 期間:3日間
- 集計単位:1時間ごとに行動のカテゴリを集計
- 行動のカテゴリ:トップページ、検索、詳細ページ、購入、その他
1. アクセスログデータ準備
今回は仮データを使っていきます。
アクセス時刻とアクセスしたURIが含まれているデータを想定します。
data = [
# 1日目
{"datetime": "2024-12-01 08:00:00", "uri": "/"},
{"datetime": "2024-12-01 08:15:00", "uri": "/search"},
{"datetime": "2024-12-01 09:10:00", "uri": "/login"},
{"datetime": "2024-12-01 09:30:00", "uri": "/detail"},
{"datetime": "2024-12-01 10:20:00", "uri": "/search"},
{"datetime": "2024-12-01 10:45:00", "uri": "/detail"},
{"datetime": "2024-12-01 11:15:00", "uri": "/detail"},
{"datetime": "2024-12-01 11:45:00", "uri": "/search"},
# 以下、省略
]
access_log_df = pd.DataFrame(data)
以下の表データでもよく読めば分析できそうですが、パターンを捉えるには難しいと思います。
datetime | uri | |
---|---|---|
0 | 2024-12-01 08:00:00 | / |
1 | 2024-12-01 08:15:00 | /search |
2 | 2024-12-01 09:10:00 | /login |
3 | 2024-12-01 09:30:00 | /detail |
4 | 2024-12-01 10:20:00 | /search |
access_log_df テーブル
datetime | uri | |
---|---|---|
0 | 2024-12-01 08:00:00 | / |
1 | 2024-12-01 08:15:00 | /search |
2 | 2024-12-01 09:10:00 | /login |
3 | 2024-12-01 09:30:00 | /detail |
4 | 2024-12-01 10:20:00 | /search |
5 | 2024-12-01 10:45:00 | /detail |
6 | 2024-12-01 11:15:00 | /detail |
7 | 2024-12-01 11:45:00 | /search |
8 | 2024-12-01 13:10:00 | /detail |
9 | 2024-12-01 13:30:00 | /message |
10 | 2024-12-01 14:00:00 | /search |
11 | 2024-12-01 15:00:00 | /detail |
12 | 2024-12-01 17:30:00 | /message |
13 | 2024-12-02 08:00:00 | / |
14 | 2024-12-02 08:20:00 | /search |
15 | 2024-12-02 09:00:00 | /detail |
16 | 2024-12-02 09:30:00 | /faq |
17 | 2024-12-02 10:00:00 | /detail |
18 | 2024-12-02 10:45:00 | / |
19 | 2024-12-02 11:00:00 | /search |
20 | 2024-12-02 11:40:00 | /faq |
21 | 2024-12-02 15:30:00 | /detail |
22 | 2024-12-02 17:15:00 | /faq |
23 | 2024-12-02 17:45:00 | /message |
24 | 2024-12-03 08:10:00 | /search |
25 | 2024-12-03 08:40:00 | /detail |
26 | 2024-12-03 09:15:00 | /search |
27 | 2024-12-03 09:30:00 | /detail |
28 | 2024-12-03 11:45:00 | /search |
29 | 2024-12-03 13:30:00 | /search |
30 | 2024-12-03 14:30:00 | /message |
31 | 2024-12-03 15:00:00 | /detail |
32 | 2024-12-03 16:00:00 | /purchase |
2. URIをカテゴライズ
次は、URIを行動のカテゴリに分類していきます。
これにより、どのようなカテゴリに属するかを決めます。
実際のデータでは、様々なURIが出現します。
どのようにカテゴリ分類するかが、今回の可視化の肝になります。
# URI リストとカテゴリ
uri_categories = {
"トップ": ["/"],
"検索": ["/search"],
"ログイン": ["/login"],
"詳細": ["/detail"],
"購入": ["/purchase"],
}
category_dict = list(uri_categories) + ["その他"]
# ['トップ', '検索', 'ログイン', '詳細', '購入', 'その他']
# URIをカテゴリに分類する関数
def classify_uri(uri):
for category, uris in uri_categories.items():
if uri in uris:
return category
return "その他"
#URIをカテゴライズ
categorized_log_df = access_log_df.copy()
categorized_log_df["category"] = categorized_log_df["uri"].apply(lambda x: classify_uri(x))
categorized_log_df["category_index"] = categorized_log_df["category"].apply(lambda x: category_dict.index(x))
また、この行動カテゴリごとの色も決めておきます。
hex_colors = [colors.to_hex(color) for color in plt.get_cmap('tab10', 10).colors]
category_color_dict = dict(zip(category_dict, hex_colors[:len(category_dict)]))
# {'トップ': '#1f77b4',
# '検索': '#ff7f0e',
# 'ログイン': '#2ca02c',
# '詳細': '#d62728',
# '購入': '#9467bd',
# 'その他': '#8c564b'}
ここまででデータは以下のようになっています。
datetime | uri | category | category_index | |
---|---|---|---|---|
0 | 2024-12-01 08:00:00 | / | トップ | 0 |
1 | 2024-12-01 08:15:00 | /search | 検索 | 1 |
2 | 2024-12-01 09:10:00 | /login | ログイン | 2 |
3 | 2024-12-01 09:30:00 | /detail | 詳細 | 3 |
4 | 2024-12-01 10:20:00 | /search | 検索 | 1 |
3. アクセスログデータを行動時系列に整形
次に、時間単位でログを整形します。
まずは、日付と時間のカラムを追加します。
# 日付カラムdate と 時間カラムtime 追加
categorized_log_df["datetime"] = pd.to_datetime(categorized_log_df["datetime"])
categorized_log_df["date"] = categorized_log_df["datetime"].dt.date
categorized_log_df["time"] = categorized_log_df["datetime"].dt.hour
datetime | uri | category | category_index | date | time | |
---|---|---|---|---|---|---|
0 | 2024-12-01 08:00:00 | / | トップ | 0 | 2024-12-01 | 8 |
1 | 2024-12-01 08:15:00 | /search | 検索 | 1 | 2024-12-01 | 8 |
2 | 2024-12-01 09:10:00 | /login | ログイン | 2 | 2024-12-01 | 9 |
3 | 2024-12-01 09:30:00 | /detail | 詳細 | 3 | 2024-12-01 | 9 |
4 | 2024-12-01 10:20:00 | /search | 検索 | 1 | 2024-12-01 | 10 |
今回は時間帯ごとに1つのデータになるようgroupby
を用いて同じ1時間内のログを1つに集計します。
色々な集計方法が考えられますが、今回はfirst()
を用いて1番目のログを採用します。
他には最頻値を用いるなども考えられます。
# 1時間ごとに集計
categorized_log_df = categorized_log_df.groupby(["date", "time"], as_index=False).first()
date | time | datetime | uri | category | category_index | |
---|---|---|---|---|---|---|
0 | 2024-12-01 | 8 | 2024-12-01 08:00:00 | / | トップ | 0 |
1 | 2024-12-01 | 9 | 2024-12-01 09:10:00 | /login | ログイン | 2 |
2 | 2024-12-01 | 10 | 2024-12-01 10:20:00 | /search | 検索 | 1 |
3 | 2024-12-01 | 11 | 2024-12-01 11:15:00 | /detail | 詳細 | 3 |
4 | 2024-12-01 | 13 | 2024-12-01 13:10:00 | /detail | 詳細 | 3 |
4. ヒートマップに表示
集計が終わったデータをヒートマップで可視化するため、X軸に日付、Y軸に時間が来るようピボットテーブルに変換します。
# ピボットテーブルを作成
pivoted_df = categorized_log_df.pivot(index="time", columns="date", values=["category", "category_index"])
このままでは、アクセスの無い時間帯を表現できないため、抜けている時間帯をNaNで埋めます。
NaNで埋めることでヒートマップ上で空欄にすることができます。
# 抜けている時間帯をNaNで埋める
pivoted_df = pivoted_df.reindex(index=range(24))
最後に、ヒートマップにします。
グリッド線を入れると時間帯ごとの区切りが見えてわかりやすくなります。
#@title 行動時系列を作成
fig, ax = plt.subplots(figsize=(8, 6))
n_categories = len(category_dict)
# ヒートマップ
sns.heatmap(pivoted_df["category_index"].astype(float),
annot=pivoted_df["category"],
fmt="",
cmap=hex_colors[:n_categories],
cbar_kws={'orientation': 'vertical',
"boundaries": np.linspace(-0.5, n_categories - 0.5, n_categories + 1),
'ticks': np.arange(n_categories),
'format': FixedFormatter([f"{i}. {v}" for i,v in enumerate(category_dict)]),
"shrink": 0.5}
)
# グリッド線の追加
for x in range(pivoted_df.shape[1]):
plt.axvline(x, color='darkgray', linewidth=0.5)
for y in range(pivoted_df.shape[0]):
plt.axhline(y, color='lightgray', linewidth=0.5)
ax.spines['top'].set_visible(True)
ax.spines['bottom'].set_visible(True)
ax.spines['right'].set_visible(True)
ax.spines['left'].set_visible(True)
ax.set_xlabel("日付")
ax.set_ylabel("時間")
ax.set_title("行動時系列")
plt.tight_layout()
plt.savefig("action_timeline.png", dpi=300)
plt.show()
以上でユーザー行動時系列を作成することができました。
まとめ
本記事では、アクセスログを用いて1時間ごとの行動をヒートマップを用いて行動時系列として図示する手順を解説してきました。何かの参考になれば幸いです。
この図示の欠点としては、時間解像度を細かくしたり行動のカテゴリを増やすほど色を見分けづらくなります。自分はこれを解消するために図の拡大・縮小ができるPlotlyを利用しています。あとは、大画面に映せると見やすいです。
実際に分析に使っている図
サービスのユーザー体験を向上させるために様々な調査をしていると思います。1人1人にユーザビリティテストをお願いするのもなかなか厳しいので、今回のような手法で分析してみるというのもいかがでしょうか。
今回のように時系列の行動データを用いることで行動の予測もできるようになるはずなので、機会があったら挑戦してみようと思います。
最後に
Lancers では、一緒に働けるメンバーを募集しています!
ご興味があれば自分のSNS宛でも大丈夫なのでぜひご連絡ください!
Discussion