amacbee's blog

Pythonの話とかデータの話とか酒の話とか

Pythonの値渡しと参照渡し

これはPython Advent Calendar 2016 7日目の記事です.
アドベントカレンダーの日程を勘違いしてしまっていたので,社内向けに作ったPython資料を使いまわすことでお茶を濁します。。。

Pythonの値渡しと参照渡しについてまとめてみます.

Pythonでは,変数や関数に値を渡す場合すべて参照渡しで渡されています.例えば挙動だけ見れば値渡しに見えるint型でも,値が変更されるまでは元の値と同一のオブジェクトを参照しています.
例を見てみましょう.

>>> def foo(a):
...   print(a, id(a))
...   a += 1
...   print(a, id(a))
...
>>> b = 0
>>> print(b, id(b))
0 4297514880  # 'b' 用の領域が確保されている
>>> foo(b)
0 4297514880  # ここまで 'b' と同じ領域を参照している
1 4297514912  # ここで 'a' 用に別領域が確保される
>>> print(b, id(b))
0 4297514880  # 'b' の値は変更されていない

渡された値を変更した際に元の値自体も変更されてしまうかどうかは,オブジェクトの型に依存します.

  • 変更不可(Immutable)な型
    • int, float, str, tuple, bytes, frozenset 等
  • 変更可能(Mutable)な型
    • list, dict, set, bytearray 等

変更可能な型では,渡された値が変更されると元の値も変更されてしまいます.(新しく領域が確保されません)

>>> def foo(a):
...   print(a, id(a))
...   a.append(1)
...   print(a, id(a))
...
>>> b = [0]
>>> print(b, id(b))
[0] 4332159560
>>> foo(b)
[0] 4332159560     # 'b' と同じ領域を参照している
[0, 1] 4332159560  # 'a'に変更を加えても参照先が変更されない
>>> b
[0, 1]  # 'a'が変更された影響で'b'の値も変更されている

変更可能な型のオブジェクトをコピーして別のオブジェクトとして利用したい場合は,copy.deepcopy()関数を利用します.

>>> import copy
>>> a = [1, 2]
>>> print(a, id(a))
[1, 2] 4447440008
>>> b = copy.deepcopy(a)  # deepcopy()関数を利用して,値が同じだが参照先の違うオブジェクトを作成する
>>> b.append(3)
>>> print(b, id(b))  # 'a' とは参照先の違うオブジェクトが生成されている
[1, 2, 3] 4447398408
>>> print(a, id(a))  # 'b' の変更の影響を受けない
[1, 2], 4447440008

Pythonの参照渡しではまりがちなケースを2つほど紹介しておきます.

  • 変数の初期化時
  • 変更可能な値をデフォルト引数として使う時

まず変数の初期化のケースについて紹介します.例えば以下のような例です.

>>> a = [[]] * 2
>>> a
[[], []]
>>> a[0].append(1)  # aの0番目のリストに1を追加したい
>>> a
[[1], [1]]          # a中の全てのリストに1が追加されてしまっている

a = [[]] * 2の操作で,参照先が同じオブジェクトを2つ生成してしまっています.

# a[0]およびa[1]がそれぞれ同じ領域を参照している
>>> id(a[0])
4447397000
>>> id(a[1])
4447397000

上記のような初期化を行いたいケースでは,リスト内包表記を活用しましょう.

# リスト内包表記を使うと問題ない
>>> a = [[] for _ in range(2)]
>>> a
[[], []]
>>> a[0].append(1)
>>> a
[[1], []]
>>> id(a[0])
4447397512
>>> id(a[1])
4447396936

次に,変更可能な値をデフォルト引数として使うケースについて説明します.デフォルト引数とは,Pythonのキーワード引数に対しデフォルト値として設定する引数のことです.

>>> class A:
...   def __init__(self, hikisuuu=[]):
...     self.hikisuuu = hikisuuu

上記のクラスでは,hikisuuuのデフォルト引数として[]が定義されています.
このデフォルト引数ですが, クラスや関数が作成されたときに一度だけ作成される という性質をもっているため,度々以下のような現象が発生します.

>>> x = A()             # Aクラスのインスタンス'x'を作成
>>> x.hikisuuu.append(1)
>>> x.hikisuuu
[1]
>>> y = A()             # Aクラスのインスタンス'y'を作成
>>> y.hikisuuu.append(2)
>>> print(y.hikisuuu, id(y.hikisuuu))  # 'x'と'y'のインスタンス変数'hikisuuu'の参照先が同じになってしまっている
[1, 2] 4447397896
>>> print(x.hikisuuu, id(x.hikisuuu))
[1, 2] 4447397896

デフォルト引数はクラスが作成された際に1度しか初期化されません.そのため,xとyは全く違うインスタンスなのにも関わらず,両者のインスタンス変数hikisuuuが共有化されてしまっています.上記のようなケースを避けるために,Pythonでデフォルト引数を定義する場合は変更不可な値を利用することをおすすめします.
あるいはAクラスは以下の通り書き換えられます.

>>> class A:
...   def __init__(self, hikisuuu=None):
...     if hikisuuu is None:
...       self.hikisuuu = []

参考

#PyLadiesTokyo 1年の活動振り返り

この記事はGeekWomenJapan Advent Calendar 2016 3日目の記事です.

に引き続き,今年もPyLadies Tokyoの1年の活動を振り返ってみます.

PyLadies Tokyo

※ほとんど昨年の使い回し

f:id:amacbee:20151205131830p:plain:w400

PyLadies Tokyoは,女性Pythonistaのための国際的コミュニティPyLadiesの東京支部として,2014年9月に設立されました.2014年10月に初のMeetupを行ってから,定期的にMeetupや他コミュニティとのコラボイベント等を開催しています.2016年には,Meetup以外にもPyCon JP 2016に参加したり,輪読会を行ったり,合宿を実施したりしました.

PyLadies Tokyo Meetup(2016年)

PyLadies Tokyoは,月一回程度のMeetupを中心に活動しています.
2016年に行ったMeetupのテーマはこんな感じです.

