週刊少年ジャンプの短命作品を，機械学習で予測する （前編：データ分析）

# 1. はじめに
　[週刊少年ジャンプ](https://www.shonenjump.com/j/)（以下，ジャンプ）は，日本で最も売れている漫画雑誌[^発行部数]です．言うまでもなく，私は大ファンです．

　ジャンプ編集部の連載会議は非常にシビアです．ジャンプ作家の奮闘を描いたフィクション漫画「[バクマン。](https://www.amazon.co.jp/dp/B00A47VW2Y/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)」では，編集部が[毎号の読者アンケート](https://ja.wikipedia.org/wiki/%E9%80%B1%E5%88%8A%E5%B0%91%E5%B9%B4%E3%82%B8%E3%83%A3%E3%83%B3%E3%83%97#.E3.82.A2.E3.83.B3.E3.82.B1.E3.83.BC.E3.83.88.E8.87.B3.E4.B8.8A.E4.B8.BB.E7.BE.A9)をもとに各漫画の人気を評価し，掲載順や打ち切り作品を決定する様子が描かれています[^掲載順]．連載開始から10週以内（単行本約1冊分）で連載が打ち切られてしまうことも珍しくありません．とても厳しい世界です．

　本記事では，機械学習を使って，短命作品（10週以内に終了する作品）の予測を行います．**究極の目標は，ジャンプ編集部より先に打ち切り作品を予測し，好みの作品が危ない場合はアンケートを出して打ち切りを回避することです**[^ジャンプ]．我々は読者アンケートの結果を知ることができないので，掲載順の履歴を入力とし，短命作品か否かを出力する[多層パーセプトロン](http://deeplearning.net/tutorial/mlp.html)[^多層]を[TensorFlow](https://www.tensorflow.org/)で実装します．学習には，[Web API](https://mediaarts-db.bunka.go.jp/webapi_prototype.html)で[文化庁メディア芸術データベース](http://mediaarts-db.bunka.go.jp/)から取得した，約46年分の目次情報を利用します．

　本記事は２部構成の予定です．前編（今回）はデータの取得と分析を行い，[後編](http://qiita.com/haltaro/items/62d49875ed658ac8a93f)（次回）はデータの学習とテストを行います．下図は前編の分析結果の一部です．

![ana.png](https://qiita-image-store.s3.amazonaws.com/0/151195/0829a0d7-49c9-fe38-18b4-f8a73d7d5d35.png)

[^発行部数]: 日本雑誌協会調べ．2015年10月1日～2016年9月30日における[少年向けコミック誌発行部数](http://www.j-magazine.or.jp/data_002/m6.html#001)，[男性向けコミック誌発行部数](http://www.j-magazine.or.jp/data_002/m6.html#001)，[少女向けコミック誌発行部数](http://www.j-magazine.or.jp/data_002/w5.html#001)，[女性向けコミック誌発行部数](http://www.j-magazine.or.jp/data_002/w5.html#002)参照．

[^掲載順]: 作中では，主に読者アンケートの結果をもとに，掲載順や打ち切り作品を決定していました．以下の記事によると，ジャンプ編集部は「必ずしも読者アンケートの結果だけを考慮しているわけではない」と，これを否定したようです．[「ジャンプ」編集部がアンケート至上主義の噂を否定も…読者は複雑](http://news.livedoor.com/article/detail/10678107/)

[^多層]: 予定です．性能によっては別の手法も検討します．

[^ジャンプ]: 前述したように，実際には，ジャンプ編集部は様々な要素を考慮して打ち切り作品を決定されています．本記事は，あくまでも一ジャンプファンの妄想として，ご理解頂ければと思います．

# 2. 環境
　[`anaconda`](https://www.continuum.io/Downloads)で，以下のような仮想環境`comic`を作成します．

```bash
conda create -n comic python=3.5
source activate comic
conda install pandas matplotlib jupyter notebook scipy scikit-learn seaborn scrapy
pip install tensorflow
```

　`yml`ファイルは[こちら](https://github.com/haltaro/predicting-comic-end/blob/master/env.yml)です．後編で必要になるので，[`tensorflow`](https://www.tensorflow.org/)や[`scikit-learn`](http://scikit-learn.org/stable/)を入れてあります．また，可視化に[`pairplot()`](http://seaborn.pydata.org/generated/seaborn.pairplot.html)を使うので，[`seaborn`](https://seaborn.pydata.org/)を入れます．

# 3. データの取得

## 3.1 ソース

　[文化庁メディア芸術データベース](http://mediaarts-db.bunka.go.jp/)に，約46年間分（1969年11月3日号-2016年7月25日号）のジャンプの目次が公開されています[^現在]．

[^現在]: 2017年4月4日時点．

![media_art.PNG](https://qiita-image-store.s3.amazonaws.com/0/151195/4bf67dcb-db62-6f36-2a19-ef917bef5cdf.png)

![wj.PNG](https://qiita-image-store.s3.amazonaws.com/0/151195/6fccf1f2-cd13-dfd6-caac-220d19adf3b8.png)

![index.PNG](https://qiita-image-store.s3.amazonaws.com/0/151195/c681d053-c183-1e93-d5fd-db0bc271e576.png)

上図は，ジャンプ1969年11月3日号の目次を調査した例です．以下では，コメント欄でご紹介頂いた，[Web API](https://mediaarts-db.bunka.go.jp/webapi_prototype.html)を用いて目次情報を抽出します．

## 3.2 Web API

　**文化庁のサーバへの負担を極力抑えるため，可能な限り[私のgithub](https://github.com/haltaro/predicting-comic-end/tree/master/data)から加工済みデータを取得頂ますようお願い致します**．notebookは[こちら](https://github.com/haltaro/predicting-comic-end/blob/master/0_obtain_comic_data_j.ipynb)ですので，使っていただけると幸いです．

　　[文化庁メディア芸術データベース　マンガ分野　WebAPI](https://mediaarts-db.bunka.go.jp/webapi_proto_documents.pdf)を用いて，分析に必要なデータを入手します．なお，python3を使ったweb APIの利用については，[Python3でjsonを返却するwebAPIにアクセスして結果を出力するまで](http://qiita.com/sakura1116/items/4a11a9f7db9f535397fa)を参考にさせて頂きました．

### import

```python3
import json
import urllib.request
from time import sleep
```
### 雑誌巻号検索結果の取得

　以下の関数`search_magazine()`を使って，週刊少年ジャンプの雑誌巻号情報を検索します．この関数で取得したユニークIDは，次節の「雑誌巻号情報の取得」に必要になります．

```python3
def search_magazine(key='JUMPrgl', n_pages=25):
    """
    「ユニークID」「雑誌巻号ID」あるいは「雑誌コード」にkey含む雑誌を，
    n_pages分取得する関数です．
    """
    
    url = 'https://mediaarts-db.bunka.go.jp/mg/api/v1/results_magazines?id=' + \
        key + '&page='
    magazines = []
    
    for i in range(1, n_pages):
        response = urllib.request.urlopen(url + str(i))
        content = json.loads(response.read().decode('utf8'))
        magazines.extend(content['results'])
    return magazines
```

　Web APIでは，`id`で「ユニークID」「雑誌巻号ID」「雑誌コード」を，`page`で検索ページ番号（1ページあたり100件，デフォルトは1）を指定することができます．週刊少年ジャンプは「雑誌巻号ID」に`JUMPrgl`を含むため，`id=JUMPrgl`を指定します．また，週刊少年ジャンプの検索結果は合計24ページ（2320件）あるため，`page`に1から24を順次指定する必要があります．詳細は[WebAPI仕様](https://mediaarts-db.bunka.go.jp/webapi_proto_documents.pdf)をご参照ください．

　なお，2017年3月31日より文化庁メディア芸術データベースのURLが変更されたため，[WebAPI仕様](https://mediaarts-db.bunka.go.jp/webapi_proto_documents.pdf)に記載のリクエストURL（`https://mediaarts-db.jp/mg/api/v1/results_magazines`）ではなく，新しいURL（`https://mediaarts-db.bunka.go.jp/mg/api/v1/results_magazines`）を使用する必要があることにご注意ください．

### 雑誌巻号情報の取得

　以下の関数`extract_data()`で必要な目次情報を抽出し，`save_data()`で目次情報を保存します．

```python3
def extract_data(content):
    """
    contentに含まれる目次情報を取得する関数です．
    - year: 発行年
    - no: 号数
    - title: 作品名
    - author: 著者
    - color: カラーか否か
    - pages: 掲載ページ数
    - start_page: 作品のスタートページ
    - best: 巻頭から数えた掲載順
    - worst: 巻末から数えた掲載順
    """
    
    # マンガ作品のみ抽出します．
    comics = [comic for comic in content['contents'] 
             if comic['category']=='マンガ作品'] 
    data = []
    year = int(content['basics']['date_indication'][:4])
    
    # 号数が記載されていない場合があるので，例外処理が必要です．
    try:
        no = int(content['basics']['number_indication'])
    except ValueError:
        no = content['basics']['number_indication']
    
    for comic in comics:
        title= comic['work']
        if not title:
            continue
            
        # ページ数が記載されていない作品があるので，例外処理が必要です．
        # 特に理由はないですが，無記載の作品は10ページとして処理を進めます．
        try:
            pages = int(comic['work_pages'])
        except ValueError:
            pages = 10

        # 「いぬまるだしっ」等，1週に複数話掲載されている作品に対応するため
        # data中にすでにtitleが含まれる場合は，新規datumとして登録せずに，
        # 既存のdatumのページ数のみ加算します．
        if len(data) > 0 and title in [datum['title'] for datum in data]:
            data[[datum['title'] for datum in 
                  data].index(title)]['pages'] += pages
        else:
            data.append({
                'year': year,
                'no': no,
                'title': comic['work'],
                'author': comic['author'],
                'subtitle': comic['subtitle'],
                'color': int('カラー' in comic['note']),
                'pages': int(comic['work_pages']),
                'start_pages': int(comic['start_page'])
            })

    # 企画物のミニマンガを除外するため，合計5ページ以下のdatumはリストから除外します．
    filterd_data = [datum for datum in data if datum['pages'] > 5]
    for n, datum in enumerate(filterd_data):
        datum['best'] = n + 1
        datum['worst'] = len(filterd_data) - n
        
    return filterd_data
```
　泥臭い話ですが，一部のギャグ漫画の扱いに苦労しました．例えば，「[いぬまるだしっ](https://mediaarts-db.bunka.go.jp/mg/magazine_works/545?ids%5B%5D=545)」は，基本的に一週間に2話ずつ掲載していましたが，データベースでは各話が別々の行に記載されています．これらを１つの作品として見なす必要があるので，当該`comic`の`title`が`data`中にある場合は，別`datum`として`data`に追加せず，既存の`datum`の`pages`を加算する処理を行っています．また，例えば「[ピューと吹く!ジャガー](https://mediaarts-db.bunka.go.jp/mg/comic_works/83870)」は，その人気に関係なく（実際めちゃくちゃ面白かったです），連載中は常に雑誌の最後に掲載されていました．これを外れ値として除外するかどうかで悩みましたが，結局残すことにしました．

```python3
def save_data(magazines, offset=0, file_name='data/wj-api.json'):
    """
    magazinesに含まれる全てのmagazineについて，先頭からoffset以降の巻号の
    目次情報を取得し，file_nameに保存する関数です．
    """
    
    url = 'https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id='
    
    #　ファイル先頭行
    if offset == 0:
        with open(file_name, 'w') as f:
            f.write('[\n')
        
    with open(file_name, 'a') as f:
        
        # magazines中のmagazine毎にWeb APIを叩きます．
        for m, magazine in enumerate(magazines[offset:]):
            response = urllib.request.urlopen(url + str(magazine['id']),
                                              timeout=30)
            content = json.loads(response.read().decode('utf8'))
            
            # 前記の関数extract_data()で，必要な情報を抽出します．
            comics = extract_data(content)
            print('{0:4d}/{1}: Extracted data from {2}'.\
                  format(m + offset, len(magazines), url + str(magazine['id'])))
            
            # comics中の各comicについて，file_nameに情報を保存します．
            for n, comic in enumerate(comics):
                
                # ファイル先頭以外の，magazineの最初のcomicの場合は，
                # まず',\n'を追記．
                if m + offset > 0 and n == 0:
                    f.write(',\n')
                
                json.dump(comic, f, ensure_ascii=False)
                
                # 最後のcomic以外は',\n'を追記．
                if not n == len(comics) - 1:
                    f.write(',\n')
            print('{0:9}: Saved data to {1}'.format(' ', file_name))
            
            # サーバへの負荷を抑えるため，必ず一時停止します．
            sleep(3)
            
    # ファイル最終行
    with open(file_name, 'a') as f:
        f.write(']')
```

　タイムアウトに柔軟に対応するため，目次情報を一括処理せず，無理やり逐次処理にしています．また，サーバに負荷をかけないよう，`sleep()`で一時停止していることにご注意ください．
 
　なお，ここでも，[WebAPI仕様](https://mediaarts-db.bunka.go.jp/webapi_proto_documents.pdf)に記載のリクエストURL（`https://mediaarts-db.jp/mg/api/v1/magazine`）ではなく，新しいURL（`https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine`）を使用する必要があることにご注意ください．

### 実行

　以上の関数を利用して，Web APIから目次情報を取得し，`data/wj-api.json`に保存します．

```python3
magazines = search_magazine()
save_data(magazines)

#   0/2320: Extracted data from https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id=323270
#         : Saved data to data/wj-api.json
#   1/2320: Extracted data from https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id=323269
#         : Saved data to data/wj-api.json
# ...
# 447/2320: Extracted data from https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id=322833
#         : Saved data to data/wj-api.json
#---------------------------------------------------------------------------
#gaierror                                  Traceback (most recent call last)
#/home/anaconda3/envs/comic/lib/python3.5/urllib/request.py in do_open(self, http_class, req, **http_conn_args)
#   1253             try:
#-> 1254                 h.request(req.get_method(), req.selector, req.data, headers)
#   1255             except OSError as err: # timeout error
```

 タイムアウトした場合は，`offset`を利用して再開します．例えば，`447/2320`でタイムアウトした場合は，`save_data(offset=448)`を実行します．

```python3
save_data(magazines, offset=448)

# 448/2320: Extracted data from https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id=322832
#         : Saved data to data/wj-api.json
# 449/2320: Extracted data from https://mediaarts-db.bunka.go.jp/mg/api/v1/magazine?id=322831
#         : Saved data to data/wj-api.json
#...
```

## 3.3 （参考）スクレイピング

　私の調査不足のため，コメント欄でご指摘頂くまでは，Webスクレイピングで目次データを取得していました．ご参考までに，当時の設定等を記載しますが，極力3.2章のWeb APIをご利用頂きますようお願い致します．

　今回は代表的なフレームワークである[Scrapy](https://scrapy.org/)を利用します．Scrapyの詳細については，本記事末尾の参考欄をご参照ください．まずは，以下のコマンドで，本記事用のプロジェクト`comic`を作成します．

```bash
scrapy startproject comic
```

　すると，以下のようなディレクトリが作成されるはずです（[公式チュートリアル](https://docs.scrapy.org/en/latest/intro/tutorial.html#creating-a-project)）．

```bash
comic/
    scrapy.cfg            # deploy configuration file

    comic/             # project's Python module, you'll import your code from here
        __init__.py

        items.py          # project items definition file

        pipelines.py      # project pipelines file

        settings.py       # project settings file

        spiders/          # a directory where you'll later put your spiders
            __init__.py
```

　以下の`comic_spider.py`を`comic/spiders`に置きます．

```python3:comic_spider.py
# -*- coding: utf-8 -*-

import scrapy


class WjSpider(scrapy.Spider):
    """
    start_urlsを起点に再帰的に以下の目次情報を抽出するspiderです．
    ここでstart_urlsは，文化庁メディア芸術データベースに登録されている
    週刊少年ジャンプの目次情報のうち，最も古いもの（1969年11月3日号）です．
    - year: 発行年
    - no: 号数
    - title: 作品名
    - author: 著者
    - color: カラーか否か
    - pages: 掲載ページ数
    - start_page: 作品のスタートページ
    - best: 巻頭から数えた掲載順
    - worst: 巻末から数えた掲載順
    """

    name = 'wj'

    start_urls = [
        'http://mediaarts-db.bunka.go.jp/mg/magazines/323270'
    ]

    n_page = 0

    def parse(self, response):
        """ spider本体です． """

        year = int(response.css('section.block tr td::text').extract()[3][:4])
        try:
            no = int(response.css('section.block tr td::text').extract()[8])
        except ValueError:
            no = response.css('section.block tr td::text').extract()[8]

　　　　　# マンガ作品のみ抽出します．
        comics = [comic for comic in response.css('table.infoTbl2 tr')
                  if len(comic.css('td::text')) > 0
                  and comic.css('td::text')[0].extract() == 'マンガ作品']
        data = []

        for comic in comics:
            title = comic.css('a::text').extract_first()

            if not title:
                continue
            
            # ページ数が記載されていない作品があるので，例外処理が必要です．
            # 特に理由はないですが，無記載の作品は10ページとして処理を進めます．
            try:　
                pages = float(comic.css('td::text')[6].extract())
            except ValueError:
                pages = 10

            # 「いぬまるだしっ」等，1週に複数話掲載されている作品に対応するため
            # data中にすでにtitleが含まれる場合は，新規datumとして登録せずに，
            # 既存のdatumのページ数のみ加算します．
            if len(data) > 0 and title in [datum['title'] for datum in data]:
                data[[datum['title'] for datum in
                      data].index(title)]['pages'] += pages

            else:
                data.append({
                    'year': year,
                    'no': no,
                    'title': comic.css('a::text').extract_first(),
                    'author': comic.css('td::text')[3].extract(),
                    'subtitle': comic.css('td::text')[4].extract(),
                    'color': comic.css('td::text')[7].extract().count('カラー'),
                    'pages': pages,
                    'start_page': float(comic.css('td::text')[5].extract())})

        # 企画物のミニマンガを除外するため，合計5ページ以下のdatumはリストから除外します．
        filtered_data = [datum for datum in data if datum['pages'] > 5]
        for n, datum in enumerate(filtered_data):
            datum['best'] = n + 1
            datum['worst'] = len(filtered_data) - n
            yield datum

        # 次号の情報を再帰的に取得します．
        next_page = response.css('li.nxt a::attr(href)').extract_first()
        if next_page is not None:
            next_page = response.urljoin(next_page)
            yield scrapy.Request(next_page, callback=self.parse)

```

　サーバに負荷をかけないよう，**必ず**`settings.py`の`DOWNLOAD_DELAY`を設定する必要があります（デフォルトでコメントアウトされていました）．また，日本語でデータを吐き出したいので，`FEED_EXPORT_ENCODING`を`utf-8`に設定します．

```python3:settings.py

### ----- 略 -----

DOWNLOAD_DELAY = 3
FEED_EXPORT_ENCODING = 'utf-8'

### ----- 略 -----

```

　以下を実行して，データを取得します．

```bash
scrapy crawl wj -o wj.json
```

# 4. データの分析

　実は，`wj-api.json`だけででかなり遊べます[^別記事]．notebookは[こちら](https://github.com/haltaro/predicting-comic-end/blob/master/1_analyze_comic_data_j.ipynb)ですので，使っていただけると幸いです．なお，以下では，`data`ディレクトリ配下に`wj-api.json`があることを前提に話を進めます．


[^別記事]: 当初の予想以上に楽しく遊べたので，二部構成にしました．

## 4.1 準備

 日本語で各作品のタイトルを表示したいので，[matplotlibで日本語を描画 on Ubuntu](http://qiita.com/conta_/items/4b031a44acceb137ec73)を参考に設定します．Ubuntu以外をお使いの方は，適宜ご対応ください．

```python3
import json
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd

sns.set(style='ticks')

import matplotlib
from matplotlib.font_manager import FontProperties
font_path = '/usr/share/fonts/truetype/takao-gothic/TakaoPGothic.ttf'
font_prop = FontProperties(fname=font_path)
matplotlib.rcParams['font.family'] = font_prop.get_name()
```

## 4.2 `ComicAnalyzer`

　`wj-api.json`分析用に，次のようなクラス`ComicAnalyzer`を定義します．

```python3:ComicAnalyzer
class ComicAnalyzer():
    """漫画雑誌の目次情報を読みだして，管理するクラスです．"""
    
    def __init__(self, data_path='data/wj-api.json', min_week=7, short_week=10):
        """
        初期化時に，data_pathにある.jsonファイルから目次情報を抽出します．
        - self.data: 全目次情報を保持するリスト型
        - self.all_titles: 全作品名情報を保持するリスト型
        - self.serialized_titles: min_week以上連載した全作品名を保持するリスト型
        - self.last_year: 最新の目次情報の年を保持する数値型
        - self.last_no: 最新の目次情報の号数を保持する数値型
        - self.end_titles: self.serialized_titlesのうち，self.last_yearおよび
                           self.last_noまでに終了した全作品名を保持するリスト型
        - self.short_end_titles: self.end_titlesのうち，short_week週以内に
                                 連載が終了した作品名を保持するリスト型
        - self.long_end_titles: self.end_titlesのうち，short_week+1週以上に
                                連載が継続した作品名を保持するリスト型
        """
        
        self.data = self.read_data(data_path)
        self.all_titles = self.collect_all_titles()
        self.serialized_titles = self.drop_short_titles(self.all_titles, min_week)
        self.last_year = self.find_last_year(self.serialized_titles[-100:])
        self.last_no = self.find_last_no(self.serialized_titles[-100:], self.last_year)
        self.end_titles = self.drop_continued_titles(
            self.serialized_titles, self.last_year, self.last_no)
        self.short_end_titles = self.drop_long_titles(
            self.end_titles, short_week)
        self.long_end_titles = self.drop_short_titles(
            self.end_titles, short_week + 1)

    def read_data(self, data_path):
        """ data_pathにあるjsonファイルを読み出して，全ての目次情報をまとめたリストを返します． """
        with open(data_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        return data

    def collect_all_titles(self):
        """ self.dataから全ての作品名を抽出したリストを返します． """
        titles = []
        for comic in self.data:
            if comic['title'] not in titles:
                titles.append(comic['title'])
        return titles

    def extract_item(self, title='ONE PIECE', item='worst'):
        """ self.dataからtitleのitemをすべて抽出したリストを返します． """
        return [comic[item] for comic in self.data if comic['title'] == title]

    def drop_short_titles(self, titles, min_week):
        """ titlesのうち，min_week週以上連載した作品名のリストを返します． """
        return [title for title in titles
                if len(self.extract_item(title)) >= min_week]

    def drop_long_titles(self, titles, max_week):
        """ titlesのうち，max_week週以内で終了した作品名のリストを返します． """
        return [title for title in titles
                if len(self.extract_item(title)) <= max_week]

    def find_last_year(self, titles):
        """ titlesが掲載された雑誌のうち，最新の年を返します． """
        return max([self.extract_item(title, 'year')[-1]
                   for title in titles])

    def find_last_no(self, titles, year):
        """ titlesが掲載されたyear年の雑誌のうち，最新の号数を返します． """
        return max([self.extract_item(title, 'no')[-1]
                   for title in titles
                   if self.extract_item(title, 'year')[-1] == year])

    def drop_continued_titles(self, titles, year, no):
        """ titlesのうち，year年のno号までに連載が終了した作品名のリストを返します． """
        end_titles = []
        for title in titles:
            last_year = self.extract_item(title, 'year')[-1]
            if last_year < year:
                end_titles.append(title)
            elif last_year == year:
                if self.extract_item(title, 'no')[-1] < no:
                    end_titles.append(title)
        return end_titles

    def search_title(self, key, titles):
        """ titlesのうち，keyを含む作品名のリストを返します． """
        return [title for title in titles if key in title]
```

　かなりわかりづらい処理をしておりますので，初期化時（`__init__()`）の動作を補足します．

1. `self.all_titles`は文字通り全ての作品名を保持します．しかし，`self.all_titles`は，明らかに読みきり作品や企画作品を含んでしまっています．
2. そこで，`min_week`以上連載した作品を`self.serialized_titles`として抽出します．しかし，`self.serialized_titles`は，最新の目次情報の時点で連載中の作品を含んでおり，連載継続期間が不正確です．例えば，「[鬼滅の刃](https://mediaarts-db.bunka.go.jp/mg/magazine_works/25517?)」は現在も連載中の人気作ですが，21週で連載が終了した作品のように見えてしまいます．
3. そこで，データベースの最新の目次情報の時点で連載が終了した（と思われれる）作品のみを`self.end_titles`として抽出します．`self.end_titles`が，本分析における全体集合です．
4. `self.end_titles`のうち，10週以内に終了した作品を`self.short_end_titles`として，11週以上継続した作品を`self.long_end_titles`として抽出します．

## 4.3 分析

　それでは，`ComicAnalyzer`を使って遊んでみます．

```python3
wj = ComicAnalyzer()
```

　まずは，短命作品最新10タイトルの，最初の10週目までの掲載順（worst）をプロットしてみます．値が大きいほど，巻頭付近に掲載されていたことになります．

```python3
for title in wj.short_end_titles[-10:]:
    plt.plot(wj.extract_item(title)[:50], label=title[:6])
plt.xlabel('Week')
plt.ylabel('Worst')
plt.ylim(0,22)
plt.legend()
```

![short.png](https://qiita-image-store.s3.amazonaws.com/0/151195/39c23f03-e203-f2d3-2d8e-3193c3584c3e.png)

　「[ギャグマンガ日和](https://mediaarts-db.bunka.go.jp/mg/comic_works/110568)」などの企画物（出張作品[^ギャグマンガ日和]）が入っているのが不満ですが，目次情報だけから除外する手立てはないです．あれ？「斉木楠雄」って10週以上連載していたんじゃ…？こういうときは，`search_title()`を使います．

[^ギャグマンガ日和]: ジャンプスクエアで連載中のはずです（[wikipedia](https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%A3%E3%82%B0%E3%83%9E%E3%83%B3%E3%82%AC%E6%97%A5%E5%92%8C)）．なお，2017年4月18日の時点で，データベースにはまだジャンプスクエアの目次情報が登録されていません．


```python3
wj.search_title('斉木', wj.all_titles)
# ['超能力者 斉木楠雄のΨ難', '斉木楠雄のΨ難']

len(wj.extract_item('超能力者 斉木楠雄のΨ難'))
# 7

wj.extract_item('超能力者 斉木楠雄のΨ難', 'year'), \
wj.extract_item('超能力者 斉木楠雄のΨ難', 'no')
# ([2011, 2011, 2011, 2011, 2011, 2011, 2011], [22, 27, 29, 33, 42, 43, 50])

len(wj.extract_item('斉木楠雄のΨ難'))
# 201
```
どうやら，「[超能力者 斉木楠雄のΨ難](https://mediaarts-db.bunka.go.jp/mg/magazine_works/1566?ids%5B%5D=1071&ids%5B%5D=1566)」で試験的に7回読み切り掲載したあと，「[斉木楠雄のΨ難](https://mediaarts-db.bunka.go.jp/mg/magazine_works/1071?ids%5B%5D=1071&ids%5B%5D=1566)」の連載を開始したみたいですね（[wikipedia](https://ja.wikipedia.org/wiki/%E6%96%89%E6%9C%A8%E6%A5%A0%E9%9B%84%E3%81%AE%CE%A8%E9%9B%A3)）． 次は，近年のヒット作（独断）の最初の10話分の掲載順を表示します．

```python3
target_titles = ['ONE PIECE', 'NARUTO-ナルト-', 'BLEACH', 'HUNTER×HUNTER']
for title in target_titles:
    plt.plot(wj.extract_item(title)[:10], label=title[:6])
plt.xlabel('Week')
plt.ylabel('Worst')
plt.ylim(0,22)
plt.legend()
```

![hit.png](https://qiita-image-store.s3.amazonaws.com/0/151195/42f6f074-31e9-dacd-1c05-5d65dbd14d70.png)

　本記事とは直接関係ありませんが，個人的に気になったので，50話まで掲載順を見てみます．

```python3
target_titles = ['ONE PIECE', 'NARUTO-ナルト-', 'BLEACH', 'HUNTER×HUNTER']
for title in target_titles:
    plt.plot(wj.extract_item(title)[:100], label=title[:6])
plt.xlabel('Week')
plt.ylabel('Worst')
plt.ylim(0,22)
plt.legend()
```

![hit50.png](https://qiita-image-store.s3.amazonaws.com/0/151195/a59b0027-2828-fb91-3ce0-2fc11be259d1.png)


　ある程度予想はしてましたが，やっぱりすごいですね．ちなみにですが，`extract_item()`を使って各話タイトルを取得しながら掲載順を見ると，マンガ好きの方はニヤニヤできます．

```python3
wj.extract_item('ONE PIECE', 'subtitle')[:10]

#['1.ROMANCE DAWN―冒険の夜明け―',
# '第2話!! その男"麦わらのルフィ"',
# '第3話 "海賊狩りのゾロ"登場',
# '第4話 海軍大佐"斧手のモーガン"',
# '第5話 "海賊王と大剣豪"',
# '第6話 "1人目"',
# '第7話 "友達"',
# '第8話 "ナミ登場"',
# '第9話 "魔性の女"',
# '第10話 "酒場の一件"']
```

　寄り道しすぎました．`seaborn`の`pairplot()`で相関分析をやってみます．ここでは，とりあえず2週目から6週目までの掲載順をプロットします．1週目を外したのは，ほとんどの作品は1週目に巻頭に掲載されるためです．なお，同じ座標に複数の点が重なって非常に見づらいので，便宜上ランダムなノイズを加えて見栄えを整えます．

```python3
end_data = pd.DataFrame(
    [[wj.extract_item(title)[1] + np.random.randn() * .3,
      wj.extract_item(title)[2] + np.random.randn() * .3,
      wj.extract_item(title)[3] + np.random.randn() * .3,
      wj.extract_item(title)[4] + np.random.randn() * .3,
      wj.extract_item(title)[5] + np.random.randn() * .3,
      '短命作品' if title in wj.short_end_titles else '継続作品']
     for title in wj.end_titles])
end_data.columns = ["Worst (week2)", "Worst (week3)", "Worst (week4)", 
                    "Worst (week5)", "Worst (week6)", "Type"]
sns.pairplot(end_data, hue="Type", palette="husl")
```

![ana.png](https://qiita-image-store.s3.amazonaws.com/0/151195/345a6612-0c2a-d810-6c81-96e2b34f52ae.png)


　ピンク色は11週以上継続した作品，緑色は10週以内で終了した短命作品です．もっとガッツリ分かれると思っていたのですが，意外と分離が難しそうですね．おそらくですが，「ギャグマンガ日和」のような企画物や，「超能力者 斉木楠雄のΨ難」のような試験的な読み切り作品がノイズになっているような気がします．掲載号の連続性で判別しても良いんですが，それだと休載作品と区別できないんですよね…．悩ましいです．
　とりあえずこのデータのまま，機械学習してみようと思います．

# 5. おわりに

　現実逃避で，気がついたらこんなものを作ってしまいました．次回が本番ですので，お楽しみ頂ければ幸いです．最後までお読み頂き，ありがとうございました！

# 参考文献

　本記事の作成にあたっては，以下を参考にさせて頂きました．ありがとうございました！:bow:

1. [バクマン。](https://www.amazon.co.jp/dp/B00A47VW2Y/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)：週刊少年ジャンプで連載されていた，漫画家漫画です．原稿料の話など，結構生々しい話が出てきて，面白かったです．
2. [Python3でjsonを返却するwebAPIにアクセスして結果を出力するまで](http://qiita.com/sakura1116/items/4a11a9f7db9f535397fa)：python3を使ったWeb APIの使い方について，参考にさせて頂きました．
3.  [Scrapy入門（１）](http://qiita.com/checkpoint/items/038b59b29df8e1e384a2)：すっきりまとまっており，大変参考に成りました．
4.  [Scrapy Tutorial](https://docs.scrapy.org/en/latest/intro/tutorial.html)：ひと通り手を動かしたら，なんとなく使い方がわかりました．
5.  [matplotlibで日本語を描画 on Ubuntu](http://qiita.com/conta_/items/4b031a44acceb137ec73)：作品名を日本語で表示する際，参考にさせて頂きました．