アニメの感想データを重回帰分析してみる

はじめに

アニメの感想データを重回帰分析してみた。統計は授業で習っただけなので、初心者が統計分からん人向けに面白いところだけ抜き出す記事。怖くない統計みたいな。

間違ってたら教えて下さいな。アニメのデータを引っ張ってきましたが、最近のアニメを見ていないので解析がうまく行ってるのかよく分からんけど、とりあえず結果を貼る。

データ取得

作品データベースというアニメ・本・映画などの評価を投稿できるWebサービスからRubyスクリプトスクレイピングしてデータを取得した。

作品データベース: アニメ、漫画、映画等の評価・情報DB

例えば「艦これ」というアニメなら下のページから「総合評価」「キャラ・設定」「ストーリー」「映像」「声優・俳優」「音楽」それぞれの点数を取得している。

艦隊これくしょん -艦これ-: 感想(評価/レビュー)[アニメ]

具体的には2015年のアニメランキングから、評価数が12以上の作品のリンクをたどった。全部で50作品の感想データが集まった。

2015年 アニメ ランキング(総合点順)

取得したデータは以下のとおりである。Rに読み込めるCSV形式と取得用のRubyスクリプトは最後に添付する。マイナスがつくと有効数字が一つ減ってるとかあるけど、面倒なので無視する。

f:id:motonari728:20160122105035p:plain

ちなみに作品データベースはアニメ以外にも、本・ゲーム・映画などのレビューが揃っているので、Rubyプログラムをある程度変えれば他の分野でもできるかもしれない。プログラムは自由に使ってもらって結構。

CSVをRに読み込む

> anime = read.csv('anime.csv', header=T, row.names=1)
> attach(anime) //する必要ないかも

元データでは「キャラ・設定」「声優・俳優」であるが、Rで読み込んだところ「キャラ.設定」「声優.俳優」になってしまったので注意されたい。

重回帰分析

ある作品で、音楽・ストーリー・映像の評価値が既にあって、総合評価がわからない場合に、総合評価を予想したい。そのためには 総合評価 = (1.5)×音楽 + (2.3)×ストーリー + (0.2)×映像 の式が必要。つまり、この式に代入すれば総合評価が出てくる。

Rが今あるデータを使って、総合評価の誤差が最も少ない係数(上の式のカッコの中身)を求めてくれる。

ここで総合評価を「目的変数」、音楽とストーリーと映像を「説明変数」という。 説明変数で目的変数を説明しようとするわけだ。この行為をモデルを作るという。

まずは 総合評価 = ( )×キャラ.設定 + ( )×ストーリー + ( )×映像 + ( )×声優.俳優 +( )× 音楽 の( )の中身を求める。

> lm.anime = lm(総合 ~ キャラ.設定 + ストーリー + 映像 + 声優.俳優 + 音楽)
> summary(lm.anime)
 
Call:
lm(formula = 総合 ~ キャラ.設定 + ストーリー + 映像 + 
    声優.俳優 + 音楽)
 
Residuals:
     Min       1Q   Median       3Q      Max 
-1.64713 -0.25562  0.06805  0.36635  1.13504 
 
Coefficients:
            Estimate Std. Error t value Pr(>|t|)  
(Intercept) -0.21543    0.24143  -0.892   0.3771  
キャラ.設定  0.28088    0.19463   1.443   0.1561  
ストーリー   0.40915    0.20188   2.027   0.0488 *
映像         0.23740    0.16670   1.424   0.1615  
声優.俳優   -0.06728    0.21851  -0.308   0.7596  
音楽        -0.01879    0.20041  -0.094   0.9257  
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
 
Residual standard error: 0.5545 on 44 degrees of freedom
Multiple R-squared:  0.7257, Adjusted R-squared:  0.6945 
F-statistic: 23.28 on 5 and 44 DF,  p-value: 2.337e-11

結果は 総合評価 = ( 0.28 )×キャラ.設定 + ( 0.41 )×ストーリー + ( 0.23 )×映像 + ( -0.07 )×声優.俳優 + ( -0.02 )× 音楽

ちなみにRが説明変数のうち、いらないものを教えてくれるのだが、

  • 確実に必要  ストーリー
  • 必要かも  キャラ・設定、映像
  • いらない 声優・俳優、音楽 となった。

つまり 「声優・俳優」「音楽」は、総合評価に影響を与えていない!!!

声豚生きてますか...


通例、モデルを作るときには確実に必要な説明変数だけで作るらしいので ストーリーのみを説明変数として分析してみる。重回帰で説明変数を1つしか使わないと、単回帰分析となる。やることは変わらん。

総合評価 = ( )×ストーリー の( )の中身を求める。

