花费许多时间学到的东西自然是要用一用的,如果工作中用不到的话,那就用来为生活增添些许乐趣吧。

看番多年,难免遇到烂番,既浪费时间,又影响心情;另外,有些优秀的番剧,可能因为某些原因,与自己失之交臂。要是能在自己看过的番剧的基础上,建立一个模型,帮自己避免烂番,发掘好番,那真是再好不过了。于是我就把自己近年来看过的番剧整理了一下,收集了若干相关信息,做成了excel表格,作为建立模型的原材料。数据是手动整理的,花费的时间比我预计的多很多,但在整理的过程中,也引发了不少回忆,所以也算不上是浪费时间。

A.关于数据

数据就是我看番的记录,不全,但应该是足够用了。反应变量是我对某番剧的评分,从5分到10分,是离散数据,在这第一次尝试中,计划将其变为二元分类数据,即5分到7分为不推荐,8分到10分为推荐。预测变量有十来个,包括番剧的年代、类型、制作公司、声优和网站评分等信息,在第一次尝试中,计划把数据弄成稀疏矩阵,使用朴素贝叶斯算法和规则学习算法来进行分类。

在查看数据之前,先载入分析需要用到的包:

library(tidyverse)
library(readxl)
library(here)
library(ggthemes)
library(corrplot)
library(tidytext)
library(widyr)
library(igraph)
library(ggraph)
library(e1071)
library(RWeka)
library(gmodels)

然后导入数据,并进行初步的清洗:

anime <- read_xlsx(here('content', 'post', 'data', 'anime_record.xlsx')) %>%
  mutate_all(str_remove_all, pattern = '\U00A0') %>% # 去掉不间断空格<U+00A0>
  select(-record_time) %>%
  mutate(studio = str_remove(studio, ',.*')) %>%
  mutate_at(vars(c('season_number', 'episode', 'year', 'rating', 
                   'db_number', 'mal_number')), as.integer) %>% 
  mutate_at(vars(c('db_rating', 'mal_rating')), as.numeric)

看一下数据的情况:

glimpse(anime)
## Observations: 186
## Variables: 15
## $ name          <chr> "新世纪福音战士", "钢之炼金术师", "妖精的旋律", "蔷薇少女", "地狱少女", "蔷...
## $ series        <chr> "新世纪福音战士", "钢之炼金术师", "妖精的旋律", "蔷薇少女", "地狱少女", "蔷...
## $ season_number <int> 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, ...
## $ episode       <int> 26, 51, 13, 12, 26, 12, 24, 24, 14, 24, 26, 25, ...
## $ year          <int> 1995, 2003, 2004, 2004, 2005, 2005, 2005, 2006, ...
## $ season        <chr> "秋", "秋", "夏", "秋", "秋", "秋", "秋", "冬", "春", "春"...
## $ origin        <chr> "原创", "漫改", "漫改", "漫改", "原创", "漫改", "轻改", "游改", ...
## $ genra         <chr> "Action,Dementia,Drama,Mecha,Psychological,Sci-F...
## $ studio        <chr> "Gainax", "Bones", "Arms", "Nomad", "Studio Deen...
## $ rating        <int> 8, 8, 8, 7, 7, 7, 7, 7, 10, 8, 7, 10, 10, 9, 6, ...
## $ db_rating     <dbl> 9.4, 9.2, 8.2, 7.7, 7.8, 8.0, 7.4, 7.7, 8.5, 8.9...
## $ db_number     <int> 69782, 48049, 13862, 6478, 17827, 2626, 12445, 2...
## $ mal_rating    <dbl> 8.33, 8.27, 7.71, 7.52, 7.73, 7.72, 7.63, 7.49, ...
## $ mal_number    <int> 416593, 538905, 562900, 77500, 90143, 48980, 193...
## $ cast          <chr> "绪方惠美/林原惠美/三石琴乃/宫村优子/山口由里子/立木文彦/清川元梦/山寺宏一/关智一/岩永...

可以看到,共有186行数据,15列变量。下面简单介绍下变量:

  • name:番剧的名称;

  • series:番剧的系列名,也就是番剧第一季的名字;

  • season_number:番剧的季数;

  • episode:番剧的总集数;

  • year:番剧首播的年份;

  • season:番剧首播的季节;

  • origin;番剧的来源,即由什么而改编;

  • genra:番剧的类型;

  • studio:番剧的制作公司;

  • rating;我个人对番剧的评分,即反应变量;

  • db_rating:豆瓣上对番剧的评分;

  • db_number:豆瓣上对番剧评分的人数;

  • mal_rating:MAL上对番剧的评分;

  • mal_number:MAL上对番剧评分的人数;

  • cast:番剧的声优

其实还有很多其他信息可以收集的,如监督、配乐、原画、作者、销量等信息,但我都没有记录,因为觉得这些信息对我看不看某个番剧没有太大的影响(毕竟人方面主要还是看声优,监督是谁,一般并不关心;而销量也没怎么关注过)。

变量介绍到此,下面开始对数据进行探索。

B.数据探索

探索前说一下缺失值的问题。整份数据中只有2个缺失值,是《死亡笔记》的豆瓣评分和豆瓣人数,因为被和谐的原因,竟然在豆瓣上连个条目都没有。不过,在最后建模的时候,我也没有用到这两个变量,所以这个问题不算个问题。

在后面的数据探索中,我频繁地使用同样的几行代码对数据进行统计,为了节省代码,就先写了个函数:

my_awesome_summary <- function(data) {
  data %>% summarise(N = n(), rating = mean(rating), 
            db_rating = mean(db_rating, na.rm = TRUE), 
            db_number = mean(db_number, na.rm = TRUE),
            mal_rating = mean(mal_rating), mal_number = mean(mal_number))
}

该函数会在分类的基础上,计算各类别的个数及若干变量的平均值。当然,最好不要用my_awesome_summary这种函数名。