また定期的なMeetupとは別に,以下のイベントを開催しました.

この他,Geek Women Japanさん主催のGeek Women New Years Party 2016Geek Women Japan 2016のイベントに共催させて頂いたり,PyCon JP 2016にPyLadies Tokyoとして参加させて頂きました!

PyCon JP 2016

PyLadies TokyoとしてPyCon JP 2016に参加しました.
私自身は当日顔を出せなかったのですが,PyLadies Tokyoスタッフのまあやさんがポスター発表をしてくれました.当日のブースの様子はこんな感じです.

当日ブースに来て頂いた皆様,本当にありがとうございました!

PyLadies Tokyo 二周年記念パーティー

PyLadies Tokyo唯一の,男性も参加可なイベントが二周年記念パーティーです!
今年も去年にまして様々なテーマのLTを聞きながら,参加者の皆様と一緒に二周年をお祝いしました.個人的にはGentooの話が一番興味深かったです.

全体写真を撮り忘れてしまったのでお祝いのケーキの写真を載せておきます笑.
この会はPyLadies Tokyoの活動を男女問わず知って頂く良い機会なので,今後とも開催していきたいです.

PyLadies Tokyo 秋合宿

去年に引き続き今年も合宿を行いました!
総参加者は12名で,一人のキャンセル者も出すことなく実施することができました.

イベントの様子はTogetterにまとまっているので,興味のある方はこちらも御覧ください.

togetter.com

お酒を飲みながらお互いの仕事について話したり,突発的なハンズオンが始まったり,Pythonについて相談し合ったり・・・と,おかげさまでかなり濃いイベントになりました!

最後に

振り返ってみると,1年目と比べても非常に精力的に活動した年になりました.個人的には,他コミュニティの方と一緒に面白い企画を色々打ち出せた点が良かったかなと思っています.来年もぜひどこかのコミュニティとコラボしたいです|д゚)チラッ
活動を手伝ってくれているスタッフの皆様,講師を引き受けて下さった皆様,そして何より参加者の皆様に感謝しています.ありがとうございました!

最後に12月のイベントについて宣伝させて下さい.

pyladies-tokyo.connpass.com

このもくもく会では,有志で集まってmicropythonで遊んでみる予定なので,興味のある方はぜひお申込み下さい!(もちろんマイコン以外でもくもくする方も大歓迎です)

その他,PyLadies Tokyoのイベントに興味がある方,Pythonを学ぶ女性仲間を増やしたいという方がいらっしゃいましたら,PyLadies JapanのSlackにぜひご参加下さい.(※Slackは女性限定です)

bit.ly

来年もよろしくお願いします.

scrapy-splashを使ってJavaScript利用ページを簡単スクレイピング

これは,クローラー/Webスクレイピング Advent Calendar 2016の1日目の記事です.

JavaScriptを利用したページをスクレイピングするためには,スクリプトを実行し,ページを適切にレンダリングする必要があります. 本記事では,そのようなケースに便利なPythonライブラリscrapy-splashを紹介します.

前置き

ScrapyやSplashを既にご存知の方は読み飛ばして下さい.

Scrapyとは?

Scrapyとは,Python製のクローリング・スクレイピングフレームワークです.フレームワークというだけあって,Scrapyにはクローリング・スクレイピングに便利なオプションがあらかじめ用意されています.

  • Scrapyに用意されている便利なオプション例
    • サイトクローリング間隔を設定
    • robots.txtを解釈したクローリングを自動的に実行可能

Scrapyを利用することで,クローリングやスクレイピングの初心者でもあっても相手サイトに迷惑をかけることなく,安全にクローラースクレイパーを作成することができます. より詳細な使い方等については,PyCon JP 2016で紹介した資料がありますのでそちらをご参照下さい.

speakerdeck.com

本記事では,pipを利用してScrapy環境を用意しました.

$ pip install scrapy

Splashとは?

Splashとは,Scrapinghub社が開発しているオープンソースJavaScriptレンダリングサービスです.

github.com

JavaScriptを実行してスクレイピングをする王道といえばPhantomJSやSeleniumを利用した方法があげられますが,Splashはこれらのツールとは違い,HTTP APIを経由してJavaScriptの実行や実行結果受取といったやりとりを行います.

  • Splashの特徴
    • スタート/ストップ時のオーバーヘッドがない
    • 複数のWebページを並列に処理可能
      • 同一Webサイトの複数のページをレンダリングする場合はキャッシュを共有出来る
    • Jupyter用のSplash Kernelをサポートしており,Jupyter Notebookを活用して便利なデバッグが可能

より詳細な説明が欲しい方は公式ドキュメントをご参照下さい. 本記事では,Splashのdockerイメージを利用してSplash環境を用意しました.

$ docker pull scrapinghub/splash
$ docker run -p 8050:8050 scrapinghub/splash

Scrapy + SplashでJavaScript利用ページをスクレイピング

前置きが長くなってしまいましたが,実際にScrapyとSplashを利用してJavaScriptを利用したページをスクレイピングする手順について紹介します.例として,以下の記事で紹介されている「テレ朝の特定ページから関連ニュースをスクレイピングする処理」を行います.

以下のページをスクレイピングします.

news.tv-asahi.co.jp

スクレイピング箇所を確認するために,ページのHTMLソースを表示してみましょう.関連ニュースを表示している処理は125行目付近にあります.

<!-- 関連ニュース -->
            <div id="relatedNews"></div>

これを見ると,単純にHTMLソースを取得しただけでは欲しい情報を得ることが出来ないことが分かります. Webページに表示されている情報は,JavaScriptを実行して関連ニュースをレンダリングした結果であるため,スクレイピングして欲しい情報を得るためには,JavaScriptを実行する処理を間にはさむ必要があります.

具体的な処理の流れ

Scrapy + Splashを利用した具体的な処理の流れは以下の通りです.
※ScrapyとSplashが入った環境を事前にご用意下さい
※Scrapyの基本的な使い方については紹介しませんのでご了承下さい

  1. scrapy-splashの導入
  2. Scrapyプロジェクトの作成&各種ミドルウェアの有効化
  3. スクレイピング項目の設定
  4. スクレイピング処理の記述
  5. スクレイピングの実行