> lm.anime2 = lm(総合 ~ ストーリー)
> lm.anime2
 
Call:
lm(formula = 総合 ~ ストーリー)
 
Coefficients:
(Intercept)   ストーリー  
    0.03459      0.77079  
 
> summary(lm.anime2)
 
Call:
lm(formula = 総合 ~ ストーリー)
 
Residuals:
     Min       1Q   Median       3Q      Max 
-1.69747 -0.19077  0.06688  0.34023  1.29066 
 
Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  0.03459    0.08506   0.407    0.686    
ストーリー   0.77079    0.07440  10.360 7.87e-14 ***
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
 
Residual standard error: 0.5635 on 48 degrees of freedom
Multiple R-squared:  0.691, Adjusted R-squared:  0.6845 
F-statistic: 107.3 on 1 and 48 DF,  p-value: 7.867e-14

総合評価 = ( 0.77 )×ストーリー でした。

1つの説明変数でうまく総合評価を予想できるの? という疑問が湧いてくるが、Rはその辺も答えてくれる。

説明変数で目的変数をどれくらい説明しているかを表す「調整済み決定係数」というのがあって、これが0.6845である。

つまり ストーリーだけで総合評価の約7割を説明できる!!!

いちおう残差分析も貼っておく。

> par(mfrow=c(2,2)) 
> plot(lm.anime2)

f:id:motonari728:20160122110412p:plain

ヤバくはないモデルだと思う。


データ(csv形式)

タイトル, 総合, キャラ・設定, ストーリー, 映像, 声優・俳優, 音楽
響け!ユーフォニアム,                         2.15,  1.39,  1.07,  2.21,  1.39,  1.54
監獄学園(プリズンスクール),                         1.59,  1.39,  1.44,  1.56,  1.94,  1.00
のんのんびより りぴーと,                         1.96,  1.93,  1.71,  2.07,  1.93,  1.86
ワンパンマン,                         1.85,  1.50,  1.50,  2.20,  1.50,  1.70
Go!プリンセスプリキュア,                         1.36,  1.91,  1.95,  2.32,  2.05,  2.09
干物妹!うまるちゃん,                         1.07,  2.19,  0.94,  1.62,  2.06,  1.62
アイドルマスター シンデレラガールズ,                         0.54,  0.71,  0.35,  0.59,  1.26,  1.38
純潔のマリア,                         1.19,  1.11,  1.00,  1.11,  1.11,  1.00
冴えない彼女の育てかた,                         0.83,  1.38,  1.00,  1.69,  1.31,  1.12
ご注文はうさぎですか??(第2期),                         1.85,  2.36,  1.55,  2.00,  2.00,  1.73
オーバーロード,                         0.91,  1.36,  0.36,  1.64,  1.36,  0.91
ゆるゆり さん☆ハイ!,                         1.67,  2.00,  1.29,  1.71,  1.86,  1.14
城下町のダンデライオン,                         1.19,  2.17,  1.17,  1.50,  1.83,  1.50
ハロー!!きんいろモザイク,                         1.20,  1.69,  0.69,  1.54,  1.77,  1.54
やはり俺の青春ラブコメはまちがっている。続,                         0.79,  1.12,  1.00,  1.00,  1.12,  1.25
六花の勇者,                         0.79,  0.90,  0.50,  0.90,  0.90,  0.80
俺物語!!,                         1.25,  2.33,  1.83,  1.67,  1.67,  1.33
SHOW BY ROCK!!,                         1.08,  1.86,  0.71,  2.29,  1.57,  1.14
わかば*ガール,                         1.00,  1.00,  0.75,  1.50,  1.17,  0.92
ダンジョンに出会いを求めるのは間違っているだろうか,                         0.59,  1.44,  1.00,  1.56,  1.44,  1.00
それが声優!,                         0.91,  1.29,  1.00,  0.57,  1.86,  1.00
Classroom☆Crisis(クラスルーム☆クライシス),                         0.77,  0.50,  0.50,  0.50,  1.50,  0.00
GANGSTA.(ギャングスタ),                         0.82,  2.50,  2.50,  1.00,  2.50,  1.00
血界戦線,                         0.73,  1.86,  1.29,  2.43,  2.14,  2.57
夜ノヤッターマン,                         0.40,  0.38,  0.38,  0.62,  1.88,  1.12
ローリング☆ガールズ,                         0.47,  1.67,  0.50,  2.00,  1.67,  1.83
プラスティック・メモリーズ,                         0.29,  0.45,  0.45,  1.45,  1.27,  1.18
実は私は,                         0.38,  1.67,  0.67,  0.33,  1.00,  -0.3
がっこうぐらし!,                         0.25,  1.36,  0.71,  1.00,  1.07,  1.14
幸腹グラフィティ,                         0.25,  0.50,  0.20,  1.40,  1.20,  1.20
モンスター娘のいる日常,                         0.20,  2.33,  1.33,  2.11,  1.89,  1.11
下ネタという概念が存在しない退屈な世界,                         0.00,  0.60,  0.10,  0.50,  1.40,  0.30
学戦都市アスタリスク,                         0.00,  0.33,  0.00,  0.33,  1.00,  0.67
落第騎士の英雄譚,                         0.00,  -0.1,  -0.3,  0.80,  0.60,  0.20
終わりのセラフ,                         -0.0,  1.71,  1.57,  1.14,  1.71,  0.86
ミカグラ学園組曲,                         -0.1,  0.43,  -0.4,  0.14,  1.00,  1.57
乱歩奇譚 Game of Laplace,                         -0.1,  0.25,  -0.2,  0.75,  0.75,  0.75
櫻子さんの足下には死体が埋まっている,                         -0.5,  -1.3,  -1.0,  1.67,  0.67,  0.67
GATE(ゲート) 自衛隊 彼の地にて、斯く戦えり,                         -0.4,  1.00,  0.56,  1.44,  1.33,  1.44
聖剣使いの禁呪詠唱<ワールドブレイク>,                         -0.4,  -0.4,  -0.9,  -0.7,  0.82,  0.18
Charlotte(シャーロット),                         -0.1,  0.73,  0.15,  1.62,  1.31,  1.19
デス・パレード,                         -1.0,  0.86,  0.86,  1.57,  1.14,  1.43
アブソリュート・デュオ,                         -0.7,  -0.1,  -0.8,  0.00,  0.38,  1.25
空戦魔導士候補生の教官,                         -0.8,  -0.5,  -1.4,  -0.7,  0.14,  -0.2
電波教師,                         -1.7,  -1.0,  -1.6,  -1.8,  -1.5,  -0.6
コメット・ルシファー,                         -1.5,  -0.5,  -2.1,  0.17,  0.50,  0.33
新妹魔王の契約者<テスタメント>,                         -1.4,  -0.7,  -1.2,  -0.5,  0.50,  -0.3
銃皇無尽のファフニール,                         -1.5,  -1.2,  -2.0,  -0.4,  0.00,  -0.6
ケイオスドラゴン 赤竜戦役,                         -1.8,  -1.2,  -0.8,  0.40,  1.40,  0.60
艦隊これくしょん -艦これ-,                         -1.2,  -0.9,  -1.9,  0.00,  0.93,  0.48

