ラベル Analysis の投稿を表示しています。 すべての投稿を表示
ラベル Analysis の投稿を表示しています。 すべての投稿を表示

2022年5月29日日曜日

Spotify再生回数トップ50の曲を分類して可視化する

Spotify APIで再生回数トップ50の曲データを取得するでは、Spotify APIで再生回数トツプ50の曲一覧を取得した。Spotify APIでは、曲名やアーティスト名などの情報のほかに、曲の特徴を取得できる。曲の特徴とは、ダンスに適した曲か、エネルギッシュさなどといったもので、これらの情報を利用して曲の分類と可視化をしてみる。


環境

WSL2(Ubuntu20.04)とJupyter。

$ lsb_release -dr
Description:    Ubuntu 20.04.4 LTS
Release:        20.04
$ python3 -V
Python 3.8.10
$ jupyter --version
jupyter core     : 4.7.1
jupyter-notebook : 6.2.0
qtconsole        : 5.0.2
ipython          : 7.20.0
ipykernel        : 5.4.3
jupyter client   : 6.1.12
jupyter lab      : 3.0.9
nbconvert        : 6.0.7
ipywidgets       : 7.6.3
nbformat         : 5.1.2
traitlets        : 5.0.5

Spotify APIで再生回数トップ50の曲データを取得するの手順でspotipyをインストールしておく。

さらに、曲を特徴でクラスタリングして可視化するためのPythonライブラリをインストールする。

$ pip3 install scikit-learn pandas matplotlib seaborn


spotipyで曲の特徴を取得

APIで取得できる特徴の説明はGet Track's Audio Featuresにある。今回はacousticness(アコースティック度)、danceability(ダンスに適した曲か)、energy(エネルギッシュさ)、instrumentalness(ボーカルがない度合い)、liveness(ライブ感)、loudness(曲全体のボリューム)、speechiness(話し言葉の度合い)、valence(曲のポジティブ度)を使用する。

今回もSpotify APIで再生回数トップ50の曲データを取得すると同様にspotipyを使う。曲の特徴を取得するには、spotifyのaudio_featuresメソッドに引数としてトラックIDのリストを渡す。取得した特徴はPandasのDataFrameにしておく。
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