b1.番名

番名肯定会对我决定看不看某个番产生一定的影响,在中二的年代,《死亡笔记》《反叛的鲁路修》这样的名字,确实很有吸引力。不过,我不知道该如何使用这一变量,所以没有在后面的模型中用到。但这一变量倒在清洗数据时发挥了一定的作用。

b2.季数

先看一下按季数进行分类的情况:

anime %>% group_by(season_number) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
season_number N rating db_rating db_number mal_rating mal_number
1 125 7.55 8.18 17152.02 7.85 242829.1
2 46 7.78 8.35 10009.17 8.00 184294.9
3 10 7.40 7.95 11612.10 7.88 108821.9
4 3 7.33 7.80 17342.00 7.27 50593.0
5 1 9.00 9.50 26317.00 8.63 38075.0
6 1 9.00 9.50 19896.00 8.71 31475.0

超过3季的番很少,实际上,也就只有《夏目友人帐》作到了第六季。考虑将第2季及后续的合并在一起,与第1季进行对比:

anime %>% mutate(season_state = ifelse(season_number == 1, 'first', 'second+')) %>% 
  group_by(season_state) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
season_state N rating db_rating db_number mal_rating mal_number
first 125 7.55 8.18 17152.02 7.85 242829.1
second+ 61 7.74 8.29 11062.00 7.97 160444.5

从评分来看,后续的番剧确实要比第1季的番剧要好一些,因此我有一个猜测,即如果某个番剧出了续集,则它更值得观看。筛选一下数据,进行对比:

anime %>% 
  mutate(season_more = ifelse(series %in% (anime %>% filter(season_number > 1) %>% 
                                             pull(series) %>% unique()), 'yes', 'no')) %>% 
  group_by(season_more) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
season_more N rating db_rating db_number mal_rating mal_number
no 82 7.40 8.11 14753.23 7.81 202685.0
yes 104 7.78 8.30 15448.27 7.96 226159.4

这样一对比就更明显了,但有一点需要注意,就是数据中的某个“没有”后续的番剧可能并不是没有后续,只是我没有去看。

第一个变量提取出来了,即有无后续。

b3.集数

下面看下集数变量的情况:

anime %>% group_by(episode) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
episode N rating db_rating db_number mal_rating mal_number
4 1 7.00 8.60 6007.00 8.03 183539.0
6 1 7.00 7.30 2778.00 6.60 10205.0
10 4 6.75 7.95 4844.50 7.73 212081.0
11 6 8.17 8.75 25299.67 8.27 180340.3
12 74 7.46 8.08 11506.42 7.72 172649.2
13 40 7.62 8.41 16667.53 7.98 175268.0
14 2 10.00 8.65 17240.50 7.61 262674.5
15 1 8.00 8.60 22429.00 8.35 403140.0
16 2 7.00 7.05 3839.00 7.00 105944.5
21 1 7.00 8.50 9951.00 7.66 65534.0
22 4 8.00 8.33 24685.50 8.30 425028.2
23 2 9.50 9.00 14174.00 8.39 289716.0
24 22 7.68 8.20 15641.68 8.00 272950.6
25 11 8.36 8.48 28003.55 8.37 453407.4
26 11 6.64 7.66 13373.00 7.71 109467.4
37 1 10.00 NaN NaN 8.66 1139551.0
51 1 8.00 9.20 48049.00 8.27 538905.0
52 1 7.00 9.00 838.00 7.60 6491.0
64 1 10.00 9.60 53720.00 9.24 862938.0

集数的类型还挺多的,我对此进行了简化,将超过20集的视为长篇,不足20集的视为短篇。这里其实并没有真正的长篇,毕竟最长的也才64集(钢炼FA),不过我没有把看过的长篇统计在内,所以这么称呼也可以吧:

anime %>% mutate(episode = ifelse(episode < 20, 'short', 'long'),
                 episode = factor(episode, levels = c('short', 'long'))) %>% 
  group_by(episode) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
episode N rating db_rating db_number mal_rating mal_number
short 131 7.55 8.20 13455.92 7.81 175963.8
long 55 7.76 8.25 19239.02 8.08 310718.2

在我的评分中,长篇的确实更好一些,但其实这也挺危险的,因为万一是个长篇的烂番的话,那可真是让人加倍的烦躁了。

第二个变量为是否是长篇。

b4.年份

还是先看一下各年份的情况:

anime %>% group_by(year) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
year N rating db_rating db_number mal_rating mal_number
1995 1 8.00 9.40 69782.00 8.33 416593.00
2003 1 8.00 9.20 48049.00 8.27 538905.00
2004 2 7.50 7.95 10170.00 7.62 320200.00
2005 3 7.00 7.73 10966.00 7.69 110749.00
2006 6 8.67 8.46 26425.20 8.16 433357.33
2007 5 8.00 8.16 14301.60 7.52 226445.60
2008 8 8.38 8.59 28915.62 8.33 288890.50
2009 11 8.09 8.55 21519.36 7.95 218160.18
2010 6 7.33 8.00 17073.00 7.91 329771.33
2011 11 7.91 8.39 27114.55 8.12 285201.36
2012 14 7.50 8.26 17267.93 7.91 251625.21
2013 13 7.23 8.03 12966.00 7.77 217733.46
2014 15 7.53 8.18 15401.33 7.87 314657.53
2015 20 7.40 7.88 9747.45 7.68 197773.05
2016 15 7.60 8.38 16410.53 8.11 241646.60
2017 16 7.69 8.28 11513.88 7.99 170416.75
2018 38 7.34 8.11 7271.58 7.72 84959.74
2019 1 9.00 9.70 15215.00 9.00 45057.00

