StatsBeginner: 初学者の統計学習ノート

統計学およびR、Pythonでのプログラミングの勉強の過程をメモっていくノート。たまにMacの話題。

Pythonの簡単なコードでメールを自動送信してみる

意外と簡単にできた

 メールを300人ぐらいに発信する必要がありまして、Toに全員入れるわけにはいかないし、BCCで送るのもダサいかなと思って、「1人1人を個別にToに指定して、同じ件名・同じ文面のメールを送る」ってのをPythonでやってみました。*1
 標準モジュールのemailってのとsmtplibってのを使って、50行程度のコードで簡単に送れました。
 1点心残りなのは、後述のとおりFromの欄に日本語の差出人名を表示させるやつが、色々調べたものの結局できませんでした。

用意するもの

 アドレスリストをCSVで用意して、本文はテキストファイルに書いておきました。

f:id:midnightseminar:20180212202431p:plain
f:id:midnightseminar:20180212202409p:plain

コード

 ネットでsmtplibを使ったPythonでのメール送信の解説を探すと、sendmail()というメソッドで送っているものと、send_message()というメソッドで送っているものがあります。sendmail()が基本なのですが、send_message()はそれをより簡単に扱えるようにラップしてくれているような感じらしいです。今回はsend_message()を使いました。


 コードは以下の通りですが、全体としては、

  1. モジュールを読み込む
  2. 差出人アドレスやメールサーバの認証情報などの設定項目をまとめて書いておく
  3. メールサーバに接続する
  4. emailモジュールのMIMETextでテキスト形式のメール本体(MIME文書)をつくる
  5. MIME文書に、差出人、件名、宛先等の情報を入れていく
  6. smtplibのsend_messageでメールを送信する
  7. サーバとの接続を終了する

 という流れになっています。

# モジュールの読み込み
import time
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import pandas as pd

# 基本的な設定たち
srv_smtp = 'XXXXXX.XXXXXX.jp'  # SMTPサーバ
srv_port = 587                 # ポート番号
srv_user = 'XXXXXX'            # サーバのユーザ名(ちなみに私が使ってるやつだとメアドがユーザ名)
srv_pw   = 'XXXXXXXX'          # サーバのパスワード
jp_encoding = 'iso-2022-jp'    # 日本語文字エンコーディングの指定
add_sender = 'XXXX@XXXXXX.jp'  # 差出人(自分)アドレスの設定
add_bcc = 'XXXX@XXXXXX.jp'     # BCCの複製を送るアドレス
add_rcp_path = '/XXXXX/XXXXX/address_test.csv'  #アドレス一覧が入ったCSVの置き場
body_path =    '/XXXXX/XXXXX/body_test.txt'     # 本文を書いたテキストファイルの置き場
mail_subject = 'くさめの件につきまして'         # 共通の件名

# 本文ファイルの読み込み
with open(body_path, 'r', encoding='utf-8') as file:
    mail_body = file.read()


# 宛先リスト読み込み
# 元ファイルには名前も入れてるけどとりあえず使わないことにする
add_rcp_df = pd.read_csv(add_rcp_path, encoding='utf-8')  # csvの読み込みはpandasでしかやったことないので…
add_rcp_list = add_rcp_df['Address'].tolist()


# SMTPサーバへの接続
server = smtplib.SMTP(srv_smtp, srv_port)
server.ehlo()
server.starttls()  # TLSでアクセス
server.ehlo()
server.login(srv_user,srv_pw)  # ログイン認証

# 送信を繰り返す
for add in add_rcp_list:
    try:
        msg = MIMEText(mail_body.encode(jp_encoding), 'plain', jp_encoding,)
        msg['From'] = add_sender
        msg['Subject'] = Header(mail_subject, jp_encoding)
        msg['Bcc'] = add_bcc
        msg['To'] = add
        server.send_message(msg)  # 送信する
        time.sleep(3)  # 3秒まつ
    except:
        # なんかあった時用
        print('An error occured when sending a mail to ' + add)
        

# サーバ接続を終了
server.close()


 MIMETextでMIME文書を作って、件名等の情報を入れた後、特定の情報だけ差し替えるというのが上手く行かなかったので、forループでメールを1通1通送る際に、MIME文書の生成自体をまるごとやり直しています。これが適切なのかよく分かってませんがとりあえずメール送信には成功しました。
 あと、メールソフトに送信済みメールが残らないので、記録用にBCCで自分のアドレス宛にメールを飛ばしています。
 1万件とか送るのであれば、受信側のメールサーバにSPAM判定されないように時間を空けて送る必要があるかと思いますが*2、300件ぐらいならべつに全部即時送信してしまっていいような気はします。上記では一応3秒ずつ空けています。

なんか止まってた

 300件の送信中、2回止まりました。
 たぶん回線が不安な環境だったので、ネット接続が切れたんだと思いますw
 エラーが出たアドレスのところを確認して、あと一応BCCでコピーを飛ばしていたメールも確認して、未送信の人だけのアドレスリストを作ってやり直しました。
 上記のコードでは、何らかのエラーが出た時の対処はまったく記述していないので、簡単なコードでむやみに大量のメール送信をすると何が起きるかわからんという点には注意が必要かと思います。

日本語文字のエンコード

 エンコーディングのところに関して、参考にしたブログ記事などが一様に、日本語のメールで伝統的に使われているという'iso-2022-jp'を指定していたので、上記コードではその通りにしてますが、↓のページに書かれているように、今はべつにUTF-8でも問題ないようです。
 実際、UTF-8でも自分の持っている幾つかのメールボックスに送ってみましたが、ちゃんと見れました(ただ、iPhoneとMacでしか確認してないので見れて当然なのかもしれません)。
 
Pythonで日本語メールを送る方法をいろいろ試した
 
 受信側の環境にも拠るのかもしれないので、私は念のため伝統的なほうを使いましたが、実際どっちのほうが安全なのかはよく知りません。
 
 

差出人表示を日本語でする方法が分からない

 上記コードで、

add_sender = 'XXXX@XXXXXX.jp'

のところを

add_sender = 'K.Yoshida <XXXX@XXXXXX.jp>'

にすると、受信側のメールソフトで差出人名を「K.Yoshida」として表示してくれたりします。


f:id:midnightseminar:20180212202526p:plain:w300


 それは今回もできたんですが、ここに日本語の名前を入れる方法というのが色々難しく、結局ちゃんとはできませんでした。
 下記のようなページで紹介されているように、

  • send_message()ではなくsendmail()で送る
  • アドレス部分はそのままに、日本語の差出人名の部分だけエンコードする。その際単に文字列としてではなくHeaderインスタンスとして生成する

 という方法でできるらしいのですが、自分でやってみたところ、受信側サーバによっては受信拒否、一応送れたサーバでも、差出人名のあとに「@」と受信サーバの情報を表す文字列がくっついた状態で表示されてしまい、要するに不正なメールとして判定されたんだと思います。この辺、正式にできるやり方を調べる必要があります。
 Stackoverflow等をみると海外でもウムラウト付きの文字などを表示させようとして苦労している人がいました。


Python3 日本語でメール送信 - textbook
Pythonでメール送信時に送信者に日本語を使用する : fujishinko 雑記帳


[追記]
下記のとおり、成功しました。
Python3でのメール送信時に日本語の差出人名を使う - StatsBeginner: 初学者の統計学習ノート
[/追記]

 

拡張

 上記のようにとりあえず単純なメールを送れる状態にしておけば、あとは

  • アドレスリストから名前を取得して、本文の一行目に「◯◯様」と可変で入れる。
  • そもそも本文自体を人に合わせて変える。
  • 途中でネット回線が切れたりするのが怖いのでAWS等の仮想マシンから発射する。
  • multipartして、base64した添付ファイルをつける


 など、今後いろいろ工夫できるかと思いました。
 ただ、私のような素人が自前のコードでメール送信をすると、なんか事故が起きそう(宛先と添付ファイルが1個ずつズレるとか)なのでなるべくやりたくはないですね。
 
 

関連リンク

mimeTEXTの説明書
https://docs.python.jp/3.3/library/email.mime.html

smtplibの説明書
https://docs.python.jp/3/library/smtplib.html

ここにemailモジュールとsmtplibモジュールを使ってメールを送る際のコード例が載っています。
https://docs.python.jp/3.3/library/email-examples.html

ここに書かれているように、From欄とかnon-ascii文字を使いたければ、Headerモジュールを使ってHeaderインスタンスとして投入する必要があると書いてあります。
https://docs.python.jp/3.3/library/email.header.html

*1:MacのAutomatorでも、group mailっていうアクションを使うとアドレス帳で指定したグループあてに「1人1人をToに指定した別々のメール」として一斉送信ができるけど、冒頭に'Dear Bob,'みたいなgreetingを入れる必要があり、しかもこれが英語仕様しかないので強制的に最後にカンマが入るという、日本人にとっては中途半端な仕様です。

*2:1万件あれば、docomo.ne.jp等に同時に数百から数千通発射することになる

Macでの年賀状作成環境について(2018年版)

 ブログの趣旨と全然違いますが、備忘のためにまとめておこうと思います。
 私は、サボる年もありますが、出す時は年賀状を200人以上に出すので、けっこうな大仕事になっています。
 しかも東京周辺の知り合いが多く、3〜4年たつと半分ぐらいの人が引っ越してしまっている(自分と同年齢から下10年ぐらいまでの知り合いは、結婚によって引っ越すケースも多いので)イメージで、住所録のメンテナンスがけっこう大変です。

