0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spotifyの週間チャートにおける、順位を取り巻く因果関係について

Posted at

はじめに

前回作成したLightGBMによる「4週間後の順位予測モデル」に続き、今回はベイジアンネットワークを用いて、Spotifyの週間チャートの裏側にある因果構造を分析しました。

前モデル作成時に重要度が高かった特徴量の内、上位5要素とrankの依存関係を可視化することを目的とします。

・Windows11
・Jupyter Notebook

目次

前処理
離散化
ネットワーク構造の学習と可視化
条件付き確率
おわりに

前処理

分析には、ベイジアンネットワークを扱うためにpgmpyと、可視化のためのgraphvizを使用しました。その他使用ライブラリは以下。

import pandas as pd
import lightgbm as lgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.metrics import r2_score
import matplotlib.pyplot as plt
import seaborn as sns
import japanize_matplotlib
import numpy as np
import os
import re

分析に用いるのは下記の特徴量です

ID 説明
previous_rank 前週の順位
weeks_on_chart チャート滞在週数
streams 週別再生回数
rank_change_1w 1週間での順位変動
peak_rank 最高順位

streamsについては、ツリーモデル作成時にはそのまま用いましたが、今回は対数化してlog_streamsとして取り扱います。

output_dir = 'outputs'
os.makedirs(output_dir, exist_ok=True)

# Load dataset
df = pd.read_csv('feature_engineered_data.csv') #used for LightGBM

# --- Log Transformation for 'streams' ---
df['log_streams'] = np.log1p(df['streams'])

# --- Visualize Distributions (Before & After Log Transformation) ---
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Plot 'streams' distribution
sns.histplot(df['streams'], ax=axes[0], kde=True, bins=50)
axes[0].set_title('Distribution of Raw "streams"')
axes[0].set_xlabel('Number of Streams')
axes[0].set_ylabel('Frequency (Count)')
axes[0].ticklabel_format(style='plain', axis='x') # Avoid scientific notation

# Plot 'log_streams' distribution
sns.histplot(df['log_streams'], ax=axes[1], kde=True, bins=50)
axes[1].set_title('Distribution of Log-Transformed "log_streams"')
axes[1].set_xlabel('Log(1 + Number of Streams)')
axes[1].set_ylabel('Frequency (Count)')

plt.tight_layout()
plt.show()

streams_distribution_comparison.png

離散化

まず初めに各特徴量を4つのカテゴリーに分割(離散化)しました。

# Select features for analysis
cols_for_bn = [
    'rank', 
    'previous_rank', 
    'weeks_on_chart',
    'log_streams', 
    'rank_change_1w',
    'peak_rank'
]
df_bn = df[cols_for_bn].copy()

# binning
for col in df_bn.columns:
    df_bn[col] = pd.qcut(df_bn[col], q=4, labels=False, duplicates='drop')

# Drop rows with NaN values
df_bn.dropna(inplace=True)

ネットワーク構造の学習と可視化

データから特徴量同士の関係性をpgmpyに学習させます。
手法はHill Climb Search (BicScore)で試しました。

ただ、単純に学習させると「今週の順位が、先週の順位の原因」といった、時間の流れに逆行した関係性を見つけてしまうことがあります。
そこで、「時間は過去から未来にしか流れない」というルールをブラックリスト化しました。

# Create a blacklist to enforce causality.
blacklist = [
    ('rank', 'previous_rank'),
    ('rank', 'log_streams'), 
    ('rank', 'weeks_on_chart'),
    ('rank', 'peak_rank'),
    ('rank_change_1w', 'previous_rank'),
    ('rank_change_1w', 'rank')
]

続いて学習・可視化です。

# Initialize the HillClimbSearch algorithm
hc = HillClimbSearch(data=df_bn)
best_model_structure = hc.estimate(
    scoring_method=BicScore(data=df_bn),
    black_list=blacklist
)

# Get the learned network structure
edges = best_model_structure.edges()