数据中的旧番挺少的,主要是因为我是对我网盘里有资源的番剧进行的整理,而很多旧番,已经找不到资源了。另外,2018年的番剧,我竟然看了38部,看来去年是挺焦虑的,需要大量看番来缓解。不过,那年番剧的整理质量似乎挺差的,高质量的番剧,主要还是集中在2008年前后。

同样为了简化,我将2015年设为节点,之前的归类为旧番,剩下的归类为新番。看一下分类后的情况:

anime %>% mutate(year = ifelse(year < 2015, 'old', 'new'),
                 year = factor(year, levels = c('old', 'new'))) %>% 
  group_by(year) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
year N rating db_rating db_number mal_rating mal_number
old 96 7.74 8.28 19839.66 7.94 276670.8
new 90 7.48 8.15 10187.38 7.84 150892.8

番还是老的香啊!

第三个变量就是是否为旧番了。

b5.季节

番剧一般都是在1月、4月、7月和10月开始播出,对应冬番、春番、夏番和秋番。先看一下各季节的番剧情况:

anime %>% group_by(season) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
season N rating db_rating db_number mal_rating mal_number
54 7.70 8.21 16097.13 7.88 240018.5
41 7.59 8.37 13337.20 8.00 185731.2
55 7.58 8.12 14144.80 7.89 218163.4
36 7.56 8.18 17270.64 7.78 210160.7

就我个人的评分而言,似乎是春番更好一些,但实际上,我看番是以补为主,以追为辅,大部分的情况下,我都意识不到这部番当年是在哪个季节里播出的,所以这个变量就先不用了。

b6.来源

来源主要包括漫画、轻小说、游戏和原创,也有个别来源不明的以及由正统小说改编的,放进了其他类别里(有一个小说改编的,我错误的放进了轻改里,但忘了是哪个了)。看一下各来源的情况:

anime %>% group_by(origin) %>% 
  my_awesome_summary() %>% 
  knitr::kable(digits = 2)
origin N rating db_rating db_number mal_rating mal_number
漫改 90 7.77 8.52 17356.28 7.92 192463.5
其他 5 7.20 8.04 5778.80 7.70 66330.2
轻改 58 7.40 7.88 10436.43 7.86 244139.0
游改 10 7.90 8.18 16663.90 7.88 282510.7
原创 23 7.52 7.92 19829.43 7.90 239227.0

我看的番剧有一半是由漫画改编的,我对这些番剧的评分也还不错,但是,我几乎从来没有因为看过某个漫画或轻小说而去看某个动画的(没有时间啊),也不会因为看了某个番剧,觉得好看,而去追原作的(一拳超人是个例外),所以这个变量对我看不看某个番剧应该也没啥太大的影响,先放弃了。

b7.类别

番剧类别一共由30多种,先看下整体的情况:

anime %>% separate_rows(genra, sep = ',') %>% 
  group_by(genra) %>% 
  my_awesome_summary() %>% 
  arrange(-N) %>% 
  knitr::kable(digits = 2)
genra N rating db_rating db_number mal_rating mal_number
Comedy 92 7.55 8.30 12362.05 7.82 192869.58
Action 70 7.61 8.09 17106.83 7.93 292958.14
School 60 7.45 8.14 13528.15 7.79 213940.98
Slice of Life 58 7.78 8.40 17216.45 7.90 159906.62
Supernatural 58 7.74 8.36 19662.47 8.11 269174.21
Drama 54 7.94 8.41 24999.26 8.09 279575.81
Romance 47 7.40 7.99 11517.55 7.76 240592.87
Fantasy 45 7.76 8.26 21097.58 8.11 285569.42
Sci-Fi 33 7.79 7.93 14952.12 7.78 216789.27
Seinen 30 7.37 8.20 11977.47 7.66 167865.30
Mystery 29 7.90 8.46 17869.39 8.02 277079.72
Shounen 25 8.04 8.75 21719.50 8.21 338354.80
Magic 24 7.42 7.97 14309.79 7.95 263051.46
Adventure 20 7.60 8.48 12175.30 8.18 346767.65
Psychological 20 7.65 8.21 19453.47 7.88 315613.35
SuperPower 17 8.24 8.42 25527.41 8.19 400225.71
Parody 16 7.69 8.05 9403.25 7.61 183292.19
Military 12 8.25 8.36 29216.67 8.11 403922.33
Ecchi 11 6.82 7.67 8229.73 7.63 247873.27
Horror 10 7.00 7.64 15405.70 7.48 266667.10
Mecha 10 7.40 7.35 19267.20 7.50 246206.90
Game 9 7.44 7.73 9294.67 7.77 389259.67
Harem 7 6.57 7.20 5682.29 7.24 154537.57
Historical 7 8.14 8.81 7680.57 8.28 89261.43
Music 7 7.00 7.97 12224.00 7.89 186041.71
Shoujo 6 9.00 9.45 52054.00 8.61 69487.67
Thriller 6 8.83 8.78 23380.00 8.50 486415.83
Space 5 7.00 7.04 1733.00 7.00 35079.80
Police 3 8.67 8.40 18267.00 8.20 588147.67
Dementia 2 7.50 8.45 37566.50 7.83 233431.00
Josei 2 9.00 9.45 8624.00 8.73 44417.50
Shoujo Ai 2 7.00 7.70 3674.50 7.07 40518.50
Sports 2 7.50 8.90 8259.50 7.83 52953.00
Vampire 2 7.50 8.85 13636.50 8.56 296321.00

我看的最多的几个类别的喜剧(Comedy)、动作(Action)、校园(School)、日常(Slice of Life)和超自然(Super Natural),但我评分最高的几个类别是军事(Military)、超能力(Super Power)和少年(Shounen)(仅统计个数超过 10的),如果不统计一下的话,我自己都不知道。另外,我最不喜欢的是后宫(Harem)。记得有次跟hj师妹聊番剧,我说我最喜欢的是《凉宫春日的忧郁》,她(似乎很鄙夷地)跟我说,她不喜欢这种男性向的后宫番。首先,我想说,《凉宫》不是后宫番啊;其次,我也很讨厌后宫番啊!

