Lean Baseball

Engineering/Baseball/Python/Agile/SABR and more...

「PythonユーザのためのJupyter[実践]入門」の感想と野球版サンプルを作った #jupyterbook #mokupy

f:id:shinyorke:20171001170611p:plain

今年も野球は終わりですね*1...こんにちは,野球の人です.

このエントリーは,PyCon JP 2017で発表した内容の続きであり, 前回のエントリーの続きでもあります.*2

Scrapyでスクレイピング&SQLite3に保存したデータを,

  • Jupyter
  • pandas
  • matplotlib

で分析と可視化をしてみましょう.

3行でまとめると

  • Jupyter本(以下,jupyterbookと略す)はいいぞ!Pythonでデータを操る人すべての必読書やぞ!
  • pandasのread_sqlとwhere,groupbyで簡単な野球統計分析ができる
  • 率系の指標(打率・出塁率・長打率・OPS)のHistogramで大雑把な打撃の傾向がつかめる

対象の読者

  • Pythonでデータ分析・可視化をされたい方
  • 前回のエントリーの続きで野球データを使った分析をしたい方
  • jupyterbookの感想や実際の利用感を知りたい方

なお,機械学習や野球統計学(セイバーメトリクス)の理論については登場しません.*3

必要なPythonレベル

  • Pythonレベル初級〜中級くらい,好きなエディタでPythonの読み書きができればOK(割と初心者向き)
  • 本を読むor人に聞きながら手を動かして進められる
  • Jupyterが使えるとなお良い(本を片手にでOK)

おしながき

Jupyter + pandas + matplotlibを用いたPyData×野球サンプル

jupyterbookを片手に, なるべく簡略かつ優しいサンプルコードを作って公開しました.

こちらは本日(10/1)に参加した #mokupy の成果でもあります.

mokupy.connpass.com

SQUEEZEさん,素敵なオフィスでもくもくさせていただきありがとうございます!*4

コード

今回はJupyter notebookベースのコードです.

このサンプルでjupyterbook

  • 第1章「Jupyter notebookを導入しよう」
  • 第2章「Jupyter notebookの操作を学ぼう」
  • 第3章「pandasでデータを処理しよう」

のほぼ全ての内容と,やる気と使い方次第で,

  • 第4章「Matplotlibでグラフを描画しよう」
  • 第5章「Matplotlibを使いこなそう」
  • 第6章「Bokehでグラフを描画しよう」

の内容はなんとか試せると思います.

※4章以降の内容は未検証(だけど多分いける)

github.com

使い方

上記リポジトリのREADME.mdに記載していますがダイジェストで紹介.

事前準備

このサンプルコードは,以下のエントリーで公開した野球データセット(ScrapyでNPBデータをスクレイピング&SQLite3に保存)が必要です.

というわけで,ここのエントリーを参考にDBを作りましょう.

shinyorke.hatenablog.com

元のコードのリポジトリはこちら.

cloneしてPython3とscrapyを入れて動かせば割とすぐ取れると思います.

github.com

環境構築

  • jupyterbookの環境構築手順に従い,Anacondaをインストール
  • こんな感じで仮想環境を作る&ライブラリを入れる
$ conda create -n jupyter-sample-baseball python=3.6
$ source activate jupyter-sample-baseball
$ conda install -y jupyter pandas notebook matplotlib bokeh

今回,bokehは使いませんでしたがいずれ使うと思う&真似する方は是非bokeh版もやってみてください.

起動

ProjectのRootディレクトリ(cloneした場合はcloneしたディレクトリ配下)にbaseball.dbをコピー,Jupyter notebookを起動

$ cp {baseball.dbまでのパス}/baseball.db jupyter-sample-baseball/
$ jupyter notebook

勘どころの解説

DB(SQLite3)からデータセット(Table)を読み込む

pandasのread_sqlで読み取り可能です...ということが第3章に記載されています.

のと,ほんの少しだけこちらを参考にさせてもらいました.*5

SQLiteのDB情報をPythonで 可視化する - slideship.com

Pythonの標準ライブラリとして梱包されているsqlite3を活用して,クエリを書いてDataframeを取得します.

なお,今回はpandasのwhereやgroupbyで絞り込む前提で考えているため,全レコードを取得しています.*6

import pandas as pd
import sqlite3

# コネクションを作る, 引数はsqlite3ファイルまでのパス(相対でも絶対でもOK)
con = sqlite3.connect('baseball.db')

# (中略)

# 続いてpandasで読み込む(全打撃データ)
df_batter = pd.read_sql('select * from batter', con)

打率ランキングを出す

pandasのDataframeを作った後は基本的に,

  • where
  • sort_values
  • head

の3つのメソッドでサマったり簡単なランキングを出せます.

これらもjupyterbookの解説を元にシンプルに書いてます.

ちなみに出しているカラムは左から

  • 選手名(name)
  • チーム(team)
  • 打席数(pa)
  • 打率(ba)
  • ホームラン(hr)
  • 打点(rbi)

です.

# セパ両リーグ合わせた打率ランキング(Best 30, 300打席以上)
df_batter[['name', 'team', 'pa', 'ba', 'hr', 'rbi']].where(df_batter['pa'] >= 300).sort_values('ba', ascending=False).head(30)

なおランキングはこんな感じです.

松山竜平(広島)が規定打数未満とはいえすごく優秀&秋山翔吾(西武)の成績が圧倒的ですね.*7

f:id:shinyorke:20171001224838p:plain

チームごとにgroupbyして基本統計量を出す

pandasでのgroupbyはたったこれだけでできます.

なおgroupbyの結果はpandas.core.groupby.DataFrameGroupByクラスのオブジェクトとして返ってきます.

df_batter_grouped = df_batter.groupby('team')

今回はチームごとの統計量をdescribe()で取ってみました.

f:id:shinyorke:20171001225640p:plain

このスクショには無いですが,巨人の本塁打数(hr)のmax値(=チーム本塁打王)が17本というところに辛みを感じました.*8

列(series)の追加とHistogramで可視化

列追加はたしかjupyterbookに無かった気がします...が必要だったので書きました.

出塁率(OBP)と長打率(SLG)の足し算で算出できるOPS(On Base Plus Slugging)を算出,Histogramで日本プロ野球全体の傾向を見てみました.

400打席以上で絞っています.

# 最後に,セイバーメトリクス指標で最も簡単なOPS(On Base Plus Slugging)を求める
df_batter['ops'] = df_batter['obp'] + df_batter['slg']

