pandas で要約統計量を計算する


データ分析に取り掛かるときに、データセットの概要を数値で表現できると便利なことがあります。
集計軸などは対象とするデータセットによって異なりますが、基本事項として要約統計量を計算しておくと、共通用語の下でデータの傾向を把握でき、作業の手戻りが少なくなることを期待できます。

Wikipedia 日本語版の[要約統計量](https://ja.wikipedia.org/wiki/%E8%A6%81%E7%B4%84%E7%B5%B1%E8%A8%88%E9%87%8F)のページでは３つの統計量について説明があります。
実際のデータに対して pandas ([Python Data Analysis Library](http://pandas.pydata.org/)) を使ってこれらの値を計算してみます。

- モーメントから求められる要約統計量
- 順序から求められる要約統計量
- 度数から求められる要約統計量

## データの用意

今回は世界銀行の API (`data.worldbank.org`) から各国の年ごとの GDP のデータを使います。

世界銀行は Indicators、Projects、World Bank financial data という３種類の API を提供しており、[For Developers](http://data.worldbank.org/developers) のページに説明があります。Indicators は時系列データで、8,000を超えるデータセットがあります。その中に *NY.GDP.MKTP.CD* というコードで表現されるデータがあります。API の使い方は [Indicator Queries](http://data.worldbank.org/developers/api-overview/indicator-queries) のページに説明がありますが、実際にその API を使って構築されているページが [GDP at market prices (current US$)](http://beta.data.worldbank.org/indicator/NY.GDP.MKTP.CD) です。
なお、似たものとしては *NY.GDP.PCAP.KD* があり、こちらは一人当たり GDP を表します。 - [GDP per capita (constant 2005 US$)](http://beta.data.worldbank.org/indicator/NY.GDP.PCAP.KD)

### API のパラメータを調整

API レスポンスのデータ形式は JSON, XML の両方が提供されていますので、結果を処理するプログラムを書けば良いのですが、 *pandas* においては *pandas_datareader* がこの機能を提供してくれます。 *pandas_datareader* は元々は *pandas.io* としてコア機能に含まれていたものですが、外部モジュールに切り出されたものです。既存のスクリプトで *pandas* をバージョンアップして警告文が表示される場合には、依存ライブラリを追加し、 `import` 文を修正しましょう。

- [pandas-datareader 0.2.1 : Python Package Index](https://pypi.python.org/pypi/pandas-datareader)

それでは、実際にデータを読み込んでみます。

```python
from pandas_datareader import wb
df = wb.download(indicator='NY.GDP.MKTP.CD', start=1960, end=2014)
df.count()
```

```python:結果
NY.GDP.MKTP.CD    165
dtype: int64
```

165件あるようです。国の数としては少ないため、 `head()` で先頭の数件を表示してみます。

```python
df.head()
```

| | | NY.GDP.MKTP.CD |
|---------|------|-----|
| country | year | |
| Canada | 2014 | 1.785387e+12 |
| | 2013 | 1.838964e+12 |
| | 2012 | 1.832716e+12 |
| | 2011 | 1.788796e+12 |
| | 2010 | 1.614014e+12 |

マルチインデクスになっており、上位が国、下位が年度を表しています。
インデクスの中身は `.index` 属性で確認できます。

```python
print(df.index.names)
print(df.index.levels[0])
```

```python:結果
['country', 'year']
Index(['Canada', 'Mexico', 'United States'], dtype='object', name='country')
```

国はカナダ、メキシコ、アメリカの３か国になっています。これは、ソースコードの実装で `pandas_datareader.wb` のデフォルトがこの３つになっているためです。各国の GDP を比較したいので、リクエスト条件を調整することにします。

[wb.download](http://pandas-datareader.readthedocs.io/en/latest/remote_data.html#indicators) は `country` 引数を受け付けますので、ここに任意のコードを与えます。 `all` で全てのコードを指定できるのですが、集約コード ([Aggregates - Regions and Income Levels | Data](http://data.worldbank.org/node/246)) も含まれてしまいますので、[Country Queries](http://data.worldbank.org/developers/api-overview/country-queries) を使ってフィルタします。

`get_countries()` 関数で一覧を取得できますので、その結果を見てフィルタ条件を決めます。

```python
df_countries = wb.get_countries()
df_countries.head()
```

| | adminregion | capitalCity | iso3c | incomeLevel | iso2c | latitude | lendingType | longitude | name | region |
|---|---|------------|-----|----------------------|----|----------|--------|--------|--------|--------|----------|--------|
| 0 |  | Oranjestad | ABW | High income: nonOECD | AW | 12.51670 | Not classified | -70.0167 | Aruba | Latin America & Caribbean (all income levels) |
| 1 | South Asia | Kabul | AFG | Low income | AF | 34.52280 | IDA | 69.1761 | Afghanistan | South Asia |
| 2 |  |  | AFR | Aggregates | A9 | NaN | Aggregates | NaN | Africa | Aggregates |
| 3 | Sub-Saharan Africa (developing only) | Luanda | AGO | Upper middle income | AO | -8.81155 | IBRD | 13.2420 | Angola | Sub-Saharan Africa (all income levels) |
| 4 | Europe & Central Asia (developing only) | Tirane | ALB | Upper middle income | AL | 41.33170 | IBRD | 19.8172 | Albania | Europe & Central Asia (all income levels) |


インデクス番号２のレコードで incomeLevel、lendingType, region の３つが Aggregates になっています。これはアフリカ全体を集約した結果のレコードです。

レコードの出現頻度を確認するために region で集計してみます。

```python
df_countries.groupby('region')['name',].count().reset_index()
```

| | region | name |
|----|-----|------|
| 0 | Aggregates | 49 |
| 1 | East Asia & Pacific (all income levels) | 37 |
| 2 | Europe & Central Asia (all income levels) | 57 |
| 3 | Latin America & Caribbean (all income levels) | 41 |
| 4 | Middle East & North Africa (all income levels) | 21 |
| 5 | North America | 3 |
| 6 | South Asia | 8 |
| 7 | Sub-Saharan Africa (all income levels) | 48 |

集約レコード (Aggregates) が49件あることが分かりましたので、これを除外して国コードを構築することにします。結果を確認すると、インデクス番号２のレコードが除外されていることが分かります。

```python
df_countries[(df_countries['region'] != 'Aggregates')]['iso2c'].head()
```

```python:結果
0    AW
1    AF
3    AO
4    AL
5    AD
Name: iso2c, dtype: object
```

### 対象とするデータセットを取得

対象とする国コードの一覧ができましたので、 `country` 引数にリストとして与えることで API を呼び出します。また、データ期間は2014年の一年間に限定します。

なお、ソースコード内にハードコードされた国コードに含まれないコードも存在するため、警告メッセージが表示されます。ハードコードされた一覧と最新の一覧の不一致により発生する事象ですので、処理はそのまま続けて問題ありません。

```python
df = wb.download(indicator='NY.GDP.MKTP.CD',
                 country=df_countries[(df_countries['region'] != 'Aggregates')]['iso2c'].tolist(),
                 start=2014, end=2014)
df.head()
```

|  |  | NY.GDP.MKTP.CD |
|----|-----|--------|
| country | year | |
| Aruba | 2014 | NaN |
| Andorra | 2014 | NaN |
| Afghanistan | 2014 | 2.003822e+10 |
| Angola | 2014 | NaN |
| Albania | 2014 | 1.321151e+10 |

このままだと少し扱い難いため、データフレームを少し変形します。

```python
d = df.unstack()['NY.GDP.MKTP.CD']
dd = pd.DataFrame(data={'gdp_2014': d['2014']}, index=d.index).reset_index()
dd.head()
```

|  | country | gdp_2014 |
|-----|-------|---------|
| 0 | Afghanistan | 2.003822e+10 |
| 1 | Albania | 1.321151e+10 |
| 2 | Algeria | 2.135185e+11 |
| 3 | American Samoa | NaN |
| 4 | Andorra | NaN |

データ件数を確認すると、全部で214件あり、gdp_2014 が NaN で無いものは185件あることが分かります。このデータについて、要約統計量を計算していきます。

```python
dd.count()
```

```python:結果
country     214
gdp_2014    185
dtype: int64
```

## モーメントから求められる要約統計量

*pandas* の DataFrame には200近いメソッドが定義されていますが、この中にモーメントを計算する関数も含まれています。

- 平均: `mean()`
- 分散: `std()`
- 歪度: `skew()`
- 尖度: `kurtosis()`

```python
ds = pd.DataFrame(index=('gdp_2014',))
ds['count'] = dd.count()['gdp_2014']
ds['mean'] = dd.mean()['gdp_2014']
ds['std'] = dd.std()['gdp_2014']
ds['skew'] = dd.skew()['gdp_2014']
ds['kurtosis'] = dd.kurtosis()['gdp_2014']
ds
```

| | count | mean | std | skew | kurtosis |
|----|----|----|----|-----|---------|
| gdp_2014 | 185 | 4.138522e+11 | 1.595941e+12 | 8.197562 | 78.277232 |


年ごとに計算を繰り返すことで、分布の変化を追跡できるようになります。
`wb.download()` を呼び出すときの *start* パラメータの値を変えて、データ変換を再実行しながら試してみましょう。

DataFrame のその他のメソッドは API ドキュメントを参照しましょう。

- [pandas.DataFrame — pandas 0.18.1 documentation](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html)


## 順序から求められる要約統計量

*pandas* の DataFrame には `describe()` メソッドがあります。
先ほど計算したデータ件数、平均、分散に加えて、最小、最大、四分位数を計算します。四分位数の50%は中央値になります。

```python
dd.describe()
```

|  | gdp_2014 |
|-------|--------------|
| count | 1.850000e+02 |
| mean | 4.138522e+11 |
| std | 1.595941e+12 |
| min | 3.785955e+07 |
| 25% | 7.962424e+09 |
| 50% | 3.299619e+10 |
| 75% | 2.178723e+11 |
| max | 1.741900e+13 |

最小と最大は6桁も異なりますので、ヒストグラムを描いたり、刈込平均を計算してみると、分布の様子をより理解出来るようになるでしょう。

ここでは、上位と下位のそれぞれ10件を棒グラフで描画してみます。
描画には [Seaborn](https://web.stanford.edu/~mwaskom/software/seaborn/) を使います。

```python
import seaborn as sns
%matplotlib inline

sns.set_style('whitegrid')
```

上位10件を表示します。グラフにしてみると、上位2件（アメリカと中国）が突出していることが分かります。

```python
sns.barplot(x='gdp_2014', y='country',
            data=dd.sort_values('gdp_2014', ascending=False).head(10).reset_index(),
           palette=sns.color_palette('Greens_r', n_colors=15))
```

![Kobito.qDSiMz.png](https://qiita-image-store.s3.amazonaws.com/0/37996/9391483a-4b79-1b47-7751-60ec61efad25.png "Kobito.qDSiMz.png")


同様にして下位10件を表示します。下位1件（ツバル）は少し上位よりも差が大きいことが分かります。

```python
sns.barplot(x='gdp_2014', y='country',
            data=dd.sort_values('gdp_2014').head(10).reset_index(),
            palette=sns.color_palette('Reds_r', n_colors=15))
```

![Kobito.EyEd8x.png](https://qiita-image-store.s3.amazonaws.com/0/37996/b689cdbe-474a-770a-e433-4ae32d115a7e.png "Kobito.EyEd8x.png")


## 度数から求められる要約統計量

上位、下位ともに分布が大きく散らばっていることが分かりましたので、ヒストグラムを描きながら度数の多い範囲を探してみます。

とりあえずでヒストグラムを描画してみると、上位2件の値に引きづられて特徴の分からないグラフになってしまいます。

```python
dd.hist(bins=30)
```

![Kobito.CDsR7C.png](https://qiita-image-store.s3.amazonaws.com/0/37996/ce150f72-96cc-fd15-8dea-6af2f28d72dd.png "Kobito.CDsR7C.png")

桁が大きく異なることが一因ですので、log10 をとってみます。

```python
import math

lg10 = dd['gdp_2014'].map(lambda v: math.log10(v))
lg10.describe()
```

```python:結果
count    185.000000
mean      10.533801
std        1.048719
min        7.578175
25%        9.901045
50%       10.518464
75%       11.338202
max       13.241023
Name: gdp_2014, dtype: float64
```

```python
lg10.hist(bins=30)
```

![Kobito.s747LE.png](https://qiita-image-store.s3.amazonaws.com/0/37996/07abc00b-41bf-0092-23ac-daf49723e19c.png "Kobito.s747LE.png")

今度は先ほど確認した特徴が見えてきました。
下位1件が左側、上位2件が右側に外れ値として存在しています。また、log10 が10から11の部分に多くの値が含まれています。もう少し正確に四分位数も確認すると、9.9から11.3に約半数が含まれていると言えます。

散布図と組み合わせてみると、傾向をよりはっきりと確認できます。

```python
d = dd['gdp_2014']
a = pd.DataFrame({'gdp_2014': d, 'log10': d.map(lambda v: math.log10(v))})
sns.jointplot(x='log10', y='gdp_2014', data=a, marginal_kws=dict(bins=30))
```

![Kobito.OvXkhN.png](https://qiita-image-store.s3.amazonaws.com/0/37996/e9d172f6-381d-0f5f-9c68-8299affff6a8.png "Kobito.OvXkhN.png")

外れ値を除外してみると、もう少しグラフの値が読みやすくなり、log10 が 12 付近での非連続性が分かります。

```python
d = dd[(lg10 > 8) & (lg10 < 13)]['gdp_2014']
a = pd.DataFrame({'gdp_2014': d, 'log10': d.map(lambda v: math.log10(v))})
sns.jointplot(x='log10', y='gdp_2014', data=a, marginal_kws=dict(bins=30))
```

![Kobito.p0OrBI.png](https://qiita-image-store.s3.amazonaws.com/0/37996/a426f86c-5e40-3060-7162-8fd88016535b.png "Kobito.p0OrBI.png")

また、中央値である10.5を中心として±1.5に範囲を限定すると、9.5付近と11.0付近の値が少ないことが分かります。

```python
d = dd[(lg10 >= 9) & (lg10 <= 12)]['gdp_2014']
a = pd.DataFrame({'gdp_2014': d, 'log10': d.map(lambda v: math.log10(v))})
sns.jointplot(x='log10', y='gdp_2014', data=a, marginal_kws=dict(bins=30))
```

![Kobito.7Y2FRC.png](https://qiita-image-store.s3.amazonaws.com/0/37996/c95d6ef1-d032-f75a-35fd-79662ed3350b.png "Kobito.7Y2FRC.png")

さて、最初に確認した Country Query のレスポンスには *incomeLevel* という属性が含まれていました。これを改めて集計してみると、以下の結果を得られます。Aggregates は集約結果なので別扱いとして、2種類の上位層、1種類の下位層、そして2種類の中間層になっています。ここまでで確認した度数密度と合わせて考えると、具体的な数値をイメージしやすくなったと思います。

```python
df_countries.groupby('incomeLevel')['name',].count().reset_index()
```

|  | incomeLevel | name |
|------|------|------|
| 0 | Aggregates | 49 |
| 1 | High income: OECD | 32 |
| 2 | High income: nonOECD | 48 |
| 3 | Low income | 31 |
| 4 | Lower middle income | 51 |
| 5 | Upper middle income | 53 |

追記 (2016/7/2)：
７月１日に世界銀行での分類が更新されました。いくつかのグループが変わっています。

- [New country classifications by income level](http://blogs.worldbank.org/opendata/new-country-classifications-2016)

分類の詳細としては、国民一人あたり総所得 (gross national income per capita) が *$1,025* 以下だと *Low income* 、 *$1,026* から *$4,035* は *Lower middle-income* 、 *$4,036* から *$12,475* は *Upper middle-income* 、 *$12,476* 以上だと *High-income* になるそうです。

- 参考： [GDPとGNI（GNP）の違いについて - 内閣府](http://www.esri.cao.go.jp/jp/sna/otoiawase/faq/qa14.html)

/追記ここまで

## まとめ

pandas を使って、モーメント、順序、度数に関する要約統計量を計算しました。
各国のGDPのように、正規分布などの特定の分布を想定することが難しい場合でも、個別の値を見たり対数を計算することで特徴を数値で表現出来るようになりました。その中では、ヒストグラム (histogram) と散布図 (scatter plot) を使い、グラフでデータ分布を確認することも有用です。

実際の利用シーンについては、PayPal の人が書いた記事（日本語訳がありがたい）が分かりやすいと思います。基本的な数値計算はライブラリがカバーしてくれますので、その数値を読んで意味を理解できるようになると、データ分析の内容にも深みが出てくると考えられます。

- [ソフトウェアのための統計学 – 前編 | POSTD](http://postd.cc/statistics-for-software/)
- [ソフトウェアのための統計学 – 後編 | POSTD](http://postd.cc/statistics-for-software-2/)