フリーランチ食べたい

機械学習、Python、ソフトウェアエンジニアリング、プロダクティビティなど

「rebuild.fmの統計学」という発表をしました!コードと簡単な解説(requests_html/pandas/matplotlib/seaborn)

「rebuild.fmの統計学」というタイトルで発表しました。

  • 資料はSpeakerDecからご覧ください!

speakerdeck.com

  • LIFULLさんで開催された「Ltech#3 【podcast × IT】LT Night !」で話しました。

lifull.connpass.com

  • 使ったコードは全Githubに上げましたが、簡単にどんなことをしたのか解説したいと思います。

python-sandbox/rebuild.fmの統計学.ipynb at master · ikedaosushi/python-sandbox · GitHub

  • そして、この内容を今週土曜日の勉強会で話そうと思ってます。まだ参加申し込みできますので興味ある方は是非!

tskubapy.connpass.com

サイトから情報の取得

f:id:mergyi:20181213052504p:plain

サイトからの情報取得にはrequests-htmlを使っています。

endpoint = 'https://rebuild.fm/{}/'
session = HTMLSession()
episodes = []
for number in tqdm_notebook(range(1, 223)):
    for plus in ['', 'a']: # Normal / Aftershow
        url = endpoint.format(str(number)+plus)
        r = session.get(url)
        if r.status_code != 200: # Aftershowがないとき404が返ってくる
            continue

        # 情報をCSS Selectorで取得
        date_ = r.html.find('#contents > div > span', first=True).text
        date_ = date_.replace('\n', '-')

        record_time = r.html.find('#contents > div > div.post > p > i', first=True).text
        record_time = record_time.replace('収録時間: ', '').replace(' |', '')

        title = r.html.find('#contents > div > h2 > a', first=True).text

        description = r.html.find('#contents > div > div.post > div.episode-description > p', first=True).text

        persons = []
        persion_elements = r.html.find('#contents > div > div.post > div.episode-description > div.episode-people > ul > li')
        for person_element in persion_elements:
            persons.append(person_element.text)

        shownotes = []
        shownote_elements = r.html.find('#show_notes_ > ul > li > a')
        for shownote_element in shownote_elements:
            shownotes.append(shownote_element.text)

        # 情報を辞書にしてappend
        episode = {
            'date': date_,
            'record_time': record_time, 
            'title': title, 
            'persons': persons,
            'shownotes': shownotes
        }
        episodes.append(episode)
        
        # サイトに負荷をかけないように1秒間ごとに
        time.sleep(1)

# 最後にpd.DataFrame形式に変換
df = pd.DataFrame(episodes)

特に難しいことはしていませんが、本編は https://rebuild.fm/1/ 、 Aftershowは https://rebuild.fm/1a/ という形になるのと、皆さんご存知の通りAftershowは回によってあるときとないときがあるので、その点を注意したコードになっています。

前処理

ここからはpandasを使ったデータ加工パートになります。

# datetime型に変換
df['date'] = pd.to_datetime(df['date'])

# shownotesの数を保存
n_shownotes = []
for i, row in df.iterrows():
    n_s = len(row['shownotes'])
    n_shownotes.append(n_s)
df['n_shownotes'] = n_shownotes

# 収録時間(ex: 52:53)を正規表現を使って分(integer)に変換
hours = df['record_time'].str.extract('(?<=^)(\d)(?=:\d\d:\d\d$)').fillna(0).astype(int)
minutes = df['record_time'].str.extract('(\d\d)(?=:)') .fillna(0).astype(int)
seconds = df['record_time'].str.extract('(?<=\d\d)(\d\d)(?=$)') .fillna(0).astype(int)
df['minutes'] = hours*60 + minutes + seconds/60

# Aftershowかどうか
df['is_aftershow'] = False
df['is_aftershow'] = df['is_aftershow'].mask(df['title'].str.contains('Aftershow', na=False), True)

日付は Feb 13-2013 のようになっているのですが、 pd.to_datetime というメソッドは何も指定することなく使うだけで正しいdatetime型にしてくれます。pandasでの時系列データの取扱いに関してはsinhrksさんのBlogが詳しいので参照してみてください。

sinhrks.hatenablog.com

# episodeのナンバー
df['show_no'] = df['title'].str.extract('^(\d\d?\d?)')
df['show_no'] = df['show_no'].mask(df['show_no'].isnull(), df['title'].str.extract('^Aftershow (\d\d?\d?)')[0])

また今回の分析の特性上、「本編とAftershowを合算したい場合がある(例えば1回あたりの収録時間を見たいなど)」ためその準備をします。

# 分割してMerge
dfm = pd.merge(df.query('~is_aftershow'), df.query('is_aftershow'), how='left', on='show_no', suffixes=('_main', '_after'))
dfm['date'] = dfm['date_main']
dfm['minutes'] = dfm['minutes_main'].fillna(0) + dfm['minutes_after'].fillna(0)
dfm['persons'] = dfm['persons_main']
dfm['n_shownotes'] = dfm['n_shownotes_main'].fillna(0) + dfm['n_shownotes_after'].fillna(0) 

一旦基本的な前処理はここまでにして、込み入った処理はこの後都度解説しようと思います。

可視化

準備が出来たので可視化していきます。

ヒストグラム

自分は基本的にグラフごとにmatplotlibのapiを都度書くことが多いのですが、ヒストグラムは細かい表示をしたいのでメソッドにしています。

def plot_hist(s, title, bins=30):
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)

    mean =s.mean().round(2)
    median = s.median().round(2)
    std = s.std().round(2)

    sns.distplot(s, ax=ax, bins=bins, kde_kws={"color": "k", "lw": 3})
    ax.set_title(title, fontsize=20)
    ax.tick_params(axis = 'x', which = 'major', labelsize = 20)
    vals = ax.get_yticks()
    ax.set_xlabel('')
    ax.set_ylabel('')
    ax.set_yticklabels(['{:,.2%}'.format(x) for x in vals])
    ax.text( 0.99, 0.99, f"平均値: {mean:.2f} \n 中央値: {median:.2f} \n 標準偏差: {std:.2f}", horizontalalignment='right', verticalalignment='top', transform=ax.transAxes, fontsize=20)

