Pythonで国会議事録から、話題の政治ワードを抽出してみた

初めまして、Aidemy研修生のぐっちーです。

 

今回はPythonの得意分野である

 ・自然言語処理(コンピュータに人間の言語を処理させる学問)

・スクレイピング(ネット上からデータを収集する行為)

 

を行い、ネット上の国会議事録データから会議内の頻出単語を抽出する

プログラミングを実装してみたいと思います。

 

 

(結果の一例↓) 

f:id:zerebom:20180509214302p:plain

 

Pythonを一通りかじったけど、何をすればいいかわからない…!

というかたの、足がかりになればなと思います!

 

研究背景

突然ですが、統計データは母集団により結果が大きく変わります。

以下の安倍内閣の支持率に対する世論調査をご覧ください。↓

 

日本テレビの世論調査

f:id:zerebom:20180506223953p:plain

 2018年4月の安倍内閣の支持率を26.7%としております。

 

 

国際ニュース通信社ロイターの調査

f:id:zerebom:20180506223956p:plain

こちらは2018年4月の安倍内閣の支持率を73%としております。

 

データを取った母集団は、以下の通り

日本テレビ→電話調査772人

ロイター社→大企業を中心に、企業223社

 

 

このようにデータの母集団が変わると、大きくデータに差が生まれてしまいます

 

 

人が取った統計だと、何が正しいかわからない。。。!

 ⇩

それならば自分でオリジナルデータを収集し、統計を取ろう!

 

  

ということで、今回のテーマを選びました。

実際にステップを踏んで検証していきましょう!

 

目次

 

実験

実験①国会での頻出単語を割り出し、話題の政治ワードを抽出する

 

実験①ではネット上の国会会議録データを収集し、

発言回数の多い順に単語を出力させ、話題を政治ワードを抽出することを目標とします。

 

 

指針は以下の通りです。

①国会会議録APIから予算委員会の安倍首相の発言を入手する

②形態素解析し、特徴語を抽出する

③出現数の多い語から現在の政治の話題を推定する

 

(API…webページの仕様書のようなもの、国会データを収集できるサイト)

(予算委員会…一年を通して開かれる議会。主に内閣のあり方について横断的に話している)

(形態素解析…日本語を最小単位に分割し、品詞を同定すること)

 

結果はこのようになります

f:id:zerebom:20180508130611p:plain

 

実験①-① スクレイピング

スクレイピングとはネット上からデータを収集することです。

今回はこちらを使います↓

国会会議録検索システム -国会会議録検索システム検索用APIについて-

使い方はリンク先に乗っているので、ざっくり説明します。

 

国会会議録検索システムには過去の議事録が、整理されて保存されおり、

プログラミング内でURLを指定すると、議事録データを返送してくれます。 

 

(検索条件を付与して、HTTPのGET情報を送信すると、

議事録が発言者・発言日などの情報を付与してXML形式で返してくれる。)

 

実際にスクレイピングしていきましょう。

import urllib
import untangle
import urllib.parse
if __name__ == '__main__':
start='1'#発言の通し番号
while start!=None:
keyword = '安倍晋三'
startdate='2017-01-01'
enddate= '2018-01-01'
meeting='予算委員会'
#urllib.parse.quoteが日本語をコーディングしてくれる
url = 'http://kokkai.ndl.go.jp/api/1.0/speech?'+urllib.parse.quote('startRecord='+ start
+ '&maximumRecords=100&speaker='+ keyword
+ '&nameOfMeeting='+ meeting
+ '&from=' + startdate
+ '&until='+ enddate)
#Get信号のリクエストの検索結果(XML)
obj = untangle.parse(url)
for record in obj.data.records.record:
speechrecord = record.recordData.speechRecord
print(speechrecord.date.cdata,
speechrecord.speech.cdata)
file=open('abe_2017.txt','a')
file.write(speechrecord.speech.cdata)
file.close()
#一度に100件しか帰ってこないので、開始位置を変更して繰り返しGET関数を送信
start=obj.data.nextRecordPosition.cdata

 

 データは以下のような形式で返ってきます↓