次のようにDataFrameには50曲分の特徴が格納される。
Index: 50 entries, 4IfrM44LofE9bSs6TDZS49 to 3oQaOjaIYPsnJbGNzXcIID
Data columns (total 9 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   danceability      50 non-null     float64
 1   energy            50 non-null     float64
 2   loudness          50 non-null     float64
 3   speechiness       50 non-null     float64
 4   acousticness      50 non-null     float64
 5   instrumentalness  50 non-null     float64
 6   liveness          50 non-null     float64
 7   valence           50 non-null     float64
 8   tempo             50 non-null     float64


クラスタリングによる分類と可視化

まずはK平均法でクラスタリングする。クラスター数は今回はとりあえず5とする。そして、その結果をもとにクラスターごとの曲を表示する。

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import spotipy
from spotipy.oauth2 import SpotifyClientCredentials
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

# language='ja'としないとアーティスト名が英語表記になる
spotify = spotipy.Spotify(client_credentials_manager=SpotifyClientCredentials(), language='ja')

def get_tracks(playlist_id):
    results = spotify.playlist_items(
        playlist_id,
        fields=None,
        limit=50,
        offset=0,
        market='JP',
        additional_types=('track', 'episode'),
    )
    items = results['items']

    tracks = []
    for item in items:
        data = {}
        track = item['track']
        data['track_id'] = track['id']
        data['track_name'] = track['name']
        data['artists'] = [ar['name'] for ar in track['artists']]
        tracks.append(data)

    return tracks

def get_track_features(track_ids):
    results = spotify.audio_features(track_ids)
    # idと分類に使用する特徴
    features = ['id', 'danceability', 'energy', 'loudness', 'speechiness', 'acousticness', 'instrumentalness', 'liveness', 'valence', 'tempo']
    track_features = []
    for re in results:
        track_features.append(
            {key: value for key, value in re.items() if key in features}
        )
    return track_features

def kcluster(df):
    X = StandardScaler().fit_transform(df)
    # とりあえずクラスター数5
    n_clusters = 5
    clusterer = KMeans(init='k-means++', n_clusters=n_clusters, random_state=0)
    kmeans = clusterer.fit(X)
    return kmeans.labels_

# プレイリストの曲一覧取得
# トップ50 日本
# URLの最後の22文字がプレイリストID
playlist_id = '37i9dQZEVXbKXQ4mDTEBXq'
tracks = get_tracks(playlist_id)
track_info = {tr['track_id']: f'{tr["track_name"]} - {"|".join(tr["artists"])}' for tr in tracks}

# 曲の特徴を取得
track_ids = [tr['track_id'] for tr in tracks]
track_features = get_track_features(track_ids)
df_feature = pd.DataFrame([tf for tf in track_features])
df_feature.set_index('id', inplace=True)
df_feature.info()

# K平均法でクラスタリング
df_feature['cluster'] = kcluster(df_feature.values)

# クラスターごとの曲名を表示
with pd.option_context('display.max_rows', None, 'display.max_columns', None):
    for cluster, rows in df_feature.groupby('cluster'):
        print(f'\ncluster {cluster}:')
        for id in rows.index:
            print(track_info[id])

結果は以下の通り。

cluster 0:
ミックスナッツ - Official髭男dism
喜劇 - 星野 源
Habit - SEKAI NO OWARI
FEARLESS - LE SSERAFIM
Dynamite - BTS
Butter - BTS
群青 - YOASOBI
一途 - King Gnu
常緑 - 大橋ちっぽけ
怪物 - YOASOBI
WA DA DA - Kep1er
The Feels - TWICE
Bye-Good-Bye - BE:FIRST
ELEVEN - IVE

cluster 1:
ベテルギウス - 優里
なんでもないよ、 - マカロニえんぴつ
水平線 - back number
残響散歌 - Aimer
STAY (with Justin Bieber) - The Kid LAROI|ジャスティン・ビーバー
Cry Baby - Official髭男dism
猫 - DISH//
I LOVE... - Official髭男dism
おもかげ (produced by Vaundy) - milet|Aimer|幾田りら

cluster 2:
M八七 - 米津玄師
ドライフラワー - 優里
カメレオン - King Gnu
シャッター - 優里
勿忘 - Awesome City Club
逆夢 - King Gnu
点描の唄 - Mrs. GREEN APPLE|井上苑子
魔法の絨毯 - 川崎 鷹也
YOKAZE - 変態紳士クラブ
ハート - あいみょん
Stand by me, Stand by you. - 平井 大

cluster 3:
W / X / Y - Tani Yuuki
シンデレラボーイ - Saucy Dog
CITRUS - Da-iCE
恋風邪にのせて - Vaundy
きらり - 藤井 風
LOVE DIVE - IVE
Permission to Dance - BTS
Mela! - 緑黄色社会
夜に駆ける - YOASOBI
三原色 - YOASOBI
115万キロのフィルム - Official髭男dism
ヨワネハキ - MAISONdes|和ぬか|asmi
Pretender - Official髭男dism
踊 - Ado
BOY - King Gnu

cluster 4:
踊り子 - Vaundy

さらに、特徴ごとにクラスター別の箱ひげ図を作成する。上記コードに以下を追記。

# 特徴別の箱ひげ図を作成
fig, ax =plt.subplots(len(df_feature.columns) - 1, 1, figsize=(12, 40), facecolor=(1, 1, 1))
for i, col_name in enumerate(df_feature):
    if col_name != 'cluster':
        sns.boxplot(x=df_feature[col_name], y=df_feature['cluster'], width=0.5, orient='h', ax=ax[i])
        sns.swarmplot(x=df_feature[col_name], y=df_feature['cluster'], color="r", orient='h', dodge=True, ax=ax[i])
plt.tight_layout()
plt.show()

結果は以下のような箱ひげ図が作成される。クラスター4に1曲だけ属する「踊り子」は、ほかの曲に比べてacousticness(アコースティック度)とinstrumentalness(ボーカルがない度合い)が高いことがわかる。



2022年4月14日木曜日

VS Code上のJupyterノートブックの配色を変える

VS CodeではPipenvとVS Codeの環境でJupyter Notebookを使うのように、Jupyterを使ってグラフなどを表示できる。Jupyter上で動作するpyLDAvisも使えるのだが、VS Codeをダークモードにしていると、以下画像のようにpyLDAvisの結果が見づらい。


そこで、VS Code上のJupyterノートブックの配色を変えてみる。


環境

WSL2(Ubuntu20.04)。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


必要なPythonライブラリなどのインストール

VS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示するために必要なPythonライブラリなどをインストールする。pyLDAvisとJupyterの他に、LDAモデル作成に使うscikit-learnもインストールする。

$ pip3 install scikit-learn jupyter pyldavis


VS Code上でpyLDAvisの表示

まずはVS Code上でJupyterノートブックを作成して、pyLDAvisでLDAの結果を表示する。PipenvとVS Codeの環境でJupyter Notebookを使うのように、拡張子ipynbのファイルを作成して、以下のコードを記述する。

import re

from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
import pyLDAvis
import pyLDAvis.sklearn

pyLDAvis.enable_notebook()

categories = ['rec.sport.baseball', 'rec.sport.hockey', 'comp.sys.mac.hardware', 'comp.windows.x']
twenty_train = fetch_20newsgroups(subset='train', categories=categories, shuffle=True, random_state=42)

def remove_figures(text):
    """数字を除去"""
    text = text.lower()
    text = re.sub(r'\d+', '', text)
    return text

vectorizer = CountVectorizer(preprocessor=remove_figures)

# BOW
X = vectorizer.fit_transform(twenty_train.data)

lda = LatentDirichletAllocation(n_components=5, random_state=0)
lda.fit(X)

pyLDAvis.sklearn.prepare(lda, X, vectorizer)

実行すると以下のようにpyLDAvisでの表示ができているが、VS Codeがダークモードだと背景色と文字色がともに黒系の色なのとても見づらい。



VS Code上のJupyterノートブックの配色を変える

Jupyterの表示結果の配色を変えるには、VS Codeの設定を変更する。VS Codeの設定ファイルを開いて(Windowsの場合は%APPDATA%\Code\User\settings.jsonにある)、以下を追記する。

"workbench.colorCustomizations": {

    "notebook.outputContainerBackgroundColor": "#830af5",

},

設定変更後、ノートブックの背景色が指定した色(#830af5)に変わる。


背景色以外の色も変更可能で、Notebook colorsに設定の一覧がある。


2022年2月13日日曜日

PipenvとVS Codeの環境でJupyter Notebookを使う

Jupyter Notebook/Labはデータ分析には便利なのだが、コード編集は使い慣れたIDEなどを使いたい。VS Codeは拡張機能をインストールすることでJupyter Notebookを使えるようになるので、今回はPipenvで構築した環境のVS Code上でJupyter notebookを使えるようにしてみる。Pipenvはパッケージ管理と仮想環境の構築の両方ができるツール。


環境

WSL2(Ubuntu20.04)。VS CodeからRemote DevelopmentでWSL2に接続する。

$ lsb_release -dr
Description:    Ubuntu 20.04.3 LTS
Release:        20.04
$ python3 -V
Python 3.8.10


VS Codeの設定

今回はVS CodeでWSL2のUbuntu上に接続して環境を作成するので、Remote DevelopmentでWSL2にSSH接続する。方法はRemote DevelopmentでSSH接続先にVS Codeの開発環境を作成するを参照。

VS CodeでWSL2にSSH接続したらVS CodeにPython拡張機能をインストールする。Python拡張機能をインストールするとJupyter拡張機能もインストールされる。Ctrl+Shift+Xで拡張機能のメニューを表示し、検索ボックスで「python」で検索すると見つかる。


Pipenvで環境作成


まずは環境を作成するディレクトリ作成する。VS Code上でCtrl+Shift+@などでターミナルを開いてディレクトリ作成。ここではホームディレクトリ配下にtestenvというディレクトリを作成。
$ mkdir testenv
次にVS Codeから「Open Folder」で作成したディレクトリを開く。この状態でVS Codeのターミナルを開きPipenvをpipでインストール。
$ pip3 install pipenv
$ pip3 show pipenv
Name: pipenv
Version: 2021.11.23
...

続いてPipenvでPython3.8の仮想環境作成。
$ pipenv --python 3.8
Pipenvでjupyterと、Jupyter Notebookの動作確認用にMatplotlibをインストール。
$ pipenv install jupyter matplotlib

jupyterのインストールが完了したら、コマンドパレットを開いて「Python: Select Interpreter」を入力する。Pythonインタープリターの一覧が表示されるので、今回Pipenvで作成したenvtest-xxxxxを選択する。



Jupyterを使う


VS Code上で新規にNotebookを作成するにはコマンドパレットで「Jupyter: Create New Jupyter Notebook」と入力するか、VS CodeのExploreで拡張子ipynbのファイルを作成する。

新規作成したら右上の「Select Kernel」をクリックするとPythonインタープリターの一覧が表示されるので、その中から今回Pipenvで作成したenvtest-xxxxxを選択する。

ここまできたらMatplotlibでグラフを作成してVS Code上で表示してみる。コードを実行してipykernelをインストールするようメッセージが表示されたらインストールする。
%matplotlib inline
import math

import matplotlib.pyplot as plt

f = lambda x: math.exp(pow(x, 2) / 16)
X = [x/10 for x in range(-50, 51)]
y = [f(x) for x in X]
plt.plot(X, y, marker='o')
上記コードを実行すると以下のようなグラフがVS Code上に表示される。


ちなみに、VS Code上のNotebookの行数表示は、コード左側の余白を選択してCtrl-L。

2021年9月7日火曜日

Googleモビリティレポートのデータを時系列クラスタリングする

Googleがコミュニティモビリティレポートを公開している。これはGoogleアカウントのロケーション履歴を有効にしているユーザー(サンプルベース)のデータに基づいた移動データで、2020 年 1 ⽉ 3 ⽇〜2 ⽉ 6 ⽇の 5 週間における該当曜⽇の中央値を基準とした増減のデータになっている。詳細はモビリティ レポートの CSV ドキュメントを参照。今回はこのデータを使って、Pythonで時系列クラスタリングをすると同様にPythonのtslearnを使って時系列クラスタリングを行う。


環境


WSL2(Ubuntu20.04)とJupyterLab。
 

その他必要なPythonライブラリをインストール。


モビリティレポートの可視化

モビリティレポートのcsvには世界中のデータが含まれるが、ここでは日本の都道府県ごとのデータのみを使う。まずはcsvをPandasのDataFrameとして読み込んで中身を確認。sub_region_1が都道府県のカラムで、retail_and_recreation_percent_change_from_baselineからresidential_percent_change_from_baselineが場所カテゴリごとの移動データになっている。

import pandas as pd

# Googleモビリティレポートのファイルパス
# 2020年1月3日から2月6日の5週間の曜日別中央値を基準とした値
MOBILITY_CSV_GOOGLE = 'Global_Mobility_Report.csv'

dfg = pd.read_csv(MOBILITY_CSV_GOOGLE)
dfg.info()



7月の都道府県ごとのtransit_stations_percent_change_from_baseline(公共交通機関の拠点)のデータを可視化してみる。

%matplotlib inline

import matplotlib.pyplot as plt
import pandas as pd

# Googleモビリティレポートのファイルパス
# 2020年1月3日から2月6日の5週間の曜日別中央値を基準とした値
MOBILITY_CSV_GOOGLE = 'Global_Mobility_Report.csv'

# 対象とする場所のカテゴリ
# 公共交通機関の拠点(例: 地下鉄、バス、電車の駅)など
MOBILITY_CATEGORY = 'transit_stations_percent_change_from_baseline'

dfg = pd.read_csv(MOBILITY_CSV_GOOGLE)
# 都道府県ごとのデータを取得
dfg_jp = dfg[dfg['country_region_code']=='JP'].dropna(subset=['sub_region_1'])

# 都道府県のチェック
pref = dfg_jp['sub_region_1'].unique()
print(pref)
assert len(pref) == 47

# 日付をインデックス、都道府県をカラムとするDataFrameに変換
dfg_cat = dfg_jp.set_index(['date', 'sub_region_1'])[MOBILITY_CATEGORY].unstack()

# 2021年7月のデータを取得
dfg_cat_jul = dfg_cat['2021-07-01':'2021-07-31']

# データのプロット
ax = dfg_cat_jul.plot(figsize=(12,8), alpha=0.75, rot=90);
ax.legend(loc='center left', bbox_to_anchor=(1.0, 0.5))
plt.show()

47都道府県分をすべてプロットしているので見づらいグラフになっているが、4連休のあたりで都道府県ごとの違いが大きいように見える。


クラスター数を決める

データをk-means法で時系列クラスタリングするが、k-means法では事前にクラスター数を決める必要がある。クラスタリングの前にシルエット値からクラスター数を決定する。シルエット値はk-means法の最適なクラスター数を選択するでも使用しているが、今回はSelecting the number of clusters with silhouette analysis on KMeans clusteringを参考にシルエット値をプロットして確認してみる。なお、メトリックとしてユークリッド距離を使う。

以下はクラスター数2~5の範囲でシルエット値を算出してプロットするコード。scikit-learnにはデータごとのシルエット値を返してくれるsilhouette_samplesメソッドがあるがtslearnにはないので自作した。平均シルエット値が1に近く、プロット幅がなるべく均等になるクラスター数がよいと言われている。

%matplotlib inline

import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
import pandas as pd

from tslearn.clustering import TimeSeriesKMeans, silhouette_score
from tslearn.preprocessing import TimeSeriesScalerMinMax
from tslearn.utils import to_time_series_dataset
from scipy.spatial.distance import cdist
from sklearn.metrics import silhouette_samples as sklearn_silhouette_samples

# Googleモビリティレポートのファイルパス
# 2020年1月3日から2月6日の5週間の曜日別中央値を基準とした値
MOBILITY_CSV_GOOGLE = 'Global_Mobility_Report210814.csv'

# 対象とする場所のカテゴリ
# 公共交通機関の拠点(例: 地下鉄、バス、電車の駅)など
MOBILITY_CATEGORY = 'transit_stations_percent_change_from_baseline'


def silhouette_samples(X, labels, **kwds):
    """シルエット値の計算
    ユークリッド距離の計算方法は以下tslernのコードを参照
    https://github.com/tslearn-team/tslearn/blob/42a56cc/tslearn/clustering/utils.py#L66-L197
    """

    X_ = X.reshape((X.shape[0], -1))
    sklearn_X = cdist(X_, X_, metric='euclidean')

    # 計算済みのデータを渡すのでmetric='precomputed'にする
    return sklearn_silhouette_samples(X=sklearn_X,
                                  labels=labels,
                                  metric='precomputed',
                                  **kwds)


def plot_silhouette_scores(ts_data, silhouette_values, n_clusters, km):
    """シルエット値のプロット
    以下のコードをほぼコピー
    https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_silhouette_analysis.html#sphx-glr-auto-examples-cluster-plot-kmeans-silhouette-analysis-py
    """   
    cluster_labels = km.labels_
    centers = km.cluster_centers_
    
    # Create a subplot with 1 row and 2 columns
    fig, (ax1, ax2) = plt.subplots(1, 2)
    fig.set_size_inches(18, 7)

    ax1.set_xlim([-0.1, 1])
    # The (n_clusters+1)*10 is for inserting blank space between silhouette
    # plots of individual clusters, to demarcate them clearly.
    ax1.set_ylim([0, len(ts_data) + (n_clusters + 1) * 10])

    y_lower = 10
    for i in range(n_clusters):
        # Aggregate the silhouette scores for samples belonging to
        # cluster i, and sort them
        ith_cluster_silhouette_values = \
            silhouette_values[cluster_labels == i]

        ith_cluster_silhouette_values.sort()

        size_cluster_i = ith_cluster_silhouette_values.shape[0]
        y_upper = y_lower + size_cluster_i

        color = cm.nipy_spectral(float(i) / n_clusters)
        ax1.fill_betweenx(np.arange(y_lower, y_upper),
                          0, ith_cluster_silhouette_values,
                          facecolor=color, edgecolor=color, alpha=0.7)

        # Label the silhouette plots with their cluster numbers at the middle
        ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))

        # Compute the new y_lower for next plot
        y_lower = y_upper + 10  # 10 for the 0 samples

    ax1.set_title("The silhouette plot for the various clusters.")
    ax1.set_xlabel("The silhouette coefficient values")
    ax1.set_ylabel("Cluster label")

    # The vertical line for average silhouette score of all the values
    ax1.axvline(x=np.mean(silhouette_values), color="red", linestyle="--")

    ax1.set_yticks([])  # Clear the yaxis labels / ticks
    ax1.set_xticks([-0.1, 0, 0.2, 0.4, 0.6, 0.8, 1])

    # 2nd Plot showing the actual clusters formed
    colors = cm.nipy_spectral(cluster_labels.astype(float) / n_clusters)
    ax2.scatter(ts_data[:, 0], ts_data[:, 1], marker='.', s=30, lw=0, alpha=0.7,
                c=colors, edgecolor='k')

    # Labeling the clusters
    # centers = clusterer.cluster_centers_
    # Draw white circles at cluster centers
    ax2.scatter(centers[:, 0], centers[:, 1], marker='o',
                c="white", alpha=1, s=200, edgecolor='k')

    for i, c in enumerate(centers):
        ax2.scatter(c[0], c[1], marker='$%d$' % i, alpha=1,
                    s=50, edgecolor='k')

    ax2.set_title("The visualization of the clustered data.")
    ax2.set_xlabel("Feature space for the 1st feature")
    ax2.set_ylabel("Feature space for the 2nd feature")

    plt.suptitle(("Silhouette analysis for KMeans clustering on sample data "
                  "with n_clusters = %d" % n_clusters),
                 fontsize=14, fontweight='bold')

    plt.show()