宛名職人にサヨウナラ

 昔は、年賀状は「宛名職人」(リンク)というMac用の年賀状ソフトで作成していました。宛名職人の住所録は、iCloudのアドレス帳に登録してある情報をvCardで書き出しておけば、差分を検出してどっちかで上書きするってのができて便利だったんですよね。
 私は一時期、年賀状ソフト、携帯電話、パソコンのメーラーなどに電話帳やアドレス帳が分散しているのが気持ち悪かったので全部統合してiCloudにぶち込んだことがあります。これで一元化されたので、iPhoneでアドレス帳を開くと、年賀状のやり取りがある人については住所も入っているという状態になりました。で、

  • 年末年始以外の時期に、誰かが引っ越したことを知った時は、iPhoneかMacでiCloud住所録を上書きする。
  • 年末に年賀状を準備する前に、iCloud住所録→宛名職人住所録の方向で上書きする。
  • 年賀状を出す。
  • 何人かは「あて所に尋ねあたりません」で戻ってきたり、向こうからくる年賀状によって私の認識している住所が異なることが判明する。
  • 届いた年賀状を見ながら、宛名職人の住所録画面を見ながら、カタカタ打ち込んで更新する。
  • 年賀状シーズンが終わったら、宛名職人住所録→iCloud住所録の方向で上書きする。


 みたいな感じでやっていました。
 ところが宛名職人は、毎年新しいバージョンのソフトを数千円で買わされるんですよね。古いバージョンでも動くときがありますが、Mac OSの新バージョンが毎年のように出るので、古いバージョンだと不具合が出るようになりました。
 しかも、何年前だったか忘れましたが、最新バージョンにしたのにトラブルが起きたことがあり、まぁ自分に原因があったのかも知れませんし詳細は忘れましたが、とにかく自分が安定して使いこなせないようなソフトは使うのをやめようと思って決別しました。

はがきデザインキットは使いこなせなかった

 その後2年ぐらいは、Windows機の筆まめを使ってやっていましたが、今年は「はがきデザインキット」(リンク)を使ってみようと思いました。Mac版があるので。
 郵政関係者に聞いたところ、はがきデザインキットは日本国内で最もたくさん利用されている年賀状ソフトらしいです。たしかに無料でこれだけの機能があれば、有料の筆まめとかを使う必要はあまりない気がします。


 ところが実際にトライしてみると、住所録の読み込みで躓きました。はがきデザインキット専用のcsvフォーマットなるものが配布されているので、それに従ってcsvを作成したのですが、文字コードを色々変えてみても読み込み後に文字化けしたので、4、5回トライして諦めました。


f:id:midnightseminar:20180103165708p:plain

宛名印字はプリントマジックで

 で、結論として今回は、宛名の印字にプリントマジック(リンク)というのを使うと、比較的簡単に作成できました。


f:id:midnightseminar:20180103165458p:plain


 こんな感じの画面で、CSVを読み込む際に、CSVのどのカラムをプリントマジックのどの項目に割り当てるかを選択します。
 ちなみに、元のCSVにはヘッダ行があるのでそれが1行目に表示されてしまっています。「一行目を無視する」をチェックすると消えますが、ヒモ付け作業が終わるまでは出しておいたほうがわかりやすいです。「項目3は姓に紐付ければいいんだな」とか分かるので。


 元の住所データを、どこで区切って保存しておくかというのは、宛名印字界隈では悩ましい問題ですね。そもそも日本の住所の決め方自体もけっこうややこしいので(以前エントリとしてまとめました)、どこで区切るのが正しいかといった定説もない気がします。


 私はもともと、


(1) 都道府県
(2) 市区町村(政令指定都市の場合は行政区まで入れる)
(3) 町・字から住居番号まで
(4) 建物名と部屋番号


 というふうに分けたデータを作っていましたが、「◯◯町1−2−3−405」みたいに、マンション名を省略してかつ部屋番号(ここでは405)をそのまま続けている住所しか把握していない場合は、(3)に「405」まで含めてしまったりしています。
 そもそも数字のところは「丁目」「番地」「号」という3点セットになっているとは限らないので、住居番号と部屋番号の境目は判別がしづらいです。


 で、今回は、プリントマジックでの印刷レイアウトを考えると、

  • 都道府県&市区町村
  • 町・字から住居番号まで
  • 建物名と部屋番号

 という3項目に分けたCSVを読み込ませると、いい感じでした。
 プリントマジックでは、たとえば項目9〜項目11をすべて「自宅住所」として指定すると、住所録画面ではこの3項目を連結した情報が表示されますが、印刷時にはこの3つを行で分けて印刷してくれます。
 その際、一行の長さをいい感じにするには、上記のような分け方がちょうど良かったです。


 住所録のうち、選択した行だけ印刷することもできるし、印刷するかしないかというデータ項目もあります。
 印刷しない人のデータなんてそもそも必要なの?と思われるかもしれませんが、「普段は出しているけど、今年はこの人から喪中のお知らせが届いたので出さない」みたいなパターンがありますし、こっちから出してないけど向こうからは届いたので年明けに「返事」を出すという場合は、その人たちだけ「印刷する」に設定して印刷するのが良いですね。


 プリンタの設定でハガキを指定すれば、ふつうに綺麗に印刷できました。印刷ソフトによっては、郵便番号の位置がプリンタとの相性で微妙にずれるため、微調整機能がついてたりしますが、今回は相性が良かったのか、何の問題もなく印刷できました。

デザイン面はPower Point

 宛名職人でも筆まめでもはがきデザインキットでもそうですが、テキストや画像を配置する操作に制限が多くてけっこうイライラしますよね。しかも年1回か(暑中見舞いを入れて)2回しか使わないので、UIのクセに習熟することがない。
 「だから全部フォトショで作る」という知り合いもいます。
 しかしフォトショなどの画像処理ソフトも、文章を多めに書く年賀状を作る場合は、テキストをいれる作業でちょっとイライラすると思います。また、文字も全部画像データとして吐き出されるというのはなんか気持ち悪い気もします。クッキリ印字するためには無駄に高解像度にしないといけないのではないか、とか。


 そこで今回は、Power Point(for Mac)で作ることにしましたが、これは大正解でした。写真や図形や文字の配置が自由自在で、とにかく使い慣れたUIなのでイライラが少ない!

  • ページ設定で、画面をはがきのサイズにする。
  • 写真や文字を配置する。この際、余白を考えずにパワポのページの端まで使う。
  • PDFとして保存する(余白のないPDFができる)
  • プリンタ(私はエプソンのもの)の設定で、余白3mmのハガキ印刷を指定。


 これでとてもいい感じに印刷できました。PDFなのでテキスト部分を無駄に解像度の高い画像にする必要もなし。


 来年もこのパターンでいくと思います。
 iCloudの住所録と同期するのは、もう色々めんどうなのでやめることにします。年賀状用のCSVを、それはそれとしてメンテナンスしていく所存です。

今更ながら、Rのアンインストール・インストール・初期設定の復習(Macの場合)

備忘のためのメモが必要

 Rのバージョンアップをしようとしたんですが、Macの場合はアップデート用のコマンドが使えないらしいので、アンインストールして新バージョンを再インストールをすることにしました。
 Rのインストール方法なんて解説サイトが山ほどあるのでこの記事をみるメリットはあまりないですが、自分がやることになった時に自分で書いたブログ記事を見直すのがたぶん一番早いので、まとめておくことにします。
 初期設定一式については、新しくパソコンを買った時とかに一通りやり直さなければならない場面もあるかもしれないですし。
 研究室ではみんなSPSSを使ってる(私もSPSSを使い始めた)ので、学生にインストールを教えることは当面なさそうですが、Rの何かのパッケージを使わないとできないことが出てきたら,教える場面もあるかもしれません。


 なお今回の環境としては、OS X SierraでR 3.4.0を使っていたのをアンインストールして、R3.4.2を入れ直しました。