http://kokkai.ndl.go.jp/api/1.0/speech?startRecord%3D1%26maximumRecords%3D5%26any%3D%E3%82%A2%E3%83%99%E3%83%8E%E3%83%9F%E3%82%AF%E3%82%B9%26speaker%3D%E5%AE%89%E5%80%8D%E6%99%8B%E4%B8%89

実験①-②形態素解析

次に取得したデータをテキストファイルとして保存し、形態素解析しましょう

形態素解析とは日本語を最小単位に分割し、品詞などを同定する行為です

 

f:id:zerebom:20180507103335p:plain

Wikipediaより

 今回はMeCabというソフトをpython内で読み込み、形態素解析します

# coding: utf-8
import MeCab
from collections import Counter
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import slothLib
import urllib3
fname = 'abe.txt'
fname_parsed = 'abe.txt.mecab'
def to_mecab():
'''「fname」を形態素解析して[fname_parsed]に保存する
    '''
with open(fname) as data_file, \
open(fname_parsed, mode='w') as out_file:
mecab = MeCab.Tagger('-d /var/lib/mecab/dic/mecab-ipadic-neologd')
out_file.write(mecab.parse(data_file.read()))
def make_lines():
'''
    各形態素を
    ・表層形(surface)
    ・基本形(base)
    ・品詞(pos)
    ・品詞細分類1(pos1)
    の4つをキーとする辞書に格納し、1文ずつ、この辞書のリストとして返す
    戻り値:
    1文の各形態素を辞書化したリスト
    '''
with open(fname_parsed) as file_parsed:
morphemes = []
for line in file_parsed:
# 表層形はtab区切り、それ以外は','区切りでバラす
cols = line.split('\t')
if(len(cols) < 2):
raise StopIteration     # 区切りがなければ終了
res_cols = cols[1].split(',')
# 辞書作成、リストに追加
morpheme = {
'surface': cols[0],
'base': res_cols[6],
'pos': res_cols[0],
'pos1': res_cols[1]
}
morphemes.append(morpheme)
# 品詞細分類1が'句点'なら文の終わりと判定
if res_cols[1] == '句点':
yield morphemes
morphemes = []

そして出力はこのようになります↓

{‘base’: ‘ミサイル’, ‘surface’: ‘ミサイル’, ‘pos’: ‘名詞’, ‘pos1’: ‘一般’}
{‘base’: ‘攻撃’, ‘surface’: ‘攻撃’, ‘pos’: ‘名詞’, ‘pos1’: ‘サ変接続’}
{‘base’: ‘等’, ‘surface’: ‘等’, ‘pos’: ‘名詞’, ‘pos1’: ‘接尾’}
{‘base’: ‘の’, ‘surface’: ‘の’, ‘pos’: ‘助詞’, ‘pos1’: ‘連体化’}
{‘base’: ‘際’, ‘surface’: ‘際’, ‘pos’: ‘名詞’, ‘pos1’: ‘非自立’}
{‘base’: ‘の’, ‘surface’: ‘の’, ‘pos’: ‘助詞’, ‘pos1’: ‘連体化’}

 

今回実験で使うのは、以下の二つです。

 

surface:(文章に表れている形)

pos:品詞

 

実験①-③ストップワード作成(統計しない単語リスト)

そのまま解析したいところですが、このまま出現数順に語を表示してしまうと、

助詞の「の」「は」など、必要ない文字が出力されてしまいます。

 

そこで、形態素解析した後の表層形をみて、必要ない語を取り除きます

 

def sloth():
import urllib3
from bs4 import BeautifulSoup
slothlib_path = 'http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt'
http = urllib3.PoolManager()
#↑urlib3系のおまじない
slothlib_file =http.request('GET',slothlib_path)
soup=BeautifulSoup(slothlib_file.data,'lxml')
soup=str(soup).split()#soupは文字列じゃないので注意
#SlothLibに存在しないストップワードを自分で追加↓
mydict=['いる','内閣総理大臣','おり','ない','あり','ある','いく','なっ','する','あっ']
soup.extend(mydict)
return soup

 

有志の方が作った、ストップワード一覧があるので活用させていただきました。↓

http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt

ここに含まれていないワードは自分で追加し、リストを結合しました。

 

これに加え今回は3語以上、かつ名詞・動詞・形容詞のみを統計しました

 