# OPSランキング
df_batter[['name', 'team', 'pa', 'ba', 'hr', 'rbi', 'obp', 'slg', 'ops']].where(df_batter['pa'] >= 400).sort_values('ops', ascending=False).head(30)

結果はこちら!ギータ強い...*9

f:id:shinyorke:20171001230645p:plain

OPSだけに絞ってHistogramはこちら.

これもjupyterbookの第3章に載っています.

pandasのDataFrameからそのままグラフに起こせますよと.

# OPSの分布
ax = df_batter['ops'].where(df_batter['pa'] > 400).hist(bins=20)
ax.set_title('histogram by OPS')
plt.show()

ボリュームゾーンがおおよそ.700〜.800台というのはだいたい合ってそうです.

f:id:shinyorke:20171001231953p:plain

コードの全容はこちらに記載していますので,ぜひぜひ参考に&自由に書き換えて遊んでもらえるとうれしいです.

https://github.com/Shinichi-Nakagawa/jupyter-sample-baseball/blob/master/baseball-sample-npb2017.ipynb

PythonユーザのためのJupyter[実践]入門の感想

著者の @iktakahiroさんはじめ,皆さまのご厚意により頂きました.

誠にありがとうございます!&Kindle版も買っちゃいました.

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

簡単にではございますが,感想と本の活かし方について触れたいと思います.

感想

PyDataの「困った」「知りたい」を解決してくれる「教科書」であり「辞書」である

PyDataが初めての方は勿論,ベテランの方にとっても大変ありがたい「教科書」であり「辞書」だなと思いました!

私自身は2014年ごろからPyDataなライブラリを使ってるのですが,

  • Jupyterのショートカット
  • pandasの「あれ,ソートどうやるんだったっけ?」「SQLで言う所のwhereってどう書くんだったっけ?」など,「やれるの知ってるけどやり方忘れる」やつ
  • matplotlibのsub plotがようわからん

みたいな困りごとや知りたいは結構あって...これは割と世の中のPyData勢もそんな感じなんじゃないかとも思っています.(きっと賛成を得られるはずw)

そんな時に,すぐに辞書として使える本が案外なかったりしてたのでやっと辞書であり,重要な勘所(DataFrameやSeries,sub plotなど)を説明してくれる教科書がようやっと出てきて感激を覚えました.

使う順番やハマる・困る勘所を抑えている

データを扱う際は,

  1. データの取得(スクレイピングしてデータセットを作る,SQLなどで塊をとる)
  2. データのクレンジング・前処理(行列を整える,欠損値を解決する,型を指定する)
  3. 分析
  4. 可視化

というような手順を取るのが普通かなと思っていますが,その時に必要な順序

  • 環境を作る・使う(Jupyter)
  • データを習得する&クレンジング・前処理(pandas)
  • 可視化する(matplotlib・bokeh)

の順にライブラリの解説や勘所が載ってるのはとても優しい構成だなと思いました.

この本自体,スクレイピングや分析の一部(例えば機械学習そのもの)はスコープ外で,そこの部分は自分たちでなんとかしろ!って感じかと思いますが,

だいたいハマる箇所は前処理だったり可視化だったりっていう印象もあるので(やることにもよります&異論は認める),これを順番に解決できる構成はほんといいなと思いました.

環境作りに対する細かい配慮

写経する時にだいたいハマる環境作りですが,こちらについても配慮がされていてホントにプロの本だな!と感心しました.

  • 有無を言わさずanaconda
  • conda installするライブラリのバージョンを固定

このエントリーをやるため,私も本に合わせてanacondaを使いましたが,特に困ることも無く円滑に進みました.*10

また,ライブラリバージョンの固定はとても良くて,下手に最新いれて動かないリスクを避ける,冪等性を保つ意味でもいいなと思いました.

Python以外にも優しい網羅性

Ruby(iRuby)やRを使った方法も紹介しています.

手前味噌で恐縮ですが,以前私がTokyuRuby会議'11でやったネタと同じ話が載ってました.

shinyorke.hatenablog.com

私自身が思ったこと

細かい話ですが,pandasのDataFrameの行選択でqueryじゃなくてwhereを使う理由をしりたいと思いました.

理由「個人的にはqueryをよく使う」*11

活かし方

jupyter,pandas,matplolib,bokehおよび周辺技術(RubyやR,Jupyterlabなど)にサービス(Cloud DatalabやAzure Notebooks)と,幅広いことが網羅的に載っている&すぐ試せるサンプルなどで構成されているので,

  • 困ったときの辞書として使う
  • 覚えたいことを写経する

といった使い方に強いと思いました.

前者(辞書)は紙でも電子でもOK,後者(写経)は紙で持ってるとベストかな...

個人的には辞書利用が増えそうなのでKindle版でも買いました.*12

なお,ハッシュタグ「#jupyterbook」で感想ブログや書評や著者の皆様の活動などが見えるのでオススメです.

まとめ

PythonとJupyter,pandasとmatplotlibをちょっと覚えることにより,大抵のセイバーメトリクスが出来るようになりますよ!というのがこの記事で言いたいポイントです.

そのお供としてのjupyterbookはかなりオススメなので,データ分析をする方や野球データで遊びたい方は是非試してみるといいと思います!

プロ野球もMLBももうすぐ終わり,CS・プレーオフを楽しみつつ,Pythonで野球データの秋を味わえたらと思います!

フィードバック&提案とかご意見などなどお待ちしております(._.)

素敵なPyDataライフを!

【Appendix】参考文献

繰り返しになりますが,今回はJupyter[実践]入門が大活躍でした!

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

【Appendix】コードの全容

Jupyter notebook 日本プロ野球分析のサンプル

*1:DeNAベイスターズCS進出おめでとうございます&読売さんは残念でした

*2:PyCon JPの続きシリーズは今回で一旦最後

*3:なぜならそんなテクノロジーを使わなくても十分証明できちゃうからです

*4:ソファーの座り心地が最高でした

*5:このスライドでSQLite3でread_sql出来ることを初めて知った

*6:全部読んでも647レコード.これぐらいならオンメモリで問題ない

*7:松山はエルドレッドと鈴木誠也が両翼にいる関係で機会損失してる感あってもったいない.誠也の故障でチームは大ダメージだけど出場機会を得られたのは皮肉&秋山翔吾は何が変わったのかすごく知りたい.

*8:1位はマギーでした,流石チーム三冠王

*9:益々強くなっててほんと怖い

*10:PyDataやるならやっぱanacondaか...と改宗しつつある自分がいる

*11:queryの方がSQLチックで好きだったりする