def show_silhouette_scores(ts_data, dates):
    # クラスター数2-5を確認
    for n_clusters in range(2, 6):
        print('クラスター数 =', n_clusters)
 
        km = TimeSeriesKMeans(n_clusters=n_clusters, metric='euclidean', verbose=False, random_state=1).fit(ts_data)
        assert len(km.labels_) == 47
        print('クラスタリング結果 =', km.labels_)

        silhouette_values = silhouette_samples(ts_data, km.labels_)
        silhouette_avg = np.mean(silhouette_values)
        print('シルエット値平均 =', silhouette_avg)

        plot_silhouette_scores(ts_data, silhouette_values, n_clusters, km)


dfg = pd.read_csv(MOBILITY_CSV_GOOGLE)

# 都道府県ごとのデータを取得
dfg_jp = dfg[dfg['country_region_code']=='JP'].dropna(subset=['sub_region_1'])

# 都道府県のチェック
pref = dfg_jp['sub_region_1'].unique()
print(pref)
assert len(pref) == 47

# 日付をインデックス、都道府県をカラムとするDataFrameに変換
dfg_cat = dfg_jp.set_index(['date', 'sub_region_1'])[MOBILITY_CATEGORY].unstack()

# 2021年7月のデータを取得
dfg_cat_jul = dfg_cat['2021-07-01':'2021-07-31']