アンインストール

 今回は試しに、関連ファイルを全て削除しようと思いました。が、後述するように、今回「完全アンインストール」を目指したものの、結局「完全」ではなかった模様です。
 ちなみにメジャーアップデートでなければパッケージは残しておいて引き継ぐことができると思いますが、今回は削除しました。


 まず、削除ツールのApp Cleanerを使った場合、消す候補として見つけてくれるのは以下のものでした。

  • "/Application/R.app"
  • "/var/db/BootCaches/"の下にハッシュ値みたいな長い名前のディレクトリがあって、その中にある"app.org.R-project.R.playlist"(私は長い名前のディレクトリが複数あって両方とも"app..."を含んでいた。)
  • "/var/db/receipts/"の中に入っている、R関連の.plistファイルと.bomファイルいくつか。(.pkgでソフトをインストールした際に管理用に生成される、設定ファイルとバイナリ一覧ファイルらしい。)
  • "/var/folders/"の下を掘っていったところに"org.R-project.R"というディレクトリがあり、この下に何かグラフィック関係の設定ファイルのようなものがいくつか入っている。


 とりあえず、全部消す。
 その他、自分で探して消すものとしては、以下で良いんじゃないでしょうか。

  • "/Library/Frameworks/R.framework"
  • "/usr/bin/R"
  • "/usr/bin/Rscript"
  • "/usr/local/bin/R"
  • "~/library/R/"


 バイナリについては、どこ(とどこ)に置かれているかが、人によって違ったりするのかな。
 "~/library/R/"には標準パッケージ以外に自分がインストールしたパッケージが入ってるんですが、ディレクトリの中はバージョンごとに分かれており、これは消さずに残しておけば、Rのマイナーアップデートであればインストール済みパッケージを全部引き継げると思います。今回は消しましたが。


 あとは、Rでデフォルトのworking directoryに設定していた場所に、

  • ".Rprofile"
  • ".Rapp.history"
  • ".RData"
  • ".Rhistory"


 があると思うので、消す。
 .Rprofileも、新たにRをインストールするときに日本語表示の設定とかで再利用するでしょうから、残しておくのがいいと思いますが、とりあえず消しました。


 さて、これで全部かなと思ったのですが、じつは再インストールした後にRを起動してFileの「Open Recent(最近使ったファイルを開く)」を触ってみたら、アンインストール済みのRで以前開いたスクリプト等が候補として表示されました。ということは、まだ何かキャッシュ的なもの?を消し切れてないようです。現時点では何なの分かりませんでした、というか調べてないです。
 またそもそも、今回は関連「ファイル」を順に削除していったわけですが、他のアプリケーションやOSが使っている設定ファイル等に書き込まれたR関連の情報は触ってないわけなので、その辺は後の課題とします。
 

再インストールと初期設定

 つぎに改めてRをインストールして初期設定をします。
 なおRを入れる前に、XQuarzの最新版がインストールされてるかは確認が必要です。古かったらアップデートする。
 インストール方法は省きますが、CRANのサイトで配布されている最新のインストーラをダウンロードしてインストールすればいいでしょう。


 インストールが済んだらRを起動して、Preferences(環境設定)で、

  • Default CRAN mirrorをJapanに設定(べつにどこ使っても良いといえば良いんでしょうけど)
  • "Rapp.history"を読み込むかどうかの設定ができますが、とりあえず読み込むでいい気がします。これは何かというと、たぶん、コンソール上でカーソルキーの上を押したりして過去の入力を辿っていくときに、前回までにRを立ち上げていたときの履歴も辿れる(その時と同じコマンドが打てる)ということかな?と思います。私はあまり使いませんが。
  • "Save workspace on exit from R"は私はNOにしてます。計算に膨大な時間がかかるような分析は滅多にしないし、スクリプトをアタマからケツまで保存してれば分析は再現可能で、ワークスペース自体を保存する必要はないので。


 という設定をしました。
 また、Rのコンソールやグラフ中で日本語の文字を表示させることがある場合、その設定が必要になります。(Mac版の)Rで日本語を扱うためにやることは主に2つあって、ロケールの設定と.RProfileの設定です。前者は人によって不要な場合もある思います。


 まずRを起動した時に、

During startup - Warning messages:
1: Setting LC_CTYPE failed, using "C" 
2: Setting LC_COLLATE failed, using "C" 
3: Setting LC_TIME failed, using "C" 
4: Setting LC_MESSAGES failed, using "C" 
5: Setting LC_MONETARY failed, using "C" 
[R.app GUI 1.70 (7434) x86_64-apple-darwin15.6.0]

WARNING: You're using a non-UTF8 locale, therefore only ASCII characters will work.
Please read R for Mac OS X FAQ (see Help) section 9 and adjust your system preferences accordingly.


 というような注意書きが出ている場合、ロケールの設定(posix標準設定の"C"をOSから引き継いでいる?)がUTF-8を使うものになってないということですが、このままだとコンソール上に日本語の文字列を出力することができません(文字コードの羅列になる)ので、設定変更が必要になります。なおロケール(locale)というのは言語と地域にまつわる設定のことです。ローカルの間違いではありませんw
 その方法については解説ページ(リンク)がありますが、ターミナルから変更する場合は、

defaults write org.R-project.R force.LANG en_US.UTF-8


 と打ちます。なお、Rのコンソールからsystem()関数で("")の中に同じコマンドを入力することでも変更できます。
 これは具体的には、"~/Library/Preferences/org.R-project.R.plist"というXMLファイルの中の、

<key>force.LANG<key>
<string>■■■■</string>


 という箇所の■■■■に書かれる情報を変更してることになります。
 「en_US.UTF-8」のところは、「ja_JP.UTF-8」にすると、コンソール上の警告文などが日本語になります。
 個人的には、何かエラーが出て原因や解決策を知りたいと思った時、英語の警告文をGoogleで検索したほうが情報が多いので、英語設定にしています。ここを英語設定にしていても、UTF-8になってさえ入れば、コンソールやグラフ上に日本語を表示することは可能です。


 次に、グラフに日本語の文字を出したい場合はこれだけでは足りなくて、定番ですが「.RProfile」(テキストで作成してworking directoryに置いておけば、起動時に読み込まれる)に設定を書き込む必要があります。
 その内容は、参考ページ(リンク)のものを丸ごと拝借してコピペでいけました。この内容は将来、Rの仕様が変わったりすると、修正が必要になるかもしれませんが、とりあえず現行版ではいけました。これの内容をきちんと自分で理解できてないのが気持ち悪いので、後ほど勉強しなければと思っております。
 なお今回、原因がよくわかりませんが、.Rprofileをテキストファイルから作成したらどうもRが起動時に読み込んでくれてない様子だったので、試しにRのコンソールから、

prof <- "
ここにコードをコピペ
"
cat(prof, file=".Rprofile")


 というふうに作成したら行けました。何でかはよくわかりません。前はいけたんですけど。

ブロックチェーンって「付加価値」が期待されてるわけでもなくね?

 久しぶりにブロックチェーンに関する記事を読んだんですが・・・


qiita.com


 「ブロックチェーンが経済のあり方を変える!」的な一部世間の風潮にクギを刺すような論調の記事ですが、金融機関や役所でブロックチェーンの活用に取り組んでいる人たちの話とは少し違うなという気がしたので、気になった点をメモしておきます。
 上のブログの方の言っている内容そのものが間違っているというより、世間でいま騒いでいる人たちと視点がずれていて噛み合ってないような気がしました。
 私が言っている世間とは、丸の内とか霞が関とかで背広を来て歩いているような人たちのことです。
 なお私はブロックチェーンの専門家でも技術者でもなく、素人の印象ベースで書いてます。

コスト削減を話題にしてる人のほうが多くね?

 上の記事ではブロックチェーンの存在意義について、

ブロックチェーン技術が提供する最大の付加価値が公明正大なビジネストランザクション処理の実現にあると考えます。


 とあるのですが、いま産業界でブロックチェーンを話題にしている人たちは、べつに「付加価値」を強調しているわけではないような気がします。
 「ブロックチェーンのおかげで初めて可能になる業務・取引がある」というような話は、ビットコイン流れのベンチャーの人たちはしてるかもしれません。しかしそういうベンチャーのエンジニアを雇って実証実験をしかけている金融機関の人からは、「業務自体は別の仕組みでもできるんだけど、ひょっとしたらブロックチェーンでやったほうが効率的かもしれない」みたいな話をよく聞く感じです。
 ブロックチェーンの最初の応用例であるビットコインは、あの謎の日本人(?)研究者の論文からしてそうであるように、「特定の組織に管理されたくない」みたいなイデオロギーから出発しているので、「管理されずにすむ」という独自の付加価値が実現されていると言えます。しかし最近行われている実証実験等の取り組みのテーマは、どっちかというと「コスト削減」的な話が多くて、「対価の支払いを増やしてもいい」と思えるような新たな付加価値が期待がされているわけでもないように思うんですよね。


 個人的に耳に入ってくる印象レベルの話なので間違ってるかもしれませんが、よく聞くのは、「ある種の業務を実現する上で、従来の中央管理型の処理よりもブロックチェーンを用いた分散型の処理のほうが低コストな場合があるかも」みたいな話です。「かも」に留まるから実証実験程度のものがいくつか行われているだけになっていますし、「場合がある」程度であって、ブロックチェーンが中央管理型の処理を完全に代替するなんていう話をしている人は個人的にはみたことがないですね。


 「ブロックチェーンだからこそ公明正大になる」という言い方は、たぶんビットコインのイメージを引きずっているのだと思いますが、すでにあまりビットコイン的ではないプロジェクトが多いと思うんですよね。
 「パブリックチェーン」と「プライベートチェーン」という区別があって、ビットコインはパブリックでまさにP2Pの理想を体現したような公明正大な仕組みですが、今は特定の企業(あるいは企業集団)が構築する閉じたビジネス範囲内でのブロックチェーン活用、つまり「プライベートチェーン」のプロジェクトがけっこう多い。そりゃ当たり前で、企業はボランティア団体ではないのでパブリックチェーンなんてものを作りたくなるのは一部のエンジニアだけであって、企業目線で見ればプライベートチェーンの方が「我が社にとってメリットあります」というプロジェクトに仕立てやすいわけです。

信用のない主体が取引できるようになる?

 冒頭の記事に、