*12:毎日持ち歩きたいってことです.

「人とWebに優しい」Scrapyの使い方サンプル〜 #PyConJP 2017のつづき(なお野球)

PyCon JP 2017で発表した野球×Pythonの分析ネタの詳細解説です.*1

プレゼンテーション:野球を科学する技術〜Pythonを用いた統計ライブラリ作成と分析基盤構築 | PyCon JP 2017 in TOKYO

speakerdeck.com

youtu.be

時間および諸々の都合(察し)で公開できなかった*2,

「人とWebに優しい」Scrapyアプリのサンプル(なお野球)

を作って公開したのでその紹介と,PyConのプレゼンで発表しきれなかった部分を簡単に紹介します.

おしながき

対象の読者

  • Pythonレベル初級〜中級
  • 好きなエディタ・環境でPythonを読み書きできる(本を読みながら・人に聞きながらでもOK)
  • データ分析とか機械学習やりたい!…けどデータ集めなきゃ!っていう方
  • なお,特段野球好き・マニアである必要はありません.野球アレルギーが無ければ大丈夫.*3

参考文献

このエントリーを読む前でも読んだ後でも目を通しておくことをオススメします.

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-

Scrapyのみならず,スクレイピングとクローラーについて事細かかつわかりやすく解説している良著でホント参考にさせてもらいました.

Scrapyを用いた日本プロ野球データ取得Exampleアプリ

こちらに準備しました.

github.com

動作環境・使い方はREADME.mdを御覧ください.

大雑把に使い方を書くとこんな感じです.

  1. コードをcloneする
  2. Python 3.6およびscrapyをインストール
  3. 「scrapy crawl batter -a year=2017 -a league=1」とコマンドを叩くとsqlite3のDBができて打者の情報をドカドカ入れてくれる
  4. 「scrapy crawl pitcher -a year=2017 -a league=1」とコマンドを叩くと投手の情報を(上に同じく)

出来上がったDBは野球好き的にたまらないと思います.

ポイント

主にScrapyの解説となります…が,(アーキテクト図を除き)Scrapyそのものの解説は端折っているので詳細を知りたい方は先ほどの「Pythonクローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―」を読むか,Scrapyの公式サイトを御覧ください.

Scrapy 1.4 documentation — Scrapy 1.4.0 documentation

全体像

Scrapyの全体アーキテクト図に今回の対象範囲を合わせてみました.

f:id:shinyorke:20170920005842p:plain

「人とWebに優しい」settings.pyの書き方

最低限守るべきポイントは以下の3つです.

  1. robots.txtに従う(Webクローリングする上での最低限のお作法)
  2. リクエストの発行回数を大幅に減らす.具体的には同一ドメインに対して並列数を絞る,ダウンロード間隔を60秒以上空ける
  3. キャッシュは必ず取る,再実行時はキャッシュを相手にする.

今回のサンプルアプリは,

  • 一日に一回,野球選手の成績を取る
  • 一覧ページを相手, 打撃成績は12球団分,投手成績も12球団分
  • Spiderは打者もしくは投手を相手するので1分以上空けたとしても15分前後で全チームの打撃成績と投手成績が入手できる

という要件のもと,

  • リクエスト数は2つもあれば十分
  • 一日一回取るぐらいならキャッシュも24時間効かせてOK(デバッグ目的でたくさん叩いても相手サイトに迷惑かけない)

という方針で設定しました.

設定内容はこんな感じです(コメントも一緒に読んでもらえるとなお良い!)

# -*- coding: utf-8 -*-

# Scrapy settings for baseball project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
#     http://doc.scrapy.org/en/latest/topics/settings.html
#     http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#     http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html

BOT_NAME = 'baseball'  # botの名前

SPIDER_MODULES = ['baseball.spiders']
NEWSPIDER_MODULE = 'baseball.spiders'


# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = 'baseball (+http://www.yourdomain.com)'

# Obey robots.txt rules
ROBOTSTXT_OBEY = True  # robots.txtに従うか否か

# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 2  # リクエスト並行数,16もいらないので2

# Configure a delay for requests for the same website (default: 0)
# See http://scrapy.readthedocs.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 60  # ダウンロード間隔(s),60秒に設定
# The download delay setting will honor only one of:
CONCURRENT_REQUESTS_PER_DOMAIN = 2  # 同一ドメインに対する並行リクエスト数. CONCURRENT_REQUESTSと同じ値でOK(サンプルは単一ドメインしか相手していない)
CONCURRENT_REQUESTS_PER_IP = 0  # 同一IPに対する並行リクエスト数. ドメインで絞るので無効化してOK

# Disable cookies (enabled by default)
#COOKIES_ENABLED = False

# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False

# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
#   'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
#   'Accept-Language': 'en',
#}

# Enable or disable spider middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
#    'baseball.middlewares.BaseballSpiderMiddleware': 543,
#}

# Enable or disable downloader middlewares
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html
#DOWNLOADER_MIDDLEWARES = {
#    'baseball.middlewares.MyCustomDownloaderMiddleware': 543,
#}

# Enable or disable extensions
# See http://scrapy.readthedocs.org/en/latest/topics/extensions.html
#EXTENSIONS = {
#    'scrapy.extensions.telnet.TelnetConsole': None,
#}

# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
    'baseball.pipelines.BaseballPipeline': 100,  # DB保存用のミドルウェア
}

# Enable and configure the AutoThrottle extension (disabled by default)
# See http://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False

# Enable and configure HTTP caching (disabled by default)
# See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
HTTPCACHE_ENABLED = True  # キャッシュの有無,勿論True
HTTPCACHE_EXPIRATION_SECS = 60 * 60 * 24  # キャッシュのExpire期間. 24時間(秒数で指定)
HTTPCACHE_DIR = 'httpcache'  # キャッシュの保存先(どこでも良い)
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'

リクエスト数やキャッシュ有効期限は要件・サイトに合わせて設定でOKですが,元々のScrapyが若干強気の設定なので注意しましょう.

「ひととWebにやさしく」しましょう!

Spider(クローラー本体)について〜Itemも添えて

取得先のURLルールを把握した上で作成しています.

なお,取得メソッドはxpathです.

Spider…の前にItem(構造体)

items.pyにあります.

投打共に野球用語の英語略称を用いてます,コメントともに見てもらえれば.*4

# -*- coding: utf-8 -*-

# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html

from scrapy import Item, Field


