いっきのblog

技術とか色々

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

Laravelで特定のページに特定のユーザー以外が入ってきたら404を返す

管理画面を作っていると、一般ユーザーにはアクセスされたくないページが出てくる。
セキュリティを考えるとIP制限などが理想だったりするが、Laravelで簡単に解決したい場合があるので今回はそちらのやり方でやってみる。

カラムの追加とミドルウェアの準備

ユーザー情報を表すuserテーブルはよく使われているので説明を省く。また、このテーブルには管理者を表すis_adminカラムを追加しておく。

ミドルウェアは以下のように作る。シンプルにis_adminではなかったら404を返すようにする。

<?php

namespace App\Http\Middleware;

use App\Models\User;
use Closure;
use Illuminate\Http\Request;

class VerifyAdminUser
{
    /**
     * @param Request $request
     * @param Closure $next
     * @param null $guard
     * @return mixed
     */
    public function handle($request, Closure $next, $guard = null)
    {
        /** @var User $user */
        $user = $request->user();

        if (!$user->is_admin) {
            abort(404);
        }

        return $next($request);
    }
}

次にApp\Http\Kernelに上記の作成したミドルウェアを追加する。

<?php
...省略

    protected $routeMiddleware = [
        'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,

         // 追加
        'administrator' => \App\Http\Middleware\VerifyAdminUser::class,
    ];

ルーティングへの追加

あとはルーティングのmiddlewareに追加するだけで良い。

<?php

    // middlewareにadministratorを追加
    // 非管理者がこのグループにアクセスしようとすると404が帰ってくる
    Route::group(['middleware' => ['auth:api', 'administrator']], function () {

        Route::get('/dashboard', 'DashboardController@index')->name('admin.dashboard');

    });

ちなみにページも何も用意してないのでただ、404が返ってくる悲しいものになってしまった。

終わりに

今回は管理者以外のユーザーには404を返すようにしたが、リダイレクトするなり他の優しい手段もあるのでそちらもやってみるといいかも。にしてもLaravelのミドルウェア便利だなー。

Think Stats(第2版)を読む:4章 累積分布関数

前回に引き続き統計のお勉強。 kzkohashi.hatenablog.com

PMFの限界

前回、PMF(確率質量関数)を用いて、一人めの子供の妊娠周期(濃い青)と二人め以降の子供の妊娠期間(水色)を確率にして棒グラフで表示した。

f:id:kzkohashi:20180728222230p:plain

このように、PMFは値の個数が少ない時(ここでいうとweeksのこと)は可視化し、全体像を掴んだり比較するのに大変便利であった。
しかし、個数が増えると各値の確率が小さくなり、ランダムノイズの影響が大きくなる。

例えば、第一子と第二子の新生児の出生時体重の分布をPMFを用いて表示してみると、以下のようになる。

f:id:kzkohashi:20180812215556p:plain

どちらもの分布も正規分布のベル曲線にいているが、解釈が難しい。確率が低すぎる値なのであまり意味してないのと、どちらが平均値が高いのか?などがパッとわからない。
これらの問題を解決するために積分布関数(CDF)を使うといいっぽいので、やってみる。

パーセンタイル

CDFに入る前に、パーセンタイルについて説明する。
パーセンタイルは学生の時にテストの結果を受け取る際に、結果として用いられることが多い。

例えば、各5人のテスト結果が「55, 66, 77, 88, 99」点だとすると、自分が「88点」ならば、「100 * 4/5」でパーセントタイル順位は80となり、パーセントタイル値は88となる。そんなテスト結果の受けとりの仕方はしたことないけど、次に進もう。

積分布関数(CDF)

CDFは各値をパーセンタイル順位に対応づける関数とのこと。パーセンタイルと一緒じゃないか、と思ってたら、違いとしては以下になる。

パーセンタイル順位は結果が0 ~ 100の範囲に対して、CDFでは0 ~ 1の範囲の確率で表現する

例として、妊娠期間をCDFで表現すると以下のようなグラフが作成できる。

f:id:kzkohashi:20180812221551p:plain

X軸が妊娠週で、Y軸がCDFとなっている。このグラフの一つの読み方としては、パーセンタイル値を見つけるというものらしい。あまりよくわかってないが、続けて例をだしてくれていて、「約10%の妊娠が36週より短いこと」「90%が41週より短いこと」が見て取れるという言い方ができる。また、度数の高い値は急な落差になっているため、39週が最頻値であることが見て取れる。

CDFを比較する

いよいよ、先ほどPMFで表現した「第一子と第二子の新生児の出生時体重の分布」をCDFを使って表現する。

f:id:kzkohashi:20180812221955p:plain

図が荒くて若干見づらいが、第一子のほうが平均値がわずかにたかそうなのがわかる。ただ、図だとやっぱり最頻値などはぱっと見わからないんだなと思った。

パーセンタイル派生統計量

CDFを一旦計算すれば、パーセンタイル値とパーセンタイル順位の計算は容易とのこと(そりゃあそうだw)。
パーセンタイル派生要約統計量を計算するのに使える。例えば、50位パーセンタイル値は中央値(median)と呼ばれる。また、***四分位範囲(interquartile range, IQR)と呼ばれる分布の広がりの尺度も表せる。IQRは75位パーセンタイル値と25位パーセンタイル値との差。
一般的に、パーセンタイル値は分布の形状の要約によく用いられる。Wikipediaで詳しくは調べとけとのこと。

分位数 - Wikipedia

ここらへんは以前何かの講義受けた時に少しやったようなやってないような。

乱数

ここ少し省略すると、CDFの形状がなんであれ、パーセンタイル順位の分布は一様と言いたかった。

f:id:kzkohashi:20180812225456p:plain

だいたい直線に近くなるとのこと。

パーセンタイル順位を比較

パーセンタイル順位は異なるグループに対する評価尺度を比較する時に役に立つ。

例えば、「男性の20代のマラソン」と「男性の30代のマラソン」の順位を比べるとすると

  • 「男性の20代のマラソン」で1000人中100位、つまりはパーセント順位が90%
  • 「男性の30代のマラソン」に10年後に出る際に、同じパーセント順位を維持するにはどれくらいのタイムを維持してれば良いのか?

の計算は過去の「男性の30代のマラソン」のデータをもとに、その際のパーセント順位90%のタイムを見ればわかるよねって話。

終わりに

今回もそんなに難しくなかったけど、自分の例で何か一度やっておきたいなー。