いっきのblog

技術とか色々

Laravel ShiftでLaravelを簡単アップグレード!

Laravel #2 Advent Calendar 2018の11日目です!
今回は、Laravelのアップデートのやり方について書きたいと思います。

バージョンをあげる時の課題

僕らのチームは元々Laravelのバージョンを5.5を使っていて、そろそろあげたいよねって話をしてました。
ただ、「調査や変更による工数がかかる」という(多分心理的な)課題があり、そんな時にメンバーの一人が「Laravel Shift」使えば楽そうですよという話から今回に至りました。

Laravel Shiftとは

簡単にいうと、課金すればLaravelのアップグレードを自動でやってくれるというものです。

laravelshift.com

なかなか便利そうな感じが漂ってますが、いっきにバージョンアップすることはできないので一つずつバージョンをあげていきます。
今回は5.5から5.7にあげたいので、5.5から5.6($9)、5.6から5.7(7$)の計16$でバージョンアップをしたいと思います。

f:id:kzkohashi:20181210202747p:plain

5.5から5.6へのアップグレード

アップグレードは簡単で、GitHubと連携し、クレジットカードを入力すれば始まります。

f:id:kzkohashi:20181210203204p:plain

数分待つと、指定したブランチへプルリクが自動で作られます。
(自分が英語で話した気分になるので、すごくできた気分になれます)

f:id:kzkohashi:20181210203417p:plain

主なプルリクの内容は以下です。

  • PSR-2によるコードスタイルの変更のコミット(準拠してなかったのでいい機会なので変更)
  • アップグレードに伴う各種ファイルの修正コミット
  • 自動で直せない部分を、コメントで教えてくれる

自動で直せない部分もあるのかよっと思うかもしれませんが、コメントで教えてくれるのでそれの通り直すだけなので楽チンです。
修正後は、「ユニットテスト」と「E2Eテスト」をしてバグがないことを確認したらマージします。

5.6から5.7へのアップグレード

こちらも同じように行います。

f:id:kzkohashi:20181210204250p:plain

ユニットテストだと全てのテストを網羅できてないので、E2Eテストはかなり念入りに行い、無事マージ/リリースを行います。

アップグレードによるバグ

大きいバグは1ヶ月たった今もまだありません。
ただ、5.7にあるbootstrap/cacheservices.phppackages.phpのオーナー権限がおかしい問題のせいでデプロイのたびにすごく困ってます・・。
修正済みなので、composer updateすればもう大丈夫です。

github.com

と、おもったら今度はTestResponseJSON周りがおかしくてテストがこける。。。 マージはされたので早くreleaseしてほしいと願っております(12/11現在)

github.com

終わりに

最後はあまり関係ないバグの話になってしまいましたが、かかった工数は全部合わせても数時間です。 たった16$でエンジニアの工数をあまり使わずでき、新規開発にすぐ取り組めて、ビジネスサイドにもほぼ迷惑かからない最高のツールでした。ありがとうございます。

Genderize.ioを使って性別判定してみる

以前、顔写真を使って性別判定についての検証をした。
顔写真による性別判定はそこそこ判定が高くすごく満足しているのだが、顔がない場合の判定はどうすればいいんだろうと思い、ちょい調べてみた。

kzkohashi.hatenablog.com

名前による判定

Genderize.ioと呼ばれる、名前による男女の判定はすでに存在していた。
自分たちでもできそうな気もしなくもないけど。。。とりあえず使ってみる。

https://store.genderize.io/

デモでapiが使えるため、実際に自分の名前出たていてみると

curl https://api.genderize.io/?name=kazuki
{"name":"kazuki","gender":"male","probability":1,"count":16}

jsonでデータがかえってくるため、各項目の意味を調べると以下になる。

name: 入力した名前
gender: 性別
probability: 確からしさ(精度)
count: ヒットしたデータ数

公式には、probabilitycountを使って自分たちで閾値みたいなのを決めてねと書いてある。
今回だとprobability1なのでほぼあってそうに見えるけど、countが少ないため少し怪しいのかなーと思う。kazukiでも女性の名前はいるので、日本語は少し弱いのかな?という印象を受ける。

終わりに

いくつか名前の検証をした感じだと良さそう。本番である程度試したらまた検証記事書こうと思う。

DataFrameで特定カラムでユニークに集計する方法

DataFrameで特定カラムでユニークに集計する方法についてのメモ。

利用するデータ

今回利用するデータは、一つのアカウントに対して、複数の本のタイトルが紐づいてるデータとする。

df[['account_id', 'title']]

f:id:kzkohashi:20180828224809p:plain

本のタイトルがいくつ紐づいてるか集計すると以下のようになる。