计划将这一变量转换成稀疏矩阵,并去掉个数出现太少的类别。

b8.制作公司

我看的番剧共涉及到将近50个制作公司,还是先看一下整体的情况:

anime %>%  
  group_by(studio) %>% 
  my_awesome_summary() %>% 
  arrange(-N) %>% 
  knitr::kable(digits = 2)
studio N rating db_rating db_number mal_rating mal_number
A-1 Pictures 16 7.31 7.71 15227.56 7.81 317868.50
J.C. Staff 15 7.27 7.87 11284.93 7.68 140272.27
Kyoto Animation 13 8.38 8.63 17731.00 8.04 259475.31
Studio Deen 12 7.25 8.17 10774.08 7.84 135950.00
Mad house 8 8.38 8.46 14708.00 8.30 442931.00
Shaft 8 7.38 8.43 12112.88 7.99 202859.38
Bones 7 8.86 9.31 30097.43 8.68 474203.29
Brain’s Base 7 8.43 9.06 45882.00 8.45 184809.14
Sunrise 7 7.86 8.23 20527.71 8.08 293667.14
Production I.G 6 7.67 8.30 14079.50 7.86 166226.17
White Fox 6 8.33 8.53 13787.50 8.26 266070.67
Lerche 5 7.20 7.86 8175.40 7.78 184249.20
Shuka 5 8.40 9.04 13336.60 8.34 87420.00
SilverLink. 5 7.40 8.44 6791.80 7.81 149994.60
ufotable 5 8.20 8.84 21870.60 8.34 271011.80
Wit Studio 5 8.20 8.64 34170.60 8.05 405481.40
Doga Kobo 4 7.25 7.97 19771.00 7.63 132846.75
StudioPierrot 4 6.75 7.32 19858.75 7.25 385754.25
AIC Build 3 7.33 7.40 8502.67 7.42 251730.00
Diomedea 3 7.00 7.37 3347.67 7.22 66933.67
feel. 3 7.67 8.77 11638.67 8.31 155367.33
Gonzo 3 7.00 7.33 1005.67 6.68 7201.67
Sate light 3 5.67 5.73 999.33 6.83 17787.33
Clover Works 2 6.00 8.05 8494.00 7.62 108598.50
David Production 2 7.50 8.95 22642.50 7.71 44781.00
Geno Studio 2 7.50 8.85 1280.00 7.97 33665.00
Kinema Citrus 2 8.00 8.85 6844.50 8.39 142476.00
Nomad 2 7.00 7.85 4552.00 7.62 63240.00
P.A. Works 2 8.00 8.65 13699.50 8.35 400245.50
Seven 2 7.00 8.10 7988.50 7.48 94999.50
Shin-Ei Animation 2 7.50 8.70 6996.50 7.78 73195.50
Xebec 2 7.00 7.35 2025.50 7.28 81748.50
8bit 1 7.00 8.10 7069.00 8.37 78138.00
AIC 1 6.00 6.70 691.00 7.44 26658.00
Arms 1 8.00 8.20 13862.00 7.71 562900.00
C-Station 1 7.00 9.30 3409.00 8.33 73442.00
Gainax 1 8.00 9.40 69782.00 8.33 416593.00
Imagin 1 8.00 8.10 12540.00 8.33 249632.00
Kamikaze Douga 1 7.00 7.50 5351.00 7.33 50269.00
Orange 1 8.00 8.90 17278.00 8.46 79433.00
Pierrot Plus 1 7.00 7.10 1525.00 6.71 14046.00
Studio 3Hz 1 7.00 7.40 1648.00 7.30 101380.00
Tatsunoko Production 1 8.00 8.00 9768.00 7.52 196840.00
TMS Entertainment 1 8.00 8.80 15681.00 8.05 99415.00
TNK 1 6.00 7.10 15272.00 5.95 242096.00
Trigger 1 6.00 6.80 1352.00 7.27 115457.00
TROYCA 1 7.00 7.40 5314.00 7.46 62885.00

看的最多的三个制作公司分别是A-1,节操社(J.C. Staff)和京阿尼(Kyoto Animation),没想到我竟然看了那么多节操社的作品。为了更直观的对比,我选择个数超过5的11个公司,对评分进行了可视化:

anime %>%  
  group_by(studio) %>% 
  my_awesome_summary() %>% 
  filter(N > 5) %>% 
  gather(rater, score, c('rating', 'db_rating', 'mal_rating')) %>% 
  select(studio, rater, score) %>% 
  ggplot(aes(studio, score, fill = rater)) + 
  geom_col(aes(group = rater), position = 'dodge') + 
  scale_x_discrete(labels = c('A-1\nPictures', 'Bones', "Brain's\nBase", 'J.C.\nStaff',
                              'Kyoto\nAnimation', 'Mad\nhouse', 'Production\nI.G',
                              'Shaft', 'Studio\nDeen', 'Sunrise', 'White\nFox')) + 
  scale_y_continuous(breaks = 1:10, labels = 1:10) + 
  coord_cartesian(ylim = 1:10) + 
  scale_fill_manual(values = c('#009E73', '#E69F00', '#56B4E9'),
                    labels = c('豆瓣评分', 'MAL评分', '个人评分')) + 
  labs(x = '', y = '', fill = '') + 
  theme_tufte() + 
  theme(legend.position = 'top')

可以看到,骨头社(Bones)的评价是最高的,而节操社的评价是最低的。如果某个番剧是骨头社、京阿尼、疯房子(Mad House)或者白狐社(White Fox)制作的,我基本上就可以放心的观看了(我对这几个公司的评分都要高于MAL上的评分)。

