Pandasで何かに白黒つけたい

もふもふPandasに触れてみようの巻

長い前置き

プログラミング初心者で、基本的に独学なMoniは、自分のプログラミングに対する芯的な部分というか、バックグラウンド的なものがグラグラとしているという自覚があります。

私が好きな漫画家の福満しげゆき先生も何かの漫画の中で同じようなことを述べられていたと思います。
確か、自分という人間は基礎的な部分がグラグラしている、とかなんとか。
エッセイでも似たタイトルのものを出されているみたいです。
グラグラな社会とグラグラな僕のまんが道

当時大学生でモラトリアムを謳歌していた私は、その表現にとても共感してしまったものですが、いい大人な年齢になった今でも感覚はそんなに変わっていなくて、ふとした時に焦燥感を覚えています。
しかし、最近はいっそ、グラグラなことが自分のアイデンティティーなのかなと考えて開き直るようにしています(できる社会人のビジネス用語では、これを臭いものに蓋といいます)。

さて、ちょうど先ほど、福満先生のうちの妻ってどうでしょう?(7)(最終巻)を読んだのですが、 この巻を最後に連載が全く無くなり失業状態になるそうなので、天地神明に誓ってステマの類ではないですが、 みなさんも興味があったら福満先生の漫画を買ってあげてください。

What's Pandas?

さて、だいぶ話が逸れましたがプログラグラミングの話をしましょう。
いや、私はプロではないのでグラグラミングになるのでしょうか。
とにかく、グラグラな私はプログラミング界隈に疎いのですが、Pythonでデータ分析を行う、PyDataというものが流行っているらしいと日課のネットサーフィンをしていたら小耳に挟んだのです。
なになに…データを扱うにはPandasというパッケージを使えばいい?他に必要なmatplotlibやnumpyなら学生時代に少し使ったことあるなあ…
とにかくデータ分析ってかっこいいし、Pandasについて勉強してみよう!

ということで、公式にあるチュートリアルの10 Minutes to pandasを読みました。

何を分析するか

なんでもいいのですが、最近プロ野球をよく観ているのでプロ野球のデータにしました。

データの取得方法は、NPBのウェブページへのアクセスでhtmlから取得することにします。
データは各チームの選手の基本情報(名前、背番号、生年月日、etc.)を取ることにします。
アウトプットは、各球団の選手の年齢分布を表示することとします。

コーディングする

まず骨組みを考え、全体を2つの関数に分けることにしました。
・指定されたチームの選手データを取得し、DataFrame形式で返す。
・指定されたチームの全選手の満年齢をヒストグラム表示する。

選手データ取得

urllib2でhtmlを取得。
こんな感じで、引数で'db'とかのチーム名を入れて、各チームのデータを取得できるようにしておきました。

req = urllib2.Request('http://bis.npb.or.jp/teams/rst_%s.html' % team_name)

HTMLの分析

BeautifulSoupを使って分析をします。あったかい(略)
これも初めて使うので、Beautiful Soup 4.2.0 Doc. 日本語訳を読みました。

まず、選手データがあるDivだけfindで抽出します。
次に、その中からfindallで、全てのテーブルを検索します。
ここで、find
allを使用した理由は、育成選手がいる場合、支配下選手とテーブルが分かれているからです。

soup = BeautifulSoup(html)

# HTMLから選手のテーブルの部分だけ抽出
main_div = soup.find('div', id='tedivmaintbl')
# すべてのtableタグを検索
table_list = main_div.find_all('table')

次に、テーブルの子要素を読み、DataFrameに追加していきます。
for文でタグの子要素を取得するには、contentsとchildren属性を使う方法があるらしいです。 contentsはリストで格納されていて、childrenはイテレータとのこと。
とりあえずcontentsを使いました。

ここで、よくわからなかったのが、contentsのリストの中にu"\n"というNavigableString objが入ってくることでした。 邪魔なので、省くために、from bs4 import element as bs4elementとしてBeautifulSoupからelementクラスをインポートしておいて、isinstance(row, bs4element.Tag)で、Tag objのときだけ処理するようにしました(よい方法なのか不明)。

各行の要素は、[x.string for x in row.contents]のようにリスト内包表記を使い、string要素だけ取得しました。

取得した行データをデータフレームに1行ずつ逐次追加したかったので、以下のようにappendメソッドを使いました。
リストのappendとは異なり、破壊的に追加されていくわけではないようです。

s = pd.Series(data, index=header)
df = df.append(s, ignore_index=True)