df.groupby(['title']).agg({'title': 'count'}).sort_values(by='title', ascending=False)

f:id:kzkohashi:20180828224220p:plain

このままだとあるアカウントが同じ本のタイトルに何回も紐づいてしまうため、アカウントごとに同じタイトルの本が複数紐づいてる場合は一つとカウントしたい。

agg関数をうまく使ってユニークにする

若干怪しいところあるけど、とりあえずはこれでできた。

unique_df = df.groupby(['account_id','title']).title.agg(lambda x: x.unique()).to_frame()
unique_df.reset_index(drop=True)
unique_df.groupby('title').agg({'title': 'count'}).sort_values(by='title', ascending=False)

f:id:kzkohashi:20180828224714p:plain

agg関数で集計する際に、値をユニークにしてるがミソ。

終わりに

SQLならこうできるのに!というのがまだあるため、もっとDataFrameに慣れないとなー。

Python(pandas)でざっくりデータを確認する方法

原さんのブログ読んでいたら、データの傾向を見る方法について簡単に書かれていたので真似したみた。

toohsk.hateblo.jp

結論から言うと、確かに傾向を見るのはすごく楽だし、癖にしたい。

要約統計量の確認

実はブログを見る以前に、「ヒストグラム」と「要約統計量」については学んでいた。

kzkohashi.hatenablog.com

ただ、自分が調べてみたいものではなかったせいのもあり、今思うと全然理解できてなかったんだなと悔しみ。。

気を取り直して、今回は色々な著名人のツイートデータを使用する。
pandasにはdescribeと呼ばれる要約統計量をサクッとだせる便利すぎる関数があるのでそれ使う。

import pandas as pd
import csv

chomei_df = pd.read_csv('../data/tyomeijinn.csv')

chomei_df.describe()

f:id:kzkohashi:20180827230050p:plain

favorite_countを見ると、平均が374と多そうに見えるが、分散は2915とかなり散らばっている。中央値(50パーセンタイル順位)は18、75パーセンタイル順位は106と差が大きそうに見える。

ヒストグラムを使ってデータのばらつきなどを確認する

先ほどのfavrote_countを使ってヒストグラムをみて見る。

chomei_df.plot.hist(y=['favorite_count'], bins=50, alpha=0.6, figsize=(12,8), sharex=True)

f:id:kzkohashi:20180827231313p:plain

傾向的にはほぼ先ほどの要約統計量でみた通り、ほとんどのいいねが前半に集まっている。異常な値は他の数値をみづらくするので、75%以内に納めるデータでグラフ化すると

chomei_df.query('favorite_count < 106').plot.hist(y=['favorite_count'], bins=20, alpha=0.6, figsize=(12,8), sharex=True)

f:id:kzkohashi:20180827231941p:plain

だいぶみやすくなって、傾向を追いやすくなった。当たり前だけどいいね数が多くなるほど対象のツイートが減っていいっている。

終わりに

pandasを使って要約統計量やヒストグラムをだした。ざっくりとした値や傾向をみたい時には便利だなと思ったので重宝したい。

Scrapy + Selenium + Headless Chromeを使ってJupyterからスクレイピングする

以前Scrapyを利用してみたが、Jupyterで使えないのか調べて見たのと、ついでにHeadlessブラウザでスクレイピングできないかも調べてみた。

kzkohashi.hatenablog.com

Selenium + ChromeDriverのインストール

Seleniumはいつものpipでインストールする。

pip install selenium

ChromeDriverbrewでインストール。別の方法もあるみたいだが、自分はbrewがインストールされてるのでこちらで。

$ brew install chromedriver

Error: No available formula with the name "chromedriver" 
It was migrated from homebrew/core to caskroom/cask.
You can access it again by running:
  brew tap caskroom/cask

エラー。見ている先にはないので、言われた通り実行する。

$ brew tap homebrew/cask
$ brew cask install chromedriver

これでインストールできた。

最新のChromeDriverをダウンロードしたい場合は以下からいける。 sites.google.com

JupyterでSelenium + ChromeDriverの利用

まずはSelenium + ChromeDriverの使い方。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()

# ヘッドレスにするには必須
options.add_argument('--disable-gpu')
options.add_argument('--headless')

# 必須じゃないけどUserAgentとか変更したいなら追加する
options.add_argument('--lang=ja')
options.add_argument('--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36')

# Chromeを指定。ここでFireFoxなども指定できるがHeadlessはないかも。
driver = webdriver.Chrome(chrome_options=options)