# データのプロット
ax = dfg_cat_jul.plot(figsize=(12,8), alpha=0.75, rot=90);
ax.legend(loc='center left', bbox_to_anchor=(1.0, 0.5))
plt.show()

# 正規化
ts_data = TimeSeriesScalerMinMax().fit_transform(dfg_cat_jul.values.transpose())

prefs = dfg_cat_jul.columns
dates = dfg_cat_jul.index

show_silhouette_scores(ts_data, dates) 

実際にはクラスター数2から5の結果が表示されるが、以下はクラスター数4の結果。平均シルエット値だけをみると他のクラスター数がよさそうだが、ここではクラスター数4を採用する。


時系列クラスタリングの結果を可視化

クラスター数が決まったので、時系列クラスタリングして、クラスターごとの平均を可視化してみる。

%matplotlib inline

import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
import pandas as pd

from tslearn.clustering import TimeSeriesKMeans, silhouette_score
from tslearn.preprocessing import TimeSeriesScalerMinMax
from tslearn.utils import to_time_series_dataset
from scipy.spatial.distance import cdist
from sklearn.metrics import silhouette_samples as sklearn_silhouette_samples

# Googleモビリティレポートのファイルパス
# 2020年1月3日から2月6日の5週間の曜日別中央値を基準とした値
MOBILITY_CSV_GOOGLE = 'Global_Mobility_Report210814.csv'