全体は以下のような感じです。

df = pd.DataFrame()
for table in table_list:
    for row in table.contents:
        # 間に空行のNavigableString objが入ってくるのでTag objに絞る
        if isinstance(row, bs4element.Tag):
            if row['class'][0] == 'rosterMainHead':
                # ポジション名を保存しておく
                position = row.find('th', class_="rosterPos").string
                # テーブルをまとめて扱えるように列名を名前に変更
                row.find('th', class_="rosterPos").string.replace_with('Name')
                # 1列目にポジション列を追加
                header = ['Position'] + [x.string for x in row.contents]
            elif row['class'][0] == 'rosterPlayer':
                # 1列目にポジションを追加
                data = [position] + [x.string for x in row.contents]
                s = pd.Series(data, index=header)
                df = df.append(s, ignore_index=True)

データの成形

データフレームに追加したデータは、dtypesでみると、全てオブジェクトタイプになっています。 このままでは不便そうなのでいい感じに型を変換したいです。

print df.dtypes
# Name        object
# No.         object
# Position    object
# 備考          object
# 生年月日        object
# 体重          object
# 打           object
# 投           object
# 身長          object
# dtype: object

身長、体重は数値に変換したいです。
astypeを使えばいいようです。
監督の身長・体重データは掲載されていないので、dropnaで省きます。
そもそも選手じゃないのでデータから消すべきかもしれませんが。

# 身長、体重の型を変換
df[u'身長'] = df[u'身長'].dropna(how='any').astype(int)
df[u'体重'] = df[u'体重'].dropna(how='any').astype(int)

生年月日も計算で使用できるようにします。
初期状態ではYYYY.MM.DDの形式になっているので、YYYY-MM-DDになるように文字列置換してから、to_datetimeメソッドで変換します。
文字列データを取得するにはstrアクセサを使えばいいようです。

# 生年月日の型を変換
df[u'生年月日'] = pd.to_datetime(df[u'生年月日'].str.replace('.', '-'))

投打は、統計に便利そうなのでカテゴリカルデータに変換します。 身長・体重と同様に、投打も監督のデータは掲載されていないので、dropnaで省きます。

# 投打をカテゴリカルデータに変換
df[u'投'] = df[u'投'].dropna(how='any').astype('category')
df[u'打'] = df[u'打'].dropna(how='any').astype('category')

これで以下のような型になりました。
なぜか、intを指定した身長・体重がfloat64になっていますが。。

print df.dtypes
Name                object
No.                 object
Position            object
備考                  object
生年月日        datetime64[ns]
体重                 float64
打                 category
投                 category
身長                 float64
dtype: object

データのプロット

さて、ようやくまとめたデータを使う段階に入ることができました。
いま一度わたしの野望を思い出してみますと、目標は各球団の選手の年齢分布を表示することでした。

選手の満年齢を計算します。 生年月日から満年齢を計算するには閏年とかあるし、一体わたしどうしたらいいの〜?
思わず取り乱してしまいましたが、気を取り直して正座してお上品におググりましたところ、下記のような記事が見つかりました。
閏年も考えた上での年齢の計算方法

年月日をくっつけてint型として、(今日の日付-生年月日)/10000して小数点を切り捨てればいいと。
な〜るほど!
せっかく日付をdatetime型にしたのに〜(><)と思いながら以下のように書きました。

# 現在の年月日をintで
t = int(pd.tslib.Timestamp.now().strftime('%Y%m%d'))
# 選手の満年齢を求める
year = df[u'生年月日'].apply(lambda x: math.floor((t - int(x.strftime('%Y%m%d')))/10000))

このデータをヒストグラムでプロットします。
kind='hist'でヒストグラムになります。 binsはバーの数、alphaは透明度、colorはバーの色です。

# ヒストグラムをプロットして表示
year.plot(kind='hist', bins=20, alpha=0.5, color='b', xlim=(15, 50), ylim=(0, 15))
plt.show()

結果

というわけで、結果です。
年俸が12球団最低で若い印象のdb軍と、年齢層高めの印象のcd軍を表示してみました。 db軍は選手としてノってくるであろう30手前の選手が最も多いことがわかります。
cd軍は確かに40代の選手が多いですが、22〜24の選手も多いとわかります。これから期待できますね。

DB軍
CD軍

さて、何かに白黒つけることはできたのでしょうか。
また今度、もう少し有意義な分析もしてみたいです。

今回のコードは以下です。
plot_hist.py

Comments