こんな風に呼び出すと Plotできます。

plot_hist(df.query('~is_aftershow')['minutes'], '[本編] 収録時間の分布とKernel密度推定(分)')

f:id:mergyi:20181213053754p:plain
収録時間の分布

時系列

次は時系列のプロットをしていきます。

fig = plt.figure(figsize=(16, 8))
ax = fig.add_subplot(1, 1, 1)
dfm.plot('date', 'minutes', linewidth=5, linestyle='--', ax=ax)
dfm.set_index('date')['minutes'].rolling(window=4).mean().plot(linewidth=5, ax=ax)
ax.set_title('[全体] 一回当たりの収録時間は長くなっているのか(分)', fontsize=20)
ax.tick_params(axis='x', labelsize='xx-large')
ax.set_xlabel('')
ax.set_ylabel('')

f:id:mergyi:20181213054212p:plain
一回当たりの収録時間は長くなっているのか

set_index を使ってindexにdatetimeを設定するだけで簡単に時系列プロットすることができます。破線が実データで実線が4回ごとの移動平均になっているのがポイントです。 移動平均はpandasのrollingを使うと簡単に出すことができます。

続いて一ヶ月ごとのプロットもしてみます。

fig = plt.figure(figsize=(16, 4))
ax = fig.add_subplot(1, 1, 1)
ax = df.groupby(pd.Grouper(key='date', freq='1M'))['minutes'].sum().plot(linewidth=5, linestyle='--', ax=ax)
ax = df.groupby(pd.Grouper(key='date', freq='1M'))['minutes'].sum().rolling(window=4).mean().plot(linewidth=5, ax=ax)
ax.set_title('1ヶ月あたりの合計分数', fontsize=20)
ax.tick_params(axis='x', labelsize='xx-large')
ax.set_xlabel('')
ax.set_ylabel('')

f:id:mergyi:20181213054243p:plain
1ヶ月あたりの合計分数

先ほどと違うのは pd.Grouper を使って期間を設定しているところです。こちらの使い方に関してもsinhrksさんのBlogが詳しいので参照してみてください。

sinhrks.hatenablog.com

棒グラフ

次は棒グラフです。

fig = plt.figure(figsize=(16,8))

tmp_df = s_persons.value_counts()[:10].to_frame('value').reset_index().rename(columns={'index': 'name'})
ax = sns.barplot(x='value', y='name', data=tmp_df)
max_ = tmp_df['value'].max()

for i, (_, row) in enumerate(tmp_df.iterrows()):
    text = ax.text(row['value'] + max_*.03, i+0.1, row['value'], color='black', ha="center", fontsize=20)

[spine.set_visible(False) for spine in ax.spines.values()]
ax.tick_params(bottom=False, left=False, labelbottom=False)
ax.tick_params(axis='y', labelsize='x-large')
ax.set_xlabel('')
ax.set_ylabel('')
ax.set_title('出演回数ランキング', fontsize=20)
ax.patch.set_facecolor('white') 

ax.patch.set_alpha(0)
plt.grid(False)

f:id:mergyi:20181213054916p:plain
出演回数ランキング

棒グラフのプロットの仕方が下記のQiitaで書いてくださっている内容をかなり参考にしています。seabornを使ったほうが少しだけ簡単にかけるのと項目数が多いときも勝手にカッコよく表示してくれるので自分はこちらを使っています。

qiita.com

box-plot

box-plotに関しては、seabornのメソッドをそのまま使っているだけなので簡単に。(笑)

# 全員分表示すると多いのでTOP10に絞る
top10_speaker = s_persons.value_counts()[:10].index.tolist()
ax = sns.boxplot(data=dfms.query('person in @top10_speaker').sort_values('person'), x='person', y='minutes')
ax.set_title('出演者ごとの収録時間の分布', fontsize=20)
ax.tick_params(axis='both', labelsize='x-large')
ax.set_xlabel('')
ax.set_ylabel('')

f:id:mergyi:20181213055047p:plain
出演者ごとの収録時間の分布

今回はそんなに見やすくなかったので使わなかったのですが、 hue optionを使うと便利なときもあります。下のプロットは hue optionを使って本編とAftershowの分布を同時にプロットしたものです。

ax = sns.boxplot(data=dfs.query('person in @top10_speaker').sort_values('person'), x='person', y='minutes', hue='is_aftershow')
ax.set_title('出演者ごとの収録時間の分布', fontsize=20)
ax.tick_params(axis='both', labelsize='x-large')
ax.set_xlabel('')
ax.set_ylabel('')

f:id:mergyi:20181213055348p:plain
hue optionsを加えたもの

ワードクラウド

最後にワードクラウドです。驚くことにこれもPythonのライブラリで簡単に作ることができます。 まず下のように前処理をします。shownoteのテキストをスペースで区切って一般的な単語などを除いています。 本当はスペース区切りだと 「Alpha Go」などが「Alpha」「Go」になってしまって良くないので、最初に「Alpha Go」などの固有名詞が入ったの辞書を作っておくのがスタンダードですが、今回は大変なので、単純にやっています。(笑)

shownotes = []
for i, row in df.iterrows():
    tmp_shownote = row['shownotes']
    shownotes.extend(tmp_shownote)
s_shownotes = pd.Series(shownotes)

# 一般的な単語や記号を除外
ignore_word = [':', '/', '-', '–', 'to', 'your', 'for', 'the', 'and', '|', 'in', 'of', 'a', 'is', 'on', 'with', 'how', 'new', 'by', '2', 'at', 'rebuild:']

shownotes_split = []
for i, row in df.iterrows():
    tmp_shownote = row['shownotes']
    for t in tmp_shownote:
        tmp_split = t.split()
        shownotes_split.extend(tmp_split)
shownotes_split = [s.lower().replace(',', '') for s in shownotes_split]
shownotes_split = [s for s in shownotes_split if s not in ignore_word]

準備が出来たのでワードクラウドを作りたいと思います。下記のライブラリを作ってくれている方がいるので、これを使うだけで簡単に出すことができます。 詳しい使い方はDocumentを見てみてください。

