見出し画像

【VRChat】私はどこにいるの?

自分のアクティビティって見返すのって結構楽しいですよね!
私だけですかね?
普段自分がどのタイプのインスタンスでにいて、どんなワールドにいるのかをちょっとだけ可視化してみることにしました。

可視化するのは以下の情報です。
・どのインスタンスタイプにいるか?
・どのワールドに長い時間いるか?
・どのワールドの訪問回数が多いか?

6月の1か月分のデータで試してみました。
Pythonにしたのはたまたま土曜日の居場所集会で、Matplotlibを使ってるよ~という方とお話ししたので、Pythonで書きました。
NumpyとPandasは最強!

結果はこんな感じです!

デデン!!!

レイアウトの調整はめんどくさかったので適当……
デザインセンスが欲しいね。
デザイナーさんは天才だよ、みんな。

コードは以下に置いておくので勝手に使ってください!
コメントも出来るだけ書きましたので……。
それっぽくは出てますが、たぶん絶妙に微妙に間違えてるんで誰か直してください……。
出力結果が何か思ってたのと違うんできっと修正が必要……。(PUBLICが0なのはおかしいので……)

# 必要なライブラリのインポート
import matplotlib.pyplot as plt
import numpy as np
import re
from datetime import datetime, timedelta
from collections import defaultdict
import os

#pip install numpy
#pip install matplotlib が必要

# 日本語フォントの設定
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Hiragino Sans', 'Yu Gothic', 'Meirio', 'Takao', 'IPAexGothic', 'IPAPGothic', 'VL PGothic', 'Noto Sans CJK JP']

def determine_instance_type(instance_id):
    # インスタンスIDからインスタンスタイプを判定する関数
    print(instance_id)
    if '~hidden' in instance_id:
        return 'FRIEND_PLUS'
    if '~friends' in instance_id:
        return 'FRIEND'
    if '~private' in instance_id and '~canRequestInvite' in instance_id:
        return 'INVITE_PLUS'
    if '~private' in instance_id and '~canRequestInvite' not in instance_id:
        return 'INVITE'
    if '~group' in instance_id and '~groupAccessType(members)' in instance_id:
        return 'GROUP'
    if '~group' in instance_id and '~groupAccessType(plus)' in instance_id:
        return 'GROUP_PLUS'
    if '~group' in instance_id and '~groupAccessType(public)' in instance_id:
        return 'GROUP_PUBLIC'
    return 'PUBLIC'  # どの条件にも当てはまらない場合はPUBLIC

def parse_log_file(file_path):
    # ログファイルを解析し、インスタンスタイプとワールドごとの滞在時間と訪問回数を計算する関数
    instance_times = defaultdict(lambda: {'total': timedelta(), 'last_join': None})
    world_times = defaultdict(timedelta)
    world_visits = defaultdict(int)
    
    # 正規表現パターンの定義
    # ワールドIDとインスタンスタイプ
    join_pattern = r'(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}) Log.*\[Behaviour\] Joining (wrld_[^:]+:[^\~]+\~(?:hidden|friends|private|group)[^\r\n]+)'
    # ワールド名
    create_pattern = r'(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}) Log.*\[Behaviour\] Joining or Creating Room: (.+)'
    # Leftワールド
    leave_pattern = r'(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2}) Log.*\[Behaviour\] OnLeftRoom'
    
    current_instance_type = None
    current_world_name = None
    
    # 最初と最後の日付を追跡
    first_date = None
    last_date = None
    
    # ログファイルの読み込み
    with open(file_path, 'r', encoding='utf-8') as file:
        log_content = file.read()
    
    # イベントの抽出とソート
    # タイムスタンプでソート
    events = sorted(
        list(re.finditer(join_pattern, log_content, re.MULTILINE)) +
        list(re.finditer(create_pattern, log_content, re.MULTILINE)) +
        list(re.finditer(leave_pattern, log_content, re.MULTILINE)),
        key=lambda x: x.group(1)
    )
    
    # イベントの処理
    for event in events:
        timestamp = datetime.strptime(event.group(1), '%Y.%m.%d %H:%M:%S')
        
        # 最初の日付を設定
        if first_date is None:
            first_date = timestamp.date()
        # 最後の日付を更新
        last_date = timestamp.date()
        
        if 'Joining ' in event.group(0) and 'Joining or Creating Room:' not in event.group(0):
            # インスタンス参加イベントの処理
            if current_instance_type:
                # 前のインスタンスの滞在時間を計算
                if instance_times[current_instance_type]['last_join']:
                    duration = timestamp - instance_times[current_instance_type]['last_join']
                    instance_times[current_instance_type]['total'] += duration
                    world_times[current_world_name] += duration
            
            current_instance_type = determine_instance_type(event.group(2))
            instance_times[current_instance_type]['last_join'] = timestamp
        
        elif 'Joining or Creating Room:' in event.group(0):
            # ワールド参加イベントの処理
            current_world_name = event.group(2)
            world_visits[current_world_name] += 1  # 訪問回数をカウント
        
        elif 'OnLeftRoom' in event.group(0):
            # ワールド退出イベントの処理
            if current_instance_type:
                if instance_times[current_instance_type]['last_join']:
                    duration = timestamp - instance_times[current_instance_type]['last_join']
                    instance_times[current_instance_type]['total'] += duration
                    world_times[current_world_name] += duration
                    instance_times[current_instance_type]['last_join'] = None
                current_instance_type = None
                current_world_name = None
    
    # 最後のインスタンスの滞在時間を計算(ログの最後がLeaveでない場合)
    if current_instance_type and instance_times[current_instance_type]['last_join']:
        last_timestamp = datetime.strptime(events[-1].group(1), '%Y.%m.%d %H:%M:%S')
        duration = last_timestamp - instance_times[current_instance_type]['last_join']
        instance_times[current_instance_type]['total'] += duration
        world_times[current_world_name] += duration
    
    # 有効なログエントリが見つからなかった場合の処理
    if first_date is None or last_date is None:
        print(f"警告: ファイル {file_path} に有効なログエントリが見つかりませんでした。")
        return {}, {}, {}, (None, None)
    
    # 時間単位(時)で結果を返す
    return {k: v['total'].total_seconds() / 3600 for k, v in instance_times.items()}, \
           {k: v.total_seconds() / 3600 for k, v in world_times.items()}, \
           dict(world_visits), \
           (first_date, last_date)