実行環境は以下の通りです.

  • OS X EI Caption (10.11.6)
  • Python 3.5.2
    • Scrapy 1.2.1
  • Splash 2.3

作成したデモプロジェクトはGithubに置いてあります.

github.com

1. scrapy-splashの導入

ScrapyとSplashの連携には,scrapy-splashというライブラリを利用します.

github.com

scrapy-splashpipで簡単に導入出来ます.

$ pip install scrapy-splash  # ver. 0.7

2. Scrapyプロジェクトの作成&各種ミドルウェアの有効化

asahiという名前のScrapyプロジェクトを作成し,プロジェクトでscrapy-splashが利用できるように各種設定を追記していきます.

# asahiプロジェクトを作成
$ scrapy startproject asahi
$ cd asahi
$ tree .
.
├── asahi
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

scrapy-splashを利用するために,以下の5つの項目を設定します.

  • SPLASH_URL
  • DOWNLOADER_MIDDLEWARES
  • SPIDER_MIDDLEWARES
  • DUPEFILTER_CLASS
  • HTTPCACHE_STORAGE

それぞれ説明していきます.
まず最初にsettings.pyを開き,SPLACH_URLを追記します.この設定を行うことで,Scrapy側にSplashサービスの場所を教えています.

SPLASH_URL = 'http://[自分の環境のURL]:8050/'

次にsettings.pyDOWNLOADER_MIDDLEWARESの項目に設定を書き加えます.(デフォルトではコメントアウトされています)

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

ここではWebページのダウンローダーの設定をしています.scrapy_splashミドルウェアHttpProxyMiddleware(デフォルト値: 750)よりも優先させる必要があるため,scrapy_splashミドルウェアの設定値は必ず750よりも小さい値を指定して下さい.(上記をそのまま利用して頂いて問題ありません)

同様にSPIDER_MIDDLEWARESの項目に設定を書き加えます.(デフォルトではコメントアウトされています)

SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}

この項目を指定することで,Splashのcache_args機能を有効化できます.この設定によりリクエスト毎に変更されない引数 (e.g. lua_source) についてはSplash側に1度しか送信されなくなるため,リクエストキューのメモリ使用量が減り,リクエストに利用するネットワークトラフィックも削減出来ます.

最後にDUPEFILTER_CLASSHTTPCACHE_STORAGEを追記します.

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'
HTTPCACHE_STORAGE = 'scrapy_splash.SplashAwareFSCacheStorage'

現状のScrapyにはリクエストのFingerprint計算をグローバルにオーバーライドする方法が提供されていないため,上記2つのオプションで対応する形になっています.(リクエストのFingerprintを確認することで,同一リソースへの多重アクセスを抑止出来ます)
※こちらは将来的には変更されるようです

3. スクレイピング項目の設定

items.pyを編集してスクレイピング項目(=関連ニュース)を設定します.

import scrapy

class AsahiItem(scrapy.Item):
    related_news = scrapy.Field()  # 関連ニュース

4. スクレイピング処理の記述

scrapy genspiderコマンドを利用してクローラーの雛形を作成します.

scrapy genspider news news.tv-asahi.co.jp

作成した雛形はspiders/news.pyに保存されているため,こちらを編集していきます.
Splashを経由してJavaScriptを解釈したページを取得するために,NewsSpiderクラスにstart_requestsメソッドを作成し,WebページへのリクエストをSplashを経由したもの(SplashRequestクラスを使ったもの)に置き換えます.
waitパラメータでページレンダリングまでの待ち時間を設定出来ます
args引数にhttp_methodパラメータやbodyパラメータを設定することで,POSTリクエストにも対応可能です

def start_requests(self):
    yield SplashRequest(self.start_urls[0], self.parse,
        args={'wait': 0.5},
    )

SplashRequestで受け取ったresponseJavaScriptを解釈済みのHTMLなため,parseメソッドの記述は通常のHTMLページをパースする場合と同様に書けます.
最終的なnews.pyの中身は以下のようになりました.

from ..items import AsahiItem
from scrapy_splash import SplashRequest
import scrapy

class NewsSpider(scrapy.Spider):
    name = "news"
    allowed_domains = ["news.tv-asahi.co.jp"]
    start_urls = ['http://news.tv-asahi.co.jp/news_society/articles/000089004.html']

    def start_requests(self):
        yield SplashRequest(self.start_urls[0], self.parse,
            args={'wait': 0.5},
        )

    def parse(self, response):
        for res in response.xpath("//div[@id='relatedNews']/div[@class='kanrennews']/ul/li"):
            item = AsahiItem()
            item['related_news'] = res.xpath(".//a/text()").extract()[0].strip()
            yield item

5. スクレイピングの実行

実際にスクレイピング処理を実行してみましょう.scrapy crawlコマンドを利用してスクレイピングを実行し,結果をresult.csvに保存します.

$ scrapy crawl news -o result.csv

実行結果は以下の通りになりました.

related_news
豊洲市場“盛り土問題” 歴代市場長ら18人処分へ
豊洲移転は早ければ“来年冬” 小池知事が表明
豊洲市場への移転 安全性確保されれば早くて来年冬
負担増す市場業者から切実な声 豊洲市場移転問題
豊洲市場 地下の大気から指針値超える水銀検出

無事に欲しかった結果を受け取れました!

おまけ

