Genderize.ioを使って性別判定してみる
以前、顔写真を使って性別判定についての検証をした。
顔写真による性別判定はそこそこ判定が高くすごく満足しているのだが、顔がない場合の判定はどうすればいいんだろうと思い、ちょい調べてみた。
名前による判定
Genderize.ioと呼ばれる、名前による男女の判定はすでに存在していた。
自分たちでもできそうな気もしなくもないけど。。。とりあえず使ってみる。
デモでapi
が使えるため、実際に自分の名前出たていてみると
curl https://api.genderize.io/?name=kazuki
{"name":"kazuki","gender":"male","probability":1,"count":16}
json
でデータがかえってくるため、各項目の意味を調べると以下になる。
name: 入力した名前 gender: 性別 probability: 確からしさ(精度) count: ヒットしたデータ数
公式には、probability
とcount
を使って自分たちで閾値みたいなのを決めてねと書いてある。
今回だとprobability
は1なのでほぼあってそうに見えるけど、count
が少ないため少し怪しいのかなーと思う。kazuki
でも女性の名前はいるので、日本語は少し弱いのかな?という印象を受ける。
終わりに
いくつか名前の検証をした感じだと良さそう。本番である程度試したらまた検証記事書こうと思う。
DataFrameで特定カラムでユニークに集計する方法
DataFrame
で特定カラムでユニークに集計する方法についてのメモ。
利用するデータ
今回利用するデータは、一つのアカウントに対して、複数の本のタイトルが紐づいてるデータとする。
df[['account_id', 'title']]
本のタイトルがいくつ紐づいてるか集計すると以下のようになる。
df.groupby(['title']).agg({'title': 'count'}).sort_values(by='title', ascending=False)
このままだとあるアカウントが同じ本のタイトルに何回も紐づいてしまうため、アカウントごとに同じタイトルの本が複数紐づいてる場合は一つとカウントしたい。
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)
agg
関数で集計する際に、値をユニークにしてるがミソ。
終わりに
SQL
ならこうできるのに!というのがまだあるため、もっとDataFrame
に慣れないとなー。
Python(pandas)でざっくりデータを確認する方法
原さんのブログ読んでいたら、データの傾向を見る方法について簡単に書かれていたので真似したみた。
結論から言うと、確かに傾向を見るのはすごく楽だし、癖にしたい。
要約統計量の確認
実はブログを見る以前に、「ヒストグラム」と「要約統計量」については学んでいた。
ただ、自分が調べてみたいものではなかったせいのもあり、今思うと全然理解できてなかったんだなと悔しみ。。
気を取り直して、今回は色々な著名人のツイートデータを使用する。
pandas
にはdescribeと呼ばれる要約統計量をサクッとだせる便利すぎる関数があるのでそれ使う。
import pandas as pd import csv chomei_df = pd.read_csv('../data/tyomeijinn.csv') chomei_df.describe()
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)
傾向的にはほぼ先ほどの要約統計量でみた通り、ほとんどのいいねが前半に集まっている。異常な値は他の数値をみづらくするので、75%以内に納めるデータでグラフ化すると
chomei_df.query('favorite_count < 106').plot.hist(y=['favorite_count'], bins=20, alpha=0.6, figsize=(12,8), sharex=True)
だいぶみやすくなって、傾向を追いやすくなった。当たり前だけどいいね数が多くなるほど対象のツイートが減っていいっている。
終わりに
pandas
を使って要約統計量やヒストグラムをだした。ざっくりとした値や傾向をみたい時には便利だなと思ったので重宝したい。
Scrapy + Selenium + Headless Chromeを使ってJupyterからスクレイピングする
以前Scrapyを利用してみたが、Jupyter
で使えないのか調べて見たのと、ついでにHeadless
ブラウザでスクレイピングできないかも調べてみた。
Selenium + ChromeDriverのインストール
Selenium
はいつものpip
でインストールする。
pip install selenium
ChromeDriver
はbrew
でインストール。別の方法もあるみたいだが、自分は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
終わりに
Jupyter
でScrapy + Selenium + Headless Chrome
の利用はかなり簡単にできた。
Python
のスクレイピングは情報が多く、めっちゃ楽だ。
Scrapyのスクレピングが簡単すぎて今更感動した話
僕はPHP
でスクレイピングする時はGoutteを使っていた。
サッやりたい時とかは便利だったりするが、robots.txt
の中身だったりの確認やページング処理については自分で実装が必要なため手間だなと思っていた。
ふと最近Python
をよく使ってるし、スクレイピング業界で有名なScrapyを一度使ってみるか・・と思ったのがきっかけである。
Scrapyとは
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.css
でcss
を指定するとデータを取得することができる。今回は複数あるため、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章 分布をモデル化する
前回に続いて統計の勉強。
今まで扱った分布は経験分布(empirical distributions)と呼ばれているもので、すべて実際の観察に基づいた分布だったからとのこと。
そうなると、標本サイズはデータサイズが限界となり、有限となる。
また、そのほかにはCDFで特徴付けられる解析分布(analytic distribution)というものがある。普通は連続分布(continuous distribution)と呼ぶらしいが、なんで解析分布にしたんだろう・・。解析分布は、経験分布をモデル化するのに使えるとのこと。ここでのモデル(model)とは、必要がない詳細を省いた単純化のことらしい。ちょっとよくわからないが、進めていけばわかるだろう。
指数分布
簡単な指数分布(exponetial distribution)からモデル化を始める。
指数分布のCDF
の式とグラフ以下になる
母数(パラメータ)λが分布の形状を決定する。
指数分布は一連の事象を観測して、到着時間間隔(interarrival time)と呼ばれる、事象間の時間を計測する時に現れる。
なるほど・・・全然わからない。これが重要なのかな
事象が常に同じ確からしさで起こる時、到着時間間隔の分布は指数分布になる傾向がある。
だいたいわかったけど難しい単語が多いので、以下の記事がわかりやすかった。
では、どうやって実際の分布が指数分布のモデルに当てはまると断言できるのか。
1つの方法として、相補CDF(CCDF)、つまり1 - CDF(x)
で描画して見るとわかりやすくなるとのこと。
本来は直線に近くなるはずだが、そうではないためこのデータには指数分布はあってないという判断にするっぽい。
おそらく、計算で妥当性を評価するやつもありそうだけどここでは書いてなかった。
正規分布
正規分布はガウス分布とも呼ばれている。近似的に多くの現象を記述できるので、非常によく使われている。なぜ現れるのかは別の章での説明になるとのこと。
実際のデータとモデルとの比較した際に、かなり酷似しているので、適切なモデルと言える。
ただ、10パーセントタイル値より下のデータでは少し差がでているため、解きたい課題によっては適切ではない場合があるとのこと。
正規確率プロット
指数分布は相補CDFのような、モデルが適切か判断する式があったが正規分布にはない。
代わりに、正規確率プロットと呼ばれう方法を使う。
細かい説明は省くが、正規分布で用いたデータを使うと実際のデータはモデル(正規分布)より、裾のほうがズレが生じてるのがわかる。
対数正規分布
値の対数をとって、正規分布したのが対数正規分布となる。
正規分布とやり方はそんなかわらないのでスキップ。
パレート分布
パレート分析は自然や社会科学におけるさまざな現象、都市や待ち、砂粒や隕石、地震の大きさなどを記述するのに用いられてきた。式とグラフは以下になる。
指数分布ぽくなる。実際のデータを使った、CCDFを以下にのせるが、この結論は「この分布の裾はパレート分析に適合する」とのことで、他のモデルで補えない裾のモデル化とかにも使われる。
モデルが何に役に立つのか?
解析分布でモデル化するこの意味としては、モデル全般に言えることでもあるが、抽象化することで詳細を省くことができる。細かい標本特有の誤差やねじれがあっても、特異性を除去することでなめらかにしたもににできる。
また、解析モデルはデータ圧縮の一形式でもある。モデルがデータによく一致するなら、少数の母数で大量のデータを要約することができる。
終わりに
少し式が難しくなってきたが、丁寧な説明のおかげでまだついていけてる。統計検定というものが存在するので、少し受けて見たい感出てきた。
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のミドルウェア便利だなー。