class BatterItem(Item):
    year = Field()      # 年度
    team = Field()      # チーム
    name = Field()      # 名前
    bat = Field()       # 右打ち or 左打ち or 両打ち
    games = Field()     # 試合数
    pa = Field()        # 打席数
    ab = Field()        # 打数
    r = Field()         # 得点
    h = Field()         # 安打
    double = Field()    # 二塁打
    triple = Field()    # 三塁打
    hr = Field()        # 本塁打
    tb = Field()        # 塁打
    rbi = Field()       # 打点
    sb = Field()        # 盗塁
    cs = Field()        # 盗塁死
    sh = Field()        # 犠打(バント)
    sf = Field()        # 犠飛(犠牲フライ)
    bb = Field()        # 四球
    ibb = Field()       # 故意四球(敬遠)
    hbp = Field()       # 死球(デットボール)
    so = Field()        # 三振
    dp = Field()        # 併殺
    ba = Field()        # 打率
    slg = Field()       # 長打率
    obp = Field()       # 出塁率


class PitcherItem(Item):
    year = Field()      # 年度
    team = Field()      # チーム
    name = Field()      # 名前
    throw = Field()     # 右投げ or 左投げ
    games = Field()     # 登板数
    w = Field()         # 勝利
    l = Field()         # 敗北
    sv = Field()        # セーブ
    hld = Field()       # ホールド
    hp = Field()        # HP(ホールドポイント
    cg = Field()        # 完投
    sho = Field()       # 完封
    non_bb = Field()    # 無四球
    w_per = Field()     # 勝率
    bf = Field()        # 打者
    ip = Field()        # 投球回
    h = Field()         # 被安打
    hr = Field()        # 被本塁打
    bb = Field()        # 与四球
    ibb = Field()       # 故意四球(敬遠)
    hbp = Field()       # 与死球
    so = Field()        # 三振
    wp = Field()        # 暴投
    bk = Field()        # ボーク
    r = Field()         # 失点
    er = Field()        # 自責点
    era = Field()       # 防御率

batter.py(打者成績)

一つのtableタグに入ってるので愚直にtrとtdを得る感じでグイグイ回して取得しています.

# -*- coding: utf-8 -*-
import scrapy

from baseball.items import BatterItem
from baseball.spiders import TEAMS, LEAGUE_TOP, BAT_RIGHT, BAT_LEFT, BAT_SWITCH
from baseball.spiders import BaseballSpidersUtil as Util


class BatterSpider(scrapy.Spider):
    name = 'batter'
    allowed_domains = ['npb.jp']
    URL_TEMPLATE = 'http://npb.jp/bis/{year}/stats/idb{league}_{team}.html'

    def __init__(self, year=2017, league=LEAGUE_TOP):
        """
        初期処理(年度とURLの設定)
        :param year: シーズン年
        :param league: 1 or 2(1軍もしくは2軍)
        """
        self.year = year
        # URLリスト(12球団)
        self.start_urls = [self.URL_TEMPLATE.format(year=year, league=league, team=t) for t in TEAMS]

    def parse(self, response):
        """
        選手一人分の打撃成績
        :param response: 取得した結果(Response)
        :return: 打撃成績
        """
        for tr in response.xpath('//*[@id="stdivmaintbl"]/table').xpath('tr'):
            item = BatterItem()
            if not tr.xpath('td[2]/text()').extract_first():
                continue
            item['year'] = self.year
            item['team'] = Util.get_team(response.url)
            item['bat'] = self._get_bat(Util.get_text(tr.xpath('td[1]/text()').extract_first()))
            item['name'] = Util.get_text(tr.xpath('td[2]/text()').extract_first())
            item['games'] = Util.text2digit(tr.xpath('td[3]/text()').extract_first(), digit_type=int)
            item['pa'] = Util.text2digit(tr.xpath('td[4]/text()').extract_first(), digit_type=int)
            item['ab'] = Util.text2digit(tr.xpath('td[5]/text()').extract_first(), digit_type=int)
            item['r'] = Util.text2digit(tr.xpath('td[6]/text()').extract_first(), digit_type=int)
            item['h'] = Util.text2digit(tr.xpath('td[7]/text()').extract_first(), digit_type=int)
            item['double'] = Util.text2digit(tr.xpath('td[8]/text()').extract_first(), digit_type=int)
            item['triple'] = Util.text2digit(tr.xpath('td[9]/text()').extract_first(), digit_type=int)
            item['hr'] = Util.text2digit(tr.xpath('td[10]/text()').extract_first(), digit_type=int)
            item['tb'] = Util.text2digit(tr.xpath('td[11]/text()').extract_first(), digit_type=int)
            item['rbi'] = Util.text2digit(tr.xpath('td[12]/text()').extract_first(), digit_type=int)
            item['sb'] = Util.text2digit(tr.xpath('td[13]/text()').extract_first(), digit_type=int)
            item['cs'] = Util.text2digit(tr.xpath('td[14]/text()').extract_first(), digit_type=int)
            item['sh'] = Util.text2digit(tr.xpath('td[15]/text()').extract_first(), digit_type=int)
            item['sf'] = Util.text2digit(tr.xpath('td[16]/text()').extract_first(), digit_type=int)
            item['bb'] = Util.text2digit(tr.xpath('td[17]/text()').extract_first(), digit_type=int)
            item['ibb'] = Util.text2digit(tr.xpath('td[18]/text()').extract_first(), digit_type=int)
            item['hbp'] = Util.text2digit(tr.xpath('td[19]/text()').extract_first(), digit_type=int)
            item['so'] = Util.text2digit(tr.xpath('td[20]/text()').extract_first(), digit_type=int)
            item['dp'] = Util.text2digit(tr.xpath('td[21]/text()').extract_first(), digit_type=int)
            item['ba'] = Util.text2digit(tr.xpath('td[22]/text()').extract_first(), digit_type=float)
            item['slg'] = Util.text2digit(tr.xpath('td[23]/text()').extract_first(), digit_type=float)
            item['obp'] = Util.text2digit(tr.xpath('td[24]/text()').extract_first(), digit_type=float)
            yield item

    def _get_bat(self, text):
        """
        右打ち or 左打ち or 両打ち
        :param text: テキスト
        :return: 右打ち or 左打ち or 両打ち
        """
        if text == '*':
            return BAT_LEFT
        elif text == '+':
            return BAT_SWITCH
        return BAT_RIGHT

結構愚直に泥臭く書いています笑

なお,ところどころでリーグやチームの設定,数値変換などが登場するのでこれらについてはspiderパッケージ直下に便利ライブラリを作っています.