ブロックチェーン技術の適用に適したビジネスケースは「信用に乏しい主体がビジネストランザクション処理に参加する」ようなケースに他なりません。


 とありました。これも、「ブロックチェーンを使ったからこそ可能になる取引がある」みたいな意味に理解できるのですが、世間的には、「もともと取引は何らかの形で可能なのだが、そのためのコストがブロックチェーンによって下がるかもしれない」という見方の方が強調されてると感じます。
 信用の乏しい主体というのは、ここでは財務基盤が脆弱とかいう意味ではなく、ウソをつくかもしれない主体という意味だと思います(悪意を持って騙すというより、たとえば自社システムの運用が杜撰で、取引の記録がきちんと残されてないとかね)。ブロックチェーンは、過去の取引についてウソを付けなくする仕組みの一つなので。しかし彼らは、ブロックチェーンによって初めて取引に参加できるようになるんですかね?


 そもそも経済的な取引というのは原理的に、相手を信用できない場面が多々あり、何らかの形で「信頼される第三者(Trusted Third Party、TTP)」が取引の仲介プラットフォームを提供することによって成り立っていることが多いです。根本的なところから言えば、中央銀行がおカネを発行しているのもそうだし、人を騙したら警察に捕まって裁きを受けるという司法システムが国によって整備されているというのもそうですね。もう少し具体的なところで言えば、ECサイトや決済サービスなどが、TTPとして取引を仲介してくれることによって、迅速な売買取引が可能になっています。


 たとえば民法でいう契約は、契約の申込みと申込みの承諾によって成立しますが、後で揉めないためにはそれらを記録するものとして契約書を作っておいたほうがいい。何らかの債務の不履行と思われる事態があった時に、「お前、契約したじゃないか」「いや、そんな契約はしてない」みたいな争いが起きることを防ぐこと、つまり後々「ウソを付けない」ようにすることが大事なわけです。
 で、民事訴訟法には「真正な文書の成立」について定めた条項があって、簡単にいえばサインしてあるかハンコが押してあれば、裁判所はそれを本物の文書であると認めるというルールになっています。もちろん細かく言えばもっと色々な要素によって契約システムが成り立っているわけですが、ここで確認したいのは、法律や慣行や司法システムという形の、人が「嘘をつきにくくなる」ようなプラットフォームが、もともと必要とされ、提供されているということです。もちろん、契約書だけではなく、納品や委託作業の完了を確認したことの記録とか、おカネを払ったことの記録になる領収書とかについても、似たようなことがいえます。


 ECでいうと、Amazonでモノを買う時、売り手と買い手の双方は色々な嘘をつくことが考えられます。それらがすべて防止されてるわけではないですが、何かあったらTTPであるAmazonに記録が残っていて、どちらの言ってることが正しいかが証明される。そのことによって、ある程度のウソ防止が可能だからこそ、取引のプラットフォームとして成り立っています。Amazonマーケットプレイスに出品しているお店なんて全然しらないわけで、相手を信用なんてしてないのですが、Amazonが仲介してくれてるし違法行為があったら裁かれるんだからということで、そこまで躊躇せずに取引ができる状態になっています。
 つまりですね、「信用されていない主体が取引に参加できる」ことがブロックチェーンの付加価値だと言われても、それはもともと、様々な形態のTTPによってこれまでも可能になってきたし、そもそも信用できる相手なんて世の中にそうはいないものであって、TTPに頼って取引をするというのが人類の経済活動においては常態であるわけです。その付加価値は、ブロックチェーンによって初めて獲得されるようなものではない。


 ブロックチェーンの場合は、ネットワークの全体が分散的にTTPと同じような役割を果たしている*1感じなわけですが、関心はそのほうが効率的なのかどうかという点にあるのだと思います。
 たとえば取引に参加している人たち同士の契約(双方の意思表示)の記録を、中央集権的なサーバで行うことにした場合、記録はそこにしかないわけなので、24時間365日確実にアクセスできることが求められるかもしれない。すると、システムの稼働を止めないために多大なコストがかけられることになります。また、そのサーバの提供元が不正をしたり、処理に間違いがあったりしては話にならないので、厳格な運用と監視の体制が求められます。
 一方ブロックチェーンの場合、方式は色々あるんでしょうけど、たとえば本人が電子署名を打って記録を作成し、ネットワークに参加している全ノードにばらまくことになります。そして何らかのコンセンサスプロトコルによって、その記録が正しいことの確認がされ、全ノードがそれを記録していきます。このことによってウソがつけなくなるわけですが、中央にめちゃめちゃ厳重に管理されているシステムがあるわけでもなく、またネットワーク全体で情報を共有しているので、一部のノードが停止したとしてもシステム全体が落ちるわけではない。


 どっちの方式でも、「ウソをつきにくくする」(否認防止)という価値を提供している点では同じです。もたらされる基本的な付加価値は同じ*2
 で、どっちが効率的なのかは場合によると考えられ、ひょっとしたらブロックチェーンのほうが効率的なケースもあるかもしれないからということで、いろいろ実証が行われているのだと思います。
 たとえば、順序を守って処理しなければならないとか、即時に処理をしなければならないといったことが多い場合、中央の巨大システムにデータを集めてから処理したほうが合理的かも知れませんが、そのへんがある程度ゆるくていいのであれば、中央に厳格な運用体制を敷くよりも、しょぼいコンピュータをネットワーク化して分散的に処理する方式で十分かもしれない。即時性や順序性を犠牲にしていいのであれば、取引参加者のコンピュータが太い回線でつながっている必要もなくなるので、通信コストも削減できるかもしれない。
 実際ビットコインは、取引の記録が正しいものとして承認されるのにある程度時間がかかりますが、これは要するに、「特定の組織に管理されたくない」というP2Pイデオロギーの信念を実現するために、即時性を犠牲にしてるわけですね。


 IoTが進展すると、何兆台というデバイスがネットワーク化されてくる(と言われている)わけですが、大量の低スペックなコンピュータが低スペックな回線でつながっている状態だと、中央にデータを集めて処理してまた返すみたいなやり方は成り立たなくなるから、当事者デバイス間(P2Pで)でいったんやりとりを完了させて先に進み、少し時間差を持って記録の正しさが確定していくようなブロックチェーン的なアーキテクチャのほうが向いてるだろう、みたいな話も聞いたことがあります。 

どういう視点で捉えておけば良いか

 先日会社で、スマートコントラクトにブロックチェーンの仕組みが登場する意味が分からないと言っている人がいたのですが、何で分からなくなったかといえば「ブロックチェーンだからこそ可能になる取引・業務がある」という話だと思ってしまったからです。実際はそうじゃなくて、取引や業務を可能にするシステム構成として、ブロックチェーンが他の方式よりも効率的となる(かもしれない)ようなケースもある(かもしれない)という話なわけですよね。
 とりわけ、言った言わないで揉めることがないように「正しい記録」を保持し続けなければならないという点に着目し、そのためのコストが論点なのだと理解すると、その点だけが重要なわけではないにしても、最近騒いでいる人たちが何を騒いでいるのかがよくイメージできる気がします。


 冒頭で引用した記事の視点がズレている気がすると先ほど書きましたが、もう少し正確にいうと、たしかに「ブロックチェーンの仕組みだからこそ可能になる取引や業務」があるという前提でイノベーションだ何だと煽っている人は多いので、そういう人に対しては冒頭の記事のようなツッコミを入れることは適切なのだと思います。ビットコインの印象が強烈だったので、なんか新たな経済原理が登場したみたいに思っちゃってる人はそれなりにいると思うんですよね。個人的には、ビットコインの仕組みには感心したので、本当に「新たな経済原理」が登場するまで突き詰められれば面白いなとは思います。
 しかし一方で、「もともと可能だった取引や業務のうち、ある種のケースでは、ブロックチェーン方式のほうが効率的に取り扱える」みたいな話をしている人もたくさんいます。冒頭の記事はそういう方向性の議論が視野に入っておらず、ひたすら「新しい価値が生まれるのか否か」を論じている(ちょっと生まれると主張している)ので、私が聞いたブロックチェーン界隈の話とはズレていると感じた次第です。


 先ほども言及した「パブリックチェーン」と「プライベートチェーン」の区別で言えば、冒頭の記事が論じているのはもっぱらパブリックの方だと思いますが、いま金融機関とかが取り組んでるのはどっちかというとプライベートなチェーンの方が多いと思います。ガチのエンジニアの人は、ビットコイン的なオープンでパブリックなシステムを構築するのを夢としているかもしれませんが。
 まぁ「どっちが多いか」を争っても仕方ないのですが、少なくとも、プライベートなブロックチェーンの取り組みがいくつも存在しているのは事実です。プライベートなブロックチェーンにおいては、当たり前ですが、「特定の組織に管理されたくない」みたいなビットコイン的理念はもはや全く関係なくなって、単に業務上の処理を記録していくための方式として効率が良いかどうかの話になります。



 なお私が述べてるのは、最近ブロックチェーン活用を検討している産業界や政府の人たちが何を話題にしているかという話なので、視野は狭いと思います。技術者の世界ではもっと色々幅のある議論がなされているのだろうと想像してます。
 あと、私は専門家ではないので、ブロックチェーンが有望なのかどうか、どういう場合に有望なのか、実際のところは判断できません。「世間で言われているのはこういうことではないですか」と述べているだけですので、悪しからず。

追記

 ブコメで、