# 対象とする場所のカテゴリ
# 公共交通機関の拠点(例: 地下鉄、バス、電車の駅)など
MOBILITY_CATEGORY = 'transit_stations_percent_change_from_baseline'


## シルエット値プロット用の関数は省略 ##


def tsclustering(ts_data, prefs, n_clusters):

    km = TimeSeriesKMeans(n_clusters=n_clusters, metric='euclidean', verbose=False, random_state=1).fit(ts_data)
 
    # クラスタリングの結果
    result = pd.DataFrame({'clust': km.labels_}, index=prefs)
    for clust, g in result.groupby(by='clust'):
        print('Cluster: ', clust)
        print(g.index)

    # クラスターごとの中心をプロット
    for i, c in enumerate(km.cluster_centers_):
        plt.plot(c.T[0], label=i)
 
    plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
    plt.show()


dfg = pd.read_csv(MOBILITY_CSV_GOOGLE)
# 都道府県ごとのデータを取得
dfg_jp = dfg[dfg['country_region_code']=='JP'].dropna(subset=['sub_region_1'])

# 都道府県のチェック
pref = dfg_jp['sub_region_1'].unique()
print(pref)
assert len(pref) == 47

# 日付をインデックス、都道府県をカラムとするDataFrameに変換
dfg_cat = dfg_jp.set_index(['date', 'sub_region_1'])[MOBILITY_CATEGORY].unstack()