# baseball/spider/__init__.pyに書いてます
# 打撃・投球で共通して使う定義
LEAGUE_TOP = 1  # 一軍
LEAGUE_MINOR = 2  # 二軍

THROW_RIGHT = 'R'   # 右投げ
THROW_LEFT = 'L'    # 左投げ

BAT_RIGHT = 'R'   # 右打ち
BAT_LEFT = 'L'    # 左打ち
BAT_SWITCH = 'T'    # 両打ち

TEAMS = {
    'f': 'fighters',
    'h': 'hawks',
    'm': 'marines',
    'l': 'lions',
    'e': 'eagles',
    'bs': 'buffalos',
    'c': 'carp',
    'g': 'giants',
    'db': 'baystars',
    's': 'swallows',
    't': 'tigers',
    'd': 'dragons',
}


class BaseballSpidersUtil:

    @classmethod
    def get_team(cls, url):
        try:
            return TEAMS.get(url.replace('.html', '').split('_')[-1], 'Unknown')
        except IndexError:
            return 'Unknown'

    @classmethod
    def get_text(cls, text):
        """
        テキスト取得(ゴミは取り除く)
        :param text: text
        :return: str
        """
        if not text:
            return ''
        return text.replace('\u3000', ' ')

    @classmethod
    def text2digit(cls, text, digit_type=int):
        """
        数値にキャストする(例外時はゼロを返す)
        :param text: text
        :param digit_type: digit type(default:int)
        :return: str
        """
        if not text:
            return digit_type(0)
        try:
            return digit_type(text)
        except ValueError as e:
            print("変換に失敗しているよ:{}".format(e))
            return digit_type(0)

pitcher.py(投手成績)

打撃成績とほぼ同じ.

なお,投球回(ip)は小数点以下でカラムが別れるという謎仕様だったので真ん中ら辺のコードが若干面白いことになっています笑

# -*- coding: utf-8 -*-
import scrapy

from baseball.items import PitcherItem
from baseball.spiders import TEAMS, LEAGUE_TOP, THROW_RIGHT, THROW_LEFT
from baseball.spiders import BaseballSpidersUtil as Util


class PitcherSpider(scrapy.Spider):
    name = 'pitcher'
    allowed_domains = ['npb.jp']
    allowed_domains = ['npb.jp']
    URL_TEMPLATE = 'http://npb.jp/bis/{year}/stats/idp{league}_{team}.html'

    def __init__(self, year=2017, league=LEAGUE_TOP):
        """
        初期処理(年度とURLの設定)
        :param year: シーズン年
        :param league: 1 or 2(1軍もしくは2軍)
        """
        self.year = year
        # URLリスト(12球団)
        self.start_urls = [self.URL_TEMPLATE.format(year=year, league=league, team=t) for t in TEAMS]

    def parse(self, response):
        """
        選手一人分の投球成績
        :param response: 取得した結果(Response)
        :return: 投球成績
        """
        for tr in response.xpath('//*[@id="stdivmaintbl"]/table').xpath('tr'):
            item = PitcherItem()
            if not tr.xpath('td[3]/text()').extract_first():
                continue
            item['year'] = self.year
            item['team'] = Util.get_team(response.url)
            item['name'] = Util.get_text(tr.xpath('td[2]/text()').extract_first())
            item['throw'] = self._get_throw(Util.get_text(tr.xpath('td[1]/text()').extract_first()))
            item['games'] = Util.text2digit(tr.xpath('td[3]/text()').extract_first(), digit_type=int)
            item['w'] = Util.text2digit(tr.xpath('td[4]/text()').extract_first(), digit_type=int)
            item['l'] = Util.text2digit(tr.xpath('td[5]/text()').extract_first(), digit_type=int)
            item['sv'] = Util.text2digit(tr.xpath('td[6]/text()').extract_first(), digit_type=int)
            item['hld'] = Util.text2digit(tr.xpath('td[7]/text()').extract_first(), digit_type=int)
            item['hp'] = Util.text2digit(tr.xpath('td[8]/text()').extract_first(), digit_type=int)
            item['cg'] = Util.text2digit(tr.xpath('td[9]/text()').extract_first(), digit_type=int)
            item['sho'] = Util.text2digit(tr.xpath('td[10]/text()').extract_first(), digit_type=int)
            item['non_bb'] = Util.text2digit(tr.xpath('td[11]/text()').extract_first(), digit_type=int)
            item['w_per'] = Util.text2digit(tr.xpath('td[12]/text()').extract_first(), digit_type=float)
            item['bf'] = Util.text2digit(tr.xpath('td[13]/text()').extract_first(), digit_type=int)
            # 小数点以下が別のカラムに入ってるのでこういう感じになります
            if tr.xpath('td[15]/text()').extract_first():
                ip = tr.xpath('td[14]/text()').extract_first() + tr.xpath('td[15]/text()').extract_first()
            else:
                ip = tr.xpath('td[14]/text()').extract_first()
            item['ip'] = Util.text2digit(ip, digit_type=float)
            item['h'] = Util.text2digit(tr.xpath('td[16]/text()').extract_first(), digit_type=int)
            item['hr'] = Util.text2digit(tr.xpath('td[17]/text()').extract_first(), digit_type=int)
            item['bb'] = Util.text2digit(tr.xpath('td[18]/text()').extract_first(), digit_type=int)
            item['ibb'] = Util.text2digit(tr.xpath('td[19]/text()').extract_first(), digit_type=int)
            item['hbp'] = Util.text2digit(tr.xpath('td[20]/text()').extract_first(), digit_type=int)
            item['so'] = Util.text2digit(tr.xpath('td[21]/text()').extract_first(), digit_type=int)
            item['wp'] = Util.text2digit(tr.xpath('td[22]/text()').extract_first(), digit_type=int)
            item['bk'] = Util.text2digit(tr.xpath('td[23]/text()').extract_first(), digit_type=int)
            item['r'] = Util.text2digit(tr.xpath('td[24]/text()').extract_first(), digit_type=int)
            item['er'] = Util.text2digit(tr.xpath('td[25]/text()').extract_first(), digit_type=int)
            item['era'] = Util.text2digit(tr.xpath('td[26]/text()').extract_first(), digit_type=float)
            yield item

    def _get_throw(self, text):
        """
        右投げもしくは左投げか
        :param text: テキスト
        :return: 右投げ or 左投げ
        """
        if text == '*':
            return THROW_LEFT
        return THROW_RIGHT

pipelines.py(データ保存)について

spiderが生成したItemをもらってデータとして保存する処理を記述しています.

SQLite3はPython標準で使えるので追加ライブラリは不要です.*5