ブロックチェーンは分散*合意形成*が肝で、単に分散化による耐障害性や投機実行なら他の方法より明らかに不利ってイメージだったんだけど、用語の指す範囲が微妙にずれてて噛み合ってないのかなあ。


 というコメントがありましたが、まさにそこがズレてきた点でもあると思います。
 金融機関でブロックチェーンやってる人の話をきいていて、「そうか、なるほど、プライベートチェーンの取組みをやってるんだな。いやまてよ、プライベートだったらもはや、合意形成プロトコルは必要なくね・・・?」と思って私も混乱しました。
 特定の企業(企業集団)が構築する、自社業務システムや自社サービス内に閉じたブロックチェーンネットワークなら、ネットワーク内で不正が起きることを防ぐみたいなことは必要なくなり、ビットコインが実現したある種の美しいコンセンサスモデルは、必要なくなる場合があります。
 で、そのコンセンサスモデルが美しいからこそブロックチェーンに関心を持ったのに、「プライベートだからそれは要りませーん」と言われると、なんかブロックチェーンの魅力が半分以上失われたような気持になってしまいますよね。


 今も、ビットコインのようなオープンな仮想通貨システムを夢見て会社を立ち上げたようなエンジニアの人たちは、パブリックチェーンの方にこだわりを持っていると思いますし、世間的にもブロックチェーンと言えばビットコインみたいなイメージがまだあるので、そのへんでズレが起きていると思います。

*1:Trusted Third Partyという言い方だとイメージと違うとは言える。果たしている役割が同じという意味。

*2:まぁ同じとまで言うとある意味言い過ぎなのはわかってる。

Pythonメモ: Pandasで文字列検索をループするとかなり遅かった

 前回のエントリで、Pandasのデータフレームを文字列で検索すると遅かったと書いたんですが、実際に検索の方法を変えるとどれぐらい違うのかを計測してみました。
 結論から言えば、辞書型に変換してから検索したらだいぶ速くなったのですが、それが凄いというよりは、Pandasの基本がまだ分かってないということかなと思いました。こうやると速いとか遅いとかいう記事はいろいろあるみたいなのでまた勉強したいと思います。


 一応、前回のエントリでやった方法を確認のため比較することとして、PN Tableに載っている単語をランダムに1万個選んだ上で、1個1個についてPN値を選択して取ってくるという処理の時間を測ります。
 Pandas内での工夫についてはよくしらないので、Pandasではない形で検索した場合と比べました。
 時間を図る方法は単純に、処理の直前と直後に時刻を取得して引き算するだけにしました。


 まずパターン1では、単純にPN Tableを読み込んだPandasのデータフレームで、単語の列を検索してヒットした行のPN値を取ってきます。
 次にパターン2では、単語の列とPN値の列をそれぞれリストにしておいて、先に単語のリストを検索してインデックス番号を取得し、そのインデックスでPN値のリストから値を取得します。
 最後にパターン3では、単語の列をリストにしたものと、PN値の列をリストにしたものを結合した辞書(dict型)を作っておき、この辞書をキーで検索してPN値を取得します。
 なお、PN Tableの行数は55125行です。

import pandas as pd
import random
import time

# PN Tableを読み込み(パスは適当に)
pn_df = pd.read_csv('dictionary/PN_Table/pn_ja.dic.txt',\
                    sep=':',
                    encoding='utf-8',
                    names=('Word','Reading','POS', 'PN')
                   )

# PN Tableに載っている単語を10000個抽出
word_list = list(pn_df['Word'])               # 語のリスト
searchlist = random.sample(word_list, 10000)  # 探す単語をランダムに決める


# --- パターン1:pandasで純粋に検索 --- #
start_time = time.time()          ### ▼時間計測▼ ###
pns1 = []
for w in searchlist:
    pn = pn_df.loc[pn_df.Word == w, 'PN']
    if len(pn) == 1:
        pns1.append(float(pn))
    else:
        pns1.append(0.0)
print(time.time() - start_time)   ### ▲時間計測▲ ###

# --- パターン2:リストにしてインデックスで検索 --- #
start_time = time.time()          ### ▼時間計測▼ ###
word_list = list(pn_df['Word'])               # 語のリスト
pn_list = list(pn_df['PN'])                   # PN値のリスト
pns2 = []
for w in searchlist:
    if w in word_list:
        ix = word_list.index(w)  # 先にインデックスを取得
        pn = float(pn_list[ix])  # 同じインデックスでPNリストにアクセス
    else:
        pn = 0.0
    pns2.append(pn)
print(time.time() - start_time)   ### ▲時間計測▲ ###

# --- パターン3:辞書にしてキーで検索 --- #
start_time = time.time()          ### ▼時間計測▼ ###
pn_list = list(pn_df['PN'])                   # PN値のリスト
pn_dict = dict(zip(word_list, pn_list))  # さっきのリストと合体して辞書に
pn3 =[]
for w in searchlist:
    if w in pn_dict:
        pn3.append(float(pn_dict[w]))
    else:
        pn3.append(0.0)
print(time.time() - start_time)   ### ▲時間計測▲ ###


 それぞれの時間をみてみると、

# パターン1
>>> 38.16759991645813

# パターン2
>>> 15.531062126159668

# パターン3
>>> 1.524580955505371


 となりました。何回かやってみたけどだいたい似たようなもんでした。
 辞書型に変えてから検索すると、このテストの処理で25倍速ということになりました。
 これが、昨日のエントリで行ったツイートの解析処理だと、色々ループしていることもあって、1万3000ツイートの解析を行った時間でいうと200倍速ぐらいになったわけです。


 そもそもPandasではない形にしてしまっているので無理矢理感があり、もっと勉強しないとなと思います。そもそもデータフレームに対してループで処理するのって適切なんですかね。whereなどSQLライクな関数で取ってくるべき?
 ただまぁ、とりあえず昨日やった処理のどの部分で時間がかかっているかを特定し、そこだけ方法を変えることで全体の処理が圧倒的に早まるという、目先の目的は達成されたのでよかったです。

【Python】MeCabと極性辞書を使ったツイートの感情分析入門

負のオーラを自動検出したい

 前回のエントリで、著作権侵害にあたる違法アップロード動画を自分のTwitterで拡散してしまっている懸念を考えて、YouTube動画のリンクが貼ってあるツイートをまとめて削除しました。
 前回のエントリでも言いましたが、著作権侵害モノ以外にも、「残しておくとまずいツイート」は色々ある可能性があり、たとえば誹謗中傷の類いがあるかと思います。誹謗中傷ツイートを自動抽出する方法はにわかには思いつきませんが、たぶん「クソ」とか「死ね」とか「バカ」とかそういう悪口の辞書が必要になりそうです。


 ところで、言語データの分析手法として、単語ごとに感情特性を評価した辞書というものがあちこちで作られていまして、これを使ってツイートがどのような感情を帯びているか分析するということが、よくやられています。Yahoo!がそういうツールを提供してたりもします(参考リンク)。
 Yahoo!のリアルタイム検索にキーワードを入れると(この場合は「ちきりん」)、そのキーワードに関係するツイートが流れてくると同時に、画面に右の方に、ポジティブなつぶやきが多いかネガティブなつぶやきが多いかのグラフが表示されます(この場合はネガティブが74%、ポジティブが0%、中立が26%)。


f:id:midnightseminar:20170508111557p:plain


 ひょっとしたら、感情分析の手法を用いて、自分のツイートの中から「負のオーラ」が漂っているものを自動的に抽出し、削除するというアプローチがあり得るかもしれません。
 というわけで今日は簡単な感情分析を行ってみました。


 最初に言っておきますが、今回やったような単純な手法では精度が低く、実用は厳しいです。あくまでちょっと試しにやってみました程度のアウトプットになっております。
 しかしそれでも、取っ掛かりとして手を動かして解析してみるというのは、色々知識が広がるものではあります。
 
 

さまざまな辞書

 感情分析に使う辞書ですが、私が今回使ったのは、東工大の高村教授が作って公開されている「PN Table」というやつです。
 PN Table


 この辞書の中身は、

めでたい:めでたい:形容詞:0.999645
賢い:かしこい:形容詞:0.999486
善い:いい:形容詞:0.999314
適す:てきす:動詞:0.999295
過小:かしょう:名詞:-0.942846
塗る:ぬる:動詞:-0.943453
器量負け:きりょうまけ:名詞:-0.943603
固まる:かたまる:動詞:-0.943991


 こんな感じで、単語に対応する極性情報が-1〜+1の間で割り当てられており、-1に近いほどネガティブ、+1に近いほとポジティブということになっています。後述するように、ゼロがニュートラルと言っていいのかはよく分かりません。


 日本語で似たような辞書はこれ以外にもありまして、たとえば東北大の乾・岡崎研究室のページで公開されている「日本語評価極性辞書」というものがあります。
 Open Resources/Japanese Sentiment Polarity Dictionary - 東北大学 乾・岡﨑研究室 / Communication Science Lab, Tohoku University


 他には、Yahoo!JAPAN研究所の鍜治伸裕さんという方が作られた「Polar Phrase Dictionary」というのがあり、東大のサイトにそのページがありますが、ダウンロード可能なものとして公開されてるわけではないようです。
 Polar Phrase Dictionary


 今回使ったPN Tableの作成に関する高村教授らの論文は以下の場所で読めます。
 高村大也,乾孝司,奥村学(2006).スピンモデルによる単語の感情極性抽出.情報処理学会論文誌ジャーナル,Vol.47,No.02,pp.627-637,2006.
 

 ポジとかネガとかのことを「極性」と呼んでいるわけですが、PN Tableは根性で大量の単語に極性を割り振っていったわけではなく、大部分の単語の極性情報が機械的に導出されています。
 物理学の理論を応用したモデルの詳細は、難しくて理解できねーと思いよく読んでませんが、「各電子のスピンは,上向きと下向きのうちどちらかの値をとり,隣り合ったスピンは同じ値をとりやすい.我々は,各単語を電子と見なし,単語の感情極性をスピンの向きと見なす.関連する単語ペアを連結することにより語彙ネットワークを構築し,これをスピン系と見なす.」そうです(汗)
 その利点として、「我々のモデルでは,平均場近似により語彙ネットワーク上の単語の感情極性が大域的に決定される.このような大域的な最適化を用いるからこそ,語釈文やコーパスのような,シソーラスと比べてノイズが含まれやすい(すなわち,隣り合っていても同じ極性を持たないことが起こりやすい)リソースを取り入れることが可能になるのである.最短距離を利用した手法や単純なブートストラッピングを利用した手法のような既存手法では,そのようなリソースを取り入れることはできない.」らしいです(汗)(汗)


 私にはよく分かりませんが、ともかくそういう理論物理学にヒントを得たモデルによって、まず、辞書・シソーラス・コーパスから「語彙ネットワーク」を形成しておき、そこにgood/badなどすでに判明している極性情報を注入してやることで、ネットワーク内の語彙に極性情報が伝搬されて、自動的に極性辞書ができあがるというプロセスのようです。たぶん。なんか近未来的です。
 論文の冒頭の先行研究レビューの部分をみると、単語の感情特性を求めるためにこれまでどのような手法が提案されてきたかも概観できて参考になります。
 
 

