5日と20日は歌詞と遊ぼう。

歌詞を読み、統計したりしています。

2014年のオリコンとボカロのベスト100をテキストマイニングしました!

こんにちは。

今回は、2014年のオリコンのランキングとボカロのランキングを比較して、いろいろ楽しみたいと思います。
いま流行りの統計的な手法やテキストマイニングを使います♪

スクレイピング

では、さっそく、データを集めましょう。
http://www.tnsori.com/2014-oricon-single-yearly-top100
オリコンはこちらのデータを見ました。
http://vocaran.jpn.org/movieranking/sumpts/2014
ボカロのランキングはこちらを見ました。

まずは、ここから曲のリストを取り出します。取り出すことをスクレイピングと言うそうです。
スクレイピングスクリプトを書きましたよ!

require 'cgi'

page_source = open("ランキングのページのパスをいれるよ〜", &:read)
# scanは、マッチした個数に関係なく配列に入れてくれるので、配列から外す(flattenとこ)
ranks = page_source.scan(/<span class=\"rank\">(.+?)<\/span>/).flatten!
titles = page_source.scan(/<span class=\"title\"><a.+?>(.+?)<\/a><\/span>/).flatten!
# ランクとタイトルの配列をまとめてひとつにする
list = ranks.zip(titles)
list.each{|a|
	print CGI.unescapeHTML(a.join(",")) + "\n"
}

こういう感じになりました!
使った言語はRubyです。上記はボカロのバージョン。オリコンのほうはもっとカンタンだったので、正規表現を使って検索&置換で取り出しました。
こうして、ランキングの曲をカンマ区切りのデータにします。

1,【初音ミク】ストリーミングハート【オリジナル曲】
2,『初音ミク』千本桜『オリジナル曲PV』
3,【初音ミク】 ウミユリ海底譚 【オリジナル曲】
4,GUMI MV「ドーナツホール」
5,【初音ミク】 恋愛裁判 【オリジナルMV】
こんな感じのが、100行続きます。

タイトル部分には、タイトル以外の要素(「【初音ミク】」とか「【オリジナル曲】」とか)もあります。これを取り除きましょう。つまり、

1,ストリーミングハート
2,千本桜
3,ウミユリ海底譚
4,ドーナツホール
5,恋愛裁判
こうなります。正規表現とかを使って、手作業でなんとかしました。これをテキストファイルに保存しておきます。

100曲分のリストができましたので、今度はそれぞれの歌詞を手にしましょう。歌詞は初音ミク Wikiに載っているので、ここからスクレイピングすることを目論みます。
さっき作ったリストをRubyで読み込んで、歌詞を1曲ずつテキストファイルに書き出します。

# 曲目のリストCSVファイルを受け取って、各曲の歌詞をテキストで書き出す
require 'open-uri'
require 'csv'
require 'uri'
require 'cgi'

CSV.foreach("さっき作ったCSVのパス", "r") do |row|
	# 1列目にはタイトルが入ってる
	title = row[1]
	# このURLにアクセスするとなんか直接それぞれの曲のページに行ける
	url = "http://www5.atwiki.jp/hmiku/?page="+title
	url = URI.escape(url)
	# htmlを読む
	html = open(url) do |f|
		f.read
	end
	if (/<title>初音ミク Wiki -.*は見つかりません<\/title>/ =~ html)
		next
	end
	begin
		# 歌詞を拾う
		lyric = html.scan(/<h3 id=\"id_0a172479\">歌詞<\/h3>.*?<div>(.+)<\/div>.*?<h3/m)[0][0]
	rescue
		# なにもしない
	end
	if (lyric == nil)
		next
	end
	# 歌詞をきれいに
	lyric = CGI.unescapeHTML(lyric)
	lyric.gsub!(/<div>|\n/m, '') #divタグと\nを取り除く
	lyric.gsub!(/<\/div>|\n/m, '<br />') #div閉じタグを<br \/>にする
	lyric.gsub!(/\A\s+?|\s+?\Z|(<br \/>)+\Z/m, '')	#文字列の最初と最後の空白や<br />を取り除く
	lyric.gsub!("<br />", "\n")	#brタグを改行にする
	lyric.gsub!(/<\/?[^>]*>/, "")	#文中のhtmlタグは外す
	lyric.chomp!
	File.write("歌詞のテキストファイルをしまうフォルダのパス"+row[0]+"_"+title+".txt", lyric)
	sleep 1
end

こうすると、指定したフォルダに山ほどテキストファイルができます!

ほらね!!
テキストファイルには歌詞のルビが付いてたり、出典が付いてたりします。それはいらないので、手作業で消しました。これでスクレイピング完了!!

まとめ
  • Rubyを使って、ランキングから曲目のリストを取り出し、リストから個々の歌詞を取り出した。
今後の課題
  • スクレイピングはただの通過点なので、ここに1週間とかかけるのやめたい。
  • ルビとか手作業で取るのかなりダルかったので、今後はここも自動でなんとかなるようにしたい。

数を数える!

準備ができました。
では、いよいよ分析っぽいことしてみます。
手始めに、オリコン、ボカロのそれぞれの曲について、行数などを数えてみます。

# フォルダを指定すると、その中のファイルを読み込んで、字数、行数、連数をカウントします
mydirs = ["オリコンの曲が入ったフォルダのパス",
	"ボカロの曲が入ったフォルダのパス"]
mydirs.each{|mydir|
	# line, paragraph, char, ファイル名
	c = []
	l = []
	p = []
	Dir.open(mydir){|dir|
		# フォルダの中のファイル1つずつについて操作
		dir.each{|filename|
			# 「.」から始まる名前のファイルは無視
			next if /^\./ =~ filename
			# ファイル関係のエラーが起きたときのため
			begin
				lyric = File.open(mydir + "/" + filename, &:read)
				c.push(lyric.split(//).length)
				l.push(lyric.split(/\n/).length)
				p.push(lyric.split(/\n\n+?/).length)
			rescue => e
				print "error\t" + e.message + "\tname:" + filename + "\n"
			end
		}
	}
	print mydir + "\n"
	print "c" + c.to_s + "\n"
	print "l" + l.to_s + "\n"
	print "p" + p.to_s + "\n"
}

すると、こんな感じで数字がわーっと出てきます。ひょー!!

pが連、lが行、cが文字の数を示しています。連というのは、空白の行で区切られたかたまりです。
1行目には「l[65, 65, 49, 32,…」って書いてありますが、これは読み込まれたオリコン曲が、1曲目から順に65行、65行、49行、32行あることを示しています。順番はRubyのオリジナルルールになるらしく、1位の次は10位になったりしてるみたいです。
この、65, 65, 49, 32,という並びから察するに、オリコン曲はだいたい50行ぐらいあるってことみたい。ボカロ曲とどれぐらい違うか比べたいですね。

ということで、今度は別のプログラムを書いてみます。
比較のためには統計を活用したいです。統計に便利と言われている、Rという言語に乗り換えましょう。
RはRubyと名前が似ていますが、別の言語です。

oricon_c <- c(742, 720, 491, 567, 522, 697, 481, 644, 523, 538, 579, 582, 863, 658, 743, 504, 536, 628, 474, 639, 631, 936, 803, 852, 812, 722, 732, 1012, 1279, 441, 693, 813, 988, 624, 499, 767, 603, 674, 590, 526, 1079, 735, 1182, 637, 701, 565, 631, 409, 1028, 970, 1164, 676, 442, 242, 651, 508, 477, 1046, 1195, 228, 632, 477, 440, 1145, 193, 594, 583, 980, 1200, 620, 633, 1107, 298, 960, 849, 1446, 592, 610, 457, 467, 1230, 551, 748, 654, 378, 847, 698, 424, 600, 693, 262, 1075, 545, 582, 587, 326, 804, 858, 712, 853)
oricon_l <- c(65, 65, 49, 32, 72, 87, 66, 60, 41, 58, 74, 66, 60, 61, 60, 44, 38, 42, 25, 40, 43, 46, 79, 53, 47, 41, 46, 73, 64, 45, 75, 49, 89, 70, 61, 46, 69, 49, 45, 35, 58, 67, 63, 47, 77, 35, 59, 38, 76, 73, 96, 34, 23, 20, 32, 72, 38, 78, 87, 20, 48, 36, 63, 70, 17, 52, 35, 58, 81, 39, 41, 45, 23, 87, 46, 64, 44, 35, 36, 59, 77, 28, 60, 61, 41, 78, 41, 43, 65, 58, 20, 74, 41, 41, 48, 26, 58, 71, 72, 56)
oricon_p <- c(9, 11, 8, 10, 12, 12, 7, 7, 9, 12, 9, 8, 14, 9, 9, 8, 8, 9, 9, 12, 10, 12, 7, 12, 9, 9, 12, 9, 10, 9, 13, 8, 15, 9, 10, 11, 17, 12, 9, 9, 11, 17, 11, 8, 10, 12, 13, 11, 12, 12, 16, 8, 7, 3, 11, 9, 10, 14, 15, 3, 11, 9, 9, 12, 3, 16, 9, 9, 16, 5, 9, 8, 3, 20, 9, 10, 8, 10, 8, 8, 12, 7, 20, 13, 8, 9, 12, 11, 9, 9, 3, 10, 14, 9, 12, 6, 12, 12, 18, 9)
vocalo_c <- c(756, 578, 1489, 1630, 1170, 704, 864, 761, 2615, 990, 703, 495, 871, 763, 902, 897, 487, 615, 686, 1052, 649, 528, 465, 654, 793, 787, 849, 235, 605, 1240, 751, 730, 706, 708, 239, 556, 618, 747, 492, 1099, 322, 610, 430, 1109, 760, 609, 652, 668, 511, 1782, 810, 704, 651, 509, 553, 520, 654, 726, 982, 1318, 674, 666, 455, 822, 1071, 580, 675, 619, 1257, 477, 546, 789, 697, 1094, 823, 650, 892, 1082, 774, 414, 670, 1382, 975, 390, 588, 345, 812, 644, 980, 401, 610, 320, 654, 737, 674, 489, 799, 1019, 433, 1558)
vocalo_l <- c(50, 44, 77, 69, 60, 56, 50, 49, 215, 56, 52, 36, 52, 79, 71, 74, 49, 40, 52, 60, 60, 27, 36, 64, 68, 63, 72, 17, 48, 99, 38, 60, 77, 76, 24, 34, 75, 40, 45, 53, 28, 49, 56, 70, 50, 54, 54, 63, 44, 70, 59, 62, 63, 38, 51, 52, 43, 67, 74, 57, 56, 47, 41, 48, 98, 48, 44, 51, 104, 43, 43, 39, 57, 106, 62, 61, 74, 84, 78, 40, 44, 65, 96, 27, 53, 22, 61, 48, 29, 32, 35, 33, 49, 39, 59, 52, 62, 51, 39, 88)
vocalo_p <- c(11, 9, 13, 17, 11, 12, 10, 11, 38, 13, 10, 11, 12, 14, 15, 14, 12, 9, 14, 11, 13, 7, 11, 13, 14, 16, 23, 2, 9, 20, 9, 13, 15, 22, 6, 9, 25, 9, 8, 9, 7, 11, 17, 8, 10, 15, 14, 15, 10, 11, 13, 10, 15, 9, 12, 10, 9, 14, 18, 13, 15, 9, 6, 11, 8, 11, 9, 12, 21, 11, 8, 8, 12, 20, 15, 13, 14, 18, 24, 9, 10, 16, 35, 8, 11, 5, 12, 9, 6, 7, 9, 7, 9, 10, 12, 7, 13, 11, 10, 16)
# 画面を消去し、グラフを描く
frame()
par(mfrow=c(1,3))
boxplot(oricon_c, vocalo_c)
boxplot(oricon_l, vocalo_l)
boxplot(oricon_p, vocalo_p)

箱ヒゲ図を描いてみました。

3つの図は左から、字数、行数、連数を表しています。それぞれ左がオリコン、右がボカロを表しています。
箱ヒゲ図の見方についてはhttp://www.stat.go.jp/koukou/howto/process/graph/graph5.htmとかをご覧ください。昔は学校で習わなかったんですよね。
語数、行数、連数ともに、ボカロの方でひとつだけ突出している曲があります。これは『Music Wizard of OZ』という曲です。20分以上あるミュージカル仕立ての曲なので、その分だけ歌詞が長いのですね。

見ましょう♪
でもそれを除くと、ボカロもオリコンも字数に大した違いはないみたいです。いや、違いはあるって言えばあるけど…。大した違いと言えるのかは微妙です。

というわけで、平均値の差の検定をしてみます。さっきの1曲は外れ値として、対象から除くことにしました。まずは文字数について、分散の等質性を調査。

> var.test(oricon_c, vocalo_c)

すると

	F test to compare two variances

data:  oricon_c and vocalo_c
F = 0.7327, num df = 99, denom df = 98, p-value = 0.1242
alternative hypothesis: true ratio of variances is not equal to 1
95 percent confidence interval:
 0.4923731 1.0898222
sample estimates:
ratio of variances
         0.7326778

と出ましたので、帰無仮説を棄却できません。なので、t検定ができます(で合ってますよね?)。

> t.test(oricon_c, vocalo_c, var.equal=T)
	Two Sample t-test

data:  oricon_c and vocalo_c
t = -1.4617, df = 197, p-value = 0.1454
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -132.19659   19.65033
sample estimates:
mean of x mean of y
 694.0400  750.3131

有意水準5%で、帰無仮説を棄却できないことがわかりました。つまり、オリコンとボカロ曲の間で、字数には大して差がないということです。
同じように行数と連数について調べたところ、連数だけは5%水準で有意差があることがわかりました。

t.test(oricon_p, vocalo_p, var.equal=T)
	Two Sample t-test

data:  oricon_p and vocalo_p
t = -3.2554, df = 197, p-value = 0.001333
alternative hypothesis: true difference in means is not equal to 0
95 percent confidence interval:
 -3.0208291 -0.7415951
sample estimates:
mean of x mean of y
 10.24000  12.12121

なるほど〜。
オリコンの曲とボカロの曲は、文字数も行数もだいたい同じぐらいであるものの、連の数が異なり、ボカロ曲の方がわりと連数が多いことがわかりました。
どうしてでしょうか? 考えてみました。
私が思うに、ボカロ曲の複雑な構成と関係があるんじゃないでしょうか。

id:shiba-710さんがていねいに書いてくださっています。
ボカロ曲はオリコン曲と比較して構成が複雑で、曲調がどんどん変わっていきやすい傾向にあります。そのため、こまめに連に分けた表記が合うのではないかと思いました。
なお今回は私のR技術不足につき、グラフのデータをPDFに書き出してから見た目を調整しています。逆効果ではありませんように。
グラフの大きさとかは変えてないよ。

まとめ
  • Rを使ってグラフを書いた。
  • 字数、行数、連数を取り上げて、オリコンとボカロを比較した。その結果、連の数だけはボカロ曲が有意に多いという結論が出た。
  • その理由は、ボカロ曲がオリコン曲と比較して複雑な構成であることが関係するかもしれない。
今後の課題
  • RubyとRのデータの橋渡しをかっこよくやりたい(今回はコピー&ペースト)。
  • Rでもっとかっこよいグラフを描けるようになりたい。
  • ボカロ曲の方が複雑な構成であることを示すために、時間あたりの文字数を比較することが有効だと思った。曲の長さを調べられるデータベースないかな。

形態素解析

今度は、歌詞の内容に近づいてみます。引き続きRを使います。
いまから書く部分は、以前私が人力検索のクイズに回答したときと同じやり方をしているので、コードはそちらを見てください♪

最初に、オリコンとボカロの曲をそれぞれひとつのテキストファイルに統合しておきます。

で、RMeCabを使って形態素解析しました。実際には形態素じゃなくて単語と呼んだほうがいいと思うんですが、宗教上のタブーが絡むと聞いているので形態素解析と呼ばせていただきます。

oricon_best20 <- head(oricon_o[grep("代名詞", oricon_o$Info2), c(1:4)], 20)
vocalo_best20 <- head(vocalo_o[grep("代名詞", vocalo_o$Info2), c(1:4)], 20)
# 行の名前を消す(自動的に順位がつく)
rownames(oricon_best20) <- NULL
rownames(vocalo_best20) <- NULL
# 表示
oricon_best20[c(1,4)]
vocalo_best20[c(1,4)]

代名詞を頻度順にランキングさせてみました。こういう感じになります。

オリコンボカロ
 TermFreq
124.5708601
213.5923907
37.6326502
4みんな4.7050583
5あなた4.6005015
6それ4.6005015
74.3913878
8僕ら4.2868309
9どこ3.9731604
103.6594898
11ここ3.3458192
12いつ3.2412624
13これ2.5093644
14そこ2.4048076
15キミ2.091137
16貴方1.4637959
17オレ1.1501254
18なん0.9410117
19ボク0.9410117
200.8364548
TermFreq
121.0269273
213.3389570
35.1253135
4それ4.9938952
54.5339312
6あなた3.9425489
73.6140031
8僕ら2.2341110
9どこ2.1684019
10これ1.6427287
11ここ1.5113104
12みんな1.5113104
13いつ1.3798921
14そこ0.9199281
15何処0.9199281
16なん0.8542189
17キミ0.7228006
180.7228006
190.7228006
20あんた0.6570915
予想よりも、ずっと似ています。トップ3は顔ぶれもいっしょだし、スコアもそんなに違いません。
でも違うところもあります。私が注目したのは「みんな」です。オリコン曲にとても多く、ボカロ曲の3倍もあります。なんでだろう?
内容を見てみると、オリコン曲には2種類の「みんな」があることがわかります。
声を掛けるんだ
みんなは言うけど
それができない
↑(5位:AKB48心のプラカード』)

みんな積極的で
そう羨ましかった
↑(11位:乃木坂46『夏のFree&Easy』)

みんな頑張って
それ行け Get the chance!!
↑(51位:サザンオールスターズ『東京VICTORY』)

気取ったヤツは スルーでいいから
みんな勝手に Party in the club
↑(61位:2PM『ミダレテミナ』)

ひとつめは、『心のプラカード』や『夏のFree&Easy』に見られる「みんな」です。「君」と「僕」の世界に終始しがちな世界にちょっと客観性を持たせるような位置付けになっているので、「脇役的みんな」と呼べます。
ふたつめは、『東京VICTORY』や『ミダレテミナ』に見られる「みんな」です。どちらも「みんな」自体が歌詞の中心であるように見受けられるので、「主役的みんな」と呼べます。

一方で、ボカロ曲はこんな感じです。

手をあわせてください
みんな元気にいただきます
↑(11位:ギガP『+♂』)

ときにはみんなで 馬鹿騒ぎ
裸踊りで 大笑い
↑(21位:れるりり『神のまにまに』)

さあ狂ったように踊りましょう
どうせ100年後の今頃には
みんな死んじゃってんだから
↑(23位:れるりり『脳漿炸裂ガール』)

ボカロ曲には、「主役的みんな」が多く、はっきりした「脇役的みんな」はベスト100には見受けられませんでした。
オリコン曲はたくさんの人が協力して作っているのに対して、ボカロ曲はひとりでも作ることができます。聴くときのことを考えても、オリコン曲はテレビというみんなのメディアで多く流れるのに対して、ボカロ曲はスマホというプライベートなツールで多く視聴されている感じがします。
オリコン曲のほうがパブリックな印象があり、ボカロ曲のほうがプライベートな印象があります。
曲のテーマも、ボカロ曲は自分と向き合うことがテーマになりやすい傾向があると感じています。
こういうことから、「みんな」というのは自分との向き合いというよりも、少し視野の広い言葉なので、それがオリコン曲っぽさと相性がいいのかもしれないと、私は思いました。
「みんな」のほかに、「僕ら」という代名詞も、オリコン曲がボカロ曲よりもはるかに多く見られました。これにも同じような理由があるかもしれません。

まとめ
  • オリコン曲とボカロ曲で代名詞の数を比較したところ、オリコン曲のほうが「みんな」や「僕ら」が多かった。
  • その理由として、オリコン曲はテレビなどパブリックなメディアと相性が良く、ボカロ曲はスマホといったプライベートなメディアと相性がいいことが理由かもしれないと思った。
今後の課題
  • 今まで黙ってたことがあるんですけど…。

歌詞って、その特性上、同じことばを繰り返すことがよくあります。でもその繰り返しの回数をどこまで厳密に表記するかにはブレがあります。同じことを歌っていても、「WOW〜」ってだけ書いてある歌詞もあれば「WOW WOW WOW WOW」って書いてある歌詞もあります。後者が前者の4倍に評価されてしまったら変な感じがします。
それに、今回はオリコン/ボカロの曲をすべてまとめてひとつのファイルにしてしまいました。その結果、1曲に50回出てくる単語も、50曲に1回ずつ出てくる単語も、同じ評価がされてしまっています。それはなんか変な感じ。重みのつけ方がわかりません…!
今後はそういうとこにも目配せできたらいいなと思います。



以上です。今日はここまで。私よくがんばりました♪
今回どうしてこういうことをしたのか、書いておきます。
私は歌詞を読むのが趣味です。アプローチ(ってほど大したものではないですが)は主にテクスト論に近いところです。
そんな感じで長いことこのブログをやってきましたが、なんだか閉塞感が見えてきました。読める歌詞は読めるけど、読めない歌詞はぜんぜん読めないな…って日々が続いたのです。
そんなころ出会ったのが

これです。ヤバいですこれ。
進撃の巨人』という漫画を取り上げ、統計的な手法でキャラの名前とかを当てていくっていう企画です。私は衝撃を受けました。
だって、中身を読まなくても内容がわかるんだよ! これすげえよ!
そんなわけで、2014年のお正月から私は統計学のお勉強を始めました。いまはそれからちょうど1年ぐらいにあたります。なので、ここまでのお勉強のマイルストーンとしてこの記事を書きました。
ほんとだったら、ちゃんとオリコンとボカロを特徴付ける変数を見出せないとカッコ悪いし、それを説明変数にして、ランダムな曲たちをばっさばさと分類できる機械学習ができるべきでした。発表されたばかりのジャニーズ楽曲大賞とかネットの音楽オタクが選んだ2014年の日本のアルバムとか(まだ全曲ないけど)AKB48リクエストアワーとかを織り込めるぐらいの機動力をもてるべきです。
でも今の私にはできませんでした。今後に期待してください。これからもがんばります。
また、ココが間違ってるよ、もっとこうしたほうがいいよ、というご意見ありましたらお知らせください。もっと楽しい記事が書けるようになりたいです。

参考文献としては、以下の本が挙げられます。

スクレイピングについての本です。今回私が作ったのはごく単純な例ですが、この本を読めばもっと複雑なパターンでも楽しく対応できるようになります。まだ最初の方しか読めてないけど、どんどん私の血や肉になっていく感じがするので、テーマも説明も素晴らしい本です☆
この本を片手に、Twitterテキストマイニングをやってみたのが下記の記事です。



形態素解析のパートではこの本にお世話になりました。さっきの本もそうですけど、具体的にどういう手順を踏むことによって、具体的な何ができるか、が、はっきり書いてあるので、もうめっちゃわかりやすいし大好きです。溺愛しています。

統計と検定については、いくつか本を読みましたが、今回一番長く手元に置いたのは、この本です。今回は統計のことを思い出しながら、それをRで実行していく必要があったので、その両方を教えてくれるこの本がとても役に立ちました。
なお、私がこのブログを書くのに当たって全般で手元に置いている本は、

ここでご紹介しています。よければご覧ください。
最後にオマケをつけときます。オリコン曲とボカロ曲の歌詞の中から、形容動詞の語感になると判別された名詞のランキングです。
オリコンボカロ
TermFreq
1好き13.904158
2幸せ4.526935
3元気3.880230
4大事3.556878
5大好き3.556878
6無理2.910173
7大切2.586820
8がむしゃら2.263468
9だめ2.263468
10確か2.263468
11ハッピー1.940115
12マジ1.940115
13不思議1.940115
14冷静1.940115
15孤独1.940115
16自由1.940115
17きれい1.616763
18ダメ1.616763
19ラッキー1.616763
20不安1.616763
TermFreq
1好き20.763374
2ダメ5.126759
3嫌い4.101407
4馬鹿4.101407
5ハッピー3.588731
6大好き3.332393
7不思議3.076055
8大事2.819718
92.819718
10大丈夫2.563380
11簡単2.307042
12綺麗2.307042
13孤独2.050704
14当たり前1.794366
15素敵1.794366
16だめ1.538028
17不安1.538028
18単純1.538028
19幸せ1.538028
20鮮やか1.538028
ベスト5の違いがヤバい…。

では、次回は、今までどおり、歌詞ひとつを取り上げてそれを読み込むタイプの記事を書きます☆ 2月5日ごろをお楽しみに!