同上,也计划将这一变量转换成稀疏矩阵,并去掉个数出现太少的制作公司。

b9.评分及人数

3个评分变量和2个人数变量都是数值型,其中我的个人评分是反应变量,就是最终想通过模型预测的变量,而另外4个变量之间会有较高的相关,考虑只使用其中1个变量。先看一下相关的情况:

corrplot(cor(anime[, 10:14], use = 'complete.obs'), 
         method = 'number', type = 'upper')

3个评分之间都由很高的相关,鉴于此,计划只使用豆瓣评分和MAL评分其中之一作为预测变量。在后面建模的时候,发现用豆瓣评分建立的模型不如使用MAL评分建立的模型要好,因此就只使用MAL的评分了,将其评分的中位数设为节点,大于中位数的视为推荐,小于中位数的视为不推荐。至于评分人数,跟我的评分相关也不低,但都低于相应的评分与我的评分之间的相关,因此就不使用了。对于我的评分,这次先处理为二元分类变量,将5-7分设为“不推荐”,而将8-10分设为“推荐”,作为反应变量的标签。

extra 1.个人、豆瓣与MAL的对比

虽然我个人的评分于豆瓣评分、MAL评分都有很高的相关,但还是有一定的差异的。首先来看看我心中最好的番剧(即我评为10分的番剧)和豆瓣上、MAL上最好的番剧有什么不同:

anime %>% filter(rating == 10) %>% 
  select(name, rating) %>% 
  bind_cols(anime %>% arrange(-db_rating) %>% 
              select(db_name = name, db_rating) %>% slice(1:10)) %>% 
  bind_cols(anime %>% arrange(-mal_rating) %>% 
              select(mal_name = name, mal_rating) %>% slice(1:10)) %>% 
  knitr::kable(digits = 2)
name rating db_name db_rating mal_name mal_rating
凉宫春日的忧郁 10 灵能百分百Ⅱ 9.7 钢之炼金术师FA 9.24
反叛的鲁路修 10 钢之炼金术师FA 9.6 命运石之门 9.13
死亡笔记 10 夏目友人帐第三季 9.5 灵能百分百Ⅱ 9.00
团子大家族 10 夏目友人帐第四季 9.5 团子大家族 第二季 8.99
反叛的鲁路修 R2 10 夏目友人帐第五季 9.5 反叛的鲁路修 R2 8.94
团子大家族 第二季 10 昭和元禄落语心中助六再临篇 9.5 来自深渊 8.87
钢之炼金术师FA 10 夏目友人帐第六季 9.5 四月是你的谎言 8.86
凉宫春日的忧郁 2009 10 进击的巨人第三季1 9.5 昭和元禄落语心中助六再临篇 8.84
命运石之门 10 新世纪福音战士 9.4 反叛的鲁路修 8.77
一拳超人 10 夏目友人帐 第二季 9.4 物语系列第二季 8.77

可以看到,豆瓣上几乎是夏目的天下,跟我只重了钢炼FA一个。灵能2我也很喜欢,但感觉还是跟一拳超人差了一个档次。我的十佳跟MAL的十佳有5个重叠,对于物语系列能排到前10,甚感意外。

再来看看评分的整体分布:

anime %>% gather(rating, value, c('rating', 'db_rating', 'mal_rating')) %>% 
  select(rating, value) %>% 
  ggplot(aes(value, fill = rating)) + 
  geom_density(alpha = .8) +
  scale_x_continuous(breaks = 4:10, labels = 4:10) + 
  scale_fill_manual(values = c('#009E73', '#E69F00', '#56B4E9'),
                    labels = c('豆瓣评分', 'MAL评分', '个人评分')) + 
  labs(x = '', y = '', fill = '') + 
  theme_tufte() + 
  theme(legend.position = 'top')

我的评分大部分都是7分或8分,7分就是那种看不看两可的感觉,而8分就是很值得一看了。MAL上的评分集中在7分和9分之间,很少有能在MAL上拿到9分以上的番剧;而豆瓣上的评分则稍微分散一些,峰值出现在9分左右,看来豆瓣上的用户倾向于给出高分,但这也是因为豆瓣上给番剧评分的用户远少于MAL上的用户:

anime %>% gather(number, value, c('db_number', 'mal_number')) %>% 
  select(number, value) %>% 
  ggplot(aes(value, fill = number)) + 
  geom_histogram(show.legend = FALSE) +
  scale_x_continuous(labels = scales::comma) + 
  scale_fill_manual(values = c('#009E73', '#E69F00')) + 
  labs(x = '', y = '', fill = '') + 
  facet_wrap(~ number, nrow = 2, scales = 'free',
             labeller = as_labeller(c('db_number' = '豆瓣人数', 
                                      'mal_number' = 'MAL人数'))) + 
  theme_tufte() + 
  theme(legend.position = 'top')

但从趋势上来看,还是差不多的。

b0.声优

最后一个变量是声优。虽然只收集了不到200个番剧,但声优数却有600多。先看一下主要声优的情况(仅统计参演超过10部的声优):

anime %>% separate_rows(cast, sep = '/') %>% 
  group_by(cast) %>% 
  my_awesome_summary() %>% 
  filter(N > 10) %>% arrange(-rating) %>% 
  knitr::kable(digits = 2)