Rubyスクリプト

require 'nokogiri'
require 'open-uri'
require 'active_support'
require 'active_support/core_ext'

def scrape_each(href)
  buf = ""
  url = 'http://sakuhindb.com' + href
  #url = 'http://sakuhindb.com/janime/7_The_20Heroic_20Legend_20of_20ARSLAN_202015/'
  doc = Nokogiri::HTML(open(url))
  buf += doc.search("span[@itemprop='name']")[0].content


  heikin_zone = doc.css('table')[3].css('td')[7] ? 3 : 2
  heikin = doc.css('table')[heikin_zone].css('td')[7].content.slice(3..6)

  if heikin.blank?.!
    buf += ",                         #{heikin},  "
    print buf
  end
  hyouka = Array.new(5)
  doc.css('table')[heikin_zone+6].css('tr').map(&:content).each do |tr|
    case tr.slice(0..0) # 評価の項目が点数の降順になっているので、一文字目で判定して並び替える。
      when ''
        hyouka[0] = tr.slice(6..9)
      when ''
        hyouka[1] = tr.slice(5..8)
      when ''
        hyouka[2] = tr.slice(2..5)
      when ''
        hyouka[3] = tr.slice(5..8)
      when ''
        hyouka[4] = tr.slice(2..5)
    end
  end

  puts hyouka.join(',  ')
end


url = "http://www.animesachi.com/user/year_2015_1.html?sort=data_down"
charset = nil

html = open(url) do |f|
  charset = f.charset # 文字種別を取得
  f.read # htmlを読み込んで変数htmlに渡す
end
p html

charset = 'UTF-8'
doc = Nokogiri::HTML(open('http://sakuhindb.com/anime-ranking/2015/'))
#p doc.title

doc.css('td > a').each do |node|
  # 評価数が12以上でないと、評価平均が出ないので、12以上を対象とする
  hyouka = node.parent.css('b')[7].nil? ? 0 : node.parent.css('b')[7].content.to_i
  if hyouka > 12
    scrape_each(node.attribute('href').value)
  end
end