自分のツイートを評価してみる作業

 さて、PN Tableを使って自分のツイートを実際に評価してみます。
 今回は、とりあえず何となく数値っぽいものを計算するところまで行きたかったので、単純な処理だけやりました。


 前回のエントリでも使いましたが、自分のツイート全件を処理するときは、Twitterの公式サイトからダウンロードできる全ツイート履歴のCSVファイルを使うのが良いです。


f:id:midnightseminar:20170507012019p:plain


 これをPandasのデータフレームとして取り込むところから分析がスタートします。各種モジュールも最初にインポートしておきます。

# モジュールのインポート
import re
import csv
import time
import pandas as pd
import matplotlib.pyplot as plt
import MeCab
import random


# tweets.csvの読み込み
tw_df = pd.read_csv('tweets.csv', encoding='utf-8')


 このテーブルには、

  • tweet_id # ツイートごとのID(いわゆるstatus_id)
  • in_reply_to_status_id
  • in_reply_to_user_id
  • timestamp # 投稿日時
  • source # 投稿に用いたデバイス
  • text # 本文
  • retweeted_status_id
  • retweeted_status_user_id
  • retweeted_status_timestamp
  • expanded_urls # リンクが貼られている場合の、省略しない形のURL


 という10個のフィールドがありますが、今回必要とするのはtweet_idとtextだけです。
 次に、PN TableもPandasで読み込みます。先ほども例示したように、

めでたい:めでたい:形容詞:0.999645
賢い:かしこい:形容詞:0.999486
善い:いい:形容詞:0.999314
適す:てきす:動詞:0.999295
過小:かしょう:名詞:-0.942846
塗る:ぬる:動詞:-0.943453
器量負け:きりょうまけ:名詞:-0.943603
固まる:かたまる:動詞:-0.943991


 というような内容になっているので、read_csvのオプションで区切り文字を「:」と指定して読み込めばいいかと思います。*1

# PN Tableを読み込み
# パスは各自適当なものになります
pn_df = pd.read_csv('dictionary/PN_Table/pn_ja.dic.txt',\
                    sep=':',
                    encoding='utf-8',
                    names=('Word','Reading','POS', 'PN')
                   )


 まず、個々のツイートをMeCabで形態素解析して、単語に分けるとともにその基本形表記を取得します。MeCabの導入方法等は過去のエントリを参照してください。
 基本形を取得するのは、実際のツイート中に登場する未然形や連用形のように活用された形だと、極性辞書をサーチすることができないからです。


 MeCab Pythonでふつうに形態素解析をする場合、返ってくる情報は以下のような感じになります。

>>> print(m.parse('STAP細胞はあります。'))
STAP	名詞,固有名詞,組織,*,*,*,*
細胞	名詞,一般,*,*,*,*,細胞,サイボウ,サイボー
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
あり	動詞,自立,*,*,五段・ラ行,連用形,ある,アリ,アリ
ます	助動詞,*,*,*,特殊・マス,基本形,ます,マス,マス
。	記号,句点,*,*,*,*,。,。,。
EOS


 1行1語になっていることが分かります。最後にEOSという終了記号が付いて、さらに空行が1行ついてきます。
 これを後々どうやって扱うかなのですが、とりあえず私は深く考えずに、各行をdict型のデータに格納して、リストで連結しておくことにしました。

# MeCabインスタンス作成
m = MeCab.Tagger('')  # 指定しなければIPA辞書


# -----テキストを形態素解析して辞書のリストを返す関数----- #
def get_diclist(text):
    parsed = m.parse(text)      # 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'BaseForm':l[7]}
        diclist.append(d)
    return(diclist)


 これによって、1つのツイート本文が以下のような情報に変換されます。見やすくするために改行を入れますが、

[
   {'POS': '固有名詞', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'STAP'}, 
   {'POS': '一般', 'POS1': '名詞', 'BaseForm': '細胞', 'Surface': '細胞'}, 
   {'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'}, 
   {'POS': '自立', 'POS1': '動詞', 'BaseForm': 'ある', 'Surface': 'あり'}, 
   {'POS': '*', 'POS1': '助動詞', 'BaseForm': 'ます', 'Surface': 'ます'}, 
   {'POS': '句点', 'POS1': '記号', 'BaseForm': '。', 'Surface': '。'}
]


 こういう感じのリストです。
 品詞は何かに使うかもと思って一応取得したんですが、結局今回は使いませんでした。しかし修正するのが面倒なのでこのままにしておきます。
 あとで拡張していく時にも使うかもしれません。実際、1つの文章を形態素解析して「辞書のリスト」にすることにしておけば、後で色々使いまわせるような気もします。


 次に、上で得られた単語ごとのdict型データに、PN Tableから取った極性値を項目として追加したいと思います。
 最初にPN TableをPandasデータフレームとして読み込んであったので、ふつうに考えたらこのデータフレームを検索してPN値を取ってくればいいということになります。
 たとえば、

pn_df.loc[pn_df.Word == '細胞', 'PN']


 というような処理を繰り返せばPN値は取得してこれるのですが、この方法だと死ぬほど時間がかかります。
 最初、この方法で1万3000件のツイートの解析をやってみたのですが、CPU使用率が98.8%になり、処理がなかなか終わりませんでした。シャドウバースを2試合やっても終わらなかったので、外に出て剣道の素振りをして帰ってきたらようやく終わっていました。なので、たぶん30分以上はかかったと思います。


 そこで、Pandasのデータフレームを文字列で検索するのは非常に時間がかかるので、PN Table自体を{'単語':PN値, '単語':PN値, '単語':PN値...}という形のdict型データに変換した上で、単語をキーとしてアクセスしてPN値を取ってくる方法に変更したら約8秒で終わりました。8分ではなく8秒です。30分→8秒(約200倍速)。
 また、感情辞書をこういう形にしておくことにすれば、他の辞書を使った分析へと拡張するのもやりやすいような気がしました。

# PN Tableをデータフレームからdict型に変換しておく
word_list = list(pn_df['Word'])
pn_list = list(pn_df['PN'])  # 中身の型はnumpy.float64
pn_dict = dict(zip(word_list, pn_list))


# 形態素解析結果の単語ごとdictデータにPN値を追加する関数
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base])  # 中身の型があれなので
        else:
            pn = 'notfound'            # その語がPN Tableになかった場合
        word['PN'] = pn
        diclist_new.append(word)
    return(diclist_new)


 PN Tableに載っていない語をどうするかなのですが、今回は分析から除くことにしました。ゼロを割り当ててしまうとPN Table上で実際にゼロちょうどと評価されている単語(「週末」「巨体」「セレナーデ」など20語ある)なのか、載ってなかった単語なのかの区別がつかないので、適当にnotfoundと書いておきました。
 これで各ツイートが以下のような形式のデータになります。

>>> test_text = 'STAP細胞はあります。'
>>> dl_test = get_diclist(test_text)
>>> dl_test = add_pnvalue(dl_test)
>>> print(dl_test)
[
    {'PN': 'notfound', 'POS': '固有名詞', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'STAP'}, 
    {'PN': -0.746254, 'POS': '一般', 'POS1': '名詞', 'BaseForm': '細胞', 'Surface': '細胞'}, 
    {'PN': 'notfound', 'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'}, 
    {'PN': 'notfound', 'POS': '自立', 'POS1': '動詞', 'BaseForm': 'ある', 'Surface': 'あり'}, 
    {'PN': 'notfound', 'POS': '*', 'POS1': '助動詞', 'BaseForm': 'ます', 'Surface': 'ます'}, 
    {'PN': 'notfound', 'POS': '句点', 'POS1': '記号', 'BaseForm': '。', 'Surface': '。'}
]


 あとはPN値の平均をとるだけ(上記の文だとPN値を持った語が1つしかありませんが)なので、正直こんなゴツい形式のデータにする必要なかったと思いますが、先ほども述べたように「何かに使うかも」と思った経緯からこんな形になっております。