cast N rating db_rating db_number mal_rating mal_number
关智一 12 8.75 9.06 27225.92 8.52 326825.7
浪川大辅 11 8.45 8.68 25943.91 8.28 323097.7
细谷佳正 12 8.42 8.97 27083.75 8.27 371816.4
大原沙耶香 11 8.36 8.75 21912.09 8.33 272202.0
小林优 13 8.31 8.69 21555.23 8.11 237782.9
石田彰 18 8.28 8.84 35076.67 8.27 151272.8
小山力也 11 8.09 8.40 20032.91 7.87 217325.6
三宅健太 12 8.08 8.27 20692.25 8.04 369599.2
神谷浩史 25 8.08 8.79 27032.56 8.27 239633.2
小野大辅 18 8.00 8.55 20281.28 7.89 213190.6
中村悠一 22 7.95 8.14 20212.82 7.92 298512.9
梶裕贵 25 7.92 8.50 20228.08 8.10 323644.1
樱井孝宏 20 7.90 8.48 22061.85 8.14 319606.4
中田让治 13 7.85 8.35 17024.69 7.96 229432.1
诹访部顺一 13 7.85 8.24 17122.31 7.84 163293.1
井上麻里奈 16 7.81 8.47 17329.00 7.96 317493.6
宫野真守 21 7.76 8.31 16738.55 8.01 338401.7
田村由加莉 12 7.75 8.16 16199.75 7.83 248854.1
植田佳奈 12 7.75 8.51 18014.50 8.05 239926.1
福山润 19 7.74 8.49 16092.37 8.13 253515.6
佐仓绫音 19 7.74 8.18 10453.89 7.87 194947.5
泽城美雪 25 7.72 8.35 18715.68 8.01 211165.4
新井里美 14 7.71 7.91 13257.64 7.70 209135.7
花泽香菜 25 7.68 8.26 15851.72 8.00 261241.5
伊藤静 12 7.67 8.38 13296.50 7.96 171365.3
早见沙织 14 7.64 8.24 20426.71 8.10 267863.5
川澄绫子 11 7.64 8.29 18295.00 7.93 196957.9
木村良平 11 7.64 8.31 19371.82 8.11 210926.9
下野纮 11 7.64 8.13 17083.45 7.87 239582.9
悠木碧 16 7.62 8.45 16303.12 8.07 357969.2
喜多村英梨 13 7.62 8.49 14083.85 8.18 339427.3
加藤英美里 12 7.58 8.04 11545.83 8.06 244129.2
松冈祯丞 12 7.58 8.25 15437.83 8.01 339119.0
逢坂良太 11 7.55 7.84 17280.18 7.86 277002.1
冈本信彦 15 7.53 8.19 16826.20 7.93 228771.8
杉田智和 21 7.52 8.23 13374.81 7.68 149927.9
能登麻美子 18 7.50 7.94 12636.78 7.83 197120.6
佐藤聪美 12 7.50 8.22 9852.75 7.81 166250.3
钉宫理惠 13 7.46 8.08 16001.85 7.80 303504.1
日笠阳子 11 7.45 8.40 13392.18 7.97 206685.2
堀江由衣 11 7.36 8.32 14453.00 8.10 281930.8
竹达彩奈 11 7.36 7.89 15388.18 7.66 340554.2
石川界人 14 7.29 7.66 10563.36 7.47 205135.0
茅野爱衣 14 7.21 8.05 18157.86 8.08 270272.0
井口裕香 15 7.07 7.81 8491.73 7.87 239696.8

关智一以绝对的优势排在了第一名,看来只要有他出演,质量就可以得到保证。那看看他出演了哪些番剧吧:

anime %>% filter(str_detect(cast, '关智一')) %>% 
  select(name, rating, db_rating, db_number, mal_rating, mal_number) %>% 
  knitr::kable(digits = 2)
name rating db_rating db_number mal_rating mal_number
新世纪福音战士 8 9.4 69782 8.33 416593
青之文学 8 8.7 16481 7.82 40803
命运石之门 10 9.3 39302 9.13 653578
命运之夜前传 9 9.1 38651 8.42 426719
命运之夜前传第二期 9 9.1 30630 8.64 356533
心理测量者 8 8.8 26766 8.43 428052
命运之夜 无限剑制 8 8.6 20634 8.31 301393
命运之夜 无限剑制 第二季 8 8.3 13787 8.39 254243
一拳超人 10 9.3 45526 8.70 836549
昭和元禄落语心中 9 9.4 12574 8.62 55045
昭和元禄落语心中助六再临篇 9 9.5 4674 8.84 33790
命运石之门0 9 9.2 7904 8.59 118610

但我目前最喜欢的声优是神谷浩史,在我看的番剧中,他出现的次数也是最多的,来看看他的情况:

anime %>% filter(str_detect(cast, '神谷浩史')) %>% 
  select(name, rating, db_rating, db_number, mal_rating, mal_number) %>% 
  knitr::kable(digits = 2)
name rating db_rating db_number mal_rating mal_number
再见,绝望先生 8 8.5 14684 7.96 93182
夏目友人帐 9 9.3 106587 8.38 118354
夏目友人帐 第二季 9 9.4 64594 8.61 84469
化物语 8 8.6 22429 8.35 403140
无头骑士异闻录 8 8.7 38140 8.26 412301
天使的心跳 7 7.9 18793 8.28 714781
夏目友人帐第三季 9 9.5 51257 8.64 75891
伪物语 7 8.5 9086 8.20 246841
夏目友人帐第四季 9 9.5 43673 8.71 68662
猫物语(黑) 7 8.6 6007 8.03 183539
进击的巨人 9 8.9 97544 8.47 1078647
物语系列第二季 7 9.1 4844 8.77 189502
命运之夜 无限剑制 8 8.6 20634 8.31 301393
七大罪 7 8.4 7072 8.23 497232
无头骑士异闻录 承 8 8.8 9448 8.09 151912
命运之夜 无限剑制 第二季 8 8.3 13787 8.39 254243
那就是声优 7 7.2 1290 7.08 13365
无头骑士异闻录 转 8 8.7 5982 8.08 115729
进击!巨人中学校 7 7.5 1283 7.06 45391
无头骑士异闻录 结 8 8.7 5040 8.17 99909
齐木楠雄的灾难 8 9.3 37064 8.55 109573
夏目友人帐第五季 9 9.5 26317 8.63 38075
进击的巨人第二季 9 9.2 31939 8.43 471478
夏目友人帐第六季 9 9.5 19896 8.71 31475
进击的巨人第三季1 9 9.5 18424 8.47 191745