JavaScriptを利用したページについても,scrapy shellコマンドを利用してインタラクティブresponseの結果を確認することができます.(URLの頭にhttp://localhost:8050/render.html?url=をつけて,Splash経由でレスポンスを受け取っています)

scrapy shell 'http://localhost:8050/render.html?url=http://news.tv-asahi.co.jp/news_society/articles/000089004.html'

参考

転職して一週間がたちました

転職して1週間がたち,新しい生活サイクルにも慣れてきましたので近況報告をします.
3月1日から DATUM STUDIO 株式会社で働き始めました!

DATUM STUDIO株式会社は,データ分析のスペシャリストを集め,解析コンサルから分析基盤構築まで,データ活用に関する様々なことを一手に請け負う会社です.(会社説明がこれで良いのか自信がない)
会社名を出すと,データ界隈の方からは「てっきりPythonの会社に行くかと思ってたんですがRの会社に行ったんですね!」と驚かれることが多かったので,その辺の話を含めて入ってからの所感をまとめたいと思います.

  1. Rの会社なのか?
    私も入るまでみんなRを使っている会社だと思っていたのですが,実際はPythonとR半々くらいで使われています.(若干R勢の方が多いかも?)折角なのでPythonの布教活動に勤しみたいと思っています.(ご指摘を受けたのですが)このページにPythonを入れて貰えるように頑張りますね!!!!!!!!
    datumstudio.jp

  2. 業務の感じ
    常時複数の解析やら基盤構築やらの案件が走っていて,基本的に各人が己の担当をこなしていく感じです.個人的に面白いな〜と思うところは,色んなデータ・アルゴリズムに短いスパンで触れる機会が多いところです.まだたかだか一週間しかたっていないのですが,「そのアルゴリズム名久しぶりに聞いた!」という事案が頻発しました.解析に関する様々な議論がすぐ側で行われているという状況は中々刺激的です.楽しい

  3. 会社の雰囲気
    ひとことで言うと大学の研究室みたいな感じです.(こう言えば多くの人に伝わるんじゃないかなと)
    仲が良いです.あとみんなお酒が大好きで,(私が言うのもなんですが)飲み過ぎじゃない?というくらい飲んでます.入って馴染めるか心配していたんですが杞憂に終わりそうで良かった.
    あと全体的にフリーダムな印象です.入社して4日目に社員みんなで謎にディズニーシーに行くイベントがあったのですが,現地に出向いて数時間,各人自由に行動し過ぎて結局最後まで全員合流出来ませんでした\(^o^)/
    ついさっきまで隣に立っていたはずの方がいつの間にか乗り物に乗っていてこちらに向け手を振っているみたいな現象に遭遇したのは初めてです.めっちゃ笑いました,ありがとうございます(!?)

大分酔っ払って書いたのでおかしな文章になっているかもしれませんが,楽しく過ごしているのでその様子が伝わっていると幸いです! (多分大丈夫問題ないと思うので)良かったらお気軽に遊びにきて下さいねヾ(๑╹◡╹)ノ"

最後に一言

転職に際し,応援のメッセージを下さった皆様,転職祝いを送って頂けた皆様,色々と相談にのってくれた皆様,本当にありがとうございました!!! これからも引き続き頑張ります!(`・ω・´)ゞ

退職します

タイトルの通り新卒で入社して1年11ヶ月在籍した株式会社VOYAGE GROUPを退職します.
2月26日が最終出社日でした.

退職理由は私事なのでこちらには書きませんが,この場を借りてチームの皆様,#ajiting勢の皆様,同期の皆様,ならびにVOYAGE GROUPで私を支えてくれた全ての皆様へ,心からの感謝を述べたいと思います.本当ありがとうございました!お世話になりました.

振り返り

在職中にVOYAGE GROUPの子会社である株式会社adingo(現:fluct)と株式会社Zucks,2つの会社を経験しました.どちらにおいても私が担当したのはアドテク分野のデータ分析(といったら通じるはずとアドバイス頂いた)の仕事です.
入社した当初,アドテクのデータを分析している人がおらず,これからどうしていけば良いか?を考えるところからスタートしました.先輩エンジニアと協力しながらデータを整理し概要を明らかにしたり,解析基盤を整えたり(Jupyter Notebookには大変助けられました),論文を読んで他社事例を調査したり,広告配信システムについて教えて頂きながら必要な要件を吟味したり,アルゴリズムの選定から実験まで手探りながらも色々なことを経験させて頂きました.

控えめに言っても,成功より失敗の方が多かったと思います.
取り組んでみて実感したのは,シンプルで(自分たちのシステムにとって)導入コストが低く,かつ効果の高い手法を見つけるのは非常に難しいということでした.この当たり前のような話が何よりも身に沁みました.その上で,本当に欲しい機能のためにstep-by-stepでアルゴリズムを改良していくにはどうしたら良いか?をいつも考えるようになりました.機械学習を実世界でうまく使うのは難しい.これからもずっと悩まされるんだろうなと思います.
でもなんだかんだいいつつデータをみるこの仕事は好奇心も知識欲も刺激されて楽しくてワクワクして最高で大好きです\(^o^)/

振り返りでもう一つ外せないのが#ajitingです. #ajitingについては,前に以下の記事でも取り上げさせて頂きました.

amacbee.hatenablog.com

VOYAGE GROUPのAJITOという場所は,定時後にお酒を飲みながら馬鹿な話から業務の話まで色々と語れる素晴らしいところです.特にエンジニアの方々が夜毎に集まっていて,いつ行っても誰かいます.中でも私は夜だけではなく朝もAJITOを利用していたため「AJITOの主」とか「Pro AJITOr」とか呼ばれていました.
もうAJITOの指定席に座ることもないのかと思うと寂しい気持ちになりますが,積極的に遊びに行くのでぜひAJITOに呼んで頂きたいです.よろしくお願いしますmm

おわりに

書いて振り返ってみるとあっという間だったなぁという感じです.
3月1日から新しい職場で働き始める予定なのですが,勤務地が引き続き渋谷なので飲みとか飲みとか飲みとか誘って頂けると喜びます〜

これからもよろしくお願い致します!

最後に一つだけ.
ほしい物リストはこちらになります:転職祝い
お酒やつまみが届いたら宅飲みが開催される可能性があります(あるいはどこかに持ち込みます)

広告を非表示にする

MLBの野球データを用いた岩隈投手の分析 ~ セイバーメトリクスを添えて ~ (野球Hack入門編)

※この記事はPython Advent Calendar 2015の19日目の記事です(大遅刻すみません。。。)
※野球データうんぬん書いてありますが@shinyorkeさんの書いた記事ではありません
※元祖野球Hackを求めている方は本家のブログをご覧ください

ソフトバンクホークス優勝おめでとうございます

皆様,2015年の野球生活はどうでしたか?
私はといいますと,馬原選手が引退を表明して寂しい思いもしていますが,ホークスが圧倒的な優勝を果たして,柳田選手はトリプルスリーをとって,例年以上にドラフトもうまくいったし,最初から最後まで満足度の高い一年でした.*1
この盛り上がった一年を締めるのもやはり野球でしょうということで,この記事では野生の野球アナリストである@shinyorkeさんがつい先日整備・公開してくれたMLBの野球データを用いて,2015年の岩隈投手の活躍をセイバーメトリクスを使って分析していきます.*2

shinyorke.hatenablog.com

セイバーメトリクス

@shinyorkeさんのコンテンツを引用しまくってて大変恐縮ですが,以下をご参照下さい(丸投げ)

shinyorke.hatenablog.com

一言でいうと,データをもとに野球選手の能力を分析する指標です.最近はデータを使ってスポーツの戦略をたてたり選手の評価をしたりが流行ってますよね.
面白そう!と思った方には映画「マネーボール」をお勧めします.

データの用意

リポジトリをご参照下さい(丸投げ 2nd)

github.com

手順通りに進めると問題なくデータのdownload・parse処理が行われます.今回は2015年のデータを利用しました.*3

データの前処理

2015年のデータ(csv/events-2015.csv)をpandasのDataFrameに突っ込みます.

>>> import pandas as pd

>>> df = pd.read_csv('csv/events-2015.csv', low_memory=False)
>>> df.shape  # 行数と列数の確認
(189591, 160)
>>> df.columns[:20]  # 具体的な列の例
Index(['GAME_ID', 'AWAY_TEAM_ID', 'INN_CT', 'BAT_HOME_ID', 'OUTS_CT',
       'BALLS_CT', 'STRIKES_CT', 'PITCH_SEQ_TX', 'AWAY_SCORE_CT',
       'HOME_SCORE_CT', 'BAT_ID', 'BAT_HAND_CD', 'RESP_BAT_ID',
       'RESP_BAT_HAND_CD', 'PIT_ID', 'PIT_HAND_CD', 'RESP_PIT_ID',
       'RESP_PIT_HAND_CD', 'POS2_FLD_ID', 'POS3_FLD_ID'],
      dtype='object')

このデータ中には,2015年の試合で発生した様々なイベント(e.g. ピッチャーがストライクをとった,盗塁が行われた,打者がHRを打った)の情報が収められています.df.shapeで確認すると分かりますが列数が160もあります.それぞれの列や値が何を意味するのかを調べる気力がわかなかったので本家サイトをご確認下さい.
参考:Play-by-Play Data Files (Event Files)

これがセイバーメトリクス初心者に立ちはだかる最初の関門です.データの種類が膨大&独自記号で定義されているためぱっと見で何が何だか分からない仕様になっていて大分とっつき辛いです.後ほど調べて分かったのですが,Lahman’s Baseball Databaseというデータもあって,こちらはある程度統計情報まとめられてるようなので,つらさに耐えられなくなったら最初はこちらを触ってみるのも手かもしれません.

岩隈投手の成績を他投手と比較するために,投球回(Inning Pitched)の多い選手上位200名をピックアップしました.

投球回(とうきゅうかい、英: Innings pitched / IP)は、野球における投手記録の一つで投手が登板したイニングの数を表す。イニングの途中で投手が交代した場合には、登板時に取った(攻撃側に記録された)アウト一つにつき1/3ずつを加える。 by 投球回 - Wikipedia

# GAME_ID: 試合ID
# INN_CT: イニング
# PIT_ID: ピッチャーID(岩隈投手のIDはiwakh001)
# EVENT_OUTS_CT:該当イベントでアウトが発生した回数
>>> pitchers_df = pd.DataFrame(df[['GAME_ID', 'INN_CT', 'PIT_ID', 'EVENT_OUTS_CT']].groupby(['PIT_ID']).sum().EVENT_OUTS_CT)
>>> pitchers_df.columns = ['SUM_EVENT_OUT_CT']
>>> pitchers_df['IP'] = pitchers_df.SUM_EVENT_OUT_CT / 3
>>> target_pitchers_df = pitchers_df.sort_values(by='IP', ascending=False).head(200)
>>> target_pitchers_df = target_pitchers_df.reset_index(0)

# 岩隈投手の投球回 (129 2/3)
>>> print(target_pitchers_df[target_pitchers_df.PIT_ID.str.contains('iwak') == True])
       PIT_ID  SUM_EVENT_OUT_CT          IP
106  iwakh001               389  129.666667

(投球回の多い上位200人に限らず)投手全員の投球回のヒストグラムは以下のような感じです.上位200人で見た場合,最大投球回は232 2/3,最小投球回は69でした.

f:id:amacbee:20151222213835p:plain:w400

ついでに岩隈選手の基本的な統計データも確認しておきましょう.EVENT_CDという列に発生したイベントの情報が確認されています.

# ピッチャー別に見た各イベントの統計
>>> event_df = pd.crosstab(target_df.PIT_ID, target_df.EVENT_CD)
>>> event_df = event_df.reset_index(0)
>>> event_df = event_df.merge(target_pitchers_df)

# 各イベントIDをイベント名に変換
# 参考:http://www.retrosheet.org/datause.txt
>>> id2event = {0: "Unknown event", 1: "No event", 2: "Generic out", 3: "Strikeout", 4: "Stolen base", 5: "Defensive indifference", 6: "Caught stealing", 7: "Pickoff error", 8: "Pickoff", 9: "Wild pitch", 10: "Passed ball", 11: "Balk", 12: "Other advance", 13: "Foul error", 14: "Walk", 15: "Intentional walk", 16: "Hit by pitch", 17: "Interference", 18: "Error", 19: "Fielder's choice", 20: "Single", 21: "Double", 22: "Triple", 23: "Home run", 24: "Missing play"}

# 岩隈選手の基本的な統計データを確認
>>> for col, val in event_df[event_df.PIT_ID.str.contains('iwak') == True].iteritems():
>>>    if col in ('PIT_ID', 'SUM_EVENT_OUT_CT', 'IP'):
>>>         continue
>>>    print('{0}: {1}'.format(id2event[col], int(val)))
Generic out: 264
Strikeout: 111
Stolen base: 1
Defensive indifference: 0
Caught stealing: 2
Pickoff: 0
Wild pitch: 1
Passed ball: 0
Balk: 0
Other advance: 1
Foul error: 0
Walk: 20
Intentional walk: 1
Hit by pitch: 1
Interference: 0
Error: 1
Fielder's choice: 1
Single: 75
Double: 23
Triple: 1
Home run: 18

2015年の岩隈投手を分析 with セイバーメトリクス

何となくデータが揃ったところで,投手の代表的な評価指標であるK/BBDIPSWHIPを用いて岩隈投手の2015年の活躍を分析してみます.各評価指標の基準については以下のサイトを参考にしました(※おそらくNPB仕様)

www.softbankhawks.co.jp

K/BB - Strikeout to Walk ratio

K/BBとは,四球を1つ出すまでにいくつの三振を奪っているかを数値化したもので,数値が大きいほどコントロールが良く,なおかつ多くの三振を奪える投手であることを示しています.
以下の式で計算します.

K/BB = 奪三振数 / 与四球数

数値の基準は以下の通り.

  • 4.00前後:球界を代表するクラス
  • 3.00前後:リーグを代表するクラス
  • 2.00前後:平均的なクラス
# K/BB = 奪三振数 / 与四球数
>>> event_df['KBB'] = event_df[3] / event_df[14]
>>> event_df['KBB_RANK'] = event_df['KBB'].rank(ascending=False)

# K/BB値が高い上位5人
# PIT_IDの変換表:http://www.retrosheet.org/retroID.htm
>>> print(event_df.sort_values(by='KBB_RANK').head(5)[['PIT_ID', 'KBB']])
EVENT_CD    PIT_ID        KBB
162       salaf001  10.571429  # Salas, Fernando
169       schem001   8.625000  # Scherzer, Max
145       pinem001   7.428571  # Pineda, Michael
101       kersc001   7.341463  # Kershaw, Clayton
30        colob001   7.157895  # Colon, Bartolo

# 岩隈投手のK/BB(200人中14位)
>>> print(event_df[event_df.PIT_ID.str.contains('iwak') == True][['PIT_ID', 'KBB', 'KBB_RANK']])
EVENT_CD    PIT_ID   KBB  KBB_RANK
92        iwakh001  5.55        14

投手200人のK/BBのヒストグラムは以下の通りです.

f:id:amacbee:20151222214533p:plain:w400

岩隈投手のK/BB値は他投手と比較して高く優秀な成績であることが伺えます.「コントロール・アーティスト」と呼ばれる岩隈投手の制球力の高さがスコアにあらわれていますね(多分)

DIPS - Defence Independent Pitching Statistics

DIPSとは,インプレイ(フィールド内に飛んだ打球)の要素を削除して奪三振・与四球・被本塁打の数値のみを考慮した指標で,投手本来の能力に着目しています.数値がゼロに近いほど四死球が少なく,三振を奪える投手であることを示しています.
以下の式で計算できます.*4

DIPS = {(与四球 - 故意四球 + 死球) * 3 + 被本塁打 * 13 - 奪三振 * 2} / 投球回数 + 3.12

数値の基準は以下の通り.

  • 1.50前後:球界を代表する投手
  • 2.50前後:球団内のエース級
  • 3.50前後:平均的な投手
# DIPS = (与四球 - 故意四球 + 死球) * 3 + 被本塁打 * 13 - 奪三振 * 2} / 投球回 + 3.12
# 故意四球を考慮できていない(要修正)
>>> event_df['DIPS'] = ((event_df[14] + event_df[16]) * 3 + event_df[23] * 13 - event_df[3] * 2) / event_df['IP'] + 3.12
>>> event_df['DIPS_RANK'] = event_df['DIPS'].rank()

# DIPS値が低い上位5人
>>> print(event_df.sort_values(by='DIPS_RANK').head(5)[['PIT_ID', 'DIPS']])
EVENT_CD    PIT_ID      DIPS
0         allec002  1.720962  # Allen, Cody
98        kersc001  1.963840  # Kershaw, Clayton
174       smitc004  1.977143  # Smith, Carson
60        gilek001  2.034286  # Giles, Ken
5         arrij001  2.307773  # Arrieta, Jake

# 岩隈投手のDIPS(200人中85位)
>>> print(event_df[event_df.PIT_ID.str.contains('iwak') == True][['PIT_ID', 'DIPS', 'DIPS_RANK']])
EVENT_CD    PIT_ID      DIPS  DIPS_RANK
89        iwakh001  3.698406         83

投手200人のDIPSのヒストグラムは以下の通りです.

f:id:amacbee:20151222214558p:plain:w400

岩隈投手は平均的なスコアになっています.ただ岩隈投手の特徴として「ゴロクマ」と呼ばれるほど打たせてとるタイプであることがあげられるので,この指標で岩隈投手の能力を測るのは難しいかもしれません.

WHIP - Walks plus Hits Inning Pitched

WHIPとは,一イニングあたり何人の走者を出しているのかを示す指標で,数値がゼロに近いほどピンチを招く頻度の少ない投手であることを示します.
以下の式で計算できます.

WHIP = (被安打 + 与四球) / 投球回

数値の基準は以下の通り.

  • 1.00未満:球界を代表する投手
  • 1.20未満:球団内のエース級
  • 1.40以上:安定度に欠ける投手
# WHIP = (被安打 + 与四球) / 投球回数
# 被安打 = 一塁打 + 二塁打 + 三塁打 + HR

>>> event_df['WHIP'] = (event_df[14] + event_df[20] + event_df[21] + event_df[22] + event_df[23]) / event_df['IP']
>>> event_df['WHIP_RANK'] = event_df['WHIP'].rank()

# WHIP値が低い上位5人
>>> print(event_df.sort_values(by='WHIP_RANK').head(5)[['PIT_ID', 'WHIP']])
EVENT_CD    PIT_ID      WHIP
67        greiz001  0.839820  # Greinke, Zack
5         arrij001  0.855895  # Arrieta, Jake
98        kersc001  0.876791  # Kershaw, Clayton
77        harrw002  0.887324  # Harris, Will
136       osunr001  0.889952  # Osuna, Roberto

# 岩隈投手のWHIP(200人中26位)
>>> print(event_df[event_df.PIT_ID.str.contains('iwak') == True][['PIT_ID', 'WHIP', 'WHIP_RANK']])
EVENT_CD    PIT_ID      WHIP  WHIP_RANK
89        iwakh001  1.056555         26

投手200人のWHIPのヒストグラムは以下の通りです.

f:id:amacbee:20151222214616p:plain:w400

岩隈投手のWHIP値は他投手と比較して低い値となっており,ランナーをあまり出すことのない優れた投手であることが分かります.また,DIPSが比較的平均の値なのに対しWHIPが低いということは,三振よりもゴロを打たせるタイプの投手であるということを示しているのかもしれません.

まとめ

本記事では岩隈投手の2015年の成績にフォーカスをあててセイバーメトリクス入門させて頂きました.MLBのトップ層と比較しても堂々たるパフォーマンスを発揮していて,岩隈投手本当にすごい!ちなみに,Lahman’s Baseball Databaseの方に選手のSalaryのデータがあるので突き合わせてみると面白いかもしれません.

まともにretrosheetを触ったのは今回が初だったのですが,セイバーメトリクスの計算式自体は難しくないけどデータ形式が特殊で,最初の一歩までが大変な印象を受けました.各指標を計算してくれるAPIが欲しいですね(2016年の野球Hackネタなのでは)
正直複雑過ぎて結果の正確性に自信がないので皆様からのご指摘等お待ちしています.あと値の解釈があってるのかもちょっと自信ないです.*5

それにしても野球データは基本的に前処理超辛いのが多いので,すぐにいじれる環境があるってステキです.@shinyorke++
こんな感じでNPBのデータをよしなにいじれるやつも欲しry|д゚)

*1:Baseball Play Study 2015 プロ野球ふりかえりスペシャルに参加できなかったのは悔しい

*2:一部の方に予告していた「MLBの野球データを用いた岩隈の期待年収予測」のネタは引っ込めさせて頂きました.理由についてはお察し下さい

*3:今回はCSV形式のデータをそのまま利用しましたが,MySQL上にデータを持つことが可能になっています

*4:DIPSの計算方法に諸説あるようです.詳しくはリンクを参照して下さい:DIPS (野球)

*5:そもそも正確に値を出していませんが。。。

快適な昼 #ajiting のススメ

※この記事は#ajiting Advent Calendar 2015 9日目の記事です.
※VOYAGE GROUPの社員向けに振り切った記事です.AJITO利用経験者でも「なんのこっちゃw」という話が多めですがご容赦下さい.

f:id:amacbee:20151208101051j:plain:w300 f:id:amacbee:20151208101109j:plain:w300

What is #ajiting ???

#ajiting とは,VOYAGE GROUPの社内Bar「AJITO」で行われる活動全般を指します.
最近では社員 (+α) が集まってわちゃわちゃすることも広義の #ajiting として語られることがあり,語義曖昧性を含んだ単語となっています. *1

culture.voyagegroup.com

AJITOは社内Barであり,業務時間後は無料でお酒が飲めるという点が最大の特徴です.そのため, #ajiting 自体も業務時間後 / 休日の活動にフォーカスして語られることが多いです.
しかし私は思うわけです.

#ajiting の良さについて理解するためには昼 #ajiting の話は切っても切れない

・・・ということで,社内なんちゃってノマド族でありBOATの方からAJITOの管理人だと本気で信じられていた私から,快適な昼 #ajiting のススメということで紹介させて頂きます.

昼 #ajiting ?

昼 #ajiting とは,業務時間帯である9:30〜18:30にかけてAJITOでもくもく業務をすることを指します.もちろんお酒は飲めません ( フリじゃないよ ). 昼#ajiting は,#oasing *2 と並び,( 特に社内の一部のエンジニア陣から ) 愛さています.私の思う昼に #ajiting するメリットをあげてみます.

  1. 日光を浴びれる
    • 何となく健康っぽいです.この数式がよく理解できない,論文読んでて目が疲れてきた・・・そんなときは外を眺めると良いリフレッシュになります.朝夕の雰囲気はこんな感じ ↓
    • f:id:amacbee:20151207194252j:plain:w300 f:id:amacbee:20151207194231j:plain:w300
  2. 空気が澄んでいる気がする *3
  3. ( 人によりますが ) テーブルとイスの高さが絶妙で作業しやすい
    • 実のところ私はオフィスのイスよりもAJITOのイスの方が気に入ってます.ただし木のテーブルは一部傾いているため席によっては作業しづらいです
  4. だいたい静か
    • ミーティングや商談が行われることも多いです.*4
  5. 広々使える
    • いまのところ昼 #ajiting 派が少ないこともあり,広々使えます
  6. お昼時のエレベータでも大体乗れる
    • お昼時間帯はエレベータが死ぬほど混みますが,AJITOのある8Fから乗ると大体乗れます
  7. ものによっては飲料の値段が安かったりもする
    • 私は炭酸水を好んで飲んでいるのですが,何故かAJITOの方がオフィスより10円安い
  8. BOATの知り合いが増える
    • シェアオフィス「BOAT」に入っている方の知り合いが増えます.下手すると社員よりも知ってるかもしれません

逆に以下のようなケースにあてはまる場合は昼 #ajiting はおすすめしません.

  1. デュアルディスプレイ環境で作業したい
    • さすがにディスプレイをAJITOまで運ぶのは厳しいです.電源もそこまで多くないのでおすすめしません.キーボード位なら持ち込んでいる人多いです
  2. ミーティングが多め
    • ミーティングは6Fで行われることが多く,6F <--> 8Fを何度も行き来するのは結構面倒です
  3. 電話をとる必要がある
    • 電話をとることの多い職種はなかなか席が離れられないらしい ( 電話をとらないエンジニア職なので良く分かってない )

昼 #ajiting を快適にするライフハック

何となく昼 #ajiting 良さそうだな・・・と思いました?
そんなあなたに快適な昼 #ajiting を満喫するライフハック的な何かを伝授します.

  1. AJITOにMac用のアダプタを持ち込む場合には延長ケーブルつきで
    • AJITOの電源は写真のような形状になっており,かつ数も多くないので延長ケーブルを持ってくるのが吉です
    • f:id:amacbee:20151208100329j:plain:w300
  2. AJITOに移動する前にAJITOの予定をチェック!
    • AJITOは貸切で使われることも多いです.AJITOに移動してみたけど貸切だった!ということにならないようにAJITOの予定をチェックしましょう
    • AJITOが空くまで周辺の空いてる会議室を利用するのも一つの手です
    • 最近@nekoyaさんがAJITOの1日の予定を社内Slackに通知するBOTを作ってくれました ( #ajiting Advent Calendarで紹介して頂けるっぽい )
  3. AJITOの電源を利用できる席を確保する
    • 以下の図で◯印の付いている場所がコンセント箇所です
  4. AJITOが暑い / 寒い・・・そんなときはエアコンをON
    • めちゃくちゃ分かりづらいですが,以下の図で□印をつけた辺りに操作盤があります.掃除道具の入った棚の裏の壁

f:id:amacbee:20151208104124p:plain:w400

更にAJITOに移動する前に自分の予定もチェックすると一日安心して #ajiting 出来るかと思います

夜ver. AJITO --> 昼ver. AJITO

昼 #ajiting に慣れてくると,出社してすぐ発作的にAJITOに移動したくなってきます.
そんなあなたのために夜 ver. AJITOを昼 ver. AJITOに変更する毎朝の業務を伝授しておきますね.

  1. 朝出社したばかりのAJITOは写真のようにライト等が夜仕様になってます
    • f:id:amacbee:20151208101709j:plain:w300
  2. まずは昼AJITO用の蛍光灯スイッチをON.写真中で「正面全体」と書かれた13番と14番の電気をつけます.AJITOの蛍光灯は14番ですが,AJITO側の操作盤で13番側の電気も消してしまうため13番もONして下さい
    • f:id:amacbee:20151208101846j:plain:w300
  3. 白色蛍光灯はついてるけどまだ夜用の電気がついたままのAJITO
    • f:id:amacbee:20151208102451j:plain:w300
  4. もしあなたがコーヒーを好むのなら,このタイミングで自販機のコーヒー注文ボタンを押しておくのがベストです.コーヒーは出来るまで時間がかかりますからね *5
    • f:id:amacbee:20151208102459j:plain:w300
  5. AJITO側の操作盤で スイッチのライトが全てついている状態にします
    • スイッチのライトが全てついている状態 = AJITOの電気が全て消えている状態です.ライトが切れかけているやつもあってちょっと分かりづらいですが手で周囲を暗くしてみたり頑張ると分かる.写真は全てのスイッチを消した場合です(わからない)
    • f:id:amacbee:20151208102510j:plain:w300

チャレンジ!昼 #ajiting クイズ

まとめ方に困ったので,凄い適当ですが全10問の昼 #ajiting クイズ *6 を作ってみました.下に行くほど難しいかと思います.
全部答えられたら,昼AJITOr *7 の称号が授与されるのかもしれない.

  1. 昼 #ajiting --> 夜 #ajiting に変わるのは何時?
  2. 写真の椅子が使われているテーブルの場所はどこ?
    • f:id:amacbee:20151208100604j:plain:w300
  3. 写真の道具は何に使われる?
    • f:id:amacbee:20151208100618j:plain:w300
  4. AJITO内にワインクーラーがありますが,どこに置かれてる?
  5. 窓際にある木製テーブルに通常設置されているイスの数はいくつ?
  6. 昼AJITOの電気をつけるスイッチの番号は何?
  7. AJITOのゴミ袋の替えはどこにある? ( AJITO内に通常2箇所 )
  8. AJITOにあるコンセントの数は全部でいくつ? ※実は上の図で示したコンセントが全てではない
  9. AJITOで一時期話題となったシーフーちゃん.これと置き換えられて導入された飲み物は何?
  10. カクヤスさんがAJITOにお酒を補充しに来るのは大体何時?

( #ajiting を通じて学んだことを挙げ連ねてみたのですが,総務の方とか余裕で全部答えられそうだなと思った )
いくつかの答えについては記事中にあるので探してみて下さい.
答え合わせをされたい方はぜひ私と #ajiting しましょうヾ(๑╹◡╹)ノ"

Enjoy! #ajiting

本当は昼にミーティングするときに点ける電気の話とかもしたかったのですが,思ったより記事が長くなってしまったのでまたの機会に.
明日は @nissy0409240がポエム (!?) をしたためるようです.お楽しみに!

*1:#ajitingの活動については#ajiting Advent Calendar 2015をご参照下さい

*2:社内図書館OASISでの活動を #oasing と呼びます

*3:何かしら計測してみた訳ではありません

*4:貸切のときは移動する必要があったりなかったりします

*5:大体1分弱くらいかかります

*6:一部昼夜関係ない問題もあります

*7:昼に #ajiting をする人達の呼称.ajitorと書くかajiterと書くかについては諸説ある.一般的に,ajitorと書くと夜に #ajiting を楽しむ人達のことを指すことが多い.昼夜全般の知識を身につけ,1日のほとんどをAJITOで過ごすようになってくるとPro AJITOrにジョブチェンジ出来るとか出来ないとか