# 2021年7月のデータを取得
dfg_cat_jul = dfg_cat['2021-07-01':'2021-07-31']

# データのプロット
ax = dfg_cat_jul.plot(figsize=(12,8), alpha=0.75, rot=90);
ax.legend(loc='center left', bbox_to_anchor=(1.0, 0.5))
plt.show()

# 正規化
ts_data = TimeSeriesScalerMinMax().fit_transform(dfg_cat_jul.values.transpose())

prefs = dfg_cat_jul.columns
dates = dfg_cat_jul.index

# show_silhouette_scores(ts_data, dates)

n_clusters = 4
tsclustering(ts_data, prefs, n_clusters=n_clusters)

時系列クラスタリングの結果は以下の通り。4連休での差が大きい。4連休で最も移動が減少しているクラスター2には東京、大阪、愛知などの大都市圏が含まれる。逆に4連休に移動が上昇したクラスター1,3には地方の都道府県が含まれている。クラスター2に沖縄県が入っているが、これは台風で天候が悪かった影響と考えられる。

2021年1月12日火曜日

KH Coderでぴえんツイートを分析する2

KH Coderでぴえんツイートを分析するでは「今年の新語 2020」の大賞に選ばれた「ぴえん」を含むツイートをKH Coderで分析して大辞林の「ぴえん」の説明と比較してみた。今回はツイートされた時間帯に注目してKH Coderで分析してみる。ちなみに大辞林の「ぴえん」の説明は次の通り。