# 実際にURLから取得する処理
driver.get('http://localhost)

print(driver.page_source)

driver.quit()

設定も簡単で、これでデータを取得できる。
ただ、このままだとfor分で回してる時にエラーとか出て止まってしまうことがあるため、取得部分を以下のように変更した。

try:
    driver = webdriver.Chrome(chrome_options=options)
    driver.get('http://localhost')
except:
    print('error')
    pass
finally:
    driver.quit()

Scrapyを使って取得したデータの抽出

このままだと使いづらいので、Scrapyに渡して抽出する。

from scrapy.http import HtmlResponse

response = HtmlResponse(
                        driver.current_url,
                        body = driver.page_source,
                        encoding = 'utf-8')
sel = response.css('#id a::text')
if sel.extract_first():
    data = sel.extract_first().strip()

以上でできた。(あっさり・・)
補足しておくと、scrapy.httpではHTML形式だけではなく、XMLのパースにも対応してくれているので、利用する際は以下を見といたほうがいい。

Requests and Responses — Scrapy 1.5.1 documentation

終わりに

JupyterScrapy + Selenium + Headless Chromeの利用はかなり簡単にできた。
Pythonスクレイピングは情報が多く、めっちゃ楽だ。

Scrapyのスクレピングが簡単すぎて今更感動した話

僕はPHPスクレイピングする時はGoutteを使っていた。

github.com

サッやりたい時とかは便利だったりするが、robots.txtの中身だったりの確認やページング処理については自分で実装が必要なため手間だなと思っていた。

ふと最近Pythonをよく使ってるし、スクレイピング業界で有名なScrapyを一度使ってみるか・・と思ったのがきっかけである。

Scrapyとは

Python製のスクレイピングフレームワークである。

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

スクレイピングに特化しているフレームワークなだけあって、スクレイピングでやりたいことは基本的になんでもできる。(JS必須のサイトははまだ試してないが)
また、フレームワークというだけあって、やり方に沿ってプログラミングしていけば楽々できてしまうものである。実際にやってみる。

インストール

いつも通りのpip installを行う。

pip install scrapy

プロジェクトの作成

Scrapyではプロジェクトのテンプレートを作ることができる。

scrapy startproject localhost

今回はローカルのサイトをスクレイピングしてみるため、localhostを名前をつける。中身を確認すると、

$ cd localhost
$ tree 
.
├── localhost
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

データの格納場所を作る

スクレイピングしてきたデータは、Entityのようなデータの入れ物を設定する必要があるためそこを修正する。

$ vim items.py

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

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

import scrapy


class LocalhostItem(scrapy.Item):
    # この部分だけ追加
    user_name = scrapy.Field()

今回は、user_nameというデータを取ってくるためその入れ物を追加しておく。

Spiderでスクレイピングの処理を書く

実際にクローリングやスクレイピングを行うのはこのSpiderのお仕事で、その準備をする。
まずはSpiderを作るためにコマンドを叩く。

scrapy genspider local localhost.com

スクレイピングの名前、スクレイピングするホスト名という感じで書く。
実行すると、spiderディレクトリいかにlocal.pyができるので編集する。

$ vim spider/local.py

デフォルトは以下のような感じになる。

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


class LocalSpider(scrapy.Spider):
    name = 'local'
    allowed_domains = ['localhost.com']
    start_urls = ['http://localhost.com/']

    def parse(self, response):
        pass

start_urlsはクロールしたいパスなどを複数記述することができる。
ただし、HTMLの構造が似ていないと難しいので基本的には同じ系統のデータだが、パス名が違う場合のみ書く。異なる場合はまた別にspiderファイルを作る。

start_urls = ['http://localhost.com/path1', 'http://localhost.com/path2']

次に実際のデータを取得するためのロジックをparseに書く。

# 先ほど作った入れ物のインポート
from localhost.items import LocalhostItem

...省略
    def parse(self, response):
        # response.cssでデータの中身をcssみたく取ってこれる
        for sel in response.css('.column > section > .section-accountlist'):
            article = LocalhostItem()
            article['user_name'] = sel.css('.username::text').extract_first()
            yield article

response.csscssを指定するとデータを取得することができる。今回は複数あるため、forで回し、さらにそこからデータ取得し、格納する。

さらに今回はページング処理もあるので、ページングする際の「次へ」のリンクを探し、再帰を行えるようにする。

    def parse(self, response):
        # response.cssでデータの中身をcssみたく取ってこれる
        for sel in response.css('.column > section > .section-accountlist'):
            article = LocalhostItem()
            article['user_name'] = sel.css('.username::text').extract_first()
            yield article

        next_page = response.css('nav .link-next::attr("href")')
        if next_page:
            url = response.urljoin(next_page.extract_first())
            yield scrapy.Request(url, callback=self.parse)

Spiderの準備はこれで終わり。

実際にスクレイピングしてみる

spiderの名前で起動できる。

$ scrapy crawl local -o result.csv

-oオプションをつけると、データの中身をアウトプットできる。
これで、後は勝手にスクレイピングし、データ(今回だとuser_name)を書き込んでくれる。

終わりに

Scrapyはわりと有名だったので知っていたが、ここまで便利だとは思っておらずメモがてら今回は書いた。
実際にはデータベースも絡んだ実装などにもなってくると思うので、次はその辺かきたいなー。

Think Stats(第2版)を読む:5章 分布をモデル化する

前回に続いて統計の勉強。

kzkohashi.hatenablog.com

今まで扱った分布は経験分布(empirical distributions)と呼ばれているもので、すべて実際の観察に基づいた分布だったからとのこと。
そうなると、標本サイズはデータサイズが限界となり、有限となる。 また、そのほかにはCDFで特徴付けられる解析分布(analytic distribution)というものがある。普通は連続分布(continuous distribution)と呼ぶらしいが、なんで解析分布にしたんだろう・・。解析分布は、経験分布をモデル化するのに使えるとのこと。ここでのモデル(model)とは、必要がない詳細を省いた単純化のことらしい。ちょっとよくわからないが、進めていけばわかるだろう。

指数分布

簡単な指数分布(exponetial distribution)からモデル化を始める。
指数分布のCDFの式とグラフ以下になる

\large{CDF(\pmb x) = 1 - \begin{equation} e^{-λx} \end{equation}}

f:id:kzkohashi:20180813115101p:plain

母数(パラメータ)λが分布の形状を決定する。
指数分布は一連の事象を観測して、到着時間間隔(interarrival time)と呼ばれる、事象間の時間を計測する時に現れる。

なるほど・・・全然わからない。これが重要なのかな

事象が常に同じ確からしさで起こる時、到着時間間隔の分布は指数分布になる傾向がある。

だいたいわかったけど難しい単語が多いので、以下の記事がわかりやすかった。

bellcurve.jp

では、どうやって実際の分布が指数分布のモデルに当てはまると断言できるのか。
1つの方法として、相補CDF(CCDF)、つまり1 - CDF(x)で描画して見るとわかりやすくなるとのこと。

f:id:kzkohashi:20180813122947p:plain

本来は直線に近くなるはずだが、そうではないためこのデータには指数分布はあってないという判断にするっぽい。
おそらく、計算で妥当性を評価するやつもありそうだけどここでは書いてなかった。

正規分布

正規分布ガウス分布とも呼ばれている。近似的に多くの現象を記述できるので、非常によく使われている。なぜ現れるのかは別の章での説明になるとのこと。

f:id:kzkohashi:20180813123938p:plain

実際のデータとモデルとの比較した際に、かなり酷似しているので、適切なモデルと言える。
ただ、10パーセントタイル値より下のデータでは少し差がでているため、解きたい課題によっては適切ではない場合があるとのこと。

f:id:kzkohashi:20180813124410p:plain

正規確率プロット

指数分布は相補CDFのような、モデルが適切か判断する式があったが正規分布にはない。
代わりに、正規確率プロットと呼ばれう方法を使う。

細かい説明は省くが、正規分布で用いたデータを使うと実際のデータはモデル(正規分布)より、裾のほうがズレが生じてるのがわかる。 f:id:kzkohashi:20180813125136p:plain

対数正規分布

値の対数をとって、正規分布したのが対数正規分布となる。
正規分布とやり方はそんなかわらないのでスキップ。

パレート分布

パレート分析は自然や社会科学におけるさまざな現象、都市や待ち、砂粒や隕石、地震の大きさなどを記述するのに用いられてきた。式とグラフは以下になる。

\large{CDF(\pmb x) = 1 - \begin{equation} ({\frac{x}{x_m}})^{-α} \end{equation}}

f:id:kzkohashi:20180813132230p:plain

指数分布ぽくなる。実際のデータを使った、CCDFを以下にのせるが、この結論は「この分布の裾はパレート分析に適合する」とのことで、他のモデルで補えない裾のモデル化とかにも使われる。

f:id:kzkohashi:20180813132613p:plain

モデルが何に役に立つのか?

解析分布でモデル化するこの意味としては、モデル全般に言えることでもあるが、抽象化することで詳細を省くことができる。細かい標本特有の誤差やねじれがあっても、特異性を除去することでなめらかにしたもににできる。
また、解析モデルはデータ圧縮の一形式でもある。モデルがデータによく一致するなら、少数の母数で大量のデータを要約することができる。

終わりに

少し式が難しくなってきたが、丁寧な説明のおかげでまだついていけてる。統計検定というものが存在するので、少し受けて見たい感出てきた。

www.toukei-kentei.jp