# Create a BayesianNetwork model
model = BayesianNetwork(edges)

# Fit
model.fit(data=df_bn, estimator=MaximumLikelihoodEstimator)

## 4. Visualizing the Network Structure
# Initialize a new directed graph
dot = Digraph(comment='Spotify Chart Bayesian Network')
dot.attr('node', shape='ellipse', style='filled', color='skyblue', fontname='Arial')
dot.attr('edge', fontname='Arial')

# Add nodes and edges to the graph
nodes = set([item for t in edges for item in t])
for node in nodes:
    dot.node(node)
for edge in edges:
    dot.edge(edge[0], edge[1])
    
display(dot)

得られたSpotify週間チャートの因果構造がこちらです。

spotify_bayesian_network_log.png

過去の実績であるpeak_rankweeks_on_chartがその週の再生回数を支えているという現実的な因果関係が示されています。

条件付き確率

ここでは、モデルの中心的な関係性である P(rank | previous_rank, log_streams) を少し掘り下げてみます。

def display_cpt_as_dataframe(cpt):
    child_node = cpt.variables[0]
    child_states = cpt.state_names[child_node]

    if len(cpt.variables) == 1: 
        df_cpt = pd.DataFrame(cpt.values, index=child_states, columns=['P'])
        display(df_cpt)
    else: 
        values = cpt.values.flatten()
        parent_names = cpt.variables[1:]
        parent_states_product = pd.MultiIndex.from_product(
            [cpt.state_names[p] for p in parent_names], 
            names=parent_names
        )
        
        df_cpt = pd.DataFrame(
            values.reshape(len(child_states), -1), 
            index=child_states, 
            columns=parent_states_product
        )
        
        display(df_cpt)

# Iterate through all nodes in the model and display their CPTs as tables
for node_name in model.nodes:
    cpt = model.get_cpds(node_name)
    display_cpt_as_dataframe(cpt) 

cpt_rank = model.get_cpds('rank')
if len(cpt_rank.variables) > 1:
    values = cpt_rank.values.flatten()
    parent_names = cpt_rank.variables[1:]
    parent_states = pd.MultiIndex.from_product(
        [cpt_rank.state_names[p] for p in parent_names],
        names=parent_names
    )
    child_states = cpt_rank.state_names[cpt_rank.variables[0]]
    
    df_cpt_rank = pd.DataFrame(values.reshape(len(child_states), -1), index=child_states, columns=parent_states)

    plt.figure(figsize=(14, 7))
    sns.heatmap(
        df_cpt_rank, 
        annot=True,
        cmap='Blues',
        fmt='.2f',
        linewidths=.5
    )
    plt.title(f"CPT Heatmap for '{cpt_rank.variables[0]}' (P({cpt_rank.variables[0]} | {', '.join(parent_names)}))", fontsize=16)
    plt.xlabel("Parent Node States (Causes)", fontsize=12)
    plt.ylabel(f"State of '{cpt_rank.variables[0]}' (Effect)", fontsize=12)
    plt.tight_layout()
    plt.show()

cpt_heatmap_rank_log.png

このヒートマップは、previous_ranklog_streamsの状態を条件としたときの、rankの事後確率分布を可視化したものです。

横軸 log_streams previous_rank
0-0 0(最少) 0(最高:1位~50位)
0-3 0(最少) 3(最低:151位~200位)
3-0 3(最多) 0(最高:1位~50位)
3-3 3(最多) 3(最低:151位~200位)

rankを決定づける支配的な要因はlog_streamsであり、previous_rankが順位に直接与える影響は小さいことが示されています。

おわりに

LightGBMモデル作成時の特徴量重要度はprevious_rank>streamsでしたが、因果関係の文脈ではrankに直接影響を与えている本当の要素は再生回数であることが示唆されました。
プレイヤーの寄与量だけでなく、各プレイヤーがどのような働きをしているかを可視化できる興味深い分析でした。


ご覧いただきありがとうございました。
今回使用した全コードはこちらです。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?