実験①-④頻出単語をグラフに出力する

 

最後に頻度順に語数を並べて、出力しましょう。

出力にはmatplotlibライブラリを使用します。

# 形態素解析
to_mecab()
#ストップワードよみこみ
stop_word=slothLib.sloth()
# Counterオブジェクトに単語をセット
word_counter = Counter()
for line in make_lines():
for morpheme in line:
if morpheme['pos'] == '動詞' or morpheme['pos'] == '名詞' or morpheme['pos'] == '形容詞':
if len(morpheme['surface'])>3:
if not morpheme['surface'] in stop_word:
#リストに入れないと、1文字づつカウントしてしまう
word_counter.update([morpheme['surface']])
# 頻度上位30語の取得
size = 30
#sizeの数だけ、上位の単語を表示する
list_word = word_counter.most_common(size)
print(list_word)
# 単語(x軸用)と出現数(y軸用)のリストに分解
list_zipped = list(zip(*list_word))
words = list_zipped[0]
counts = list_zipped[1]
# グラフで使うフォント情報(デフォルトのままでは日本語が表示できない)
fp = FontProperties(
fname='/usr/share/fonts/truetype/takao-gothic/TakaoGothic.ttf'
)
# 棒グラフのデータ指定
plt.bar(
range(0, size),     # x軸の値(0,1,2...9)
counts,             # それに対応するy軸の値
align='center'      # x軸における棒グラフの表示位置
)
# x軸のラベルの指定
plt.xticks(
range(0, size),     # x軸の値(0,1,2...
words,              # それに対応するラベル
fontproperties=fp   # 使うフォント情報
)
# x軸の値の範囲の調整
plt.xlim(
xmin=-1, xmax=size  # -1〜10(左右に1の余裕を持たせて見栄え良く)
)
# グラフのタイトル、ラベル指定
plt.title(
'37. 頻度上位30語',    # タイトル
fontproperties=fp   # 使うフォント情報
)
plt.xlabel(
'出現頻度が高い30語',# x軸ラベル
fontproperties=fp   # 使うフォント情報
)
plt.ylabel(
'出現頻度',         # y軸ラベル
fontproperties=fp   # 使うフォント情報
)
# グリッドを表示
plt.grid(axis='y')
# 表示
plt.show()

出力は以下のようになります!

f:id:zerebom:20180507123513p:plain

[(‘安倍晋三’, 1144), (‘申し上げ’, 782), (‘いただい’, 654), (‘いただき’, 534), (‘取り組ん’, 225), (‘トランプ大統領’, 170), (‘おっしゃっ’, 153), (‘安倍政権’, 142), (‘会計検査院’, 112), (‘国際社会’, 109),…以下略]

 

かなり字が小さいですが…こんな感じです!

 

「加計学園」や「会計検査院」、「国家戦略特区」など、

今年の話題が選定出来ているのがわかりますね!

 

 

実験② TF-IDFの実装

実験①では一年間全体での各単語の重要度はわかりますが、

この月は特にこの話題を話し合った!などという情報はわかりません。

 

 

そこで、今回はどの時期にどの話題が持ち上がったかを加味する

TF-IDF計算を実装してより詳しく分析していきます!

 

実験②-①TF-IDF計算とは?

TF-IDF計算とは、いくつかの文書があった時の文書中の単語の重要度を調べる手法

のひとつです

 

 

ここでは軽く説明しますが、詳しく知りたい方は以下のサイトを参照してください↓

TF-IDFで文書内の単語の重み付け | takuti.me

 

TF…TermFrequency

それぞれの単語の文書内での頻出頻度を表す

 

 

IDF…Inverse Document Frequency

それぞれの単語がいくつの文書で共通して使われているか表す

 

f:id:zerebom:20180509110306p:plain

式で表すとこのようになり、TF-IDFはTF×IDFの値で表されます。

 

つまり、

沢山出てくる単語ほど重要(TF)という値と

出現文書数が少ない単語ほど重要(IDF)という値をかけ合わせています。

 

例えば、1年間の議事録では

トランプ大統領という単語が1年を通じてたくさん登場し、

加計学園という単語は年の終わりに集中して登場したとします。

 

この場合

トランプ大統領→(DF:高、IDF:低)