実装のポイントは

  • 初期処理でTable生成の有無を判断
  • spiderの名前でInsert先のTableを振り分け

です.

本来的にはスキーマ定義(Create Table)は別のクラスに書くべきでしょうが,今回はサンプルとしての見通しを良くするためにあえて同じファイルに書いています.

# -*- coding: utf-8 -*-

# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html

import sqlite3
from scrapy.exceptions import DropItem


class BaseballPipeline(object):

    CREATE_TABLE_BATTER ="""
    CREATE TABLE batter (
      id integer primary key,
      year integer,
      name text ,
      team text ,
      bat text ,
      games integer ,
      pa integer ,
      ab integer ,
      r integer ,
      h integer ,
      double integer ,
      triple integer ,
      hr integer ,
      tb integer ,
      rbi integer ,
      so integer ,
      bb integer ,
      ibb integer ,
      hbp integer ,
      sh integer ,
      sf integer ,
      sb integer ,
      cs integer ,
      dp integer ,
      ba real ,
      slg real ,
      obp real,
      create_date date,
      update_date date
    ) 
    """

    CREATE_TABLE_PITCHER ="""
    CREATE TABLE pitcher (
      id integer primary key,
      year integer,
      name text ,
      team text ,
      throw text ,
      games integer ,
      w integer ,
      l integer ,
      sv integer ,
      hld integer ,
      hp integer ,
      cg integer ,
      sho integer ,
      non_bb integer ,
      w_per real ,
      bf integer ,
      ip real ,
      h integer ,
      hr integer ,
      bb integer ,
      ibb integer ,
      hbp integer ,
      so integer ,
      wp integer ,
      bk integer ,
      r integer ,
      er integer ,
      era real ,
      create_date date,
      update_date date
    ) 
    """

    INSERT_BATTER = """
    insert into batter(
    year, 
    name, 
    team, 
    bat, 
    games, 
    pa, 
    ab, 
    r, 
    h, 
    double, 
    triple, 
    hr, 
    tb, 
    rbi, 
    so, 
    bb, 
    ibb, 
    hbp, 
    sh, 
    sf,
    sb,
    cs,
    dp,
    ba,
    slg,
    obp,
    create_date,
    update_date
    ) 
    values(
    ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 
    datetime('now', 'localtime'), 
    datetime('now', 'localtime')
    )
    """

    INSERT_PITCHER = """
    insert into pitcher(
    year, 
    name, 
    team, 
    throw, 
    games, 
    w, 
    l, 
    sv, 
    hld, 
    hp, 
    cg, 
    sho, 
    non_bb, 
    w_per, 
    bf, 
    ip, 
    h, 
    hr, 
    bb, 
    ibb, 
    hbp, 
    so,
    wp,
    bk,
    r,
    er,
    era,
    create_date,
    update_date
    ) 
    values(
    ?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?, 
    datetime('now', 'localtime'), 
    datetime('now', 'localtime')
    )
    """

    DATABASE_NAME = 'baseball.db'
    conn = None

    def __init__(self):
        """
        Tableの有無をチェック,無ければ作る
        """
        conn = sqlite3.connect(self.DATABASE_NAME)
        if conn.execute("select count(*) from sqlite_master where name='batter'").fetchone()[0] == 0:
            conn.execute(self.CREATE_TABLE_BATTER)
        if conn.execute("select count(*) from sqlite_master where name='pitcher'").fetchone()[0] == 0:
            conn.execute(self.CREATE_TABLE_PITCHER)
        conn.close()

    def open_spider(self, spider):
        """
        初期処理(DBを開く)
        :param spider: ScrapyのSpiderオブジェクト
        """
        self.conn = sqlite3.connect(self.DATABASE_NAME)

    def process_item(self, item, spider):
        """
        成績をSQLite3に保存
        :param item: Itemの名前
        :param spider: ScrapyのSpiderオブジェクト
        :return: Item
        """
        # Spiderの名前で投入先のテーブルを判断
        if spider.name == 'batter':
            # 打者成績
            self.conn.execute(self.INSERT_BATTER,(
                item['year'], item['name'], item['team'], item['bat'], item['games'], item['pa'], item['ab'], item['r'],
                item['h'], item['double'], item['triple'], item['hr'], item['tb'], item['rbi'], item['so'], item['bb'],
                item['ibb'], item['hbp'], item['sh'], item['sf'], item['sb'], item['cs'], item['dp'], item['ba'],
                item['slg'], item['obp'],
            ))
        elif spider.name == 'pitcher':
            # 投手成績
            self.conn.execute(self.INSERT_PITCHER,(
                item['year'], item['name'], item['team'], item['throw'], item['games'], item['w'], item['l'],
                item['sv'], item['hld'], item['hp'], item['cg'], item['sho'], item['non_bb'], item['w_per'], item['bf'],
                item['ip'], item['h'], item['hr'], item['bb'], item['ibb'], item['hbp'], item['so'], item['wp'],
                item['bk'], item['r'], item['er'], item['era'],
            ))
        else:
            raise DropItem('spider not found')
        self.conn.commit()
        return item

    def close_spider(self, spider):
        """
        終了処理(DBを閉じる)
        :param spider: ScrapyのSpiderオブジェクト
        """
        self.conn.close()

これからスクレイピングしてみよう!という方へ

まずは本やサンプルの写経がベストかなと思います.

今回紹介した野球スクレイピングについても自由にpullしたりダウンロードしたりforkしたりしてお試しで使って好きなサイトを(迷惑を書けない程度に)クローリングしてもらればと思います.

その上で,スクレイピングをやりたい方は是非,

  • Webの基本的なルールを学ぶ(HTML・Javascript・http・サーバーのしくみetc…)
  • スクレイピングの手段(今回はPythonとScrapy)をしっかり学んで自分の手足とする
  • スクレイピングする目的とゴールを明確に

という観点でぜひぜひ学んでみては如何でしょうか?

…という提案でこの件は一旦終わります.

次回予告

実際に取得した野球データで何が出来るか?を,

  • Jupyter
  • pandas
  • matplotlib

あたりでお料理してみたいと思います.

PythonユーザのためのJupyter[実践]入門

PythonユーザのためのJupyter[実践]入門

ついにこの本の出番かな…wkwk

*1:Scrapyの事をもっと知りたいニーズが多かったこと,Sprint Day(PyCon JP 2017四日目)のScrapyスプリントも盛り上がったので記事にすることにしました.

*2:後者「察し」のところは…まあね(察し)

*3:このブログを読んでる方はPythonか野球もしくはその両方に興味ある方多いので特段心配はしていない

