DatabricksにおけるKaggleのWebサーバーログ分析のウォークスルー

こちらのWebサーバーログのデータセットに対して、非常にわかりやすい分析をされている方がいらっしゃいました。

https://www.kaggle.com/datasets/eliasdabbas/web-server-access-logs/data

こちらです。他のノートブックも興味深いので後で動かしてみます。

https://www.kaggle.com/code/adrineuwera/web-usage-mining

Databricksで動かすので一部修正して、翻訳しながらウォークスルーします。

# データの取得

はじめに詰まったのがここでした。KaggleのデータセットをどのようにDatabricksに持ってくれば…。[以前](https://qiita.com/taka_yayoi/items/7fcfbaf39dac8326407b)はデータのサイズが小さかったので、ダウンロード→ボリュームへのアップロードで済みました。しかし、今回は数GBのログということもあり、以前のアプローチですとなかなかボリュームにアップロードできず。

ということで、Kaggle Public APIを使わせていただきました。

https://www.kaggle.com/docs/api

使う際には、事前にKaggleのサイトでトークンを作成しておく必要があります。トークを作成すると`kaggle.json`がダウンロードされます。こちらは後で使います。
![Screenshot 2024-07-31 at 14.33.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/ecf62d2e-004d-ee42-4cf8-6cc15214f85f.png)

Databricksクラスターを起動して以下を実行していきます。

```py
%pip install kaggle
dbutils.library.restartPython()
```

Webターミナルを開きます。以下を実行します。

```sh
mkdir /root/.config/kaggle
vi /root/.config/kaggle/kaggle.json
```
![Screenshot 2024-07-31 at 14.46.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/dbe89b8d-f165-4292-9e38-19ba256ee2a2.png)

エディタが開くので、ダウンロードされた`kaggle.json`の中身を貼り付けて保存します。ターミナルは閉じてしまっても構いません。

ノートブックで以下を実行します。

```py
import kaggle

kaggle.api.authenticate()
```

認証が通ったら、以下を実行してボリュームにデータをダウンロードして解凍します。

```py
kaggle.api.dataset_download_files('eliasdabbas/web-server-access-logs', path='/Volumes/users/takaaki_yayoi/data', unzip=True)
```

これでデータの準備ができました。
![Screenshot 2024-07-31 at 14.48.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/7f9470b1-90b9-7db0-9cf4-f2cc27dd3117.png)

# Webサーバーログの分析

```py
import numpy as np # 線形代数
import pandas as pd # データ処理、CSVファイルのI/O(例: pd.read_csv)


# 以下を実行することでdataディレクトリの全ファイルを表示します
import os
for dirname, _, filenames in os.walk('/Volumes/users/takaaki_yayoi/data'):
    for filename in filenames:
        print(os.path.join(dirname, filename))
```
```
/Volumes/users/takaaki_yayoi/data/access.log
/Volumes/users/takaaki_yayoi/data/client_hostname.csv
```

## データの前処理

このログファイルは、ユーザーのインタラクション、クローラーのアクティビティ、ビジネストレンドに関する包括的なビューを提供する、イランのeコマースであるzanbil.irから抽出された3.3GBのウェブサーバーのログから構成されています。このログファイルは、2019年にZakerとFarzinによってコンパイルされ、研究、分析目的としてHarvard Dataverse経由で利用できます。

### データフレームにログファイルをロード

それぞれのログの行から、クライアントIP、ユーザーID、タイムスタンプ、HTTPメソッド、リクエスト、ステータスコード、サイズ、リファラ、ユーザーエージェントのような適切な情報を抽出しました。

```py
import pandas as pd
import re

# ログファイルパスの定義
log_file_path = '/Volumes/users/takaaki_yayoi/data/access.log'

# ログの行から情報を抽出するための正規表現パターンの定義
regex_pattern = r'^(?P<client>\S+) \S+ (?P<userid>\S+) \[(?P<datetime>[\w:/]+\s[+\-]\d{4})\] "(?P<method>[A-Z]+) (?P<request>[^ "]+)? HTTP/[0-9.]+" (?P<status>[0-9]{3}) (?P<size>[0-9]+|-) "(?P<referer>[^"]*)" "(?P<user_agent>[^"]*)"'

# カラム名の定義
columns = ['client', 'userid', 'datetime', 'method', 'request', 'status', 'size', 'referer', 'user_agent']

# 正規表現パターンパッチングを用いて、ログファイルの最初の10000行をディクショナリーのリストに読み込み
log_data = []
with open(log_file_path, 'r') as file:
    for i, line in enumerate(file):
        if i >= 10000:
            break
        match = re.match(regex_pattern, line)
        if match:
            log_data.append({
                'client': match.group('client'),
                'userid': match.group('userid'),
                'datetime': match.group('datetime'),
                'method': match.group('method'),
                'request': match.group('request'),
                'status': match.group('status'),
                'size': match.group('size'),
                'referer': match.group('referer'),
                'user_agent': match.group('user_agent')
            })
        else:
            print("Error: Line does not match regex pattern:", line)

# ディクショナリーのリストからデータフレームを作成
logs_df = pd.DataFrame(log_data, columns=columns)
```

```py
# データフレームの最初の5行を表示
logs_df.head()
```
![Screenshot 2024-07-31 at 14.51.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/94c6397b-2757-d790-9418-a7930a81d0ad.png)

### データセットの理解と処理

```py
# データフレームの概要をチェック
logs_df.info()
```
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 9 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   client      10000 non-null  object
 1   userid      10000 non-null  object
 2   datetime    10000 non-null  object
 3   method      10000 non-null  object
 4   request     10000 non-null  object
 5   status      10000 non-null  object
 6   size        10000 non-null  object
 7   referer     10000 non-null  object
 8   user_agent  10000 non-null  object
dtypes: object(9)
memory usage: 703.2+ KB
```

```py
from datetime import datetime
import pytz
```

```py
# datetimeをパースする関数 (クラスセッションの練習)
def parse_datetime(x):
    '''
    Parses datetime with timezone formatted as:
        `[day/month/year:hour:minute:second zone]`

    Example:
        `>>> parse_datetime('13/Nov/2015:11:45:42 +0000')`
        `datetime.datetime(2015, 11, 3, 11, 45, 4, tzinfo=<UTC>)`

    Due to problems parsing the timezone (`%z`) with `datetime.strptime`, the
    timezone will be obtained using the `pytz` library.
    '''
    try:
        dt = datetime.strptime(x[1:-7], '%d/%b/%Y:%H:%M:%S')
        dt_tz = int(x[-6:-3])*60+int(x[-3:-1])
        return dt.replace(tzinfo=pytz.FixedOffset(dt_tz))
    except ValueError:
        return '-'
```

```py
logs_df['status'] = logs_df['status'].astype(int)
logs_df['size'] = logs_df['size'].astype(int)
logs_df['datetime'] = logs_df['datetime'].apply(parse_datetime)
```

```py
logs_df.info()
```
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 10000 entries, 0 to 9999
Data columns (total 9 columns):
 #   Column      Non-Null Count  Dtype                               
---  ------      --------------  -----                               
 0   client      10000 non-null  object                              
 1   userid      10000 non-null  object                              
 2   datetime    10000 non-null  datetime64[ns, pytz.FixedOffset(33)]
 3   method      10000 non-null  object                              
 4   request     10000 non-null  object                              
 5   status      10000 non-null  int64                               
 6   size        10000 non-null  int64                               
 7   referer     10000 non-null  object                              
 8   user_agent  10000 non-null  object                              
dtypes: datetime64[ns, pytz.FixedOffset(33)](1), int64(2), object(6)
memory usage: 703.2+ KB
```

### ユーザーIDカラムの削除

これは単なるハイフンのみだからです。

```py
users = logs_df['userid'].unique()
print(users)
```
```
['-']
```
```py
logs_df.drop(columns=['userid'], inplace=True)
```

### 重複の排除

分析に価値をもたらさない重複が存在します。

```py
# データフレームの重複をカウント
duplicate_count = logs_df.duplicated().sum()

# 重複の数を表示
print("Number of duplicates:", duplicate_count)
```
```
Number of duplicates: 49
```
```py
# 重複の排除
logs_df = logs_df.drop_duplicates()
```

**処理データのサンプル**

```py
# データフレームの最初の5行を表示
logs_df.head()
```
![Screenshot 2024-07-31 at 14.54.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/004cc137-9469-5cae-ce61-1bc0da92c864.png)

## プロンプトへの回答

### Q1. サイトに頻繁に訪れるトップ10の人たち

最も頻繁に訪れるユーザーを取得するには、クライアントのIPアドレス(client)とユーザーエージェント(user_agent)でグルーピングすることによるユーザー特定が必要です。

```py
# clientとuser_agentでグルーピングし、出現数をカウント、降順でソートします
frequent_visitors = logs_df.groupby(['client', 'user_agent']).size().reset_index(name='count').sort_values(by='count', ascending=False)

# トップ10の頻繁に訪問するユーザーを選択
top_10 = frequent_visitors.head(10)

index = 0
# トップ10を表示
for i, row in top_10.iterrows():
    print(f"{index + 1}. Client: {row['client']}, User Agent: {row['user_agent']}, Count: {row['count']}\n")
    index += 1
```
```
1. Client: 66.249.66.194, User Agent: Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html), Count: 778

2. Client: 66.249.66.91, User Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html), Count: 739

3. Client: 130.185.74.243, User Agent: Mozilla/5.0 (Windows NT 6.1; rv:42.0) Gecko/20100101 Firefox/42.0, Count: 660

4. Client: 66.249.66.194, User Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html), Count: 558

5. Client: 5.211.97.39, User Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_2 like Mac OS X) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.0 Mobile/14F89 Safari/602.1, Count: 474

6. Client: 207.46.13.136, User Agent: Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm), Count: 416

7. Client: 194.94.127.7, User Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36\x09Chrome 65.0, Count: 225

8. Client: 23.101.169.3, User Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0;  Trident/5.0), Count: 204

9. Client: 5.121.43.23, User Agent: Mozilla/5.0 (Linux; Android 7.0; FRD-L09) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.80 Mobile Safari/537.36, Count: 165

10. Client: 40.77.167.170, User Agent: Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm), Count: 164
```

### Q2. それぞれのセッションごとのセッションとページビュー

#### 基本的なセッション化(skip)****

```py
# セッションを特定するためにclientとuser_agentでグルーピングし、セッションごとのページビューをカウント
sessions = logs_df.groupby(['client', 'user_agent'])

# セッション情報を格納するための空のリストを初期化
session_info = []

# セッションごとの繰り返し
for (client, user_agent), session_data in sessions:
    # セッションのタイムスタンプとページビューの抽出
    timestamps = session_data['datetime'].tolist()
    pages = session_data['request'].tolist()
    
    # タプルにセッション情報を格納
    session_info.append((client, user_agent, timestamps, pages))
```
```py
session_info[1:4]
```
```
[('104.194.24.33',
  'Mozilla/5.0 (Linux; Android 8.0.0; SM-G955F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36',
  [Timestamp('2019-01-02 03:57:00+0033', tz='pytz.FixedOffset(33)')],
  ['/amp-helper-frame.html?appId=a624a1c1-0c93-466a-a546-e146710f97e6&parentOrigin=https://www-zanbil-ir.cdn.ampproject.org']),
 ('104.194.24.54',
  'Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900H Build/MMB29K)',
  [Timestamp('2019-01-02 04:24:00+0033', tz='pytz.FixedOffset(33)'),
   Timestamp('2019-01-02 04:26:04+0033', tz='pytz.FixedOffset(33)')],
  ['/image/33888?name=model-b2048u-1-.jpg&wh=200x200',
   '/image/11947?name=11947-1-fw.jpg&wh=200x200']),
 ('104.194.25.207',
  'Dalvik/2.1.0 (Linux; U; Android 5.0.2; P01V Build/LRX22G)',
  [Timestamp('2019-01-02 04:06:04+0033', tz='pytz.FixedOffset(33)'),
   Timestamp('2019-01-02 04:06:05+0033', tz='pytz.FixedOffset(33)'),
   Timestamp('2019-01-02 04:06:05+0033', tz='pytz.FixedOffset(33)')],
  ['/image/33888?name=model-b2048u-1-.jpg&wh=200x200',
   '/image/11947?name=11947-1-fw.jpg&wh=200x200',
   '/image/11926?name=sm812aaa.jpg&wh=200x200'])]
```

```py
# 最低でも5セッションをそのページビューと共に表示
for i, (client, user_agent, timestamps, pages) in enumerate(session_info[:5], start=1):
    print(f"Session {i} - Client: {client}, User Agent: {user_agent}")
    for timestamp, page in zip(timestamps, pages):
        print(f"    Timestamp: {timestamp}, Page: {page}")
    print()
```
```
Session 1 - Client: 104.156.210.196, User Agent: Dalvik/2.1.0 (Linux; U; Android 8.0.0; SM-A720F Build/R16NW)
    Timestamp: 2019-01-02 04:20:00+00:33, Page: /image/32768?name=24xs450-33.jpg&wh=200x200

Session 2 - Client: 104.194.24.33, User Agent: Mozilla/5.0 (Linux; Android 8.0.0; SM-G955F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36
    Timestamp: 2019-01-02 03:57:00+00:33, Page: /amp-helper-frame.html?appId=a624a1c1-0c93-466a-a546-e146710f97e6&parentOrigin=https://www-zanbil-ir.cdn.ampproject.org

Session 3 - Client: 104.194.24.54, User Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900H Build/MMB29K)
    Timestamp: 2019-01-02 04:24:00+00:33, Page: /image/33888?name=model-b2048u-1-.jpg&wh=200x200
    Timestamp: 2019-01-02 04:26:04+00:33, Page: /image/11947?name=11947-1-fw.jpg&wh=200x200

Session 4 - Client: 104.194.25.207, User Agent: Dalvik/2.1.0 (Linux; U; Android 5.0.2; P01V Build/LRX22G)
    Timestamp: 2019-01-02 04:06:04+00:33, Page: /image/33888?name=model-b2048u-1-.jpg&wh=200x200
    Timestamp: 2019-01-02 04:06:05+00:33, Page: /image/11947?name=11947-1-fw.jpg&wh=200x200
    Timestamp: 2019-01-02 04:06:05+00:33, Page: /image/11926?name=sm812aaa.jpg&wh=200x200

Session 5 - Client: 104.248.138.218, User Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
    Timestamp: 2019-01-02 04:35:01+00:33, Page: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
    Timestamp: 2019-01-02 04:35:01+00:33, Page: /favicon.ico
    Timestamp: 2019-01-02 04:35:01+00:33, Page: /static/images/guarantees/goodShopping.png
    Timestamp: 2019-01-02 04:35:02+00:33, Page: /static/css/font/wyekan/font.woff
    Timestamp: 2019-01-02 04:35:02+00:33, Page: /static/images/guarantees/bestPrice.png
    Timestamp: 2019-01-02 04:35:02+00:33, Page: /static/images/guarantees/warranty.png
    Timestamp: 2019-01-02 04:35:02+00:33, Page: /static/images/guarantees/support.png
    Timestamp: 2019-01-02 04:35:02+00:33, Page: /static/images/guarantees/fastDelivery.png
    Timestamp: 2019-01-02 04:35:03+00:33, Page: /m/browse/dishwasher/%D9%85%D8%A7%D8%B4%DB%8C%D9%86-%D8%B8%D8%B1%D9%81%D8%B4%D9%88%DB%8C%DB%8C
    Timestamp: 2019-01-02 04:36:00+00:33, Page: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
    Timestamp: 2019-01-02 04:36:02+00:33, Page: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
```

#### 時間の経験則を活用

セッションを特定し、セッション情報を格納するために、ソートしたデータフレームのそれぞれの行に対して、クライアント、ユーザーエージェント、閾値を超えた時間のギャップを追跡します。

```py
# 秒でセッションの閾値を定義 (10分)
SESSION_THRESHOLD_SECONDS = 10 * 60
 
# client, user_agent, datetimeでlogs_dfをソート
logs_df_sorted = logs_df.sort_values(by=['client', 'user_agent', 'datetime'])

# セッション情報を格納するためのからのリストを初期化
session_info = []

# セッションを追跡するための変数の初期化
current_client = None
current_user_agent = None
current_session_start = None
current_session_end = None
current_session_pages = []

# ソートされたデータフレームのそれぞれの行に対する繰り返し
for index, row in logs_df_sorted.iterrows():
    # clientあるいはuser_agentが変化、あるいは時間のギャップが閾値を上回った
    if (row['client'] != current_client or row['user_agent'] != current_user_agent or
            (current_session_start and (row['datetime'] - current_session_end).seconds > SESSION_THRESHOLD_SECONDS)):
        # この場合、現在のセッション情報を格納
        if current_session_start:
            session_info.append((current_client, current_user_agent, current_session_start, current_session_end, current_session_pages))
            # 最低でも5つのセッションを取得したら、ループを抜ける
            if len(session_info) >= 5:
                break
        
        # 新規セッションをスタート
        current_client = row['client']
        current_user_agent = row['user_agent']
        current_session_start = row['datetime']
        current_session_end = row['datetime']
        current_session_pages = [(row['datetime'], row['request'])]
    else:
        # そうではない場合、現在のセッション二ページを追加
        current_session_pages.append((row['datetime'], row['request']))
        # セッション終了時間を更新
        current_session_end = row['datetime']

# 最低でも5つのセッションのセッション情報を表示
index = 0
for session in session_info:
    print(f"Session {index+1}")
    print("Client:", session[0])
    print("User Agent:", session[1])
    print("Session Start Time:", session[2])
    print("Session End Time:", session[3])
    print("Pages Visited:")
    for timestamp, page in session[4]:
        print(f"    {timestamp}: {page}")
    index +=1
    print("\n\n")
```
```
Session 1
Client: 104.156.210.196
User Agent: Dalvik/2.1.0 (Linux; U; Android 8.0.0; SM-A720F Build/R16NW)
Session Start Time: 2019-01-02 04:20:00+00:33
Session End Time: 2019-01-02 04:20:00+00:33
Pages Visited:
    2019-01-02 04:20:00+00:33: /image/32768?name=24xs450-33.jpg&wh=200x200



Session 2
Client: 104.194.24.33
User Agent: Mozilla/5.0 (Linux; Android 8.0.0; SM-G955F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.99 Mobile Safari/537.36
Session Start Time: 2019-01-02 03:57:00+00:33
Session End Time: 2019-01-02 03:57:00+00:33
Pages Visited:
    2019-01-02 03:57:00+00:33: /amp-helper-frame.html?appId=a624a1c1-0c93-466a-a546-e146710f97e6&parentOrigin=https://www-zanbil-ir.cdn.ampproject.org



Session 3
Client: 104.194.24.54
User Agent: Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G900H Build/MMB29K)
Session Start Time: 2019-01-02 04:24:00+00:33
Session End Time: 2019-01-02 04:26:04+00:33
Pages Visited:
    2019-01-02 04:24:00+00:33: /image/33888?name=model-b2048u-1-.jpg&wh=200x200
    2019-01-02 04:26:04+00:33: /image/11947?name=11947-1-fw.jpg&wh=200x200



Session 4
Client: 104.194.25.207
User Agent: Dalvik/2.1.0 (Linux; U; Android 5.0.2; P01V Build/LRX22G)
Session Start Time: 2019-01-02 04:06:04+00:33
Session End Time: 2019-01-02 04:06:05+00:33
Pages Visited:
    2019-01-02 04:06:04+00:33: /image/33888?name=model-b2048u-1-.jpg&wh=200x200
    2019-01-02 04:06:05+00:33: /image/11947?name=11947-1-fw.jpg&wh=200x200
    2019-01-02 04:06:05+00:33: /image/11926?name=sm812aaa.jpg&wh=200x200



Session 5
Client: 104.248.138.218
User Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1
Session Start Time: 2019-01-02 04:35:01+00:33
Session End Time: 2019-01-02 04:36:02+00:33
Pages Visited:
    2019-01-02 04:35:01+00:33: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
    2019-01-02 04:35:01+00:33: /favicon.ico
    2019-01-02 04:35:01+00:33: /static/images/guarantees/goodShopping.png
    2019-01-02 04:35:02+00:33: /static/css/font/wyekan/font.woff
    2019-01-02 04:35:02+00:33: /static/images/guarantees/bestPrice.png
    2019-01-02 04:35:02+00:33: /static/images/guarantees/warranty.png
    2019-01-02 04:35:02+00:33: /static/images/guarantees/support.png
    2019-01-02 04:35:02+00:33: /static/images/guarantees/fastDelivery.png
    2019-01-02 04:35:03+00:33: /m/browse/dishwasher/%D9%85%D8%A7%D8%B4%DB%8C%D9%86-%D8%B8%D8%B1%D9%81%D8%B4%D9%88%DB%8C%DB%8C
    2019-01-02 04:36:00+00:33: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
    2019-01-02 04:36:02+00:33: /m/browse/sewing-machine/%DA%86%D8%B1%D8%AE-%D8%AE%DB%8C%D8%A7%D8%B7%DB%8C
```

#### 時間間隔が10分のセッションのデータフレームを作成

```py
# セッション情報の配列からセッションデータフレームの作成
session_df = pd.DataFrame(session_info)
```
```py
# カラムの設定
session_df.columns = ['client', 'user_agent', 'start_time', 'end_time',  'pages']
```
```py
# タプルからページのみを抽出
session_df['pages'] = session_df['pages'].apply(lambda x: [page[1] for page in x])
```
```py
# データフレームの最初の2行を表示
session_df.head(2)
```
![Screenshot 2024-07-31 at 14.58.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/e8544cb3-e32f-b837-bf97-ce35f82da886.png)

```py
session_df.info()
```
```
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 5 entries, 0 to 4
Data columns (total 5 columns):
 #   Column      Non-Null Count  Dtype                               
---  ------      --------------  -----                               
 0   client      5 non-null      object                              
 1   user_agent  5 non-null      object                              
 2   start_time  5 non-null      datetime64[ns, pytz.FixedOffset(33)]
 3   end_time    5 non-null      datetime64[ns, pytz.FixedOffset(33)]
 4   pages       5 non-null      object                              
dtypes: datetime64[ns, pytz.FixedOffset(33)](2), object(3)
memory usage: 328.0+ bytes
```

### Q3. 5つの最も頻繁なリファラーウェブサイト

リファラーのない行を削除し、不正なURLをフィルタリングし、最後にベースURLを抽出することで、リファラーのカラムをきれいにします。

```py
from urllib.parse import urlparse

# 'referer' が NaN である行を削除
referer_df = logs_df.dropna(subset=['referer'])

# well-formedではないURLを除外
referer_df = referer_df[referer_df['referer'].apply(lambda x: re.match(r'^https?://', x) is not None)]

# ベースURLを抽出するためにリファラーURLをパース
referer_df['base_url'] = referer_df['referer'].apply(lambda x: urlparse(x.replace('"', '')).scheme + "://" + urlparse(x.replace('"', '')).netloc)

# トップ5の最頻リファラーウェブサイトの特定
counting_referer_occurence = referer_df['base_url'].value_counts().sort_values(ascending=False)

# トップ5の取得
top_5_referers = counting_referer_occurence.head(5)

# トップ5の最頻リファラーウェブサイトの表示
for i, (referer, count) in enumerate(top_5_referers.items(), start=1):
    print(f"{i}. {referer} - Count: {count}")
```
```
1. https://www.zanbil.ir - Count: 3886
2. https://znbl.ir - Count: 141
3. https://torob.com - Count: 91
4. https://www-zanbil-ir.cdn.ampproject.org - Count: 72
5. http://www.zanbil.ir - Count: 50
```

```py
# 切り取りなしにそれぞれのカラムの完全なコンテンツを表示するために、最大カラム幅をNoneに設定
pd.set_option('display.max_colwidth', None)
```

### Q4. サポート率が25%越えの一緒に訪問されるページ

頻繁に一緒に訪問されるというには、それらは同じセッションではなくてはならないため、セッションでグルーピングされたデータフレームを使います

```py
%pip install mlxtend
```
```py
import pandas as pd
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, association_rules

# セッションごとに訪問されたページをリストのリストフォーマットに変換
pages_accessed = session_df['pages'].tolist()

# TransactionEncoderの初期化
te = TransactionEncoder()

# データをone-hotエンコードされたデータフレームにフィット、変換
onehot = te.fit_transform(pages_accessed)

# one-hotエンコードされたデータフレームをデータフレームに変換
df = pd.DataFrame(onehot, columns=te.columns_)

# Aprioriを用いて最低でもサポートが0.25の頻繁なアイテムセットを特定
frequent_itemsets = apriori(df, min_support=0.25, use_colnames=True)
```

一緒に訪問されたページを探しており、これはアイテムセットに2つ以上のページがなくてはならないことを意味するので、1より多いアイテムを持つアイテムセットにフィルタリングします。

```py
# 1より多いアイテムを持つアイテムセットになるように高頻度アイテムセットをフィルタリング
filter_frequent_itemsets = frequent_itemsets[frequent_itemsets['itemsets'].apply(lambda x: len(x)) > 1]
```

```py
# 高頻度アイテムセットを表示
print("Frequent Itemsets:")
filter_frequent_itemsets
```
![Screenshot 2024-07-31 at 15.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/fc38c7b3-c307-4b4e-4ceb-aec2b5f6f445.png)

**洞察:** /image/11947?name=11947-1-fw.jpg&wh=200x200 と /image/33888?name=model-b2048u-1-.jpg&wh=200x200 はともに一緒に訪問されています。

### Q5. リフト値が2.05を超えるアソシエーションルール

```py
# リフトが2.05を超えるルールの取得
rules = association_rules(frequent_itemsets, metric='lift', min_threshold=2.05)
```

```py
# ルールを整形して表示
print("Association Rules with Lift > 2.05:\n")
for index, rule in rules.iterrows():
    antecedents = ', '.join(list(rule['antecedents']))
    consequents = ', '.join(list(rule['consequents']))
    support = rule['support']
    confidence = rule['confidence']
    lift = rule['lift']
    print(f"Rule {index+1}: {antecedents} -> {consequents}")
    print(f"Support: {support:.4f}, Confidence: {confidence:.4f}, Lift: {lift:.4f}\n")
```
```
Association Rules with Lift > 2.05:

Rule 1: /image/33888?name=model-b2048u-1-.jpg&wh=200x200 -> /image/11947?name=11947-1-fw.jpg&wh=200x200
Support: 0.4000, Confidence: 1.0000, Lift: 2.5000

Rule 2: /image/11947?name=11947-1-fw.jpg&wh=200x200 -> /image/33888?name=model-b2048u-1-.jpg&wh=200x200
Support: 0.4000, Confidence: 1.0000, Lift: 2.5000
```

### Q6. 10の高頻度シーケンシャルパターン

GSPアルゴリズムは、ユーザーのナビゲーションのシーケンスで頻繁に生じるパターンを特定し、共通するブラウジング挙動の理解の助けとなります。

```py
%pip install gsppy
```

```py
import argparse
import logging
import random
from gsppy.gsp import GSP
logging.basicConfig(level=logging.DEBUG)
```

```py
result = GSP(pages_accessed).search(0.25)
result
```
```
[{('/image/33888?name=model-b2048u-1-.jpg&wh=200x200',): 2,
  ('/image/11947?name=11947-1-fw.jpg&wh=200x200',): 2},
 {('/image/33888?name=model-b2048u-1-.jpg&wh=200x200',
   '/image/11947?name=11947-1-fw.jpg&wh=200x200'): 2}]
```

```py
max_length = 3
# 最大長に基づき、高頻度のシーケンシャルパターンをフィルタ
filtered_patterns = [pattern for pattern in result if len(pattern) <= max_length]

# 高頻度のシーケンシャルパターンを表示
for pattern in filtered_patterns:
    print(pattern)
```
```
{('/image/33888?name=model-b2048u-1-.jpg&wh=200x200',): 2, ('/image/11947?name=11947-1-fw.jpg&wh=200x200',): 2}
{('/image/33888?name=model-b2048u-1-.jpg&wh=200x200', '/image/11947?name=11947-1-fw.jpg&wh=200x200'): 2}
INFO:py4j.clientserver:Received command c on object id p0
```

> **0.25より少なくすると、GSPの実行に数時間を要しRAMを食い尽くしました**

### Q7. 類似したナビゲーションパターンを持つユーザーのクラスターを示すグラフ

```py
import numpy as np
import matplotlib.pyplot as plt
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# 分析に適したフォーマットにパターンを設定
patterns = onehot

# K-meansを用いたユーザーのクラスター
kmeans = KMeans(n_clusters=3,  n_init=10)
clusters = kmeans.fit_predict(patterns)
```

```py
# クラスターの可視化
plt.figure(figsize=(10, 6))
plt.scatter(patterns[:, 0], patterns[:, 1], c=clusters, cmap='viridis')
plt.title('Clusters of Users with Similar Navigational Patterns')
plt.colorbar(label='Cluster')
plt.show()
```
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/77ea4429-e552-f509-f9a2-a88a34e3e2c4.png)