# 各ツイートのPN平均値をとる関数
def get_pnmean(diclist):
    pn_list = []
    for word in diclist:
        pn = word['PN']
        if pn != 'notfound':
            pn_list.append(pn)  # notfoundだった場合は追加もしない            
    if len(pn_list) > 0:        # 「全部notfound」じゃなければ
        pnmean = mean(pn_list)
    else:
        pnmean = 0              # 全部notfoundならゼロにする
    return(pnmean)


 全部notfound、つまりPN Tableに載っている単語を1語も含まないツイートを0点と評価するのはよく考えたら適切ではなく、分析から除外すべきですが、やりなおしが面倒なのでコードはこのままにしておきますw
 ここまでできれば、あとはツイートの1件1件に対して、「形態素解析」「PN値の追加」「PNの平均値の算出」を繰り返していき、1つのリストにまとめます。

# pn値のリストを作成(一応時間を測りました)
start_time = time.time()               ### ▼時間計測▼ ###
pnmeans_list = []
for tw in tw_df['text']:
    dl_old = get_diclist(tw)
    dl_new = add_pnvalue(dl_old)
    pnmean = get_pnmean(dl_new)
    pnmeans_list.append(pnmean)
print(time.time() - start_time)        ### ▲時間計測▲ ###


 これを、ツイート全件履歴データフレームの右端に追加して、PN極性値でソートし、CSVで吐き出します。

# 一応、本文テキストから改行を除いておく(最初にやれ)
text_list = list(tw_df['text'])
for i in range(len(text_list)):
    text_list[i] = text_list[i].replace('\n', ' ')


# ツイートID、本文、PN値を格納したデータフレームを作成
aura_df = pd.DataFrame({'tweet_id':tw_df['tweet_id'],
                        'text':text_list,
                        'PN':pnmeans_list,
                       },
                       columns=['tweet_id', 'text', 'PN']
                      )


# PN値の昇順でソート
aura_df = aura_df.sort_values(by='PN', ascending=True)


# CSVを出力(ExcelでみたいならUTF8ではなくShift-JISを指定すべき)
aura_df.to_csv('aura.csv',\
                index=None,\
                encoding='utf-8',\
                quoting=csv.QUOTE_NONNUMERIC\
               )

 
 

結果をみてみる

 さてどんな結果が得られたのかみてみたいと思います。
 まずは、最もネガティブな方から十数件をみてみますと、


f:id:midnightseminar:20170507012149p:plain


 ベッキーさんの不倫事件を擁護しているツイートが最もネガティブという判定になりました。いきなり誤評価です。
 なんでこんなことになったのか確認してみます。

>>> test_text = 'ベッキーは果たしてそんなに悪いのか'
>>> dl_test = get_diclist(test_text)
>>> dl_test = add_pnvalue(dl_test)
>>> for w in dl_test:
...     print(w)
... 
{'PN': 'notfound', 'POS': '一般', 'POS1': '名詞', 'BaseForm': '*', 'Surface': 'ベッキー'}
{'PN': 'notfound', 'POS': '係助詞', 'POS1': '助詞', 'BaseForm': 'は', 'Surface': 'は'}
{'PN': 'notfound', 'POS': '一般', 'POS1': '副詞', 'BaseForm': '果たして', 'Surface': '果たして'}
{'PN': 'notfound', 'POS': '一般', 'POS1': '副詞', 'BaseForm': 'そんなに', 'Surface': 'そんなに'}
{'PN': -1.0, 'POS': '自立', 'POS1': '形容詞', 'BaseForm': '悪い', 'Surface': '悪い'}
{'PN': 'notfound', 'POS': '非自立', 'POS1': '名詞', 'BaseForm': 'の', 'Surface': 'の'}
{'PN': 'notfound', 'POS': '副助詞/並立助詞/終助詞', 'POS1': '助詞', 'BaseForm': 'か', 'Surface': 'か'}


 要するに、このツイートのなかでPN Tableに載っていた単語が「悪い」しかなく、「悪い」はPN Table上では最もネガティブな語ということになっているので、最もネガティブなツイートという判定になったわけですね。
 そもそもこの文、「か」を付けた反語になっているわけですが、このように文法構造を無視して単語だけで評価すると無理があるということが、この1例からも分かります。
 「良くない」とか「優れていないというわけでもない」みたいな表現を的確に評価しようと思ったら、「良い」「優れる」の部分だけみるのではなく、例えば係り受け解析というのを行って、これらの表現が打ち消されたりしていないかをきちんと調べないといけません。
 係り受け解析にはCaboChaというツールがありますので、これは後日また使ってみようかと思います。


 また、「オムライスなう」がなぜネガティブな評価になるのかというと、

>>> test_text = 'オムライスなう'
>>> dl_test = get_diclist(test_text)
>>> dl_test = add_pnvalue(dl_test)
>>> for w in dl_test:
...     print(w)
... 
{'PN': 'notfound', 'POS': '一般', 'POS1': '名詞', 'BaseForm': 'オムライス', 'Surface': 'オムライス'}
{'PN': -0.9999969999999999, 'POS': '自立', 'POS1': '形容詞', 'BaseForm': 'ない', 'Surface': 'なう'}


 このように、「なう」が形容詞の「ない」として判定されており、ないは否定的な言葉なので、ネガティブな評価となってしまってるわけです。上の出力には出てませんが、別途確認したら「連用ゴザイ接続」という活用形として判定されてました。
 「なう」は一つの典型例ですが、要するにTwitter独自の表現を辞書に取り込まないと、適切に評価できないことが分かります。


 つぎにポジティブなほうから十数件をみてみましょう。


f:id:midnightseminar:20170507012214p:plain


 これはネガティブ側に比べれば比較的当たってる気もしますが、「楽天ソーシャルニュースってどこが面白いんだ」のように、反語的な表現が適切に評価できていないのは先ほどみたのと同じですね。


 「ない」の扱いはややこしいので、ややこしい処理を実装すべきなんですが、試しに逆に「ない」を辞書から削除して分析してみたらネガティブランキングは以下のようになりました。

# PN Tableから「ない」を削除
rem_ix = list(pn_df[pn_df.Word == 'ない'].index)  # 2個ある
pn_df = pn_df.drop(rem_ix)


f:id:midnightseminar:20170507123034p:plain


 次に、全体として極性値の分布がどうなっているのかをみてみます。
 matplotlibでヒストグラムを描きます。

x1 = list(aura_df['PN'])
plt.hist(x1, bins=50)
plt.title('P/N Frequency of My Tweets')
plt.xlabel("P/N value")
plt.ylabel("Frequency")


f:id:midnightseminar:20170507012228p:plain


 全体的に、負の値に偏っています。ゼロのところに山ができているのは、上述のとおりPN Tableに載っている単語を1語も含まないツイートが0点と評価されてるからで、これは適切な処理ではないので無視してください。
 ところでこの結果が、ネガティブなツイートが多いことを意味しているのかというと、そうでもない可能性があります。というのも、PN Table自体のヒストグラムも取ってみると、

x2 = list(pn_df['PN'])
plt.hist(x2, bins=50)
plt.title('P/N Frequency in PN Table')
plt.xlabel("P/N value")
plt.ylabel("Frequency")


f:id:midnightseminar:20170507012238p:plain


 こんなふうになっており、そもそも大半が負の値を持つ語であるということが分かります。そういう、辞書のクセなんでしょうが、結局どのへんがニュートラルなのかはよく分かっておりません。
 
 

まとめ

 今回は、負のオーラを発する自分のツイートを発掘するために、感情分析を試みました。結果的には、単にツール(MeCabや極性辞書)の使い方を学んだだけに終わり、精度的に使いものになるような処理はできてないため、「まとめて削除」まではしていません。


 その主な原因は、わざわざ分析してみなくても誰でも分かる当たり前のことですが、

  • 文法構造を考慮に入れていない
  • Twitterの独特の表現を辞書に取り込むことができていない

 といった点になると思われます。
 しかしまぁ、そのあたりに課題があるということを、具体例をもって体験できたので、勉強にはなりました。今後は、処理を少しずつ改善して、納得のいく結果が出るかどうかをまた検証していきたいと思います。


 最後に、感情分析に関する、参考になりそうな研究(日本語のもの)を列挙しておきます。


 山本湧輝,熊本忠彦,本明代(2015).ツイートの感情の関係に基づくTwitter感情軸の決定.第7回データ工学と情報マネジメントに関するフォーラム,E5-2.
 
 鳥倉広大,小町守,松本裕治(2012).Twitterを利用した評価極性辞書の自動拡張.言語処理学会第18回年次大会発表論文集,pp.551-554.
  
 菅原久嗣(2010).感情語辞書を用いた日本語テキストからの感情抽出,修士論文(東京大学).
 
 

補記

 あとで気づいたんですが、PN Tableには基本形だけみると同一となる語(形容詞の「ない」と助動詞の「ない」など)がいくつかあるので、基本形だけ見てPN値を取ってきているところの処理には、誤りが含まれる可能性があります。取り急ぎ修正はしてないです。以下は例です。
 こういうのを考えると、上で使わなかった、形態素解析結果の品詞情報が、マッチング精度を上げるのに使えますし、さらに項目を追加して読み方の情報も取っておくべきですね。

22 助ける たすける 動詞 0.998356
487 助ける すける 動詞 0.990702