github.com

plt.figure(figsize=(15,12))
wordcloud = WordCloud(background_color="white", width=900, height=500).generate(" ".join(shownotes_split))
plt.imshow(wordcloud)
plt.grid(False)
ax.tick_params(bottom=False, left=False, labelbottom=False)
ax.set_xlabel('')
ax.set_ylabel('')
[spine.set_visible(False) for spine in ax.spines.values()]

f:id:mergyi:20181213055638p:plain
ワードバスケット

最後に

  • 以上が「rebuild.fmの統計学」の作り方になります。
  • Pythonは便利なライブラリが本当に充実しているので簡単に色々なデータ収集/分析/可視化できますね。
  • Pythonでのデータ解析に興味持たれた方は是非チャレンジしてみてください。勉強会の参加もお待ちしています!笑
  • これからも良いPodastライフ&Pythonライフを!

Gist

rebuild.fmの統計学

人間のためのイケてるPython WebFramework「responder」、そして作者のKenneth Reitzについて

この記事は Python その2 Advent Calendar 2018 - Qiita の1日目です。

responderとは

GitHub - kennethreitz/responder: a familiar HTTP Service Framework for Python

f:id:mergyi:20181201121349p:plain

f:id:mergyi:20181201180257p:plain
Starの遷移

  • この記事では、responderが他のWeb Frameworkと比べて何が素晴らしいのか ということと、responderから見えてくる Kenneth Reitzというエンジニア について書きたいと思います。

Python WebFrameworkは戦国時代だが…

みなさんが「PythonのWebFramework」と言われて思いつくものはなんでしょうか。Django、Flaskでしょうか。まず、Python WebFramework界隈がどんな状況か見てみます。主流なFrameworkを時系列にまとめてみました。

f:id:mergyi:20181201161918p:plain
Python WebFrameworkの歴史

この図から下記のことがわかります。

  • 毎年のように新しいWebFrameworkが出ており 乱立 しているとも言える
  • 2011年以降はデファクトになっているWebFrameworkはない
    • sanicはstar数は多いですが、導入例の数を見るとデファクトとは言えない気がします
  • 2015年以降、GraphQL、asyncなどモダンな機能を備えたWebFrameworkが出てきているが、定着はしていない(未だにDjango、Flaskがメインに使われている)

PythonのWebFrameworkと聞くと、「また新しいWebFrameworkか(うんざり)」「DjangoとFlaskでいいじゃん」と思う方ももしかしたらいるかもしれませんが、今見たように、モダンな機能を備えたWebFrameworkが定着していない、という問題があったり、また今年はFlaskの作者がFlaskのメンテナンスを(実態として)辞めるという出来事もあり、実は今 新しいデファクトになるWebFrameworkが求められている と言えると思います。そしてこのresponderがそのデファクトになるのでは…と期待しながらこの記事を書いています。

responderを使う理由

まず、responderの魅力について書いていきたいと思います。

基本的な思想

READMEには下のような記載があります。

The primary concept here is to bring the niceties that are brought forth from both Flask and Falcon and unify them into a single framework, along with some new ideas I have.

記載の通り、FlaskとFalconの良さを1つにまとめたのが基本的な思想で、そこにKenneth Reitzが持っていた幾つかアイディアを加えたのがresponderになります。

使いやすいAPI

responderの一番の魅力はフレンドリーで使っていて楽しいAPIだと思います。 ToyExampleでresponderと、FlaskとFalconで書いたものを比較してみたいと思います。

Hello World

まずはHello worldです。

responder

import responder 

api = responder.API()

@api.route("/")
def hello_world(req, resp):
    resp.text = "Hello, World!"

Flask

from flask import Flask

app = Flask()

@app.route("/")
def hello_world():
    return "Hello, World!"

Falcon

import falcon

class HelloResource:
    def on_get(self, req, resp):
        resp.body = "Hello World!"

app = falcon.API()
app.add_route('/', HelloResource())

まさに、responderはFlaskとFalconの中間という感じですね。 これだけだとFalconよりはシンプルに書けそう、というのはわかりますが、さらにFlaskが簡単そうで、responderの魅力が伝わりませんね。

動的URL、QueryStringから情報取得&status_code、headerセットしてjsonをresponse

それでは次にほんの少しだけ複雑な例で見てみたいと思います。 URLとQueryStringからパラメータを受け取って、headerとstatus_codeを設定してjsonで返してみます。

responder

import responder 

api = responder.API()

@api.route("/hello/{who}")
def hello_to(req, resp, *, who):
    foo = req.params.get("foo", "")
    resp.headers["X-Pizza"] = "42"
    resp.status_code = 200
    resp.media = {
      "Hello": who,
      "Foo": foo
    }

Flask

from flask import Flask, jsonify, make_response, request

app = Flask()

@app.route("/hello/<who>")
def hello_to(who):
    foo = request.args.get("foo", "")
    body = jsonify({
        "Hello": who,
        "Foo": foo 
    })
    resp = make_response(body, 200)
    resp.headers["X-Pizza"] = "42"
    return resp

途端にFlaskの方は気になる箇所が噴出してきます。

  • responseの構築方法に一貫性がない
  • 追加で何かやろうとするたびに import でオブジェクト/メソッドを呼び出さないといけない
  • <> 記法がPythonに馴染みがない
  • QueryStringsへローカル変数でないrequestオブジェクトからアクセスしないといけない
  • (ここに書いてないですが)status_codeの返し方など色々な書き方ができてしまう

などです。

※ ちなみに自分はFlaskに精通しているわけではないのでもっと洗練された書き方ができるのかもしれません。ただドキュメントページで一番最初に出てくる通りにやるとこうなります。

responderでは、これらの問題点が全て解決されていることがわかります。

  • responseの構築方法が一貫している
  • importする必要があるのは responderのみ
  • {} 記法(f-string記法)で馴染みがある
  • QueryStringsへのアクセスはローカル変数のrequestオブジェクトから行える
    • さらにこのreqestオブジェクトは requests ライブラリで互換している