```py
import networkx as nx
import matplotlib.pyplot as plt

# グラフの作成
G = nx.Graph()

# グラフにノード(ユーザー)を追加
for i in range(len(patterns)):
    # ノードの属性としてクラスターを割り当て
    G.add_node(i, label=f"User {i}", cluster=clusters[i])  

# (同じクラスターで)類似したユーザー間にエッジを追加
for i in range(len(patterns)):
    for j in range(i + 1, len(patterns)):
        if clusters[i] == clusters[j]:
            G.add_edge(i, j)

# グラフの可視化
plt.figure(figsize=(10, 8))
# グラフのレイアウト
pos = nx.spring_layout(G)  

# クラスターごとに色付けしてノードを描写
node_color = [clusters[node] for node in G.nodes()]
nx.draw_networkx_nodes(G, pos, node_color=node_color, cmap=plt.cm.tab10, node_size=200)

# エッジの描画
nx.draw_networkx_edges(G, pos, alpha=0.5)

plt.title('Clusters of Users with Similar Navigational Patterns')
# plt.axis('off')
plt.show()
```
![download (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168882/5b19a310-aeaa-8143-bf67-c64928cbee0f.png)


### はじめてのDatabricks

[はじめてのDatabricks](https://qiita.com/taka_yayoi/items/8dc72d083edb879a5e5d)

### Databricks無料トライアル

[Databricks無料トライアル](https://databricks.com/jp/try-databricks)