卡米亚真是出演了不少热门番剧。

这一变量也是转换成稀疏矩阵,并去掉出现次数太少的声优。

extra 2.声优关系网络

这种图挺难画好看的,花了不少时间来调整:

anime %>% separate_rows(cast, sep = '/') %>% 
  group_by(name) %>% 
  mutate(linenumber = group_indices()) %>% 
  pairwise_count(cast, linenumber) %>% 
  group_by(item1, item2) %>% 
  count(sort = TRUE) %>% 
  ungroup() %>% 
  filter(n > 4) %>% 
  graph_from_data_frame() %>% 
  ggraph(layout = "fr") + 
  geom_edge_link(aes(alpha = n, width = n), color = 'grey50') +
  scale_edge_width(range = c(.5, 2)) +
  geom_node_point(color = '#D55E00', size = 4) +
  geom_node_text(aes(label = name), size = 2, color = 'black') + 
  theme_void() + 
  theme(legend.position = '')

仔细看的话,可以看到不少有意思的地方,但这不是主题,略过。

C.建模与评估

数据都探索完了,接下来就可以建立模型,进行分类了。

c1.清洗数据

不过首先还是应该对数据进行清洗,需要清洗的地方前面以前都提到了:

anime_clean <- anime %>%   
  mutate(season_more = ifelse(series %in% (anime %>% filter(season_number > 1) %>% 
                                             pull(series) %>% unique()), 1, 0),
         lengthy = ifelse(episode > 20, 1, 0),
         old = ifelse(year < 2015, 1, 0),
         mal_recommand = ifelse(mal_rating > median(mal_rating), 1, 0),
         recommend = ifelse(rating > 7, 1, 0),
         recommend = factor(recommend, levels = c(1, 0), 
                            labels = c('recommend', 'not recommend'))) %>% 
  bind_cols(anime %>% mutate(row = row_number()) %>% 
              separate_rows(genra, sep = ',') %>% 
              cast_sparse(row, genra) %>% 
              as.matrix() %>% 
              as_tibble() %>% 
              select(which(colSums(.) > 9)),
            anime %>% mutate(row = row_number()) %>% 
              cast_sparse(row, studio) %>% 
              as.matrix() %>% 
              as_tibble() %>% 
              select(which(colSums(.) > 4)),
            anime %>% mutate(row = row_number()) %>% 
              separate_rows(cast, sep = '/') %>% 
              cast_sparse(row, cast) %>% 
              as.matrix() %>% 
              as_tibble() %>% 
              select(which(colSums(.) > 4))) %>% 
  select(recommend, everything()) %>% 
  select(-3:-16) %>% 
  mutate_if(is.numeric, factor, levels = c(0, 1), labels = c('No', 'Yes'))

这大概是我接触R以来写的最长的一段数据清洗的代码了,tidyverse化的R代码,真是越看越好看。现在的数据共有182个变量:

dim(anime_clean)
## [1] 186 182

c2.创建训练数据集和测试数据集

然后将数据分为训练集和测试集,训练集包括124行数据,测试集包括剩余的62行数据:

set.seed(0509)
anime_train <- anime_clean %>% sample_n(124)
anime_test <- anime_clean %>% setdiff(anime_train)

c3.朴素贝叶斯算法

先试试朴素贝叶斯算法:

anime_class <- naiveBayes(anime_train[, -1], anime_train$recommend)
anime_pred <- predict(anime_class, anime_test[, -1])

CrossTable(anime_pred, anime_test$recommend, prop.chisq = FALSE, 
           dnn = c('predicted', 'actual'))
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  62 
## 
##  
##               | actual 
##     predicted |     recommend | not recommend |     Row Total | 
## --------------|---------------|---------------|---------------|
##     recommend |            28 |             3 |            31 | 
##               |         0.903 |         0.097 |         0.500 | 
##               |         0.800 |         0.111 |               | 
##               |         0.452 |         0.048 |               | 
## --------------|---------------|---------------|---------------|
## not recommend |             7 |            24 |            31 | 
##               |         0.226 |         0.774 |         0.500 | 
##               |         0.200 |         0.889 |               | 
##               |         0.113 |         0.387 |               | 
## --------------|---------------|---------------|---------------|
##  Column Total |            35 |            27 |            62 | 
##               |         0.565 |         0.435 |               | 
## --------------|---------------|---------------|---------------|
## 
## 

结果似乎还不错,有3个不推荐的分类为了推荐,另有7个推荐的分类为了不推荐。比起错过好番,我更怕遇到烂番,因为我稍微有些强迫症,一旦看了某个番剧,就会把它坚持看完,哪怕它再烂。看看是哪些番剧分类错了吧:

anime_test %>% bind_cols(predict = anime_pred) %>% 
  select(name, recommend, predict) %>% 
  filter(recommend != predict) %>% 
  knitr::kable(digits = 2)
name recommend predict
再见,绝望先生 recommend not recommend
我的妹妹不可能那么可爱 recommend not recommend
我的妹妹不可能那么可爱第二季 recommend not recommend
甲铁城的卡巴内瑞 not recommend recommend
齐木楠雄的灾难 recommend not recommend
暗杀教室Q not recommend recommend
擅长捉弄的高木同学 recommend not recommend
卫宫家今天的饭 not recommend recommend
MegaloBox recommend not recommend
千绪的通学路 recommend not recommend

暗杀教室Q作为暗杀教室的衍生剧,其实还蛮好看的,我评分的时候其实也在犹豫。有几个错的挺离谱的,比如MegaloBox,那真是强烈推荐啊;而甲铁城的卡巴内瑞,那真是强烈不推荐啊!