(若者言葉で)軽度の悲しみや落胆、また喜びや感激の気持ちを表す語。


環境


Windows10 Pro Version 2004
Windows版 KH Coder Version 3.Beta.01g
形態素解析器はMeCabで辞書としてmecab-ipadic-neologdを使用


分析対象のツイート


Twitter APIで収集した「ぴえん」を含むツイート(リツイート除く)で、期間は12月5日~12月11日の1週間。ここではKH Coderでぴえんツイートを分析するの再分析を行ったときと同じ状態で分析を行う。つまり、特定のツイートを除外したり、いくつかの語を強制抽出したりしている。

KH Coderで読み込むツイートはCSVとして保存してあり、1時、2時などの時間帯情報の列(hour)もある。時間帯ごとのツイート数は以下の通り(%は全体に占める割合)。

*0時 6194 5.74%
*1時 3634 3.36%
*2時 2126 1.97%
*3時 1284 1.19%
*4時   761       0.70%
*5時   689       0.64%
*6時 1090 1.01%
*7時 2286 2.12%
*8時 2632 2.44%
*9時 2678 2.48%
*10時 3078 2.85%
*11時 3489 3.23%
*12時 4949 4.58%
*13時 4197 3.89%
*14時 3846 3.56%
*15時 4405 4.08%
*16時 5075 4.70%
*17時 6287 5.82%
*18時 7265 6.73%
*19時 7890 7.31%
*20時 8408 7.79%
*21時 8694 8.05%
*22時 9024 8.36%
*23時 8020 7.43%

KH Coderで分析


今回はKH Coderで対応分析を行った。「抽出語×外部変数」では1時間ごとの時間帯を外部変数として指定した。さらに「最小出現数」と「最小文書数」は同数とし、対象が80語以上になるようにした上で、差異が顕著な上位80語を分析に使用した。以下は原点付近を拡大した結果。

0~23時の時間帯がほぼ時計回りに並んでいる。時間帯によってツイート内容が違っていると考えてよさそう。1日のうちでツイート数の多い21時、22時付近にはより多くの語が配置されている。

続いて、時間帯ごとの特徴語を出力してみる。KH Coderで読み込んだCSVに「hour」という時間帯の列があるとして、[ツール]>[外部変数と見出し]で「hour」列を選択して「特徴語」ボタンを押すと時間帯ごとの特徴語をExcelやCSVで出力できる。以下は12時台から23時台までの特徴語(数値はJaccard係数)。




昼間は「仕事」が特徴語になっている時間帯が多い。仕事に関係したツイートで「ぴえん」が使われることが多いのかもしれない。夜間は「ありがとう」が特徴語になっているが、大辞林の説明で言うところの喜びや感激の気持ちを表しているのだろうか。

2020年12月27日日曜日

KH Coderでぴえんツイートを分析する

三省堂が選んだ「今年の新語 2020」の大賞に「ぴえん」が選ばれた。

「今年の新語 2020」の選考結果

4種類の辞書の説明が掲載されているが、そのうちの大辞林の説明は次のようになっている。

(若者言葉で)軽度の悲しみや落胆、また喜びや感激の気持ちを表す語。