55117 ない ない 形容詞 -0.999882
55120 ない ない 助動詞 -0.999997


37424 頭 がしら 名詞 -0.466818
40768 頭 あたま 名詞 -0.513451
42098 頭 ず 名詞 -0.534602
42411 頭 つむり 名詞 -0.539412
43175 頭 かぶり 名詞 -0.551798
45300 頭 つぶり 名詞 -0.591628
50473 頭 かしら 名詞 -0.777183
52463 頭 とう 名詞 -0.980576


1781 人気 にんき 名詞 0.967650
3272 人気 じんき 名詞 0.213135
3851 人気 ひとけ 名詞 0.114632
10822 人気 ひとげ 名詞 -0.141334


2303 縁 えん 名詞 0.887527
38778 縁 ふち 名詞 -0.485352
41377 縁 へり 名詞 -0.523025
43027 縁 えにし 名詞 -0.549426
43872 縁 ゆかり 名詞 -0.564371
50448 縁 よすが 名詞 -0.775915


37424 頭 がしら 名詞 -0.466818
40768 頭 あたま 名詞 -0.513451
42098 頭 ず 名詞 -0.534602
42411 頭 つむり 名詞 -0.539412
43175 頭 かぶり 名詞 -0.551798
45300 頭 つぶり 名詞 -0.591628
50473 頭 かしら 名詞 -0.777183
52463 頭 とう 名詞 -0.980576


 また、その件を掘っていて、PN Table内に不可解な情報をみつけました。

15907 ホーム ホームラン 名詞 -0.199562
19438 ホーム ホームスパン 名詞 -0.238954
21561 ホーム ホーム 名詞 -0.263255
21588 ホーム ホームドクター 名詞 -0.263565
21936 ホーム ホームステイ 名詞 -0.267942
23736 ホーム ホームドラマ 名詞 -0.289906
23854 ホーム ホームシック 名詞 -0.291620
26676 ホーム ホームグラウンド 名詞 -0.327826
28151 ホーム ホームルーム 名詞 -0.347714
28695 ホーム ホームストレッチ 名詞 -0.354835
32726 ホーム ホームヘルパー 名詞 -0.408128


11151 太刀 たちうち 名詞 -0.145596
21081 太刀 たちさばき 名詞 -0.257552
21521 太刀 たちうお 名詞 -0.262799
21820 太刀 たちすじ 名詞 -0.266431
22494 太刀 たちかぜ 名詞 -0.274693
28816 太刀 たちとり 名詞 -0.356338
33218 太刀 たち 名詞 -0.414187
36886 太刀 たちさき 名詞 -0.459479
48426 太刀 たちもち 名詞 -0.673302


1063 大人 たいじん 名詞 0.982811
2714 大人 だいにん 名詞 0.397852
3153 大人 おとなしい 形容詞 0.243448
3471 大人 おとな 名詞 0.178366
3786 大人 うし 名詞 0.124680
30217 大人 おとなびる 動詞 -0.375421
52038 大人 おとなげない 形容詞 -0.960539


2277 トップ トップ 名詞 0.911679
32105 トップ トップダウン 名詞 -0.400048
32413 トップ トップニュース 名詞 -0.404087
34165 トップ トップコート 名詞 -0.426013
37862 トップ トップマネージメント 名詞 -0.472922


1509 キング キング 名詞 0.974291
7688 キング キングメーカー 名詞 -0.092334
15140 キング キングサイズ 名詞 -0.191609


 自動生成された辞書なので、こういう誤りみたいなものも含まれてるんでしょうね。
 また、「人気」とか「大人」に関して実際にはほとんど使わない読み方が登録されているあたりに、このテーブルが、コーパスよりは日本語辞書を土台にして作られていることが伺えます。

*1:読み仮名のことを「reading」としているサイトがあったのでそうしましたが自信ないです。pronounciationとかphoneticとかが正解なのかな?POSというのは品詞(part of speech)のことです。PNはポジネガの値という意味で付けました。

エンジニア泣かせの「日本の住所」は誰が決めているのか

 住所のデータを機械で扱おうと思った時、日本の住所は「1の1」と「1丁目1番」と「1-1」のように表記が統一されていないこと、数字の部分だけでなく町名や字名の部分も複数の書き方があること、漢字の旧字新字が混じること、アメリカ等のようにカンマでの分かち書きがされていないことなどの理由によって、処理が難しいというのはよく知られた話です(分かち書き問題についてのわかりやすい記事はこちら)。
 それで苛立ったエンジニアの人が「なんで統一ルールを作らねぇんだよ!」とブチ切れたりすることがあったりするわけですが(私はエンジニアではないのでブチ切れません)、「じゃあその統一ルールは誰が作ればいいんだ?」と考えると、「そもそも誰が決めてるんだっけ?」という疑問に行き当たります。
 そこで、日本の住所が何に基づいてどのように決定されているのかについて、備忘のために要約しておきました*1。未確認事項が2点残っており、分かり次第追記します。というか知ってる方いたら教えてください。


 法的根拠のある権限に基づいて、誰かが定義したり決定したりしている住所というのは、以下の1〜5ですべて説明できるはずです(6は法的根拠なしです)。

  1. 市区町村名:当該地域の議会の議決を経て、都道府県知事が国に届け出る。
  2. 町名又は字名:地方自治法260条に従って、市区町村が議決の上都道府県知事に届け出、知事が告示する。
  3. 街区:町又は字の下の単位であり、「番地」とかが該当。住居表示に関する法律に従って市区町村が決める。
  4. 住居番号:街区に含まれる住居に番号を振るもの。「○番●号」の「●号」のこと。住居表示に関する法律に従って市区町村が決める。
  5. 地番:不動産登記法に基づいて登記所(法務省の出先機関)が決める、土地に振られた番号。住居表示とは別物なので注意。
  6. 部屋番号等:住居番号に集合住宅の番号まで含めている場合以外は、法律上の根拠はない。


 「2.町又は字」には、「大字」「小字」「町」「丁目」などが全部含まれます。「丁目(丁)」は、地方自治法の概念上は「町又は字」に含まれるので、たとえば「霞が関1丁目」で一つの町・字を構成しています。郵便番号は丁目の手前までを表すので、混乱しやすいです。
 またこれは、「市区町村」というときの「町」とは別モノです(間違える人はいないでしょうが)。私は昔、「大阪府豊中市新千里北町」という「町」に住んでましたが、こういう「町」のことです。


 京都市内の「上ル」とか「西入ル」とかは有名で(わかりやすい解説はこちら)、単位としては「町又は字」の上位(前に付ける)にあたるのですが、これは慣習的な呼称であって法的な位置づけはありません。つまり、誰かが法令に則って決定したり定義したりしているわけではない。
 ただしこれらは住基の情報として登録されているとのことなので、住民基本台帳法に則った市区町村長の権限による住民登録事務として、お墨付きが与えられているとは言えるかもしれません。
 なお、「京都市上京区今出川通浄福寺西入二丁目東上善寺町」のように、丁目が町の前に付いている場合がありますが、これが「町又は字」の一部なのか、慣習的な表記の一部なのか確認していません。そのうち分かったら追記します。


 「3.街区」には、「街区方式」と「道路方式」があり、「街区方式」の場合はたいてい数字になっていて、いわゆる「番地」(「○番●号」の「○番」)がこれに相当します。この「○番」の数字を街区符号と呼びます。街区符号は数字が一般的ですが、アルファベットや漢字の場合もあります。「道路方式」の場合は、住所は「○○通り●号」とかになります。


 「3.街区」と「4.住居番号」をあわせて、「住居表示」と言います。住居表示に関する法律は市街地を対象としたものなので、田舎では住居表示が整備されていません。その場合は、町・字より下の単位として「5.地番」が住所として用いられることになります。つまり「1 + 2 + 5」という住所になっているということです。


 集合住宅の部屋番号は、「4.住居番号」として定義される(つまり住居表示の一部になっている)場合と、「6.部屋番号等」に相当するような、住居表示に含まない「方書」として定義されている場合の両方があります。ちなみに方書は戸籍の登録時には含まないことになっています。


 「5.地番」は登記所、つまり国の出先機関が決めているので扱いがややこしく、住居表示によっても表現できるし地番によっても表現できるというような家があり得るわけですが、戸籍法では、戸籍を登録するときに住居表示を用いても地番を用いても良いことになっています。
 地番は、不動産登記法では「市、区、郡、町、村及び字」の下に付ける番号および枝番のことになってるんですが、この法律でいう「字」に、「2.町又は字」の「町」が含まれるのか分かりませんでしたので、わかったら追記します。
 「番外地」とはこの「地番」が振られていない土地(例えば、明治以来ずっと国有地である場所は登記されたことがないため地番がない)のことです。


 こうやって整理してみると、住所表記の統一ルールを作ると言った場合、そもそも「住居表示」が全ての建物をカバーしていないという点がネックになりそうですね。決定権限の異なる「地番」とまざった形で戸籍が作られているというのはややこしいです。
 京都市の例のような慣習的な呼称については、何通りもある可能性がありますが、どれか1個を選んで「町又は字」に取り込んで自治体が決定してしまえばいいような気もします。もともと法的根拠がないのだから、新たに定義することによって「歴史的な呼称が消えてしまう」というわけでもないでしょう。

*1:Wikipediaと法律と役所のホームページを読んだだけです