カルチャーとデータサイエンス

ビジネスの意思決定は無理でも、せめてカルチャーはデータサイエンスしたい

今年ブログで書きたいテーマ:2020年

今更ですが新年あけましておめでとうございます。

2019年はInputも十分したし、記事なり音楽なりをOutputする準備くらいまではできた年でした。今年はよりOutputを加速させられればと考えています。

とは言いつつ、すぐに記事に書けるテーマが今手元にないので、今年ブログで書きたいテーマを備忘録的に整理したいと思います。

言語処理100本ノックの続き

ほそぼそと続けています。誰も興味がなくても一通り書ききるとこまではやりたい・・

TensorFlowでAI向井秀徳を作る

pira-nino.hatenablog.com

前から書いているエントリの元ネタは↑の記事なんですが、将来的には深層学習も向井秀徳ネタでやりたいと考えています。

決定木や回帰は2019年にあらかたやったのですが、深層学習はまだ手を付けていないので・・ 解散までに書ければいいなぐらいの温度感です。

Kaggleで便利な処理(関数)を切り出して紹介

最近仕事でもちょいちょいPython書いているのですが、Kaggleのコンペで覚えた前処理・精度評価のテクニックが役立つことが多いなと気づきました。わざわざどのコンペだったかなーとnotebookを見に行くのも面倒なので、備忘録的にこのブログにも書き出したいと考えてます。

そしてもちろんKaggleのコンペも良い結果出したいですね。。

アーティストのTwitterフォロワー数とyoutubeの再生回数の散布図をPlotする

もともとこのブログを始めた際に一番やりたかったテーマでした。

ゆるい相関を描くことができるのは予想通りなんですが、むしろ外れ値(どっちかの世界でカルト人気をもつアーティスト)にどんなのがあるかを見てみたいと考えています。

たしか去年WikipediaからバンドマンのTwitterアカウントを取得するスクリプトを書いたはずなので、それを思い出すところから始めます。

LiveCoding

Amazon CAPTCHA

去年こんな本を見つけ、Live Codingなる世界に少しだけ足を踏み入れました。

「作ったコードは必ずオーディエンスに公開しながら演奏する」文化、異質すぎてどの界隈とも健全に交わることはないだろうと思う反面、まだまだ黎明期であるこの領域から、今後世間一般に認知されるアーティストが生まれてほしいです。


DOMMUNE Tokyo - live coding performances - algorave tokyo x yorkshire

人間味がないとか思われそうな反面、結局人間が音楽を作るインターフェースがGUIDAW)からCUIに変わっただけであり、むしろ退化しているし言語で書くから人間の営みにより近づいているんじゃないかなとも思うんですよね。。

僕は僕でLofi Hip-Hopとの融合(あえて先端ぽいインターフェースでスカスカな音楽を作る)みたいなのに挑戦していたのですが、どうしても上手くいかず...気が向いたら作ったものを紹介したいです。

終わりに

はじめに書いた2020年のOutputする目標、今年は文章だけでなく音楽や映像も出せたらいいなと考えています。興味があればお付き合いください。

2020年もよろしくお願いします。

言語処理100本ノックをひたすら解く ~ No.10 ~ 19(UNIXコマンドの基礎)

はじめに

fyiwdwytm.hatenablog.com

↑の続きです。

当記事の目的

言語処理100本ノック 2015の問題10 ~ 19を実装します。

UNIXコマンドはほぼ素人なので、先人の知恵になるべく頼っていきます。。

参考元

回答の参考として、主に以下Qiitaの記事を参照しました。

qiita.com

qiita.com

準備

前回と同じく、実装はjupyter notebook上で行います。UNIXコマンドもnotebook上の記法(!を最初につけるマジックコマンド)で実装します。

また、入力ファイルとしてhightemp.txtを使用します。ipynbファイルと同じディレクトリにinputディレクトリを切り、その中から呼び出すこととします。同様に、出力を伴う問題のファイルはoutput配下に出力することとします。

hightemp.txt : 気象庁が公開している「歴代全国ランキング>観測史上の順位>最高気温の高い方から」を基に,手作業でタブ区切り形式に整形したものです.利用規約等はこちらのページを参照して下さい.- 言語処理100本ノック 2015

UNIXコマンドでわからないものはman <コマンド名>コマンドで逐一調べます。

実装

10. 行数のカウント

(問)行数をカウントせよ.確認にはwcコマンドを用いよ.

(解)python

def nlp010(filename):
    with open(filename) as f:
        result = len(f.readlines())
    return result

nlp010("./input/hightemp.txt")

(解)unix

!wc -l ./input/hightemp.txt

(注釈)

  • with open():ファイルを読み込みます。オプションはr: 読み込み、w:書き出し等ありますが、指定なしの場合は読み込みが設定されます。その他使い方はPythonでファイルの読み込み、書き込み(作成・追記) | note.nkmk.meを参照してください。
  • wcコマンド:指定したファイルの文字数をカウントします。-lオプションで文字ではなく行数をカウントします。コマンド名はword countの略でしょうかね。

11. タブをスペースに置換

(問)タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.

(解)python

def nlp011(filename):
    with open(filename) as f:
        for line in f:
            print(line.replace("\t", " "), end=" ")

nlp011("./input/hightemp.txt")

(解)unix

#sed
!sed 's/    / /g' ./input/hightemp.txt

#tr
!tr '\t' ' ' < ./input/hightemp.txt

#expand
!expand -t 1 ./input/hightemp.txt