def process_log_folder(folder_path):
    # 指定されたフォルダ内のすべてのログファイルを処理する関数
    total_instance_times = defaultdict(float)
    total_world_times = defaultdict(float)
    total_world_visits = defaultdict(int)
    log_file_pattern = r'output_log_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.txt'
    
    first_date = None
    last_date = None
    
    for filename in os.listdir(folder_path):
        if re.match(log_file_pattern, filename):
            file_path = os.path.join(folder_path, filename)
            file_instance_times, file_world_times, file_world_visits, (file_first_date, file_last_date) = parse_log_file(file_path)
            
            # ファイルに有効なデータがある場合のみ処理
            if file_first_date is not None and file_last_date is not None:
                # 各ファイルの結果を合算
                for instance_type, duration in file_instance_times.items():
                    total_instance_times[instance_type] += duration
                for world_name, duration in file_world_times.items():
                    total_world_times[world_name] += duration
                for world_name, visits in file_world_visits.items():
                    total_world_visits[world_name] += visits
                
                # 全体の日付範囲を更新
                if first_date is None or file_first_date < first_date:
                    first_date = file_first_date
                if last_date is None or file_last_date > last_date:
                    last_date = file_last_date
    
    if first_date is None or last_date is None:
        print("警告: 処理可能なログファイルが見つかりませんでした。")
        return {}, {}, {}, (None, None)
    
    return dict(total_instance_times), dict(total_world_times), dict(total_world_visits), (first_date, last_date)

def create_charts(instance_times, world_times, world_visits, date_range):
    # グラフを作成する関数
    fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(20, 20))
    fig.suptitle(f'VRCインスタンスタイプとワールドの滞在時間・訪問回数分析\n期間: {date_range[0]} から {date_range[1]}', fontsize=16, y=0.95)
    
    # インスタンスタイプの円グラフ
    labels = list(instance_times.keys())
    sizes = list(instance_times.values())
    total_hours = sum(sizes)
    ax1.pie(sizes, labels=labels, autopct=lambda pct: f'{pct:.1f}%\n({pct*total_hours/100:.1f}時間)', startangle=90)
    ax1.axis('equal')
    ax1.set_title('インスタンスタイプ別滞在時間割合', fontsize=14, pad=20)
    
    # インスタンスタイプの棒グラフ
    colors = plt.cm.Set3(np.linspace(0, 1, len(labels)))
    ax2.bar(labels, sizes, color=colors)
    ax2.set_ylabel('滞在時間 (時間)', fontsize=12)
    ax2.set_title('インスタンスタイプ別滞在時間', fontsize=14, pad=20)
    for i, v in enumerate(sizes):
        ax2.text(i, v, f'{v:.1f}', ha='center', va='bottom')
    plt.setp(ax2.get_xticklabels(), rotation=45, ha='right')
    
    # トップ10ワールドの滞在時間棒グラフ
    top_worlds_time = sorted(world_times.items(), key=lambda x: x[1], reverse=True)[:10]
    world_names_time, world_durations = zip(*top_worlds_time)
    ax3.bar(world_names_time, world_durations)
    ax3.set_ylabel('滞在時間 (時間)', fontsize=12)
    ax3.set_title('滞在時間トップ10ワールド', fontsize=14, pad=20)
    for i, v in enumerate(world_durations):
        ax3.text(i, v, f'{v:.1f}', ha='center', va='bottom')
    plt.setp(ax3.get_xticklabels(), rotation=90, ha='right')
    
    # トップ10ワールドの訪問回数棒グラフ
    top_worlds_visits = sorted(world_visits.items(), key=lambda x: x[1], reverse=True)[:10]
    world_names_visits, visit_counts = zip(*top_worlds_visits)
    ax4.bar(world_names_visits, visit_counts)
    ax4.set_ylabel('訪問回数', fontsize=12)
    ax4.set_title('訪問回数トップ10ワールド', fontsize=14, pad=20)
    for i, v in enumerate(visit_counts):
        ax4.text(i, v, str(v), ha='center', va='bottom')
    plt.setp(ax4.get_xticklabels(), rotation=90, ha='right')
    
    plt.tight_layout(rect=[0, 0.03, 1, 0.95])
    plt.show()

# メイン処理
log_folder_path = ''  # VRCのログフォルダへのパスを指定してください
instance_times, world_times, world_visits, date_range = process_log_folder(log_folder_path)

if date_range[0] is None or date_range[1] is None:
    print("エラー: 有効なログデータが見つかりませんでした。プログラムを終了します。")
else:
    create_charts(instance_times, world_times, world_visits, date_range)
    print(f"ログの期間: {date_range[0]} から {date_range[1]}")

#VRChat #VRC #メタバースやってます #インスタンス #Python #Matplotlib  


この記事が参加している募集

この記事が気に入ったらサポートをしてみませんか?