これらの解決方法はすべて、Falconから得ています。 端的にいうと、responderは Flaskの少しイケてないところをFalconのデザインで圧倒的に改善した Frameworkだと言えると思います。

非同期処理が簡単に書ける

APIのデザインはFlask、Falconから得ていますが、responderにはそのどちらも持っていない機能のあります。その1つが非同期処理が簡単に書けることです。 公式docsに書いてあるExampleですが、例えば画像をアップロードするAPIなどで、「responseは先に返してバックグラウンドでアップロード処理を行いたい」場合に、async/await記法を使って簡単に書くことができます。

import time

@api.route("/incoming")
async def receive_incoming(req, resp):
    @api.background.task
    def process_data(data):
        """Just sleeps for three seconds, as a demo."""
        time.sleep(3)

    data = await req.media()
    process_data(data)

    # Immediately respond that upload was successful.
    resp.media = {'success': True}

GraphQL APIが簡単に書ける

またGraphQL APIも簡単に書くことができます。grapheneというWebFrameworkを使っています。

import graphene

class Query(graphene.ObjectType):
    hello = graphene.String(name=graphene.String(default_value="stranger"))

    def resolve_hello(self, info, name):
        return f"Hello {name}"

schema = graphene.Schema(query=Query)
view = responder.ext.GraphQLView(api=api, schema=schema)

api.add_route("/graph", view)

他にもモダンな機能いろいろ…

その他にも、たくさんのモダンな機能を備えています。 この辺りは2018年に開発されただけあるな、という感じです。

  • FlaskなどのWSGI Applicationのマウント
  • SPA対応
  • HSTS対応

詳しくは公式docを御覧ください。(既に公式docの完成度が高い…)

https://python-responder.org/en/latest/tour.html

結論: responderは素晴らしい

シンプルでわかりやすいAPI、GraphQL対応,モダンな機能などresponderは素晴らしいWeb Frameworkですね!

https://media0.giphy.com/media/3ohs7R91OkVnSoKq4g/giphy.gif?cid=3640f6095c0266dc33556f4c41ef82ee

responderの裏側(構成要素)

外側から見たresponderの魅力がわかったので、内側も少しだけ解説しておこうと思います。

APIデザイン

APIデザインは上述したようにFlaskとFalconの良いとこ取りになっています。

ASGIサーバー

ASGIサーバー機能はresponderでは実装されておらず、uvicornというライブラリを使っています。

github.com

GraphQL

GraphQL機能もresponderでは実装されておらず、上述したようにgrapheneというWebFrameworkを使っています。

github.com

実は機能として実装しているものは少ない

このように、実はresponderは0から実装されたものではなく、既存の良いプロダクトを上手く組み合わせて作られている ことがわかります。これは responder の重要な特徴であり、また作者の Kenneth Reitz の性質を非常に表している要素です。

Kenneth Reitzが今まで開発したプロダクト

Kenneth Reitz というエンジニアの性質を知るために彼が今までに作ったプロダクトを見ていきたいと思います。

requests

GitHub - requests/requests: Python HTTP Requests for Humans™ ✨🍰✨

おそらく一番有名なライブラリだと思います。pythonを書いたことがある人の多くが一度は使ったことがあるのではないでしょうか。 requestsはpython標準moduleのurllibを使いやすくラップしたもの です。

pipenv

GitHub - pypa/pipenv: Python Development Workflow for Humans.

今年PyPAの公式Toolにも加えられた環境管理用のライブラリです。pipenvは主に標準moduleのvenvをラップしたもの です。

requests_html

GitHub - kennethreitz/requests-html: Pythonic HTML Parsing for Humans™

requests_htmlは、今年の上旬に公開されたライブラリで、requetsやbeautiful soup、pyppeteer、pyqueryなどをラップした 、htmlの取得、パースが簡単に行えるライブラリです。 知名度はまだそんなにないですが、responder同様かなりオススメです。

ライブラリの共通点

先ほど、responderについて、既存の良いプロダクトを上手く組み合わせて作られている と書きましたが、このように Kenneth Reitz の既存のライブラリは全てこの性質が当てはまることがわかります。Kenneth Reitzはrequests、pipenvのように、とても人気のあるライブラリを開発していますが、実は新しい機能の開発はほとんどしておらず、Kenneth Reitzのプロダクトの本質は APIデザイン にあると思います。

Kenneth Reitzは天才DX(Developer Experience)デザイナーである

Kenneth Reitzがどういうエンジニアなのか、ということを考えるときに「DX(Developer Experience)デザイナー」という言葉が自分の中でしっくりきます。

gfx.hatenablog.com

DX: Developer Experience (開発体験)とは、あるシステムを「気持ちよく開発・保守できるかどうか」を示すもの 開発者は開発・保守という行為を通じたそのシステムのユーザーであり、DXはUXの一種である DXがよいと日々の開発を楽しめるようになり、気持ちに余裕ができる

自分がrequestsやpipenv、そしてresponderを使っているとき、便利なのはもちろんなのですが、何よりも 「楽しい」 と感じます。Kenneth Reitzは楽しいと思えるAPIデザインをする天才だと思います。

例えば、前述したrequests_htmlでは下のようなAPIがあります。

from requests_html import HTMLSession
session = HTMLSession()
r = session.get('https://python.org/')

r.html.find('.foo > .bar', first=True) # first=True!

これはselectorで要素を取得して最初の要素を返すAPIなのですが、 この first=True というoptionがKenneth Reitzらしいと非常に感じます。 普通は下のように配列のインデックスとしてアクセスするようにのみAPIを設計するほうが一般的で first=True は醜い、と感じる人も多いと思います。

r.html.find('.foo > .bar')[0]

でも実際に書いてみると、 first=True の使い勝手の良さに気づくのではないでしょうか。実際、自分はこのAPIがとても気に入っています。 このAPIの例はかなり細かいものですが、Kenneth Reitzはこういった 「攻めている」だけど使うとしっくりくるAPIデザイン をすることにおいて天才的だといつも感じます。

requests_htmlでは他にも、1メソッドでHeadless Chromeインストールから実行までしてくれる下のオシャレなAPIも個人的なお気に入りです。