(注釈)

  • replace(): 第一引数に置換前文字列、第二引数に置換後文字列を指定して文字を置き換えます。ここで指定した\tはタブのエスケープ文字です。
  • sedコマンド: 基本的な使い方はsed 's/<置換前文字列>/<置換後文字列/g>' <ファイルPATH>でファイルに含まれる文字を置換します。他の使い方はsedコマンドで覚えておきたい使い方12個(+3個) | 俺的備忘録 〜なんかいろいろ〜が大変参考になりました。
  • macではUNIXにてタブのエスケープ文字\tに対応していないようで、jupyter notebook上では変換に失敗してしまいます... ターミナルでの実行時はctrl + V入力後にタブキーを入力することでタブを挿入することができます。jupyter notebok上での解放が分かるかた、アドバイスお待ちしています..
  • tr コマンド: tr '置換前文字列' '置換後文字列'で文字を置き換えます。<はパイプラインで、ここではtxtファイルをtr`コマンドの標準入力に入れていることを指します。
  • expand コマンド: タブを空白に変換するコマンドです。 -t <数値>オプションで、タブを指定した数値分の空白(1で半角)に置き換えます。

12. 1列目をcol1.txtに,2列目をcol2.txtに保存

(問)各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.

(解)python

# python
def nlp012(input: str, col: int) -> str:
    with open(input, "r") as f:
        data = list(l.split('\t') for l in f)
    
    with open("./output/nlp012/col{}_p.txt".format(str(col+1)), "w") as f:
        for i in data:
            f.write(i[col] + '\n')

nlp012("./input/hightemp.txt", 0)
nlp012("./input/hightemp.txt", 1)

(解)unix

!cut -f 1 ./input/hightemp.txt > ./output/nlp012/col1_u.txt
!cut -f 2 ./input/hightemp.txt > ./output/nlp012/col2_u.txt

(注釈)

  • pythonの方は、入力ファイル名と列番号を指定して、該当列のファイルを作成する関数を実装しました。
  • cutコマンド: テキストファイルを横方向に分割するコマンドです。-f <数値>オプションによって、区切り文字に分割された列の何番目の項目を取得するかを設定します。区切り文字は-dオプションで指定できますが、デフォルトは空白かタブ?のようです。

13. col1.txtとcol2.txtをマージ

(問)12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.

(解)python

def nlp013(input1:str, input2:str) -> str:
        with open(input1, "r") as f1:
            data1 = f1.readlines()
    
        with open(input2, "r") as f2:
            data2 = f2.readlines()
            
        result = [i.strip()+"\t"+j.strip()+"\n" for i, j in zip(data1, data2)]
        
        with open('./output/nlp013/col12_p.txt', 'w') as f:
            for i in result:
                f.write(i)
            
nlp013("./output/nlp012/col1_p.txt", "./output/nlp012/col2_p.txt")

(解)unix

!paste ./output/nlp012/col1_u.txt ./output/nlp012/col2_u.txt > ./output/nlp013/col12_u.txt

(注釈)

  • pythonのコードでは2つのファイルを引数に入れる関数を実装しました。col1.txtcol2.txtを内包表記で結合し、resultに格納した後ファイルを書き込みます。
  • pasteコマンドは2つのファイルを列方向に結合します。リダイレクトしてcol12_u.txtファイルに出力しています。

14. 先頭からN行を出力

(問)自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.

(解)python

def nlp014(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        for i, line in enumerate(f):
            if i >= n: 
                break
            print(line.strip())

nlp014("./input/hightemp.txt")

(解)unix

!head -n 5 ./input/hightemp.txt

(注釈)

  • 今回Jupyter Notebookにて実装しているため、自然数Nをコマンドライン引数ではなく標準入力で受け取るようにしています。input()で受け取ることが可能です。
  • メモリ節約のため、一行ずつ読み込み・処理する形式(readlines()などを使用しない)をとっています。
  • headコマンドは-nオプションで指定した行数分だけファイルを出力します。

15. 末尾のN行を出力

(問)自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.

(解)python

def nlp015(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        lines = f.readlines()
        
        for line in lines[-n:]:
            print(line.strip())

nlp015("./input/hightemp.txt")

(解)unix

!tail -n 5 ./input/hightemp.txt

(注釈)

    1. とは異なり、一度全てのファイルをreadlines()で読み込む実装方針をとっています。
  • tailコマンドはheadコマンドの対となるコマンドです。

16. ファイルをN分割する

(問)自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.

(解)python

def nlp016(filename: str):
    n = int(input("自然数nを指定"))
    with open(filename) as f:
        lines = f.readlines()
        
    line = math.ceil(len(lines) / n)
    outputs = [lines[idx:idx+line] for idx in range(0,len(lines), line)]
    
    for i, output in enumerate(outputs):
            with open('./output/nlp016/output{}_p.txt'.format(i), 'w') as f:
                for j in output:
                    f.write(j)
            
    
nlp016("./input/hightemp.txt")

(解)unix

!split -l $(expr $(wc -l ./input/hightemp.txt | awk '{print $1}') / 5) ./input/hightemp.txt ./output/nlp016/output

(注釈)

  • pythonのコードでは、まず受け取った自然数nを1ファイルあたりの行数linesに変換しています。math.ceil関数で小数点以下は切り上げます。
  • outputsの内包表記の処理が少しややこしいかもしれません。nで指定した行数分でグルーピングして2次元のリストを作成しています。下記の簡単な実装例(26字の英文字を6文字置きに区切る)も参考にしてみてください。
# a~z26字のリストを6文字おきに区切る
list = [chr(i) for i in range(97, 97+26)] #a~zの文字列を生成
output = [list[i:i+6] for i in range(0, 26, 6)] #range(0, 26, 6): 0~26の範囲で6個置きに数値を出力

print(output)
# [['a', 'b', 'c', 'd', 'e', 'f'], ['g', 'h', 'i', 'j', 'k', 'l'], ['m', 'n', 'o', 'p', 'q', 'r'], ['s', 't', 'u', 'v', 'w', 'x'], ['y', 'z']]を出力
  • unixコマンドはかなり複雑になっています。splitコマンドのオプション-lは分割数ではなく1ファイルあたりの行数を入力するため、その計算が必要です。まずinputファイルの行数をwcコマンドでカウントします。出力は24 hightemp.txtのように行数とファイル名が同時に表示されてしまうので、awkコマンドで結果をパースし行数のみを取得します。最後に、式評価を行うexprコマンドで行数を自然数で分割し、1ファイルあたりの行数を取得します。
  • splitコマンド、macでは連番ファイル名を指定できず、○○a, ○○b, ...という名称で出力されます。

17. 1列目の文字列の異なり

(問)1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.

(解)python

def nlp017(filename: str):
    with open("./input/hightemp.txt") as f:
        data = list(l.split('\t') for l in f)
        result = set()
        for i in data:
            result.add(i[0])
            
    return result
            
    
nlp017("./input/hightemp.txt")

(解)unix

!cut -f 1 ./input/hightemp.txt | sort| uniq 

(注釈)

  • pythonのコードでは、まずファイルを行列区切りの2次元リストに格納し、次にリストの1列目のみをset型に格納しています。set型は06. 集合でも使用しました。
  • unixコマンドでは、3つのコマンドを使用し、それぞれの出力をパイプラインで他のコマンドにわたすように実装しています。cutコマンドではファイルの一列目のみを取得し、sortコマンドで並び替えた後、uniqコマンドで重複を削除します。uniqコマンドを使用するには予め並び替えが必要になります。

18. 各行を3コラム目の数値の降順にソート

(問)各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい)

(解)python

def nlp018(filename: str):
    import sys
    with open("./input/hightemp.txt") as f:
        data = f.readlines()
        
        for line in sorted(l , key=lambda l: l.split('\t')[2], reverse=True):
            print(line.strip())
    
nlp018("./input/hightemp.txt")

(解)unix

!sort -k 3 -r ./input/hightemp.txt 

(注釈)

  • sorted(): 引数にリストを指定し、ソートされたリストを返却します。引数keyではソートする対象を指定しますが、ここではラムダ式(無名関数)を使用しています。リストを渡して3列目を返却する無名関数をキーに指定しています。
  • sortコマンドは-kオプションで何列目をキーにするかを指定できます。-rオプションは降順で返却することを指定します。

19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる

(問)各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.

(解)python

def nlp019(filename: str):
    result = {}
    import sys
    with open("./input/hightemp.txt") as f:
        data = list(l.split('\t') for l in f)
        
        for line in data:
            result[line[0]] = result.get(line[0], 0) + 1
    
        for key, value in sorted(result.items(), key=lambda kv: kv[1], reverse=True):
            print("{} {}".format(value, key))
    
nlp019("./input/hightemp.txt")

(解)unix

!cut -f 1 ./input/hightemp.txt | sort | uniq -c | sort -k 1 -r

(注釈)

  • pythonのコードは18. と似ていますが、途中の結果をリストではなく辞書に格納しています。辞書resultにはkeyに一列目の値、valueに出現回数を格納しています
  • uniqコマンドは、-cオプションをつけることで重複を除いた文字列だけでなく、各文字列の出現頻度を出力することが可能です。

Todo

  • 続き(No.20~以降)

おわりに

UNIXだけではなく、pythonの記法(内包表記、lamdaなど)のよい復習になりました。

次回に続きます。

言語処理100本ノックをひたすら解く ~ No.00 ~ 09(準備運動)

はじめに

pira-nino.hatenablog.com

上記記事みたいなことをやりたいのですが、そもそも自然言語の前処理にそれほど明るくないということで・・まずは修行したいと思います。

当記事の目的

言語処理100本ノック 2015の問題00 ~ 09を実装します。いろいろ忘れているPythonの文法等もまとめて復習します。

参考元

回答の参考として、主に以下Qiitaの記事を参照しました。

qiita.com

準備

実装は全てPythonで行います。

方針としては、pyファイルではなくjupyter notebook上で、関数やクラス形式で統一して実装することとします。

実装

00. 文字列の逆順

(問)文字列"stressed"の文字を逆に(末尾から先頭に向かって)並べた文字列を得よ.

(解)

def nlp000(s: str):
    return s[::-1]

nlp000("stressed") 
# 'desserts'

(注釈)

  • リストのスライスを使用します。[i:j:n]でi文字目からj文字目をn個ごとに取得できます。
  • i, jを指定しなければ全ての文字を取得します。
  • nを負の値で指定すれば、逆順に文字列を取得できます。-1で1つずつ逆順で取得します。

01. 「パタトクカシーー」

(問)「パタトクカシーー」という文字列の1,3,5,7文字目を取り出して連結した文字列を得よ.

(解)

def nlp001(s: str) -> str:
    return s[::2]

nlp001("パタトクカシーー")

(注釈)

  • 01と同じくスライスを使用します。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

(問)「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.

(解)

def nlp002(s1: str, s2: str) -> str:
    result = "".join([i + j for i, j in zip("パトカー", "タクシー")])
    return result

nlp002("パトカー", "タクシー")

(注釈)

  • zip(): 複数のイテラブルなオブジェクト(文字列、リストなど)をまとめる際に使用します。for文のループ等に便利です。

03. 円周率

(問) "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."という文を単語に分解し,各単語の(アルファベットの)文字数を先頭から出現順に並べたリストを作成せよ

(解)

# 自分の答え
def nlp003(s: str) -> str:
    list = s.split()
    result = [len(i) for i in list]
    return result

nlp003("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
# [3, 1, 4, 1, 6, 9, 2, 7, 5, 3, 5, 8, 9, 7, 10]


# 模範解答
def nlp003a(s: str) -> str:
    import re
    return list(len(m) for m in re.findall('\w+', s))

nlp003a("Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics.")
# [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

(注釈)

  • リスト内包表記で書きます。この場合の構文は[式 for 変数 in イテラブル]となります。
  • 内包表記はコードがすっきりするし、処理も速いそうなので、積極的に使用します。(参考→Pythonのリスト内包表記の書き方 | HEADBOOST
  • 自分の答えと模範解答(「参照」項より」)を記載しています。自分の答えでは記号(ピリオドなど)の処理が上手くできていないのが分かります。模範解答では正規表現を用いていますね。正規表現は今後の問題にも含まれるので、そのときに注釈を加えます。

04. 元素記号

(問) "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

(解)

def nlp004(s: str) -> str:
    list = s.split()
    first_char = [1, 5, 6, 7, 8, 9, 15, 16, 19]
    result = {}
    for i, word in enumerate(list, 1):
        if i in first_char:
            result[word[0]] = i
        else:
            result[word[0:2]] = i
    return result
    
nlp004("Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.")
# {'H': 1, 'He': 2, 'Li': 3, 'Be': 4, 'B': 5, 'C': 6, 'N': 7, 'O': 8, 'F': 9, 'Ne': 10, 'Na': 11, 'Mi': 12, 'Al': 13, 'Si': 14, 'P': 15, 'S': 16, 'Cl': 17, 'Ar': 18, 'K': 19, 'Ca': 20}

(注釈)

  • 水兵リーベは海外だとこんな覚え方するんですね。
  • 辞書型(マップ型)は、{key: value}の組み合わせで要素を格納するオブジェクトです。pythonだと辞書型という呼び名が一般的で、連想配列とかmapという言い方は他の言語でよく用いられているような印象です。
  • enumerate(リテラブル, n)は、for文で要素とそのインデックス番号を同時に取得したい場合に用います。nに任意の数値を指定することで、インデックスの開始位置を指定できます。通常のリストは最初の要素が0番目となるので、ここでは1番目に振り直すよう定義しています。

05. n-gram

(問)与えられたシーケンス(文字列やリストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

(解)

# 自分の答え
def nlp005(s: str, n: int) -> str:
    result = [s[idx:idx + n] for idx in range(len(s) - n + 1)]
    return result

#文字n-gram
print(nlp005("I am an NLPer", 1))
print(nlp005("I am an NLPer", 2))

# ['I', ' ', 'a', 'm', ' ', 'a', 'n', ' ', 'N', 'L', 'P', 'e', 'r']
# ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']

#単語n-gram
words = "I am an NLPer".split(' ')
print(nlp005(words, 1))
print(nlp005(words, 2))

# [['I'], ['am'], ['an'], ['NLPer']]
# [['I', 'am'], ['am', 'an'], ['an', 'NLPer']]

(注釈)

  • n-gram: 任意の文字列や文書を連続したn個の文字で分割するテキスト分割方法のことです。特に、nが1の場合をユニグラム(uni-gram)、2の場合をバイグラム(bi-gram)、3の場合をトライグラム(tri-gram)と呼びます。(参考→Nグラム(えぬぐらむ)とは - コトバンク
  • 文字n-gramは文字単位、単語n-gramは単語単位でテキストを分割します。英語の場合は、単語単位でのテキスト分割は半角スペースで分割します。

06. 集合

(問) "paraparaparadise"と"paragraph"に含まれる文字bi-gramの集合を,それぞれ, XとYとして求め,XとYの和集合,積集合,差集合を求めよ.さらに,'se'というbi-gramがXおよびYに含まれるかどうかを調べよ.

(解)

class Nlp006:
    def __init__(self, n):
        self.n = n
        self.s1_gram_ = None
        self.s2_gram_ = None
    
    def fit(self, s1: str, s2: str) -> str:
        self.s1_gram_ = {s1[idx:idx + self.n] for idx in range(len(s1) - self.n + 1)}
        self.s2_gram_ = {s2[idx:idx + self.n] for idx in range(len(s2) - self.n + 1)}
        
    def predict(self, type):
        if type == "union":
            return self.s1_gram_ | self.s2_gram_
        elif type == "intersection":
            return self.s1_gram_ & self.s2_gram_
        elif type == "difference":
            return self.s1_gram_ - self.s2_gram_

nlp006 = Nlp006(n=2)
nlp006.fit("paraparaparadise", "paragraph")
print(nlp006.predict(type="union"))
# {'ad', 'ra', 'gr', 'ap', 'pa', 'ph', 'is', 'ar', 'ag', 'di', 'se'}

print(nlp006.predict(type="intersection"))
# {'pa', 'ap', 'ar', 'ra'}

print(nlp006.predict(type="difference"))
# {'ad', 'is', 'se', 'di'}

(注釈)

  • 機械学習チックに、fit関数とpredict関数を持つクラスを作成しました。
  • 集合型(セット型)は辞書型と同じく{}で定義されるオブジェクトですが、重複要素を持たない、順番を持たないという特徴があります。そのため和集合や積集合の演算に便利です。

07. テンプレートによる文生成

(問)引数x, y, zを受け取り「x時のyはz」という文字列を返す関数を実装せよ.さらに,x=12, y="気温", z=22.4として,実行結果を確認せよ.

(解)

def nlp007(x:str, y:str, z:str) -> str:
    return "{}時の{}は{}".format(x, y, z)

nlp007(x=12, y="気温", z=22.4)
# '12時の気温は22.4'

(注釈)

  • format():文字列変換に使用します。文字列型(str)が持つメソッドとしても機能します。{}は置換フィールドと呼ばれ、format()で指定する引数に置き換えられます。

08. 暗号文

(問)与えられた文字列の各文字を,以下の仕様で変換する関数cipherを実装せよ.

  • 英小文字ならば(219 - 文字コード)の文字に置換
  • その他の文字はそのまま出力

この関数を用い,英語のメッセージを暗号化・復号化せよ.

(解)

def nlp008(s: str) -> str:
    result = "".join([chr(219 - ord(i)) if "a" <= i <= "z" else i for i in s ])
    return result

print(nlp008("The rain in Spain stays mainly in the plain."))
# 暗号: 'Tsv izrm rm Skzrm hgzbh nzrmob rm gsv kozrm.'

print(nlp008(nlp008("The rain in Spain stays mainly in the plain.")))
# 復号: The rain in Spain stays mainly in the plain.

(注釈)

  • ord(): 文字を文字コードに変換する関数です。
  • chr(): 文字コードを文字に変換する関数です。
  • この暗号化方法は、同じ暗号化をもう一度繰り返すことで復号が可能です。

09. Typoglycemia

(問) スペースで区切られた単語列に対して,各単語の先頭と末尾の文字は残し,それ以外の文字の順序をランダムに並び替えるプログラムを作成せよ.ただし,長さが4以下の単語は並び替えないこととする.適当な英語の文(例えば"I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")を与え,その実行結果を確認せよ.

(解)

def nlp009(s: str) -> str:
    from random import sample
    list = s.split()
    result_list = []
    for i in list:
        if len(i) <= 4:
            result_list.append(i)
        else:
            result_list.append(i[0] + "".join(sample(i[1:-1], len(i) - 2)) + i[-1])
    result = " ".join(result_list)
    return result

nlp009("I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind .")
# 例: "I cdol'unt bliveee that I cuold atllcuay unestradnd what I was riadneg : the poanehmnel pewor of the human mind ."

(注釈)

  • 「この ぶんょしう は いりぎす の ケブンッリジ だがいく の けゅきんう の けっか... 」という有名な日本語のネタがありますね。
  • random.sample(): ランダムに複数の要素を重複なしで取得します。第一引数にリスト、第二引数に取得したい要素の数を指定します。

Todo

  • 言うまでもなく続き(No.10~以降)
  • 内包表記、文字コードの扱いはどこかで復習する

おわりに

もともと歌詞分析とか音楽周りのことを書くブログの予定だったけどネタがない・・ 気が向いたら更新します。

向井秀徳は結局「諸行無常」しか言ってないんじゃないか?をデータサイエンス ~自然言語処理編~

はじめに

ちょうど記事書いてたら逆噴射バンドの東京公演当たった・・

f:id:fyiwdwytm:20191013152328p:plain

この画像の生成を行うところまでを頑張ります。

当記事の目的

向井秀徳が結局「諸行無常」しか言っていない気がしていたのでこの際はっきりさせようと思います。

本テーマは2部構成でお送りします。テーマが思いついたらまた記事を足します。

  1. 歌詞取得編 →
  2. 自然言語処理編 → この記事

この記事は自然言語処理編です。前の記事で作った歌詞データのcsvを読み込んで、単語の分解や、どの単語を最も多く使用しているか?を分析したいと思います。

参考元

前の記事と同様に、以下のサイトを参考にさせていただきました。

qiita.com

加えて、上記Qiitaの元記事?が以下の記事に当たるようで、後半部分は一部参考にしています。

pira-nino.hatenablog.com

また、自然言語処理に関する言葉の定義は以下サイトから引用しています。

www.nltk.org

準備

実装方法の定義

言語は前の記事と同じくPythonを使用します。

加えて形態素解析としてMeCabを使用します。形態素解析の意味は下記を参照してください。

形態素とは意味を持つ最小の言語単位であり、形態素解析とは与えられた文を形態素単位に区切り、各形態素に品詞などの情報を付与する処理である。形態素の定義はもともと欧米語の言語学から来たものであるため、まちまちであり確立されていないが、日本語形態素解析の立場から言えば、辞書に記載されている見出し語を形態素と見なし、その単位に分割すると同時に、見出し語にひもづく品詞や標準形などを形態素に付与する処理であると言える。したがって、必要以上に単語や形態素の区別に気をつける必要はない。 www.nltk.org

英語であれば形態素の区切りとして" "(空白)が使用されますが、日本語の場合はそのような分かち書きがなされない言語です。更に同じ単語でも位置や句読点の違いから意味が異なる場合があります。

Mecabはその単語を区切るための処理エンジンと、区切る際に参照する辞書を一緒に提供する機能、と考えてください。

環境構築

形態素解析器のインストール

筆者は分析前から既にMecabの環境構築を済ませていましたが、これから環境構築を行う場合は以下を参考にMecabやその他必要なパッケージをインストールします。

fresopiya.com (2019年時点でおそらく最新の手順書?)

PythonからMecabを使用する場合、Pythonバインディングを行うためmecab-python3のインストールが必要となります。

またWeb用語等の新しい言葉を使用する場合には、辞書mecab-ipadic-NEologdも合わせてインストールしておくと便利です。

■Matplotlib, Seabornの日本語化

qiita.com

歌詞をSeabornの棒グラフで表示する際に、グラフ描画のパッケージであるMatplotlib及びSeabornを使用します。

一方これらのパッケージはそのまま日本語を載せると文字化けしてしまうので、上記URLを参考に日本語対応できるようにします。

(MatplotlibおよびSeabornのインストールがまだの人はpip installconda installしましょう!Anacondaを使っている人はすでにインストールされているはずです。)

実装

コードを実装していきます。前記事と同じくJupyter Notebookにて実装します。

最初の手順でMecabの機能をサンプル文で検証し、その後に前回作成した歌詞データcsvを読み込ませて検証します。

形態素解析を行った後は歌詞に出てくる単語の頻度をランキング化し、棒グラフやWordCloud(後述)で描画・分析します。

1. 形態素解析器の検証

はじめにMecabの使い方もかねて、試し書きのコードを実装します。

import MeCab

# 1. 形態素解析器をインスタンス化し、parseToNodeメソッドでオブジェクトに処理結果を格納
text = "繰り返される諸行無常 よみがえる性的衝動"
mecabTagger = MeCab.Tagger("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/") # 辞書:mecab-ipadic-neologdを使用
node = mecabTagger.parseToNode(text)

#2. 表層形(surface)と素性(feature)を格納するデータフレームを作成
surface_and_feature = pd.DataFrame()
surface = []
feature = []

#3. nodeオブジェクトのアトリビュートから表層形、素性を抽出
while node:
    surface.append(node.surface)
    feature.append(node.feature)
    node = node.next
    
surface_and_feature['surface'] = surface
surface_and_feature['feature'] = feature

surface_and_feature

f:id:fyiwdwytm:20191013151517p:plain

形態素解析器をmecabTaggerという形でインスタンス化します。その後、形態素解析を行うメソッドparseToNode()を実行し、結果をnodeオブジェクトに格納しています。

形態素解析の結果、nodeオブジェクトには2つのアトリビュートが格納されます。

  1. 表層形(surface): 単語そのもの。文中において文字列として出現する形式
  2. 素性(feature): 単語の情報のリスト

なおfeatureという単語について、機械学習界隈では「特徴量」と訳すことが多いですが、自然言語処理では「素性(そせい)」ということが多いそうです。

次に、各単語の素性には複数の情報がリストで格納されているので、更に確認します。

text = "繰り返される諸行無常 よみがえる性的衝動"
mecabTagger = MeCab.Tagger("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/")
node = mecabTagger.parseToNode(text)

# 素性(feature)のリストの中身(品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音)をデータフレームに格納
features = pd.DataFrame(columns=["pos","pos1","pos2","pos3","ctype","cform","base","read","pronounce"])
posses = pd.DataFrame
list = []
while node:
    tmp = pd.Series(node.feature.split(','), index=features.columns)
    features = features.append(tmp, ignore_index=True)
    node = node.next

辞書mecab-ipadic-neologdを使用する場合、素性(feature)の中身には品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音の8つがリストとして格納されます。

ちなみにデータフレームの先頭と末尾にあった「BOS/EOS」ですが、はnodeの先頭と末尾をそのまま表す値だそうです(要確認)。

Mecabの使い方を確認したところで、本番の処理を記載していきます。

2. パッケージインポート

必要なパッケージをインポートします。

import MeCab #自然言語処理
import numpy as np #行列演算
import matplotlib.pyplot as plt #グラフ描画
import pandas as pd #データフレーム操作
import re #正規表現
import seaborn as sns #グラフ描画
from PIL import Image #WordCloud使用で必要な依存パッケージ
from wordcloud import WordCloud #WordCloudを描画
%matplotlib inline 

WordCloudとはこの記事の冒頭に載せた画像のような、文字をマッピングした画像を生成するパッケージです。

3. 関数(形態素解析用)の定義

# 関数① get_word_list
def get_word_list(lyric_list):
    m = MeCab.Tagger ("-Ochasen -d /usr/local/lib/mecab/dic/mecab-ipadic-neologd/")
    lines = []
    for text in lyric_list:
        keitaiso = []
        m.parse('')
        node = m.parseToNode(re.sub('\u3000',' ',text)) #補足①
        while node:
            #辞書に形態素を入れていく
            tmp = {}
            tmp['surface'] = node.surface
            tmp['base'] = node.feature.split(',')[-3] #原形(base)
            tmp['pos'] = node.feature.split(',')[0] #品詞(pos)
            tmp['pos1'] = node.feature.split(',')[1] #品詞再分類(pos1)
            
            #文頭、文末を表すBOS/EOSは省く
            if 'BOS/EOS' not in tmp['pos']:
                keitaiso.append(tmp)
                
            node = node.next
        lines.append(keitaiso)
    
    #名詞・動詞・形容詞の場合は原形、それ以外の品詞で活用が存在しない場合は表層形をリストに格納する
    word_list = [] 
    for line in lines:
        for keitaiso in line:
            if (keitaiso['pos'] == '名詞') |\
                (keitaiso['pos'] == '動詞') |\
                (keitaiso['pos'] == '形容詞') :
                if not keitaiso['base'] == '*' :
                    word_list.append(keitaiso['base'])
                else: 
                    word_list.append(keitaiso['surface'])

    return word_list

関数① get_word_list - 複数の曲の歌詞のリストを受け取って、単語のリストに変換します。 - 動詞「笑う」という単語をとっても、「笑わない」「笑いたい」「笑う」など複数の活用形が考えられます。これらを単語に切ったとき、「笑わ」「笑い」「笑う」は全て「笑う」と扱うのが望ましいです。なので、単語を切った後、原形(動詞であれば終止形)があるものは全て原形をリストに格納します。

補足① メソッドre.sub - re.sub(正規表現パターン,置換先文字列,処理対象文字列)で使用し、正規表現で指定した文字列を別の文字列に置換します。 - \u3000はunicodeで全角スペースを表します。歌詞の取得時に全角スペースがエンコードされて残っているのを取り除く処理、ということです。

4. 歌詞の形態素解析・データフレーム化

# csvインポート
mukai_lyrics = pd.read_csv("mukai_lyrics.csv")

# 関数get_word_listによる文字列リストword_listの生成
word_list = get_word_list(mukai_lyrics.Lyric.tolist())

# word_listから、単語とその出現頻度をマッピングしたデータフレームwords_dfの生成
word_freq = pd.Series(word_list).value_counts() #pandasのSeriesに変換してvalue_counts()
words_df = pd.DataFrame({'noun' : word_freq.index,
             'noun_count' : word_freq.tolist()})

words_df.head(10)

f:id:fyiwdwytm:20191013154159p:plain

前回の記事で作成した歌詞データのcsvから歌詞のカラムLyricsを指定し、先程作成した関数を通してリストword_listを生成します。

word_listには各歌詞に含まれる単語が1次元のリストとして格納されています。重複された単語をカウントするために、単語と出現回数をマッピングしたデータフレームwords_dfを生成します。

出力結果を見ると、既に一番使用している単語は「俺」であるという身も蓋もない結果が出ていますが、「する」「いる」「れる」など、単語自体には歌詞の意味が入っていないであろう単語が気になります。

次の項にてこれらの単語をストップワード(stop words)として除外します。

5. 特定の単語(stop words)の除外

stop_words = ['いる','する','れる','てる','なる','られる','よう','の','いく','ん','せる','いい','ない','ある','しまう','さ']
droplist = []
for i in stop_words:
    droplist.append(words_df[words_df.noun == i].index[0])

words_df = words_df.drop(droplist).reset_index(drop = True)

words_df.head(10)

ストップワードは記事冒頭で参考リンクにあげている記事で使用されたものをそのまま使用しました。

f:id:fyiwdwytm:20191013154837p:plain

笑う・狂うが動詞で最も頻度が多い、というのは納得の結果ではないでしょうか。

6. 検証①棒グラフ

words_df_50 =words_df[0:50]

f, ax = plt.subplots(figsize=(18, 6))
sns.set(font="IPAexGothic")
sns.barplot(x=words_df_50['noun'], y=words_df_50['noun_count'])
plt.xticks(rotation='90')
plt.xlabel('単語リスト', fontsize=15)
plt.ylabel('出現頻度', fontsize=15)
plt.title('向井秀徳の歌詞に含まれる単語の出現回数', fontsize=15);

f:id:fyiwdwytm:20191013160218p:plain

ランキング1~50位をグラフ化しました。

記事タイトルにもあった「諸行無常」は20位ぐらいに落ち着いています。

それ以外の考察は8. 考察の項でまとめて実施します。

7. 検証②WordCloud

# WordCloudを描画する関数の実装
def draw_wordcloud(df,col_name_noun,col_name_quant,fig_title):
    word_freq_dict = {}
    for i, v in df.iterrows(): #単語とその頻度をデータフレームから辞書化
        word_freq_dict[v[col_name_noun]] = v[col_name_quant]
    fpath = "/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc"
    
    # WordCloudをインスタンス化
    wordcloud = WordCloud(background_color='white',
                        font_path = fpath,
                          min_font_size=15,
                         max_font_size=200,
                         width=1000,
                         height=1000
                         )
    wordcloud.generate_from_frequencies(word_freq_dict) #補足①
    plt.figure(figsize=[20,20])
    plt.imshow(wordcloud,interpolation='bilinear')
    plt.axis("off")
    plt.title(fig_title,fontsize=25)
    
draw_wordcloud(words_df, 'noun','noun_count','向井秀徳Word Cloud')

f:id:fyiwdwytm:20191013152328p:plain

補足① メソッドwordcloud.generate_from_frequencies()

  • 関数の中ではインポートしたクラスWordCloudインスタンス化し、その後メソッドgenerate_from_frequenciesを呼び出しています。
  • メソッドgenerate_from_frequenciesはkey: 単語名、value: 出現回数となる辞書word_freq_dictを引数とし、出現回数が大きくなるほど文字が大きくなるWordCloud画像を生成します。
  • 生成した画像はplt.imshowにてプロットします。

WordCouldから分かる情報は「なんとなくこの単語はいっぱい登場している」ぐらいのものですが、より直感的でわかりやすいですね。

8. 考察

棒グラフやデータフレームから、改めて分かる情報を考察します。

f:id:fyiwdwytm:20191013160218p:plain

前提条件として、あくまで歌ネットから取得した歌詞ベースで取得しているので、繰り返しの省略などが入っているなど、実際に歌っている回数や他の歌詞サイトと差異が発生している可能性があります。あくまで歌ネットベースでの考察とご理解ください。

もうちょっと聴き込んでいればマシな考察ができるんでしょうけど。。文脈まで分かる方は是非ご自身でコードを実装して推察してみてください。

1. 一番使用している動詞は「笑う」

  • 「俺」の次に登場回数が多く、合計75回です。
  • もっと物騒な単語が多いのかと思えば、全体を見ると意外と朗らかな歌詞を歌っているのかもしれないですね。3位は「狂う」ですけど

2. 「街」「都市」など、大きめの市町村をテーマにしている歌詞が何曲かある

  • 「街」が同率3位(44回)、「都市」が7位(35回)です。福岡の久留米市か、もしくは東京にインスパイアされているのでしょうか。
  • 向井秀徳といえば「冷凍都市」という言葉のイメージが強いですが、「冷凍」だけで見ると18位(24回)なので、「冷凍都市」のセットよりも「都市」だけで多く使用していることが分かります。

3. 「性的」より「衝動」の方が出現回数が多い

  • 「よみがえる性的衝動」という言葉を分解すると、よみがえる=40位(17回)、性的=21位(23回)、衝動=11位(29回)です。
  • 「性的衝動」という言葉はほぼセットで使用されていることが読み取れますが、それよりも多い頻度で「衝動」という言葉だけで使われているときがあるようです。
  • そりゃ「性的」だけ出していたら歌詞的に浮くでしょうけど...「衝動」とセットにして口馴染みのある言葉にする向井秀徳のワードセンスはすごいですね。

4. 「パンツ」だけで16回言っている

  • 「サイボーグのオバケ」で15回言っているはずなので、後1回はどこに...
  • ちなみに「下着」は114位(10)回です。これも「黒い下着」がほぼ占めていると考えられます。


サイボーグのオバケ

Todo

  • sns.barplotへのデータラベル追加(何件あったかの数値をグラフ上に表示する。ax.annotateを使おうとしたがどうやら軸名に日本語を使っているせいでエラーになる・・?
  • sklearn.TfidfVectorizerによる単語のベクトル化。単語の出現頻度をTF-IDFというロジックによって頻度・希少性を元にベクトル化が可能です。単語同士による組み合わせ(「冷凍」と「都市」)のクラスタリングや、機械学習と組み合わせれば「向井秀徳っぽい文章」のモデル化とその判定器を作ることができるかもしれません。
  • LDAおよびWord2Vecによる単語のベクトル化、及びクラスタリング

ailaby.com

おわりに

この記事も本来は前処理程度のことしかやっておらず、本来であればTodoの通りTF-IDFやWord2Vecを用いた単語のベクトル化まで行うのが自然言語処理・・と言いたいところです。が、まだ学習が間に合っておらず一旦ここまでとします。

先人の方々が既にこのあたり実装されているようなので、参考にさせていただきつつ実装を進めようと思います。

pira-nino.hatenablog.com

おまけ

f:id:fyiwdwytm:20191013184222p:plain


Number Girl - Num-Ami-Dabutz

向井秀徳は結局「諸行無常」しか言ってないんじゃないか?をデータサイエンス ~歌詞取得編~

はじめに

f:id:fyiwdwytm:20191010082624p:plain
逆噴射バンド
逆噴射バンドの抽選、当たるといいですね。

当記事の目的

向井秀徳が結局「諸行無常」しか言っていない気がしていたのでこの際はっきりさせようと思います。

本テーマは2部構成でお送りします。テーマが思いついたらまた記事を足します。

  1. 歌詞取得編 → この記事
  2. 自然言語処理編 →

この記事は歌詞取得編、言わば事前準備です。歌詞の文字をカウントしたりする前に、まず歌詞サイトから向井秀徳が書いた曲の歌詞を全部取得するコードを実装したいと思います。

参考元

本記事は、歌詞の取得先からその取得方法、分析の方法の何から何まで、以下の記事を参考にさせていただきました。

qiita.com

初めてスクレイピングをする人や、ちょっとpandasの扱い方が怪しい方向けにも、できるだけ中のコードの動きや参考にしたサイトを説明しながら書いていこうと思います。

準備

一旦コードを書く前に、スクレイピング対象やその方法をざっくり確認していきます。

1. 取得サイトの確認

www.uta-net.com

歌ネットを使います。普通の用途としてはあまり使ったことはないですね、昔は自動でitunesに歌詞入れるソフトとかあったな・・

https://www.uta-net.com/artist/7546/みたいな感じでパスパラメータにアーティストのIDを含んでいるので、ちょっと取得が楽そうです。他のサイトも同じかな?

2. 取得アーティストの確認

ご存知、「NUMBER GIRL」並びに「ZAZEN BOYS」にて向井秀徳が作詞した曲を取得対象とします。

曲提供では、最近だと椎名林檎の「神様、仏様」とか、ちょっと昔だとhalの「6階の少女」とかあるけど、取得が面倒なので今回は対象外です。

ちなみに6階の少女、アルバム「ブルー」に入っているverは演奏が向井+54-71でめちゃくちゃかっこいいので是非聞いてほしいです。たしかサブスクで聴けるはずです。

3. 取得方法の定義

言語はPythonを使用、スクレイピングのコアな手法としてはBeautiful Soupを使います。Beautiful Soupは、htmlのような非構造化データを行列を持つ構造化データに変換する(=スクレイピング)ことに特化したライブラリです。

そもそもスクレイピングって?という方は以下の記事をご確認ください。 qiita.com

実装

コードを実装します。なお、実装するコードはすべてJupyter Notebookで書くことを前提としています。

  1. パッケージインポート
  2. 関数(スクレイピング用)の定義
  3. 曲一覧の取得
  4. 歌詞の取得
  5. データ整形
  6. 出力

1. パッケージインポート

必要なパッケージを読み込みます。まだインストールしていなかったらpip installなりconda installしましょう。

import requests # 補足①
from bs4 import BeautifulSoup
import pandas as pd # データフレーム操作
import re # 補足②
from time import sleep #補足③
import sys 
import numpy as np # 行列演算

補足① requests

Beautiful SoupはURLからそのままhtmlを抜き出すことはできません。なので、URLからHTMLを取得するためにrequestsを使用します。

import requests
response = requests.get("https://www.google.com/")

これで、response.textにはHTMLが格納されます。他にもAPIを叩いたりできるみたいですね。

補足② re

正規表現で文字を取得するのに使用します。正規表現って何って方は下から。

qiita.com

補足③ time

歌詞取得の際に曲数分だけ歌ネットにアクセスする処理を行うので、サイト負荷を考えて取得毎に数秒停止する処理を入れるのに使用します。

2. 関数の定義

今回は各バンドごと、大まかに分けて2回のスクレイピングを実装します。

  1. アーティストのページにて曲目一覧のデータフレームを取得
  2. 1のデータフレームを元にして、各曲の歌詞・CD番号名を取得

f:id:fyiwdwytm:20191010235504p:plain

使用する関数は以下3つです。

# 関数①
def scraping_web_page(url):
    sleep(1)
    html = requests.get(url)
    soup = BeautifulSoup(html.content, 'html.parser')
    return soup

# 関数②
def scraping_songlist(url):
    html = requests.get(url)
    soup = BeautifulSoup(html.content, 'html.parser')

    # タグを含むリストtagsを作成
    tags = []
    tags.append(soup.find_all(href=re.compile('/song/\d+/$'))) # href属性が/songs/数値であるタグをすべてリストで取得
    tags.append(soup.find_all(href=re.compile('/song/\d+/$'))) # 上と同じ処理だが、tags[0]はURLのパスパラメータ、tags[1]は曲名に変換する
    tags.append(soup.find_all(class_=re.compile('td2'))) # 歌手名を取得
    tags.append(soup.find_all(class_=re.compile('td3'))) # 作詞者を取得
    tags.append(soup.find_all(class_=re.compile('td4'))) # 作曲者を取得

    # リストtagsからHTMLタグを除いたリストcontentsを作成
    contents = []
    for i, tag in enumerate(tags):
        tmp_list = []
        for element in tag:
            if i == 0: # tags[0]はURLのパスパラメータに変換するためhref属性から、それ以外はstring型の部分から格納値を取得する。
                tmp_list.append(element.get('href')) 
            else:
                tmp_list.append(element.string)
        contents.append(tmp_list)

    # リストcontentsをデータフレームartist_dfに変換
    artist_df = pd.DataFrame({
        'URL' : contents[0],
        'SongName' : contents[1],
        'Artist' : contents[2],
        'Lyricist' : contents[3],
        'Composer' : contents[4]})

    # URLパスパラメータにドメイン名を加えて完全なURLを生成
    artist_df.URL = artist_df.URL.apply(lambda x : 'https://www.uta-net.com' + x)
    return artist_df

# 関数③
def scraping_lyrics(artist_df):
    # 各歌詞サイトのsoupオブジェクトを集めたリストsoupsを生成
    soups = []
    for i, url in artist_df.URL.iteritems():
        soups.append(scraping_web_page(url))
        
    #歌詞、発売日、商品番号をリストに格納する。
    lyrics = []
    sales_dates = []
    cd_nums = []
    for soup in soups:
        lyrics.append(soup.find(id='kashi_area').text) # 該当歌詞のサイトから歌詞エリアを取得する
        sales_dates.append(soup.find(id='view_amazon').text[4:14]) # 該当歌詞のサイトから発売日を取得する
        cd_nums.append(soup.find(id='view_amazon').text[19:28]) # 該当歌詞のサイトからCD番号を取得する(後ほどアルバム名に変換する)

 # inputとなるデータフレームに歌詞、発売日、商品番号の列を追加する。
    artist_df['Lyric'] = lyrics
    artist_df['Sales_Date'] = sales_dates
    artist_df['CD_Number'] = cd_nums 
    return artist_df

関数①scraping_web_page

  • 間隔を空けてhtmlの非構造データを取得します。

関数②scraping_songlist

  • アーティスト一覧のURL(https://www.uta-net.com/artist/<アーティスト番号>/)から、URL、曲名、バンド名、作曲者、作詞者を取得します。

関数①scraping_web_page:

  • 各歌詞のURLから、曲の歌詞、発売日、アルバム名を取得します。大量にURLをスクレイピングする処理が走るので、関数①を利用することで負荷を下げます。

3. 曲目一覧の取得

NUMBER GIRLZAZEN BOYSの曲目一覧をそれぞれ関数scraping_songlistで取得します。その後、取得した2つのデータフレームを向井秀徳の曲一覧として結合します。

nb_songs = scraping_songlist("https://www.uta-net.com/artist/4309/")
zazen_songs = scraping_songlist("https://www.uta-net.com/artist/7546/")
mukai_songs = pd.concat([nb_songs, zazen_songs], ignore_index = True)
mukai_songs

f:id:fyiwdwytm:20191011004502p:plain

4. 歌詞の取得

向井秀徳の曲一覧mukai_songsを引数として、関数scraping_lyricsを呼び出します。mukai_songsに歌詞、発売日、CD番号を付与したデータフレームmukai_lyricsを作ります。

関数を作ってあるので、ここでは処理を呼ぶだけです。

mukai_lyrics = scraping_lyrics(mukai_songs)
mukai_lyrics

5. データ整形

CD番号をアルバム名に変換する処理と、カバー曲(向井秀徳が作曲していない曲)をデータから取り除く処理をします。

CD番号をアルバム名に変換

分析上必要ではないですが、どの曲がどのアルバムに入っているかは分かるようにします。

# CD番号の一覧を取得
mukai_cd_number = mukai_lyrics.groupby('CD_Number')[['URL']].count() #URL列をCD_Numberでグループ化して数をカウント
mukai_cd_number

f:id:fyiwdwytm:20191011211110p:plain

後は根気でwikiで調べ、マッピングしたもので変換します。

mukai_album_list = {
    ':DQC-964S':'すとーりーず',
    ':MSAL-000':'ZAZEN BOYS III',
    ':MSAL-1MA':'ZAZEN BOYS',
    ':MSAL-4DK':'',
    ':MSAL-4MA':'ZAZEN BOYS II',
    ':TOCT-220':'DESTRUCTION BABY',
    ':TOCT-221':'鉄風 鋭くなって',
    ':TOCT-222':'NUM-AMI-DABUTZ',
    ':TOCT-241':'SCHOOL GIRL DISTORTIONAL ADDICT',
    ':TOCT-243':'SAPPUKEI',
    ':TOCT-247':'NUM-HEAVYMETALLIC',
    ':TOCT-256':'OMOIDE IN MY HEAD 1 〜BEST & B-SIDES〜',
    '':'URBAN GUITAR SAYONARA',
}
mukai_lyrics['Album_Name'] = mukai_lyrics['CD_Number'].apply(lambda x : mukai_album_list[x])

カバー曲をデータから取り除く

念のため作曲者の一覧を出力すると、別の作曲者の曲が1曲入っていることがわかります。

mukai_lyricist_list = mukai_lyrics.groupby('Lyricist')[['URL']].count()
mukai_lyricist_list

f:id:fyiwdwytm:20191011211800p:plain

調べると、Pixiesのカバー曲「Wave of mutilation」が入っているようでした。

# Black Francisの曲を取得
mukai_lyrics.loc[mukai_lyrics['Lyricist'] == 'Black Francis' ]

f:id:fyiwdwytm:20191011211915p:plain

参考:


NUMBER GIRL Wave of mutilation

レコードから除く処理を行います。

mukai_lyrics.drop(6, inplace=True)
mukai_lyrics.reset_index(drop=True,inplace=True)
mukai_lyrics

一旦データフレームとしてはこれで完成です。

6. 出力

使い回せるよう、CSVにして保存します。

mukai_lyrics.to_csv("mukai_lyrics.csv")

Todo

  • Beautiful Soupでの曲取得 ~ データフレームの格納はもっとスマートにできる方法がありそう
  • Pandasの操作方法(特にレコード抽出)もよりよい方法がありそう(アドバイス、お待ちしています)
  • なんかアルバムが足りない気がする?歌詞ネットに全部のアルバムが上がっていない可能性あり

おわりに

次回はこのCSVを使って自然言語処理を行います。

分かち書きの他、頻出単語をランク付けするところまで実行します。