c4.规则学习算法

再试试规则学习算法:

anime_JRip <- JRip(recommend ~ ., data = anime_train[, -2])
anime_pred <- predict(anime_JRip, anime_test[, -1:-2])

CrossTable(anime_pred, anime_test$recommend, prop.chisq = FALSE, 
           dnn = c('predicted', 'actual'))
## 
##  
##    Cell Contents
## |-------------------------|
## |                       N |
## |           N / Row Total |
## |           N / Col Total |
## |         N / Table Total |
## |-------------------------|
## 
##  
## Total Observations in Table:  62 
## 
##  
##               | actual 
##     predicted |     recommend | not recommend |     Row Total | 
## --------------|---------------|---------------|---------------|
##     recommend |            29 |             3 |            32 | 
##               |         0.906 |         0.094 |         0.516 | 
##               |         0.829 |         0.111 |               | 
##               |         0.468 |         0.048 |               | 
## --------------|---------------|---------------|---------------|
## not recommend |             6 |            24 |            30 | 
##               |         0.200 |         0.800 |         0.484 | 
##               |         0.171 |         0.889 |               | 
##               |         0.097 |         0.387 |               | 
## --------------|---------------|---------------|---------------|
##  Column Total |            35 |            27 |            62 | 
##               |         0.565 |         0.435 |               | 
## --------------|---------------|---------------|---------------|
## 
## 

结果跟朴素贝叶斯算法差不多,只是在FN上少了1个。看下在规则学习算法下,哪些番剧被分类错了吧:

anime_test %>% bind_cols(predict = anime_pred) %>% 
  select(name, recommend, predict) %>% 
  filter(recommend != predict) %>% 
  knitr::kable(digits = 2)
name recommend predict
轻音少女 第二季 not recommend recommend
我的妹妹不可能那么可爱 recommend not recommend
我的妹妹不可能那么可爱第二季 recommend not recommend
游戏人生 not recommend recommend
东京喰种 not recommend recommend
刀剑神域Ⅱ recommend not recommend
擅长捉弄的高木同学 recommend not recommend
工作细胞 recommend not recommend
千绪的通学路 recommend not recommend

有4个番剧,两种算法都分类错了,看来应该找找原因,但是对于俺妹系列,我似乎应该把对它们的评分改了。另外,这一算法的一个严重错误是对工作细胞的分类,这么火的番剧,怎么可能不推荐呢?

D.局限与展望

第一次尝试就到此为止了,存在很多问题,比如,对于某些变量,我只是将其变为了二元分类变量,损失了很多信息。后面考虑使用决策树和随机森林等算法重新进行分类,并且打算再花些时间,建立一个验证数据集,收集30个左右我没有看过的番剧,看一看真实的效果如何。

这篇博客就到此为止了,这是我花时间最多的一篇博客了。

sessionInfo()
## R version 3.5.3 (2019-03-11)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 17134)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=Chinese (Simplified)_China.936 
## [2] LC_CTYPE=Chinese (Simplified)_China.936   
## [3] LC_MONETARY=Chinese (Simplified)_China.936
## [4] LC_NUMERIC=C                              
## [5] LC_TIME=Chinese (Simplified)_China.936    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] gmodels_2.18.1  RWeka_0.4-40    e1071_1.7-1     ggraph_1.0.2   
##  [5] igraph_1.2.4    widyr_0.1.1     tidytext_0.2.0  corrplot_0.84  
##  [9] ggthemes_4.1.1  here_0.1        readxl_1.3.1    forcats_0.4.0  
## [13] stringr_1.4.0   dplyr_0.8.0.1   purrr_0.3.2     readr_1.3.1    
## [17] tidyr_0.8.3     tibble_2.1.1    ggplot2_3.1.0   tidyverse_1.2.1
## 
## loaded via a namespace (and not attached):
##  [1] viridis_0.5.1     httr_1.4.0        jsonlite_1.6     
##  [4] viridisLite_0.3.0 modelr_0.1.4      gtools_3.8.1     
##  [7] assertthat_0.2.1  highr_0.8         cellranger_1.1.0 
## [10] yaml_2.2.0        ggrepel_0.8.0     pillar_1.3.1     
## [13] backports_1.1.3   lattice_0.20-38   glue_1.3.1       
## [16] digest_0.6.18     polyclip_1.10-0   rvest_0.3.2      
## [19] colorspace_1.4-1  htmltools_0.3.6   Matrix_1.2-15    
## [22] plyr_1.8.4        pkgconfig_2.0.2   broom_0.5.1      
## [25] haven_2.1.0       bookdown_0.9      scales_1.0.0     
## [28] gdata_2.18.0      tweenr_1.0.1      ggforce_0.2.1    
## [31] generics_0.0.2    farver_1.1.0      withr_2.1.2      
## [34] lazyeval_0.2.2    cli_1.1.0         magrittr_1.5     
## [37] crayon_1.3.4      evaluate_0.13     tokenizers_0.2.1 
## [40] janeaustenr_0.1.5 fansi_0.4.0       nlme_3.1-137     
## [43] SnowballC_0.6.0   MASS_7.3-51.1     xml2_1.2.0       
## [46] class_7.3-15      blogdown_0.11     tools_3.5.3      
## [49] RWekajars_3.9.3-1 hms_0.4.2         munsell_0.5.0    
## [52] compiler_3.5.3    rlang_0.3.3       grid_3.5.3       
## [55] rstudioapi_0.10   labeling_0.3      rmarkdown_1.12   
## [58] gtable_0.3.0      R6_2.4.0          gridExtra_2.3    
## [61] lubridate_1.7.4   knitr_1.22        utf8_1.1.4       
## [64] rprojroot_1.3-2   rJava_0.9-11      stringi_1.4.3    
## [67] Rcpp_1.0.1        tidyselect_0.2.5  xfun_0.6