加計学園→(DF:高、IDF:高)

となるので年の終わりの文書では加計学園という単語がより重く、特徴づけられます。

 

 

 

実験②-②sklearnのTfidfTransformerクラスを使用

 TF-IDFの仕組みは平易なため、自分で実装することも可能ですが、

今回は別の検証にも応用できるように、sklearnのTfidfTransformerというメソッドを使って実装しました

 

 

—————————————————— 実装———————————————–

①前処理した安倍総理の発言を3つの文書に分け、リストに入れる。

a.2017-01-01~2017-04-01

b.2017-04-01~2017-08-01

c.2017-08-01~2017-12-01

 

そして、文書を形態素解析し、CounterVectorizer(後述)に入力できる形に成型する。

 

今回は3文字以上の名詞のみを選定しました。

to_mecab('/home/share/idf/abe_1.txt','/home/share/idf/abe_1.txt.mecab')
copus=[]
word_counter = Counter()
for line in make_lines('/home/share/idf/abe_1.txt.mecab'):
for morpheme in line:
if morpheme['pos'] == '名詞':
if len(morpheme['surface'])>2:
if not morpheme['surface'] in stop_word:
word_counter.update([morpheme['surface']])
copus.append(morpheme['surface'])
copus.append(' ')
#ストップワードを除く3文字以上の名詞が全て格納される
#joinにより、一つなぎの文字列になる a=''.join(copus)

 

②CounterVectorizerクラスを用いて単語の出現回数を計測し、各文書を特徴ベクトルに変換する。

CounterVectorizerは、sklearnに用意されているクラスで、

文書中の単語の出現回数を数え、その値をもって文書をベクトル化します。

実装は以下の通りです↓

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
#カウンターを定義
count=CountVectorizer()
#各文書をarray型に代入する
docs=np.array([a,b,c])
bag=count.fit_transform(docs)
#各単語の出現回数を表示
print(count.vocabulary_)
#各単語の出現回数を、疎な特徴ベクトルとして出力
print(bag.toarray())

 

出力は以下のようになります