今回はTwitterのデータをKH Coderで分析して、Twitter上の「ぴえん」の使われ方と大辞林の説明を比較してみる。


環境


Windows10 Pro Version 2004
Windows版 KH Coder Version 3.Beta.01g
形態素解析器はMeCabで辞書としてmecab-ipadic-neologdを使用


分析対象のツイート


対象はTwitter APIで収集した「ぴえん」を含むツイート(リツイート除く)で、期間は12月5日~12月11日の1週間。多数投稿するユーザーのツイートを除外するために収集期間に8回以上投稿したユーザーのツイートを除外した(8回に特別な理由はないが、中央値は1なので投稿1回のユーザーが多い)。ちなみにツイート数上位10ユーザーの投稿数は次の通りで、中には1000を超えるユーザーもいた。

1326
128
116
90
76
73
60
54
42
40

さらに、ツイートからURL、@ではじまるユーザーネーム、絵文字を除去した。最終的な対象ツイートは109660件で、日ごとのツイート数は次の通り。

12/05 16669
12/06 16142
12/07 15257
12/08 14825
12/09 15686
12/10 15581
12/11 15500

ツイートの収集や集計などはPythonで行っているが詳細は省略。


KH Coderで分析

まずは抽出語を確認。もちろん「ぴえん」は圧倒的な最上位にある。

次に共起ネットワークを作成した。設定では「最小出現数」と「最小文書数」は同数とし、対象が80語以上になるようにした上で、上位80語を使用した。


赤枠の箇所では「ぱおん」や「ぴえんこえてぱおん」「ぴえんヶ丘どすこい之助」という「ぴえん」の上位語? らしいが出現している。mecab-ipadic-neologdを使ってもこれらは認識できなかったようだ。これらの語は後の再分析でKH Coderで強制的に語として抽出できるようにする。

オレンジ枠では、Peing(Twitterで利用できる質問サービス)を利用した投稿とDMを募集する投稿が多くを占めている。それぞれ「みんなからの匿名質問を募集中!」と「会う話したい。」という同じフレーズではじまっているツイートが多い。同じフレーズが多いと共起ネットワークに表れやすくなってしまうので、これらのツイートは除外する(後の再分析前に別途Pythonで処理)。

さらに、「ぴえん」はすべてのツイートに含まれるため、後の再分析でKH Coderで使用しない語に設定する。また「言う」「思う」は分析に寄与しないと考えられるので同様に使用しない語に設定する。


KH Coderで再分析

もろもろの設定などをして再度共起ネットワークを作成した。

まずは、「みんなからの匿名質問を募集中!」と「会う話したい。」という同じフレーズではじまるツイートを除外した結果、対象ツイートは108001件となった。

続いて強制抽出する語の設定。KH Coderでは強制抽出する語のリストをテキストファイルから読み込める。次のようなテキストファイルを用意。

---cell---
ぴえんヶ丘どすこいの助
ぴえんヶ丘どすこい之助
ぴえんヶ丘どすこいのすけ
ぴえんこえてぱおん
ぴえん超えてぱおん
ぱおん

使用しない語のリストもテキストファイルから読み込める。「ぴえん」「言う」「思う」以外に抽出語上位の記号、ひらがな一文字もリストに加えておく。

---cell---
ぴえん
思う
言う
ー
()
え
あ
ま

再度共起ネットワークを作成する。設定は前回と同じ。


peingなどの投稿で使われた語が出現しなくなった。また、「ぴえん超えてぱおん」といった語が認識されていて、設定などの効果があったことが確認できた。ただ、ここで改めて抽出語を確認してみると、「ぴえん」と関係がありそうな抽出語52番目の「悲しい」が共起ネットワークに出現していない。これは「悲しい」と共起している語が多くないためと考えられる。


そこで多次元尺度構成法を使ってみる。さらに、気持ちを表す形容詞に注目しようと思うので形容詞B(ひらがなのみの形容詞)も対象に加えた。



気持ちを表す形容詞に注目すると、大辞林の説明と一致するのは緑枠の「悲しい」や「嬉しい」「楽しい」で、それに「辛い」を含めていいと思う。ただ、赤枠の「怖い」は「悲しみ」や「落胆」と関連づけるのは微妙。形容詞ではないが気になったのが「怖い」の右隣に布置された「怒る」。辞書の説明と一致していないように思えたが、確認すると「怒られた」という文脈で使われていることが多いので「落胆」に相当すると考えられる。全体としては、おおむね大辞林の説明と一致していると言えそう。