【Kaggle】アニメデータセットの作成から分析まで：前編


![mal00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/aeea3b77-f9a7-8735-f560-b78ee8f7247d.jpeg)



## はじめに

こんにちは。千々松のいるです。
今回はデータ分析のためにアニメレビューサイトのデータセット作成からデータ分析までを実施しました。

**Kaggle**の[アニメで有名なデータセット](https://www.kaggle.com/CooperUnion/anime-recommendations-database)が4年前からまったく更新されていなかったことに悲しさを感じたことが今回の経緯になります。

本データセット元となるサイトは過去から最新のほとんどのアニメの評価とレビューが網羅されている大変素晴らしいサイト[(MyAnimeList.net)](https://myanimelist.net/)です。

本サイトを見て頂ければ分かりますが、海外の人でも日本人と同じように、あるいは日本人以上に熱心にアニメを見られていて、また、アニメのレビューをしっかり**データ化**されていることに驚くと思います。

実際、Kaggleのアニメ評価データセットを作成されたのも海外の方ですし、日本は諸外国に比べて本当にデータが少ないなぁと思いました。

ということで、今回は日本のアニメオタクを代表させてもらった気分でアニメ評価のデータセットの作成・簡単な分析までを実施させて頂きますのでご興味のある方は是非ご覧頂けたらと思います。

前編ではデータセットの作成までを行います。
後編は[こちら](https://qiita.com/Noiru_CH/items/08a0cc5befd74769ee52)（現在4/25作成中）。

## レシピ

～前編：データセット作成編～
1. [APIアカウントの取得](#1-apiアカウントの取得)
2. [APIを利用したスクレイピング](#2-apiを利用したスクレイピング)
3. [データセットとしてKaggleに公開](#3-データセットとしてkaggleに公開)
～後編：データ分析編～
4. ジャンルによるグルーピング
5. KNNを用いたユーザベースリコメンデーション



## Data & Code
作成したDataとCodeを先に置いておきます。
Data: [Anime Recommendations Database vol.2](https://www.kaggle.com/noiruuuu/anime-recommendations-database-vol2)
Code: [NoiruCH@github.com](https://github.com/NoiruCH/anime_recommend)

## Description
animes.csvとratings.csvの2つのデータから成るデータセットです。
### animes.csv
2020年冬アニメまでの15,221本のアニメのデータです。
アニメ名や平均得点などのデータが格納されていて、以下の10カラムで構成されています。
[anime_id, title, genre, media, episodes, rating, members, start_date, season, source]

### ratings.csv
108,000人のユーザのアニメに対する1~10点の評価点のデータです。
以下の3カラムで構成されています。
[user_id, anime_id, rating]
参考に、これは[私の評価データ](https://myanimelist.net/animelist/Noiru_CH)となりますが、これを108,000人分取得しました。


## 環境
Python 3.9.0
pandas 1.1.5
OS: Windows 10 Home
IDE: VisualStudio Code


## 1. APIアカウントの取得

海外大手のアニメレビューサイト[MyAnimeList.net](https://myanimelist.net/)からAPIを用いてデータセットを作成していきます。
まずがアカウントの取得からトークンの生成までを実施します。

[こちらの記事](https://myanimelist.net/blog.php?eid=835707)がとても分かりやすかったので、この記事通りに進めていきます。

### 1-1. IDの作成

APIを使うにあたってまずIDを発行する必要があります。マイページのAPIタブからCreate IDを押下します。

![mal01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/5cd43d34-a5f2-0ccf-5dfb-f7a7c297d2b4.png)

そうすると簡単な英作文を問われるので適切に回答します。営利目的でなければ比較的ラフなものでも良いかと思われます。

登録しおえるとマイページのAPIタブに「Clients Accessing the MAL API」の行が新設され右のEditボタンからIDの情報を確認することができます。

![mal02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/791cbfa2-1c56-0af0-6af5-51ca326f203d.png)

内容はぼかしていますがClient IDは後で使用するので控えておいてください。

### 1-2. ユーザ認証

ユーザ認証を行うには先ほどのClient IDとcode_challengeが必要になります。
code_challengeというのは、文字[AZ] / [az] / [0-9] / "-" / "。"のみを含む高エントロピーの暗号化ランダム文字列らしく以下のcodeでローカル環境から発行します。

``` python:generate_verifier.py
import secrets

def get_new_code_verifier() -> str:
    token = secrets.token_urlsafe(100)
    return token[:128]

code_verifier = code_challenge = get_new_code_verifier()

print(len(code_verifier))
print(code_verifier)
```

128文字で出力されたものをcode_challengeとして使用していきます。

それでは、client_idとcode_challengeをご自身のものに書き換えて以下をブラウザで叩きます。

``` http
https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id="CLIENT_ID"&code_challenge="CODE_CHALLENGE"&state=RequestID42
```

すると以下のページにリダイレクトするのでAllowを押下します。

![mal03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/487cacdb-76eb-cc75-953a-49d26e616d67.png)

英作文で指定したURLにcodeが付与されていますので、それを記録しておきます。

### 1-3. アクセストークンの取得

ここまできてやっとアクセストークン取得準備ができました。少々長かったですね。

アクセストークンの取得にはHTTP GETメソッドではなく**POST**メソッドを利用する必要があります。

HTTP POSTメソッドを利用するには**cURL**を利用する人が多いと思いますが、私は普段使ってるIDEがVisualStudioCodeということもあり、今回はVisualStudioCodeの拡張機能**REST Client**を利用しました。

まず、VisualStudioCodeに以下の拡張機能をインストールします。

![mal04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/e1aec477-5918-9481-502b-5125c5664db3.png)

あとは以下のようなhttpファイルを作成します。"大文字"で記されている部分は個々人で異なるので各自ご入力ください。

``` http:access_token.http
POST /v1/oauth2/token HTTP/1.1
Host: myanimelist.net
Content-Type: application/x-www-form-urlencoded

client_id="CLIENT_ID"
&code_verifier="CODE_VERIFIER"
&grant_type=authorization_code
&code="CODE"
&client_secret="CLIENT_SECRET"
```

すると、HTTP Responseにaccess_tokenとrefresh_tokenがjson形式で返ってきたと思います。

最後に返ってきたaccess_tokenを使ってAPIを呼び出してみましょう。

``` http:get.http
GET http://api.myanimelist.net/v2/users/@me
Authorization: Bearer "ACCESS_TOKEN"
```

以下のようにHTTP StatusCode 200で返ってきていたら成功です。

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
Date: Fri, 23 Apr 2021 08:58:21 GMT
Server: Apache
Cache-Control: no-cache
Pragma: no-cache
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Robots-Tag: noindex,nofollow
X-Cache: Miss from cloudfront
Via: 1.1 f4af4b1945a48ea980406b6f98124b10.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRT51-C4
X-Amz-Cf-Id: yupn0hhvLL4XV2c3TDMahIpTcqVnIHTZ6x5EUlAvNgDllb00R8dcHQ==

{
  "id": 12644329,
  "name": "Noiru_CH",
  "birthday": "",
  "location": "",
  "joined_at": "2021-04-23T01:23:40+00:00",
  "picture": "https:\/\/api-cdn.myanimelist.net\/images\/userimages\/12644329.jpg?t=1619168400"
}
```


## 2. APIを利用したスクレイピング

4年前のアニメデータセットと同様にanime_idを共通キーとして**animes.csv**と**ratings.csv**を作成していきます。

### 2-1. animes.csvの作成

まず始めに、アニメタイトルの方から取得していきます。
前回のデータセットは列として[anime_id, name, genre, type, episodes, rating, members]を抽出していたのですが、今回は以下の列を追加しています。
[start_date, start_season, source]
APIからrequestsライブラリを使ってJSON形式でデータを抽出していきます。
ACCESS_TOKENは皆さま個々人のものが必要になります。
API_referencesは[こちら](https://myanimelist.net/apiconfig/references/api/v2)になります。


```python:get_animes.py
import requests
import json
import pprint
import time
import pandas as pd
from tqdm import tqdm

url = "https://api.myanimelist.net/v2/anime/"
ACCESS_TOKEN = "ACCESS_TOKEN"
params = {'fields': 'id, title, genres, media_type, num_episodes, mean, num_list_users, start_date, start_season, source'}
headers = {}
headers['Authorization'] = 'Bearer ' + ACCESS_TOKEN

def get_animes(anime_id, url=url):
    anime_id = str(anime_id)
    url = url + anime_id
    response = requests.get(url, params=params, headers=headers)
    response = response.json()
    pprint.pprint(response)

    if response != {'error': 'not_found', 'message': ''}:
        # main_pictureの削除
        del response['main_picture']

        # genre_listの結合
        genres = response['genres']
        genre_list = []
        for genre in genres:
            genre_list.append(genre['name'])
        re_genres = ','.join(genre_list)
        response['genres'] = re_genres

        # start_seasonの結合
        season = response['start_season']
        re_season = season['season'] + '_' + str(season['year'])
        response['start_season'] = re_season
        anime_list.append(response)

anime_list = []
for i in tqdm(range(1, 42_401)):
    try:
        get_animes(i)
    except Exception as e:
        print(e)
        print('\n\n time to sleep 30 seconds. \n')
        time.sleep(30)


columns = ['anime_id', 'title', 'genres', 'media', 'episodes', 'rating', 'members', 'start_date', 'season', 'source']
anime_df = pd.DataFrame(anime_list)
anime_df.columns = columns
# headerだけの空リストを作成
empty_df = anime_df[:0]
empty_df.to_csv('data/animes.csv', index=False)
# 空csvにアニメのデータフレームを追記
anime_df.to_csv('data/animes.csv', mode='a',header=None ,index=False)
```

1日くらいスクリプトを回し続けることで、結果的に15,221本のアニメのデータフレームを取得することができました。


### 2-2. ratings.csvの作成

次に、ユーザごとの評価データを取得していくのですが、APIはユーザIDではなく**ユーザ名**でしか取得できない仕様となっています。
なので、トップページのCommunityタブのUsersからてきとうにユーザ名を抽出していきます。

![mal05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/6ed3c1ad-ccd3-96e4-e11f-c230df929284.png)

ユーザ検索では年齢の範囲が設定できるので試しに検索してみると1,000,000人近いユーザを確認することができました。
でも、1,000,000人すべてのユーザから仮に一人当たり100件の評価データを取得できたとすると評価データ数が100,000,000件を1億件を超えてしまいますので、今回は1/10の108,000人を標本抽出しました。

```python:get_users.py
import requests
import pprint
import time
import numpy as np
import pandas as pd
from tqdm import tqdm
from bs4 import BeautifulSoup as bs

url = 'https://myanimelist.net/users.php?cat=user&q=&loc=&agelow=14&agehigh=34&g=&show='

def get_users(url, num):
    url = url + str(num)
    response = requests.get(url).text
    soup = bs(response, 'html.parser')
    items = soup.find_all(class_='borderClass')
    user_list = []
    for i in range(1,25):
        users = items[i].find('a').text
        user_list.append(users)
    return user_list

users_list = []
for num in tqdm(range(0,108_001,24)):
    user_list = get_users(url, num)
    users_list.extend(user_list)
    time.sleep(3)

sr = pd.Series(users_list, name='users', index=np.arange(1,len(users_list)+1))
sr.to_csv('data/users.csv')
```

これで108,000人のユーザ名のデータをpandasのシリーズとしてcsv化することができました。

次に、このユーザ名の情報をもとにAPIを用いてユーザごとの評価データを取得していきます。

```python:get_ratings.py
import requests
import time
import pandas as pd
from tqdm import tqdm

# users_listのimport
users_df = pd.read_csv('data/users.csv')
users_df.columns = ['user_id', 'users']

url = 'https://api.myanimelist.net/v2/users/@me/animelist?fields=list_status&limit=1000'
ACCESS_TOKEN = "ACCESS_TOKEN"
headers = {}
headers['Authorization'] = 'Bearer ' + ACCESS_TOKEN

def get_rates(users_index):
    url = 'https://api.myanimelist.net/v2/users/'+users_df.iloc[users_index]['users']+'/animelist?fields=list_status&limit=1000'
    response = requests.get(url, headers=headers)
    response = response.json()

    anime_list = response['data']
    anime_id_list = []
    rating_list = []
    for i in range(len(anime_list)):
        if anime_list[i]['list_status']['status'] == "completed":
            anime_id = anime_list[i]['node']['id']
            rating = anime_list[i]['list_status']['score']
            anime_id_list.append(anime_id)
            rating_list.append(rating)

    df = pd.DataFrame({'anime_id': anime_id_list,
                        'rating': rating_list})
    df['user_id'] = users_df.iloc[users_index]['user_id']
    df = df.reindex(columns=['user_id', 'anime_id', 'rating'])
    df.to_csv('data/new_rating.csv', mode='a', index=False, header=None)

# headerだけの空リストを作成
empty_df = pd.DataFrame(np.random.randn(1,3), columns=['user_id', 'anime_id', 'rating'])
empty_df = empty_df[:0]
empty_df.to_csv('data/new_rating.csv', index=False)

for i in tqdm(range(len(users_df))):
    try:
        get_rates(i)
    except Exception as e:
        print(e)
        print('\n\n time to sleep 30 seconds. \n')
        time.sleep(30)
```

相当な量になるので丸1.5日くらいスクリプトを回し続けてやっとデータを取得することができました。
取得した評価データは合計11,039,694件となりました。

## 3. データセットとしてKaggleに公開

あとは作成したデータセットをKaggleのDatasetsに公開するだけです。

以下のようにKaggleトップページの左ペインのDatasetsから+ New Datasetを押下します。

![mal06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/a29d9ba7-9e6a-94dc-69d9-fa2cb6c8a7b2.png)

すると自分のデータセットのページが作成されますので、後はデータセットの説明やサムネの画像などを設定していきます。

そうしてできたページが[こちら](https://www.kaggle.com/noiruuuu/anime-recommendations-database-vol2)になります！

![mal07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1217166/7ca3d937-982b-3273-9c8a-2ef8890549c4.png)

後編では、今回作成したデータセットを用いて簡単な分析をしていきたいと思います（分析に関しては、機械学習ビギナーなので温かい目で見て頂ければ幸いです）

それでは、また後編で！


## 参考
1. [MyAnimeList authorization Reference](https://myanimelist.net/apiconfig/references/authorization)
2. [MyAnimeList APIconfig References](https://myanimelist.net/apiconfig/references/api/v2)
3. [Anime Recommendations Database](https://www.kaggle.com/CooperUnion/anime-recommendations-database)