{'thaad': 4225, '教育再生実行会議': 2721, 'テーマ': 637, '機密情報': 3030, '保育士': 1482, '君たち': 1950, 'あずかり': 3, '締めくくり': 3539, 'スター': 546, 'ノドンミサイル': 702, '北東アジア': 1758, '長時間労働': 3965, '二十三年': 1298, '輸出企業': 3841, 'クリーン': 425, '在宅医療': 2123, '獣医師会': 3212, '不規則': 1179, '昨年末': 2872, 'グループ': 434, '知ったか': 3342, '手持ち': 2635, '相対的貧困率': 3323, '東北地方': 2968, '一万三千人': 1036, 'ポスト': 879, '新潟県': 2766, 'アフガン': 271, '五年間': 1374, '佐々木': 1465, '三十万円': 1118, '労働力人口': 1719, 'シークレットサービス': 523, 'パチンコ': 746
[[  1   1   1 ... 302  33  13]
[  0   0   0 ...   0   0   0]
[  0   0   0 ...  13   0   0]]

 

③TfidfTransformerを使い、TF-IDF計算を実装する。

②でベクトル化された文書に計算を加えて、TF-IDFの式に従って値を変更します。

from sklearn.feature_extraction.text import TfidfTransformer
#tfidfを実装する
'''
use_idf...Falseにすると、dfのみで出力する
norm...正則化の手法。ベクトルの長さが1になる
smooth_idf…idfの計算式が変わる。Faluseにするとlogの後ろの+1がなくなる
'''
tfidf=TfidfTransformer(use_idf=True,norm='l2',smooth_idf=True)
#arrayの出力を成型する(小数点第2位まで)
np.set_printoptions(precision=2)
#tfdifでdocsを変換したのち、array型に変換し出力する
print(tfidf.fit_transform(count.fit_transform(docs)).toarray())

 

出力は以下のようになります。

 

[[0. 0. 0. … 0.17 0.02 0.01] [0. 0. 0. … 0. 0. 0. ] [0. 0. 0. … 0.06 0. 0. ]]

 

ぱっと見て、正則化のおかげでベクトルの長さが1になっていることがわかりますね!

 

実験②-③Excelに出力し、データを読み解く

最後にこのベクトルから、どの単語が各文書で重要か調べてみましょう!

今回はExcelに出力したいと思います。

from sklearn.feature_extraction.text import TfidfTransformer
tfidf=TfidfTransformer(use_idf=True,norm='l2',smooth_idf=True)
np.set_printoptions(precision=2)
features = count.get_feature_names()
ans=tfidf.fit_transform(count.fit_transform(docs))
#出力をarray型にする
ans_array=tfidf.fit_transform(count.fit_transform(docs)).toarray()
data={
#単語名
"index":features,
#各文書の各単語のTF-IDF値
"num1":ans_array[0],
"num2":ans_array[1],
"num3":ans_array[2]
}
df=pd.DataFrame(data)
df.to_excel('/home/share/idf/sample.xlsx', sheet_name='abe_2017')

出力はこちら↓

以下は文書1(2017-01-01~2017-04-01)でソートしてあります。

f:id:zerebom:20180509142246p:plain

文書1を特徴づける単語は「tpp」「北朝鮮」「トランプ大統領」などになります!

しかしこれは、実験①とは異なり、単なる出現回数順ではありません。

 

例えば、行番号24番と、25番を見てみましょう。

 「労働時間」という単語は文書1のみ出現していますが、

「明らか」はすべての文書で出現しています。

 

つまり「労働時間」のIDFは「明らか」より、

大きくなり出現回数のわりにTF-IDF値は大きくなります。

 

 

実際に調べてみると、

「労働時間」の出現回数は124回

「明らか」の出現回数は210回となっていました。

 

しっかりと出現文書数が少ないほうがより重みづけされる機能が実装されていますね!

 

( ※文書1の出現単語数が多いのは、2月に予算委員会は多く開かれるためです。

文書1はほかに比べて分量が多いです)

考察

TF-IDF計算は文書の数が多い時に発揮されます。

今回の実験では3文書しかないため、すべての文書で出現する文字が多くなり、

IDFの値に差があまりつきません。

 

文書の数を12個に分け、月別で調べてみても面白いかもしれないですね!

(予算委員会は月ごとの開催回数はかなり違いますが)

 

今回の実験を発展させる方法

 

今回は安倍総理の発言のみをスクレイピングしましたが、

議員毎にスクレイピングし、何に主眼を置いて話しているか等を調査する事や、

議員毎のクラスタリングをしても面白いですね。

 

また、今回の実験②では文章をベクトル化し、数値で表すことが出来ました。

ベクトル化をすると、文章に対して数学的アプローチをかけることができるようになるので、クラスタリング等より高度な技術が可能になります。

 

 機会があれば今回の実験の続編も行いたいなと思います!

感想 

 

 今回は自然言語処理・スクレイピングがテーマでしたが、

この実験を行うにあたっては、テーマ以外にも

  • ローカル環境の構築構築
  • HTTP通信(xmlや文字コードの扱い方)
  • MeCabの辞書作成
  • sklearnのパラメータの理解、渡すデータ型

 

などで非常に苦戦しました汗

 

しかし、苦戦する中で自分でエラーを解決する力や、横断的な知識を身に着けることができ他と思います。

是非、ProgateやAidemyでの学習が一通り終わった方は自分でテーマを設定し、

アウトプットする機会を設けて挑戦してみてください!

環境紹介

今回私が使用した環境はこちらです⇩

・windows10

・VMwareWorkstation14(仮想OSを起動するソフト)

・ubuntu 16.04 32bit(Linuxディストリビューションの一つ)

 

・python3.6.5

・MeCab (形態素解析ソフト)

・Atom(テキストエディタ)

 

参考文献

・冒頭の世論調査↓

jp.reuters.com

 

日本テレビ世論調査

 

・Linuxの導入方法↓

koeda3.hatenablog.com

・MeCabのダウンロード方法↓

 

qiita.com

 

・Atomのダウンロード方法↓

Python入門 初心者でも出来るAtomエディタでの開発方法

 

・ストップワードの実装↓

testpy.hatenablog.com

 

自然言語処理の基礎知識まとめ↓

qiita.com

機械学習プログラミングのまとめ(sklearnの使い方等)

book.impress.co.jp