*4:例えばデットボールは英語の正式名称「Hit By Pitch」の略称でhbpと呼びます.

*5:余談ですがガチの野球分析基盤はSQLAlchemyからMySQLを扱う方式にしています.

キーワードは「世代交代」「多様性」「混沌」 - PyConJP 2017ふりかえり #pyconjp

f:id:shinyorke:20170910230510j:plain

写真は講演中のやつです,今年もご清聴ありがとうございました.

どうもこんばんは,野球の人です.38歳になりました.*1

9/7(木)から本日(9/10)にかけて四日間で開催されたPyConJP 2017,今年も盛会のまま無事終わりました.

私はConference本体二日間+スプリントDayに参加しました.

普通に感想・所感…を書くのもあんまりおもしろくないので,

「世代交代」「多様性」「混沌」,の3つのワードを軸に一筆残したいと思います.

おしながき

全体を通して

サクッと今年の感想はこんな感じです.

  1. 登壇者・参加者・スポンサー全てで「世代交代」が進んでフレッシュになった印象を受けた
  2. (技術系のカンファレンスだが)必ずしもエンジニア・プログラマーのイベントではない!という「多様性」が増してきた
  3. 故に、「カルチャー」「レベル感」「立ち位置の違い」による「混沌」が増えた気がする

これらを,

  • 参加者・スタッフさん
  • トークセッション・LT
  • コミュニティ
  • スポンサー

の軸で書きつつ,最後は自分自身のことについてふりかえって自分のPyConJP 2017を締めたいと思います.

参加者・スタッフさんについて

参加者

PyConJP 2017の感想,というより直近のPythonコミュニティの変化という感想です.

去年の中頃〜今年にかけて, 毎回参加してるPython mini Hack-a-thonや少しだけお手伝いをさせてもらっているPython入門者の集い,そして今年に入ってから主催しているPythonもくもく自習室 @ Rettyオフィス*2,その他のイベントetc…で新しい人が増えたような印象があります.

私がPyConJPに参加しはじめた2013年(日本でPyCon APACをやった年)はほぼほぼエンジニア(Web・組み込み・インフラ中心,まれにデータの人)っていう感じだったのですが,あれから4年経過した今では,

  • 研究者・データサイエンティスト
  • プロダクト・サービスに付いてるデータアナリスト・分析者(データサイエンティストではなく,プランナーに近い人)
  • 他の言語からキャリアチェンジを狙っている方
  • 昔からPython触ってるけど突然復帰した方

…などなど,今までのエンジニア文脈と異なる方々が増えて多様性が増した印象を感じています.

そして(昔から触ってからの復帰な方を除き)大体の方が若い.

それを感じたのは初日のこの出来事です(初日のLT,Twitterより引用)

Python触ってる人はエンジニアな人が多くて本職かそうじゃないかに関係なくWebはちょろっと触ってるだろう…という時代は終わったんだなと実感しました.*3

この辺はデータ分析や機械学習といったPyDataの影響が大きいと思っていますが,この流れは個人的に「PyData元年」と思っている2014年からできたのかなと?と思ってます.

この辺の発表とかですね.

www.slideshare.net

www.slideshare.net

この流れからPyData.Tokyoもできましたしね…

PyConJPスタッフ

今年から座長がたかのりさんから吉田さんに変わり,スタッフも一気に世代交代をしてとてもフレッシュな印象を受けました.

特にスピーカーとして登壇した初日が印象的で,なんとなくバタバタしている感を感じたので

ワイ「接続チェックしちゃいますねー」

ワイ「タイムキーパーは誰がやるんですか?時計はどこ??」

ワイ「水もらっちゃいますね」

…などなど,自然に勝手に仕切ってよしなに回す方に脳みそと体が動きました笑

その他スポンサー周りとか全体の仕切りとか多少の混乱・バタバタもありましたが「世代交代で変わりゆくPyCon」という感じで私自身は結構楽しめました.

今となってはいい思い出ですし,この経験がPyConやスタッフの皆さんの経験として生きていけば良いのかなと思っています.

トークセッションとLTについて(自分のトークを除く)

自分のトークについてのふり返りは最後に.

色々聞いたトークの中で印象に残ったものを.

トークセッション

3〜6回くらいしゃべってる常連勢から初顔のルーキーまで,これも「多様性」を感じるラインナップで良かったのかなと思っています.

そんな中で私が印象に残ったトークはこの3つです.

Pythonで大量データ処理!PySparkを用いたデータ分析のきほん

手前味噌で恐縮ですが,Rettyの同僚chie hayashida氏の発表です.

この先のエピソードはやや内輪ネタです,念のため.

speakerdeck.com

社内で「PyCon JP行きたければCfP書こうぜ」活動してたら真っ先にCfP出したのが林田さんで最初からストーリーが良かったのと,

何よりもPythonとSparkを繋ぐいい話(DataframeとかPyDataライブラリとの相性とか)を織り交ぜ,Python使いたちにもフックがかかる話が多くてホント良く出来てるなあと感心しました.

Spark全く知らない私ですが,当日の発表や前日深夜のレビュー(Slackでやりとしてチェックしてた)で聞いたり見せてもらったりして,すごく面白かったです.

Pythonで実現する4コマ漫画の分析・評論 2017

slideship.com

去年は自分の発表と被ってしまい,生で見られなかったS治さんの4コマ漫画分析・評論の発表です.

4コマ漫画分析&評論に対する技術的アプローチと情熱,考えられた発表のストーリーどれも完璧なのですが,やはり私は「情熱駆動開発」に惹かれます

情熱駆動開発

  • 自分が欲しいものを誰かが作ってくれることはないと気付く
  • やりたいことがプログラムで解決できそうか当たりを付ける
    • Pythonなら様々な分野のライブラリが存在する
  • やりたいことの情報に色々触れる
  • 勉強会に行く
  • 界隈の情報を流してくれる人をSNSでフォローする
  • 論文を読む
  • 勉強する
  • 実現する → ✌ (‘ω’ ✌ )三 ✌ (‘ω’) ✌ 三( ✌ ‘ω’) ✌

※元のスライドより引用させていただきました

  • 自分が欲しいものを誰かが作ってくれることはないと気付く
  • やりたいことがプログラムで解決できそうか当たりを付ける
  • 勉強する
  • 実現する

といったあたりは自分がやってる野球Hackとの共通点や思うところが多く,分野は異なりますがゆゆ式分析には負けないぞ!とエネルギーをもらっています.

LT