r.html.render() # Headless Chromeでrendering

Kenneth Reitz以外でDeveloper Experienceデザインに長けたエンジニアとして、真っ先に浮かぶのがkerasの作者のFrançois Cholletです。Theano、Tensorflowをわかりやすく楽しく開発できるようにラップしたkerasを開発した彼も間違いなく、Developer Experienceデザインの天才だと思います。

f:id:mergyi:20181201165116p:plain
機械学習/深層学習 市井の賢者から学ぶ(https://www.slideshare.net/yutakashino/ml15m2018-1027)より

最後に

  • Web Framework for Humanなresponderの紹介をして、作者のKenneth ReitzのDXの才能について書きました。
  • 是非responderで楽しいWebApplication開発を体験してみてください。
  • もしかしたら今後のデファクトスタンダードになるかもしれません…!
  • 自分はこれからもKenneth Reitzのfor HumanでDXの高いプロダクトを楽しみにKenneth Reitzを応援していきたいと思います。

つくばPythonもくもく会 No.2 開催レポート&No.3開催のお知らせ

つくばPythonもくもく会 No.2 を開催しました& No.3 を12/15(土) に開催します。

  • 先週の日曜(11/18)にNo.2を開催しました tskubapy.connpass.com
  • No.1 から参加者が大幅に増え合計13人の方に参加いただきました 🙏 ありがとうございます!
  • 早速ですが、No.3のイベントを作成しました。connpassからお気軽にご参加ください。 tskubapy.connpass.com

開催してみて

  • 今回一番驚いたことは、様々なバックグラウンドを持つ方に参加いただいたことでした。
  • 大学生から研究員、フリーランスSIer、そしてクリーニング店まで想像を超える様々な方とお話できて刺激的でした。
  • またエリアも茨城だけでなく、東京や栃木など離れた場所からお越しくださった方もいらっしゃいました。
  • またTsukubaRをオーガナイズしていらっしゃる、Uryu Shinya (@u_ribo) | Twitter さんともお話することができ、以前からお話したいと思っていたのでとても嬉しかったです。 www.meetup.com

  • No.1 は実は参加者が3人だけでかなりこじんまりと開催したのですが(笑) 、今回は参加者が 突然4倍以上 になり、上手く会を進行できるか少し心配だったのですが、参加者の皆さんがとても親切で積極的に会に参加してくださったので、無事成功したと(個人的には)思っております!

参加いただいた方の声

ありがたいことにTwitterで会についてコメントもいただいたのでシェアさせてください! 実は今回ハッシュタグを用意しておらず、反省事項でした。笑 次回からは事前にハッシュタグを用意しようと思いました。

少しだけ内容

次回参加するか悩んでいる方のために、内容を少しだけ紹介させてください。

目標説明

最初にもくもく会の目標を説明させていただきました。 「目的」としてしまうと、少しカッチリしすぎてしまうかなと思い、あえて「目標」という言葉を使っています。

f:id:mergyi:20181123173844p:plain

自己紹介

自己紹介では、一般的な自己紹介に加えて「聞かれたら答えられること/(あるいは質問したいこと)」という項目を追加しました。これは、この会の目標が交流にあるためで、それぞれの参加者の得意なことやモチベーションが共有されていた方が、その後のもくもくタイムでも交流が生まれやすいかな、と思って追加した項目でした。結果としてこれは上手くいき、その後自己紹介で話していたことをきっかけにして雑談や質問が飛び交っていたように感じました。

f:id:mergyi:20181123174058p:plain

もくもくタイム

その後は各自で作業するもくもくタイムでした。

f:id:mergyi:20181123174320j:plain

紹介が遅れましたが、開催場所としてはTsukuba Place Labさんを使わせていただいていて、雰囲気がよくスペース的にも余裕があって、リラックスして作業できたと思います。(↓に貼るアンケート結果を見ても良い結果でした) 会場を使わせていただきありがとうございました。

tsukubaplacelab.com

LT

会の終わりにLTの発表を受け付けることにしています。ただいきなりLTするのはかなりハードルが高いと思うので、ひとまず自分の方でLTを行いました。LTと言っても今回は自分のBlogに話したいリンクをまとめて紹介する形式を取りました。BlogはTwitterの方でもシェアいただき、たくさんの方に読んでいただけて、良かったです。

blog.ikedaosushi.com

アンケート

開催後に開催時間/場所についてアンケートを取らせていただきました。こちらに結果を貼らせていただきます。

f:id:mergyi:20181123170234p:plain

f:id:mergyi:20181123170253p:plain

全体的に時間場所については問題なさそうなので来月も同じ時間/場所で実施する予定です。 今後過半数が「駅の近くが良い」という回答であれば、場所変更を考える予定です。

回答いただいたみなさん、ありがとうございました。

さいごに

Pythonに関するオススメの書籍/サイト/サービス/Podcast/イベントの情報をシェア

Pythonをこれから勉強したい or 最新情報のキャッチアップがわからない、という方に

tskubapy.connpass.com

  • つくばPythonもくもく会 No.2を開催しました!
    • 参加者の方の中にはこれからPythonを始めたい、という方もいらっしゃったので、今まで自分が勉強してきた中で役に立ったものをリストアップして、LTしました。
    • 自分は機械学習系のエンジニアをしていて、そっちの方向で学んでいることも多いので、「言語について」と「機械学習/データ解析」という分け方をして紹介したいと思います。
  • 機械学習/データ解析」に関してはPythonに関係するものだけに絞っています。
  • 書籍に関してはたくさん挙げられても迷ってしまうと思ったので、本当は他にもたくさん素晴らしい書籍があるのは理解しつつ、「あえて選ぶなら」を載せています。

学習する上で注意すること

勉強する教材を選ぶ上で個人的に気にした方がいいと思っていることは↓です。

  • 最新の情報かどうか
    • Pythonに関わらずWeb技術はOutdatedになるのが早いです。常にその情報が最新に更新されているか確認する必要があります。
    • そういう意味で公式のチュートリアルなどは(ある程度)安心してできると思います。
  • 誰が書いているか
    • 書籍、インターネットには大量の情報が存在して玉石混交だと思います。その中でいい情報を選択するには「著者がどんな人か」を調べるのは非常に有用です。
    • 例えば自分が勉強したいライブラリがあったとき、その作者が書いている本はとても良いと思います。
    • pandasを学びたいときには作者のWes McKinneyが書いた「Pythonによるデータ分析入門」を読みましょう
    • kerasを学びたいときには作者のFrancois Cholletが書いた「PythonとKerasによるディープラーニング」を読みましょう

書籍

言語について

初級の方が読むのに良い本

入門 Python 3 という選択肢もありますが、自分はこちらの方が実践的で楽しく学べると思います。

中級から上級になりたい方が読むのに良い本

Fluent Python ―Pythonicな思考とコーディング手法

Fluent Python ―Pythonicな思考とコーディング手法

さらに実践的なTipsを学びたい人向け

Effective Python ―Pythonプログラムを改良する59項目

Effective Python ―Pythonプログラムを改良する59項目

包括的に学ぶというよりは、細かい実践的なTipsがたくさん載っています。FluentPythonを読み終えた後にExtensionとして読むのがオススメ。

機械学習

理論と実践を包括的に学びたい

scikit-learnとTensorFlowによる実践機械学習

scikit-learnとTensorFlowによる実践機械学習

現時点での決定版だと思います。Scikit-learn/Tensorflowの使い方とその裏側にある理論がバランスよく網羅的に記載されています。

↑が出る前の自分の中では決定版でした。ページ数的にはこちらの方が少し手っ取り早いと思います。 ただしDeepLearningについては記載なし 第二版からはDeepLearningの章も追加されています。(追記: ブコメで指摘いだきました。ありがとうございます。)

PythonとKerasによるディープラーニング

PythonとKerasによるディープラーニング

Kerasについて学びたい方はKeras作者のFrancois Cholletが書いたこちらの本がオススメです。

前処理、データ解析について学びたい

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

Pythonによるデータ分析入門 第2版 ―NumPy、pandasを使ったデータ処理

pandasの作者Wes McKinneyの著書。第一版を間違って買わないように注意。(Python2系なので)

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

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

Jupyterだけでなくpandasやmatplotlibについても詳しく書いてあります。

ベイズの勉強をしたい

Pythonによるベイズ統計モデリング: PyMCでのデータ分析実践ガイド

Pythonによるベイズ統計モデリング: PyMCでのデータ分析実践ガイド

現時点でのPythonでのベイズ統計を学ぶ決定版だと思います。もう少し詳しい感想はこちらに書きました。

blog.ikedaosushi.com

Pythonで体験するベイズ推論 PyMCによるMCMC入門

Pythonで体験するベイズ推論 PyMCによるMCMC入門

↑が出る前は自分の中で決定版でした。↑より少しとっつきやすいと思います。 また、英語版はGitHub上でJupyterNotebook形式で全て読めます。 github.com

サービス/サイト

言語を学びたい

Python チュートリアル — Python 3.6.5 ドキュメント 公式チュートリアルです。基本的に間違った内容が書いていない、かつ常に最新版にアップデートされていくので、信頼性が高いです。

Python Boot Camp(初心者向けPythonチュートリアル) — PyCon JP

PyConJP が主催しているPython BootCampのチュートリアルです。こちらも定期的に全国で開催されながらアップデートされていくので、Outdatedな内容は少ないと思います。

有名ライブラリの情報を包括的に知りたい

github.com Webアプリケーション, Desktopアプリケーション、音声解析...などジャンルごとに有名なPythonライブラリがまとめてあります。 Pythonで何か作りたいものがあるけど、どのライブラリを使って良いかわからないときは始めにここを見て、Star数を見て判断するのが良いでしょう。

GitHubじゃ!Pythonじゃ! – GitHubからPython関係の優良リポジトリを探したかったのじゃー、でも英語は出来ないから日本語で読むのじゃー、英語社会世知辛いのじゃー

Python(など)の有名GitHubリポジトリを翻訳してくれているサイトです。RSSで購読しておくのがオススメです。

最新動向を追いたい

メーリス

www.pythonweekly.com

python.libhunt.com

メーリスに登録していると毎週木曜/金曜日あたりに、「今週あったPython界隈での主な動き」を配信してくれます。 このメーリスを購読していれば基本的に大きな動きは見逃さないでしょう。

github-trending-repos

github.com

github-trending-reposは面白いリポジトリで、各言語ごとにIssueが切られていて、最近ホットなライブラリがそのIssueに書き込まれるシステムになっています。 自分が気になるカテゴリをGitHubの機能でWatchしておくのが良いです。自分が購読しているのが↓のPythonとjupyterです。

Python New weekly trending repos in Python · Issue #11 · vitalets/github-trending-repos · GitHub

Jupyter https://github.com/vitalets/github-trending-repos/issues/110

機械学習を勉強したい

オンラインコース

オンラインコースはfast.aiとCourseraがオススメです。 シラバスを見ることができるので最初にチェックして自分に合っているものを受講するのが良いでしょう。

course.fast.ai

www.coursera.org

www.coursera.org

www.coursera.org こちらはPythonではなくMatlab/Octaveなのですが、とてもオススメなので例外的に入れさせてもらいました。

ドキュメント

scikit-learnのドキュメントは単なるライブラリの説明を超えて機械学習に関するエッセンスがてんこ盛りです。

scikit-learn: machine learning in Python — scikit-learn 0.20.0 documentation

記事

参考になりそうな記事です。

employment.en-japan.com

qiita.com

qiita.com

Podcast

言語について

talkpython.fm

www.kennethreitz.org requests, pipenvのKenneth ReitzがやっているPodcastです。

機械学習

softwareengineeringdaily.com

twimlai.com GoogleBrainのJeff Deanや、fast.aiのJeremy Howardなど機械学習のトップエンジニアが出演しているPodcastです。

イベント

言語について

pyconjp.connpass.com

PyCon JPは日本で一番大きなPythonのイベントです。毎年一回9月に開催されています。

Python Boot Camp(初心者向けPythonチュートリアル) — PyCon JP

Python BootCampはPyCon JPが定期的に全国各地で開催しているイベントです。 熟練されたPythonistaが直接Pythonを教えてくれるのでおすすめです。

機械学習

pydatatokyo.connpass.com

機械学習やデータサイエンスに限定したPythonの勉強会です。

machine-learning15minutes.connpass.com

最後に

  • 「これもあるよ!」「こっちの方がいいよ!」というものがあれば、是非コメントで教えてください
  • それではよいPythonLifeを!

scikit-learnのRandomForest.feature_importances_のコードを追う

feature_importances_をちゃんと理解する

  • feature_importances_ とは sklearn.ensemble.RandomForestClassifiersklearn.ensemble.RandomForestRegressor (など)で特徴量の重要度を出力するメソッドです。
  • とても便利で、EDAやモデルの精度向上のためのアイディアを得るためによく使用しますが、「この重要度って何を表しているの?」と聞かれたときにパッと説明できなかったので調べてみました。
  • ちなみにドキュメントには↓の1行だけ説明があります。

    The importance of a feature is computed as the (normalized) total reduction of the criterion brought by that feature. It is also known as the Gini importance.

開発者が回答しているStackOverflow

もう少し細かい説明としてはscikit-learnの開発者 Gilles Louppe がstackoverflowで↓のように質問に回答しています。

stackoverflow.com

There are indeed several ways to get feature "importances". As often, there is no strict consensus about what this word means.

In scikit-learn, we implement the importance as described in [1] (often cited, but unfortunately rarely read...). It is sometimes called "gini importance" or "mean decrease impurity" and is defined as the total decrease in node impurity (weighted by the probability of reaching that node (which is approximated by the proportion of samples reaching that node)) averaged over all trees of the ensemble.

記載されている通り、gini importance あるいは mean decrease impurity と呼ばれ、ノードの不純度(impurity)をアンサンブル木で平均したものになります。 これが簡潔で正しい回答なのですが、一応コードベースでも見てみたいと思います。

該当コード

順番に見ていきます。まず大元の sklearn.ensemble.forest.BaseForest クラスのメソッドです。

https://github.com/scikit-learn/scikit-learn/blob/1128094271923c66f9e602372ba7ee8b7f565e52/sklearn/ensemble/forest.py#L365 ※ 該当部分以外を省略しています

    def feature_importances_(self):
        all_importances = Parallel(n_jobs=self.n_jobs,
                                   **_joblib_parallel_args(prefer='threads'))(
            delayed(getattr)(tree, 'feature_importances_')
            for tree in self.estimators_)

        return sum(all_importances) / len(self.estimators_)

わかりやすいコードで、ここで各アンサンブル木のall_importancesを平均していることがわかります。

呼ばれているのは、 sklearn.tree.tree.BaseDecisionTree のメソッドで、単純に compute_feature_importances を呼び出しているだけです。

https://github.com/scikit-learn/scikit-learn/blob/a80bbd9403fea9cf4aa46dfef26a4b31a608957b/sklearn/tree/tree.py#L513 ※ 該当部分以外を省略しています

    def feature_importances_(self):
        return self.tree_.compute_feature_importances()

ここからはCythonで書かれた sklearn.tree._tree.Tree クラスのメソッドを呼び出しています。

https://github.com/scikit-learn/scikit-learn/blob/a80bbd9403fea9cf4aa46dfef26a4b31a608957b/sklearn/tree/_tree.pyx#L1062 ※ 該当部分以外を省略しています

    cpdef compute_feature_importances(self, normalize=True):
        cdef np.ndarray[np.float64_t, ndim=1] importances
        importances = np.zeros((self.n_features,))
        cdef DOUBLE_t* importance_data = <DOUBLE_t*>importances.data

        with nogil:
            while node != end_node:
                if node.left_child != _TREE_LEAF:
                    # ... and node.right_child != _TREE_LEAF:
                    left = &nodes[node.left_child]
                    right = &nodes[node.right_child]

                    importance_data[node.feature] += (
                        node.weighted_n_node_samples * node.impurity -
                        left.weighted_n_node_samples * left.impurity -
                        right.weighted_n_node_samples * right.impurity)
                node += 1

        importances /= nodes[0].weighted_n_node_samples
        return importances

こちらが特に重要な部分でnodeの不純度(impurity)から左右のnodeの不純度を引いたもの(それぞれ重みをかけている)をその特徴量の不純度にしています。

importance_data[node.feature] += (
  node.weighted_n_node_samples * node.impurity -
  left.weighted_n_node_samples * left.impurity -
  right.weighted_n_node_samples * right.impurity)

この不純度を各アンサンブル木で平均したものが feature_importances_ になります。

さいごに

  • ざっくりとですが、コードを追って、 RandomForestfeature_importances_ を理解しました。
  • ちなみに他の指標としては mean decrease accuracy があります。
  • これはOOBを用いて測る指標で、該当変数をモデルから除いた際の予測精度の低下を計算します。RのRandomForestにはこの指標も実装されています。

機械学習関係の主要人物20人を調べてみました

先日のイベントで調べよう!と思うきっかけがありました

  • 下のTweetのように先週は「MachineLearning15Minitues」というイベントに参加してきました。
  • 株式会社バクフーの柏野さんの発表が個人的にとても好きな内容で是非皆さんにも見てもらいたいのですが、本筋の内容以外で一点気になる内容がありました。

  • それは下のスライド群で、実は柏野さんは「この辺りの方々は皆さんご存知だと思います。今回はなしません」と仰っていたのですが、恥ずかしながら半分くらいしかわかりませんでした。。

f:id:mergyi:20181104012727p:plain

  • そこで自分の勉強の兼ねてそれぞれの方々を調べてみることにしました!
  • ちなみに調べていて自分が追加した方がいいかなと思った方を一部追加しています。

紹介方法

下記のような形式で紹介していきたいと思います。 f:id:mergyi:20181104013035p:plain それぞれ、人物ごとに調べられるように参考リンクも貼りました。

創造主

まず創造主は3人です。

f:id:mergyi:20181104012926p:plain

イノベーター

イノベーターは6人います。

f:id:mergyi:20181104013155p:plain f:id:mergyi:20181104013209p:plain

ボス

f:id:mergyi:20181104013304p:plain f:id:mergyi:20181104013334p:plain

アンファン・テリブル

アンファン・テリブルジャン・コクトーの小説から生まれた言葉で「恐るべき子供たち(ここでは若者たち)」という意味ですね。5人のアンファン・テリブルを紹介します。

f:id:mergyi:20181104013410p:plain f:id:mergyi:20181104013431p:plain

最後に

というわけでまとめて見ました。全体を通して印象としては、主要人物がGoogle(とDeepMind)に本当に多いです。Googleという会社は改めてモンスターだなと思いました。 ちなみに、柏野さんのスライドの中だと下の三人を見つけることができませんでした…特に左右の2人は絶対見たことがあるので、思い出せなくて悔しいです。どなたかご存知の方がいたら教えてください。 f:id:mergyi:20181104014933p:plain

※ 追記: 2018-11-24 1人はYarin Gal でした。

Yarin (@yaringal) | Twitter

%macro/%store を組み合わせてJupyterでのライブラリ読み込みを劇的に効率化する

Jupyterで読み込むライブラリを毎回書いてませんか?

  • 機械学習やデータ解析をJupyterで行うときに読み込むライブラリって大体決まっていますよね。
  • 毎回1、2つ目のセルにライブラリ読み込み処理をズラッと書いてないですか?
  • 人によってはスニペットアプリなどで管理している方もいるかもしれませんが、Jupyterの %macro%store を使うことでもっと短く、シンプルに、効率的にライブラリの読み込みを行うことができるので紹介したいと思います。

Notebookの最初のセルはこんな風になっていませんか?

これは自分がいつも読み込むライブラリ群で、自分はAlfredのsnipetを使って呼び出せるようにしていました。

%load_ext autoreload
%autoreload 2
%matplotlib inline
from IPython.display import Image

import os, sys, re, datetime
from pathlib import Path

pj_dir = Path(os.getcwd()).parents[1]
data_dir = pj_dir/'data'
img_dir = pj_dir/'images'
src_dir = pj_dir/'src'
sys.path.append(str(src_dir))

import numpy as np
import pandas as pd
import dask.dataframe as dd
from scipy import stats 

from tqdm import tqdm_notebook
from dotenv import load_dotenv

from matplotlib import pyplot as plt
from cycler import cycler

ちなみにmatplotlibの初期設定は %matplotlib inline と一緒に呼び出すとリセットされてしまう という既知のバグがあるので、セルを分けてmatplotlibの設定をしています。

Changing rcParams in same Jupyter notebook cell as %matplotlib inline has no effect · Issue #11815 · matplotlib/matplotlib · GitHub

plt.style.use("bmh")
plt.rcParams['axes.prop_cycle'] = cycler(color=plt.get_cmap('Set1').colors)
plt.rcParams["figure.figsize"] = (16, 4)
plt.rcParams["font.family"] = "YuGothic"

また、scikit-learnで複数モデルを比較したいときは、↓のような読み込みが必要になりますし、

import sklearn
from sklearn.model_selection import KFold

from sklearn import feature_extraction
from sklearn import preprocessing

from sklearn.linear_model import Ridge
from sklearn.neighbors import KNeighborsRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn import metrics
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
from catboost import CatBoostRegressor

さらにDeepLearningしたいときは↓のような読み込みも追加されます。(これは大分少ないほうだと思います。RNNするときはこの3倍くらいはインポートしてる気がします。)

from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras.layers.advanced_activations import LeakyReLU
from keras import initializers

非常に長いですね。 上記を全部入力するとこんな感じです。

f:id:mergyi:20181025170010p:plain

全然メインロジックに集中できない! ですね。

%macro で解決しよう

そこで %macro を使います。 %macro はiPythonのMagicCommandの1つでJupyterからも使うことができます。

Built-in magic commands — IPython 7.0.1 documentation

使い方は簡単です。まず保存から

保存

ソースはこちら👇

はてなブログでJupyterNotebookのいい共有方法がわからないので良い方法ご存知の方がいたら教えてください。

python-sandbox/20181025_record_macros.ipynb at master · ikedaosushi/python-sandbox · GitHub

f:id:mergyi:20181025170413p:plain

%macro を使っているのは下記の部分です。

%macro -q __imp 1
%macro -q __mpl 2

このように

%macro -q __imp [保存したいセルの実行番号]

と書くことで保存することができます。使うときは

__imp

でさきほど保存したセルが呼び出されます。

これだけでは、同じNotebook内で使えるだけなので、さらに %store を使って保存します。

%store __imp
%store __mpl

これで保存されたので、KernelやProcessを再起動しても使うことができるようになります。

完全に余談なのですが、 %store で保存された変数は↓にあるようです。これを書いていて初めて知りました。

Where are %stored Jupyter files located? - Stack Overflow

%IPYTHONDIR/<profile_name>/db/autorestore/<variable_name>

読み込み

ソースはこちら👇

python-sandbox/20181025_use_macros.ipynb at master · ikedaosushi/python-sandbox · GitHub

f:id:mergyi:20181025171858p:plain

読み込みも簡単で、

%store -r __imp
%store -r __mpl

のように書いて、

__imp
__mpl

と呼び出すだけです。これでライブラリのインポートとmatplotlibの設定を読み込むことができました。ちゃんとnumpyの読み込みとmatplotlibの設定も読み込めています。

さらに踏み込んで

さらに毎回保存された変数を取り出すのがめんどくさい!という方は .ipython_profile に下記のように記載することで、自動で %store された変数を呼び出すこともできます。

c.StoreMagics.autorestore = True

最後に

  • Jupyter MagicCommand %macro%store を組みわせて簡単にライブラリを読み込む方法を紹介しました
  • Jupyterに関するTipsは最近だとPyConJP 2018で発表されていた「Jupyterで広がるPythonの可能性」やリクルートテクノロジーズさんのブログ記事「あなたの生産性を向上させるJupyter notebook Tips」にもかなりまとめられていたのでおすすめです。

github.com

recruit-tech.co.jp

それでは良きJupyterLifeを!