ジョブフェアのLTを除いた,一般枠LTの感想です.

今年も当日先着順でのLTでした.

個々のLTについて触れるのはここではやめますが,レベルがメチャクチャ高い,LTらしいLTも合った一方,正直ベースに言うと「え,そのレベルでLTやるん?」というなんだか微妙…もっと言うと不快な気持ちになった発表*4もあり,今年のPyConJPのコンテンツの中でも一番満足度が低いコンテンツだったと思います.

全員が聞くLTでやる…というより,オープンスペースみたいなアンカンファレンスの場でやってくれるのが一番良かったのでは?と思いました.

LTは事前応募制に戻してレビュー必須にして欲しいなあ…

レベル感

@chezou さんのエントリーを読んで「なるほど?」と考えさせられました.

medium.com

一方で発表は、今回初日のキーノートがただの会社の紹介に終止したマーケティングで終わったり、海外勢のデータ系がやってみた系が多かったりと結構辛かった一方で、日本人の発表者は非常に濃い良い発表が多く良かったなぁと思いました。

(中略)ただ、自腹で1万円払って来年も行くかというとプログラム編成がデータ系が横に並んで厳しかったり、発表のクオリティの分散が激しかったりと悩ましいなぁというところです。自分はデータ系しかほぼ見てないのですが、2年前に参加したときはjanomeの話とか濃い話がありそう思わなかったのですが、RubyKaigiの方が講演のクオリティの平均が高い(ハズレを引きにくい)のかなと思いました。aodagさんのパッケージングの話とか(資料しか見てないけど)昔からの人の発表は安定感あったのですが…。

ここは新しい人・プレーヤーが入ってきた「世代交代」「多様性」の負の部分であり、今回明らかになった「混沌」かなと思いました.

2015と比べてどうだった,とかRubyKaigiとの比較は参加したこと無いので言及できませんが*5,ハズレを引く確率(発表クオリティの分散)は確かにあるなと思いました.

私は幸いにも聞いた話でハズレは引きませんでしたが,去年のPyConでも「おや?」っていう発表もあったので残念ながら真実なのかなと思いました.

敢えて要因を探すなら,

  • 2015の頃と比べてトラック数が増えた
  • CfPはもっと増えてる
  • CfPはメチャクチャ書けていても発表が「あっ(察し」というのはまあまあある

…などなど幾つかあると思いますが,世代交代・多様性が進んでいるPythonコミュニティはそろそろ「外部に残すもののクオリティ」「Pythonコミュニティ以外に対するアプローチ・視座の高さ」を持つべき季節に来てるのかなと思いました.

これは発表する側だけでなく,トーク選考をする側(一般公募含む)や参加者みんなの課題だと思っています.

私も個人的には野球が期待されてるクオリティ満たしてるかちょっと不安になりました(真顔).

コミュニティについて

PyDataやPyLadies(Tokyoだけでなくて日本全国津々浦々)も設立して3年経ち,主要メンバーの交代や人の入れ替わりが顕著になってきたと思いました.

一方でどちらもコミュニティ活動の成果がイベントやメディア、書籍などでのアウトプットとして残り始めていて円熟期になってるのかなと思いました.

いい面だけでなくて色々大変そうな所もあるかと思いますが,大きな意味でのPythonコミュニティのイチ員としてサポートできたらなあと思っています.

理由:楽しいから

スポンサーについて

所属しているRettyがスポンサーで…という件はTechブログで後日公開できれば…と思っているので割愛.

今年はPython界隈のツワモノ多いSQUEEZEさんやモノタロウさんやビザスクさん*6そして弊社などフレッシュな顔ぶれが揃って面白くなった印象があります.

私とPyConJPについて

f:id:shinyorke:20170911010726j:plain

やっと野球の発表のふり返り.

結論から言うとメチャクチャ楽しかったです.

今年の発表

speakerdeck.com

youtu.be

去年のPyCon mini Hiroshima 2016, モノタロウTech Talk, 今年のPyCon mini Kumamoto 2017, Tokyu Ruby会議そしてKawasaki.rbで徐々に成果を見せつつ,完成した集大成として発表をさせていただきました.

shinyorke.hatenablog.com

shinyorke.hatenablog.com

shinyorke.hatenablog.com

これらの発表で少しずつプロダクトを作っては中間成果を披露しつつ,kawasaki.rbで一旦仕上げ,PyConの直前に発表予定のネタをブログでチラ見せするという今までとは違うアプローチで望みました.

shinyorke.hatenablog.com

shinyorke.hatenablog.com

「広島強すぎ」「AirflowはTurbulenceだった」等,この辺のネタはこれらの壁打ちの成果でできたモノで,最後は正直広島が強すぎて辛かったのを前夜に降ってきたキーワード「隙きあらば野間」に救われました.

スライドの枚数は増えましたがストーリーが明確になったのと,話の筋を暗記していたので問題なくこなせました.

会場での反応およびtogetterで見る感じでも結果は上々だっと思います.

togetter.com

PyConJP 2018どうすんの?

来年ももちろん参加したいと思います.

トークで喋るかどうかは終わった直後であんまり真面目に考えていませんが,

  • 何かの間違いでプロ野球・MLBのチームに本気でJOINする*7
  • 私より強烈な野球ハッカーが現れてCfPを出してくる

事がない限り,多分トークのCfPは出すんじゃないかなと思っています.

毎年トークの準備は大変だし発表前は決まって腹痛&怖くて口から内臓が出そうになりますがいざ喋るとクッソ楽しいのでこのワクワクを忘れない限り,

「世代交代」「多様性」「混沌」を無視しつつ,我が道を行こうと思います.

というわけでこれからもよろしくです!&Python界隈のイベント・コミュニティでお会いしましょう!

*1:ほしいものリスト

*2:今更だけど何故始めたのか?的な話をどこにも語ってないのでこのブログもしくはRettyのTechブログで書きたいなというお気持ち

*3:質問の仕方が?という鋭いツッコミが真意かと思うけどまあこの風潮はあると思う.

*4:マサカリを投げる・アンサーLTをする人はお祭り気分を大切にしつつ,元ネタに対するリスペクトとレベル的に超える所を大切にしてほしいとおもった(個人の見解です)

*5:言及できないけど,経験則としてKawasaki.rbやTokyuRuby会議に参加してる感じだと練度が高いエンジニアが揃っており,LTや発表のレベルは高いのは間違いないです.Ruby愛やRailsなどにやや固執している感は感じますが.

*6:念のため補足すると前職です

*7:間違いというより最終目標なのでこれは譲れない夢