Pythonでちゃんと統計・確率の基礎をやってみた（準備〜データ編）

# はじめに
以下の書籍を参考にしながら，統計の基礎を学び直した軌跡の，準備〜データ編です．

https://www.amazon.co.jp/Python%E3%81%A7%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B%E7%B5%B1%E8%A8%88%E8%A7%A3%E6%9E%90%E3%81%AE%E5%9F%BA%E7%A4%8E-%E8%B0%B7%E5%90%88-%E5%BB%A3%E7%B4%80-ebook/dp/B07HHV6LL8/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=1657859730&sr=8-1

書籍を参考にしながら勝手に解釈してまとめたり，省略したり補足したりしていますので，書籍に沿って進めたい方は購入をオススメします！界隈で有名な一冊ですし，統計指標のビジュアライズが秀逸で理解を助けてくれます．


この記事は5部構成の1つ目です．以下も参照ください．
- **準備〜データ編**・・・この記事
- [確率編1](https://qiita.com/o93/items/1b3f379da3e62c6252d8)・・・投稿済み
- [確率編2](https://qiita.com/o93/items/a2e02cf98126f47cd66c)・・・投稿済み
- 確率編3・・・まだ
- 推測編・・・まだ

# 動機
問題にぶつかったときに都度ピンポイントで調べたり勉強して誤魔化してきたものの，特に統計や確率の基礎知識が凸凹していてはいけない！と思い立ちました．

勉強したい分野は他にも沢山ありますが，統計・確率は機械学習などに限らず，あらゆる分野で使われているし，知っておいて損は無いランキング上位に位置している分野だと勝手に思っています．

# 注意点
今回の記事では，あくまで統計の基礎にフォーカスしています．機械学習やデータ分析を実装するためのプログラミングがメインではないため，ライブラリやサービスの説明／コメントは最小限に留めています．ただし，今回フォーカスした領域について，エンジニアという立場から，なるべくコードを交えて解りやすく説明することは意識しています．分かりにくいところなどがあればコメントください．随時修正・加筆していきます！

# 使用している技術
- [Python](https://www.python.org/) - プログラミング言語
- [NumPy](https://numpy.org/) - 数値計算ライブラリ
- [SciPy](https://scipy.org/) - 科学計算ライブラリ
- [pandas](https://pandas.pydata.org/) - データ解析ライブラリ
- [matplotlib](https://matplotlib.org/) - グラフ描画ライブラリ
- [seaborn](https://seaborn.pydata.org/) - グラフ描画ライブラリ
- [Colaboratory](https://colab.research.google.com/?hl=ja) - ↑をブラウザ上で手軽に実行できるサービス

これらについて軽く触ったことがあれば理解出来る内容になっているはず．特にColabはアクセスするだけで無料ですぐに動かせます！seabornは書籍では使われていませんが，ケースによってはmatplotlibより簡単にグラフ描画できることがあったため，使用しています．

- [今回の記事のサンプルコード](https://github.com/o93/python_stat_relearn_sample)
    - 書籍のコードとは異なる部分が多いです
- [書籍のサンプルコード](https://github.com/ghmagazine/python_stat_sample)
    - Pythonやライブラリのチュートリアルなども充実しています！

# 構成
書籍のタイトルにある**統計解析**とは，以下によると，

https://www.sas.com/ja_jp/insights/analytics/statistical-analysis.html

> 大量のデータを収集・探索・分析し、その結果を提示することを通じて、データに含まれるパターンや傾向を明らかにする

とのこと．これができるスキルを身に付けるため，以下の3つに関する技術をPythonによるプログラミングを通じて順番に学んでいきます．

1. [データ](https://qiita.com/o93/private/91feef63ea70070462d9)・・・この記事
1. 確率
1. 推測

ここで，これら3つのワードを使って引用文を言い換えると，「**確率**的に得られた**データ**から，観測対象の特性を**推測**すること」が，**統計解析**です．統計解析に確率の知識が必要になってくる理由は，得られるデータが観測対象のほんの一部であることがほとんどだからです．

# 準備
## Colaboratory
Colaboratoryを使用すればPythonや各種ライブラリをそのまま実行できるので，準備はシンプルです．ただしGoogleアカウントの登録は必須．

まずGoogleドライブにアクセスし，適当なディレクトリで右クリック
![スクリーンショット 2022-07-16 15.25.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/accc755c-2622-f617-c75f-79a406f34c08.png)

`その他` → `Google Colaboratory`を選択
[Colaboratory へようこそ](https://colab.research.google.com/?hl=ja#scrollTo=GJBs_flRovLc)を参考にテキストやコードを書いて実行
![スクリーンショット 2022-07-16 15.36.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/d3b6c074-99a7-c98c-7f39-a6758622ca07.png)
Colabでは，最後の行のコードの実行結果が，`print`不要でそのまま出力表示されます．

## 使用するデータ
書籍で使用しているデータはgithub上で公開されています．Colab上で`git clone`して配置します．今回は`python_stat_sample/data`ディレクトリをColabのデフォルトでディレクトリである`/content`配下に配置します．

コマンドの頭に`!`を付加すると，以下のようなコマンドもColab上で実行することができます．


```:必要なデータをダウンロード
!git clone https://github.com/ghmagazine/python_stat_sample.git
```
```:実行結果
Cloning into 'python_stat_sample'...
remote: Enumerating objects: 130, done.
remote: Total 130 (delta 0), reused 0 (delta 0), pack-reused 130
Receiving objects: 100% (130/130), 8.22 MiB | 12.64 MiB/s, done.
Resolving deltas: 100% (59/59), done.
```

```:移動してデータ一覧を表示
!mv ./python_stat_sample/data ./data
!ls ./data
```
```:実行結果
ch10_access.csv   ch11_potato.csv	 ch12_scores_reg.csv  ch3_anscombe.npy
ch10_enquete.csv  ch11_training_ind.csv  ch1_sport_test.csv   ch4_scores400.csv
ch11_ad.csv	  ch11_training_rel.csv  ch2_scores_em.csv
```

最後に，matplotlibで日本語を表示しようとすると文字化けしてしまうため，日本語化対応のライブラリをインストールしておきます．
```:日本語化
!pip install japanize-matplotlib
```
```:実行結果
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting japanize-matplotlib
  Downloading japanize-matplotlib-1.1.3.tar.gz (4.1 MB)
     |████████████████████████████████| 4.1 MB 8.9 MB/s 
Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from japanize-matplotlib) (3.2.2)
Requirement already satisfied: numpy>=1.11 in /usr/local/lib/python3.7/dist-packages (from matplotlib->japanize-matplotlib) (1.21.6)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->japanize-matplotlib) (1.4.3)
Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->japanize-matplotlib) (2.8.2)
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->japanize-matplotlib) (3.0.9)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->japanize-matplotlib) (0.11.0)
Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from kiwisolver>=1.0.1->matplotlib->japanize-matplotlib) (4.1.1)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.1->matplotlib->japanize-matplotlib) (1.15.0)
Building wheels for collected packages: japanize-matplotlib
  Building wheel for japanize-matplotlib (setup.py) ... done
  Created wheel for japanize-matplotlib: filename=japanize_matplotlib-1.1.3-py3-none-any.whl size=4120275 sha256=58635acca06dee9bc3ad73aa947b7f55d8ffea99e9cf5b35977b33a06123b0ef
  Stored in directory: /root/.cache/pip/wheels/83/97/6b/e9e0cde099cc40f972b8dd23367308f7705ae06cd6d4714658
Successfully built japanize-matplotlib
Installing collected packages: japanize-matplotlib
Successfully installed japanize-matplotlib-1.1.3
```


これで準備が整いました．
早速，内容に入っていきましょう！

# データの種類
データの種類には様々な種類がありますが，書籍で扱われているのはテーブルデータ（構造化データ）ですので，こちらに絞ってデータの種類を見ていきます．

## 表（DataFrame）
Pandasを使用してcsvファイルを`DataFrame`として読み込みます．

```pyhton:csvファイルの読み込み
import pandas as pd

df = pd.read_csv('data/ch1_sport_test.csv', index_col='生徒番号')
df
```
実行結果
![スクリーンショット 2022-07-17 8.55.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/18bd541b-a221-ae5e-1ce0-9ad9ac4e9f6d.png)

Pandasの`DataFrame`は表形式で表示されます．コードの途中で表を表示したいときは`display(df)`とすればOK．右上のペンがキラキラしたようなアイコンを押すと，フィルタリングや並び替えが可能な表形式に切り替えることができます．便利ですね！

![スクリーンショット 2022-07-17 8.56.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/e55d06ca-7ca2-19d0-4e55-60ff9282ad08.png)

使い方は[こちら](https://colab.research.google.com/notebooks/data_table.ipynb)を参照．ここからMarkdownに変換してQiitaに貼り付けることができたので，以降はこの機能を使っていきます．

|生徒番号|学年|握力|上体起こし|点数|順位|
|:-:|:-:|--:|--:|--:|--:|
|1|1|40\.2|34|15|4|
|2|1|34\.2|14|7|10|
|3|1|28\.8|27|11|7|
|4|2|39\.0|27|14|5|
|5|2|50\.9|32|17|2|
|6|2|36\.5|20|9|9|
|7|3|36\.6|31|13|6|
|8|3|49\.2|37|18|1|
|9|3|26\.0|28|10|8|
|10|3|47\.4|32|16|3|

## 列（Series）
`DataFrame`は`生徒番号`や`学年`など列データの集まりです．

```python:点数列の表示
df['点数']
```

```:実行結果
生徒番号
1     15
2      7
3     11
4     14
5     17
6      9
7     13
8     18
9     10
10    16
Name: 点数, dtype: int64
```
実行結果が少し分かりにくいのですが，`index`に`生徒番号`を設定しているため，左側に`生徒番号`が，右側に`点数`のデータが表示されています．エンジニアに親しみのある1次元配列で表現すると分かりやすいかもしれません，，，

```python:1次元配列
df['点数'].values
```

```:実行結果
array([15,  7, 11, 14, 17,  9, 13, 18, 10, 16])
```
実体としては一次元配列として保持されているようです．

ここでこの1次元のデータのことを，統計やデータ分析の分野では**変数**や**特徴**などと呼びます．Webエンジニアからすると変数はもっと広い意味を持っているイメージで，個人的には最初違和感を感じてしまいました．そこは，

> 1次元配列のデータを界隈では「変数」と表現する

と何処かに書いてあるルールとして認識するしかないと思っています．

## 変数の分類
変数は以下のように大きく2つに分類して扱うことが多いです．

- **質的変数**・・・性別や血液型など，種類を区別する（カテゴリ変数とも）
- **量的変数**・・・身長やアクセス数など，量を表現する

また**尺度水準**によって更に細かく分類できます．

- **名義尺度**・・・順序や大小がなく単に区別するだけ
- **順序尺度**・・・順序や大小があるが，比較ができず差にも意味が無い
- **間隔尺度**・・・順序や大小に加えて，差に意味があるが，比に意味が無い
- **比例尺度**・・・順序，大小，差，比すべてに意味がある

書籍では名義・順序は質的変数，間隔・比例は量的変数に分類しています．しかし質的変数・量的変数は，「分類」というより変数を「どう扱うか」によって変わってくる感覚があります．順序尺度に当てはまる変数を，量的変数として扱おうと思えば扱うことができます．あくまで実践的な感覚であって理論的には間違っているのかもしれませんが..

更に別の切り口での分類．これは明快ですね．

- **離散型**・・・とびとびの値
- **連続型**・・・切れ目が無い連続した値

これらの分類情報を基に，読み込んだデータの種類を表にしてみました．

|変数名|変数|尺度|型|
|:--|:-:|:-:|:-:|
|生徒番号|質的|名義|離散|
|学年|量的|間隔|離散|
|握力|量的|比例|連続|
|状態起こし|量的|比例|離散|
|点数|量的|比例|連続？|
|順位|質的|順序|離散|

点数について今回は連続型としていますが，例えば`80.5`点みたいな点数が存在しないケースでは，厳密には連続型とは言えないんじゃないか？だったり，仮に`10`点満点のテストだったら明らかに離散しているので，ケースバイケースで変わってくるような気がします．

# 1次元データ　
次に，変数（1次元データ）の特徴を把握するために，様々な指標を用いてデータを整理する方法について記載します．1次元データは変数と記載したにも関わらず，以降も**データ**というワードを多用しています..わかり易さを重視していると信じて，呼称はなるべく書籍に寄せることにします．

結果表示を見やすくするため，表示設定を行っておきます．

```python:表示設定
import numpy as np
import pandas as pd

%precision 3
pd.set_option('precision', 3)
```

使用するデータを読み込みます．
```python:読み込んで情報を表示
df = pd.read_csv('data/ch2_scores_em.csv', index_col='生徒番号')
df.info()
```
```:実行結果
Int64Index: 50 entries, 1 to 50
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   英語      50 non-null     int64
 1   数学      50 non-null     int64
dtypes: int64(2)
memory usage: 1.2 KB
```
英語の数学のテスト結果が50件，というデータです．最初の5件を表示してみます．

```python:最初の5件を表示
df.head()
```
|生徒番号|英語|数学|
|:-:|--:|--:|
|1|42|65|
|2|69|80|
|3|56|63|
|4|41|63|
|5|57|76|

## 中心の指標（代表値）
ここから，データを要約する1つの値を求めるいくつかの指標について説明します．先程読み込んだデータから英語の点数10人分を抽出して使用することにします．

```python:英語の点数10件分に識別子を付ける
data = np.array(df['英語'][:10])
index = pd.Index(list('ABCDEFGHIJ'), name='生徒')
scores_df = pd.DataFrame({'点数': data}, index=index)
scores_df
```
|生徒|点数|
|:-:|--:|
|A|42|
|B|69|
|C|56|
|D|41|
|E|57|
|F|48|
|G|65|
|H|49|
|I|65|
|J|58|

### 平均値（Mean／Average）
みんなが知っている全部の点数を足して件数で割ったものです．
```python:平均値
np.mean(scores_df)
```
```:実行結果
点数    55.0
dtype: float64
```

### 中央値（Median）
データの大きさを順番に並べたとき，ちょうど中央に位置をする値です．大きな外れ値に平均値が引っ張られてしまうのに対し，中央値はその影響を受けにくいという特徴があります．

```python:中央値
np.median(scores_df)
```
```:実行結果
56.500
```
今回のケースではデータが10件で偶数なので，中央に位置する値が2つあります．その場合，2つの値の平均値が中央値として抽出されます．

### 最頻値（Mode）
データの中で最も多く出現する値です．値のバリエーションが少ない質的変数でよく使われる指標なので，サンプルデータはこの場で作成します．

```python:最頻値
pd.Series([0, 1, 1, 2, 3, 3, 3, 4]).mode()
```
```:実行結果
0    3
dtype: int64
```
少し分かりにくいですが，`3`が最頻値です．NumPyでは直接最頻値を求める関数が無かったため，pandasの`Series`の関数を用いています．

## ばらつきの指標
平均値だけを用いてデータを分析するケースはよく目にしますが，データのばらつき（分布）を確認すれば，データをより詳細に分析できます．

### 偏差（Deviation）
1つの値が平均値からどれだけ離れているか，を表す指標です．

```python:偏差
mean = np.mean(scores_df['点数'])
scores_df['偏差'] = scores_df['点数'] - mean
scores_df
```
|生徒|点数|偏差|
|:-:|--:|--:|
|A|42|-13\.0|
|B|69|14\.0|
|C|56|1\.0|
|D|41|-14\.0|
|E|57|2\.0|
|F|48|-7\.0|
|G|65|10\.0|
|H|49|-6\.0|
|I|65|10\.0|
|J|58|3\.0|

その値が平均と比べてどの程度ばらついているか，値毎に確認することができます．

### 分散（Variance）
しかし，データ全体のばらつきを確認するために偏差を平均してみると，，，

```python:偏差の平均
np.mean(scores_df['偏差'])
```
```:実行結果
0.000
```
となってしまいます..平均との差は，負の値と正の値の合計が同じになるからです．そこで，偏差を2乗した平均を考えます．これが**分散**です．

```python:偏差の2乗の平均
scores_df['偏差二乗'] = scores_df['偏差'] ** 2
np.mean(scores_df['偏差二乗'])
```
```:実行結果
86.000
```

NumPyの`var`を使って点数から直接算出することができます．
```python:np.varを使った標本分散
np.var(scores_df['点数'])
```
```:実行結果
86.000
```

分散について注意点を2つ挙げておきます．
- pandasの分散はデフォルト設定が不偏分散になっているため計算結果が異なる
    - `Series.var`関数の引数`ddof=0`とすれば標本分散になる
    - 詳細は[書籍](https://www.amazon.co.jp/Python%E3%81%A7%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B%E7%B5%B1%E8%A8%88%E8%A7%A3%E6%9E%90%E3%81%AE%E5%9F%BA%E7%A4%8E-%E8%B0%B7%E5%90%88-%E5%BB%A3%E7%B4%80-ebook/dp/B07HHV6LL8/ref=tmm_kin_swatch_0?_encoding=UTF8&qid=1657859730&sr=8-1)を参照
- なぜ分散が絶対値の平均ではなく2乗の平均なのかは，色々と都合が良いかららしい，，，
    - 詳細は以下を参照

https://math.nakaken88.com/textbook/master-variance-definition/

### 標準偏差（Standard Deviation）
ここで，改めてデータを確認します．
```python:各変数を確認
scores_df
```
|生徒|点数|偏差|偏差二乗|
|:-:|--:|--:|--:|
|A|42|-13\.0|169\.0|
|B|69|14\.0|196\.0|
|C|56|1\.0|1\.0|
|D|41|-14\.0|196\.0|
|E|57|2\.0|4\.0|
|F|48|-7\.0|49\.0|
|G|65|10\.0|100\.0|
|H|49|-6\.0|36\.0|
|I|65|10\.0|100\.0|
|J|58|3\.0|9\.0|

```python:各変数の平均
scores_df.mean()
```
```:実行結果
点数      55.0
偏差       0.0
偏差二乗    86.0
dtype: float64
```
偏差二乗の平均が分散になるので，`86.0`が点数の分散です．偏差を2乗しているため，点数と同じスケールでデータのばらつきを表現することができていません..偏差は`13.0`とか`-7.0`なのに対して，偏差二乗は`100`を超える数字が目立ちます．

そこで，分散をルートすることで求められる**標準偏差**という指標が使われます．2乗した値をルートで囲って元のスケールに戻したということです．

```python:分散をルート
np.sqrt(np.var(scores_df['点数']))
```
```:実行結果
9.274
```

NumPyの`std`を使えば一気に計算できます．pandas使用時の注意点は分散のときと同じです．
```python:np.stdで計算
np.std(scores_df['点数'])
```
```:実行結果
9.274
```

点数と同じスケールでばらつきを表現できていますね！

### 最小値／最大値（Min／Max）
これはそのまま，データの最小値と最大値です．

```python:最大値と最小値を求める
scores_df['点数'].max(), scores_df['点数'].min()
```
```:実行結果
(69, 41)
```

### 分位数（Quantile／Percentile）
データの中央に位置する値が中央値でしたが，最小値を`0.0`，最大値を`1.0`としたとき，その間の割合に位置する値が，**分位数**です．外れ値の影響で，最小値や最大値ではデータの特性を上手く表せない，というようなケースで有用です．

`quantile`で求めるときは`0.0`〜`1.0`を，`percentile`ではパーセンテージ`0〜100`を指定します．どちらも実行結果は同じ．ちなみに`0.5`を指定すると中央値と同じ値になります．
```python:quantileによる0.25と0.75の分位数
np.quantile(scores_df['点数'], 0.25), np.quantile(scores_df['点数'], 0.75)
```
```python:percentileによる25%と75%の分位数
np.percentile(scores_df['点数'], 25), np.percentile(scores_df['点数'], 75)
```
```:実行結果
(48.250, 63.250)
```

特に，以下の分位数をまとめて**四分位数**と呼びます．
- 25%・・・第一四分位数
- 50%・・・第二四分位数（中央値）
- 75%・・・第三四分位数

また，書籍には出てきませんでしたが，これまで挙げてきた以下5つをまとめて**5数要約**と呼びます．
- 最小値
- 第一四分位数
- 中央値
- 第三四分位数
- 最大値

文字通り，データを要約する5つの数ということですね．これらは後に説明する四分位範囲や，箱ひげ図に用いられています！

### 範囲（Range）
最大値と最小値の**範囲**を求めて，データのばらつきを表現します．

```python:範囲を求める
scores_df['点数'].max() - scores_df['点数'].min()
```
```:実行結果
28
```

標準偏差と比較したときの違いは以下
- 標準偏差は平均値との差が基になっているため，範囲の半分以下になる
- 範囲は平均値ではなく最大値
- 外れ値の影響を大きく受ける

### 四分位範囲（IQR）
先ほど説明した分位数を用いて範囲を算出します．最大値と最小値を用いた範囲と比べて外れ値の影響が小さい指標です．

```python:四分位範囲を求める
np.quantile(scores_df['点数'], 0.75) - np.quantile(scores_df['点数'], 0.25)
```
```:実行結果
15.000
```
`quantile`の引数として指定している割合を調整することで，ばらつき範囲を調整することができます．

## 正規化（Normalization）
平均や分散に依存せずに，データの相対的な位置関係が分かるように変換することが，**正規化**です．

例えば，高校や大学受験で用いられる**偏差値**も，正規化の指標の1つです．テストの平均点が何点であれ，そのテストを受けた全員の中での自分の位置関係を正しく教えてくれます．

### 標準化（Standardization／Z-score）
偏差を標準偏差で割って求めたものが，**標準化**されたデータです．

**Zスコア**や**基準化変量**などと呼ばれるようです．個人的にはscikit-learnの`StandardScaler`をよく使っていて，「標準にスケーリングされたデータ」みたいなイメージを持っています．言葉が似ていて混乱しがちですが，あくまで標準化は正規化の1つで，Min-Max法など他にもいくつか正規化の手法があります．

```python:標準化を実行
scores_df['Zスコア'] = scores_df['偏差'] / np.std(scores_df['点数'])
scores_df['Zスコア']
```
```:実行結果
生徒
A   -1.402
B    1.510
C    0.108
D   -1.510
E    0.216
F   -0.755
G    1.078
H   -0.647
I    1.078
J    0.323
Name: Zスコア, dtype: float64
```
```python:平均と標準偏差
np.mean(scores_df['Zスコア']), np.std(scores_df['Zスコア'])
```
```:実行結果
(-0.000, 1.000)
```
平均は`0.0`，標準偏差は`1.0`になります．標準化は機械学習で頻繁に用いられていますが，標準化によって失われている情報もある，ということを認識した上で使用すべきかと思います．

例えば，標準化されたデータをいくらこねくり回しても，平均点が`55`で，分散が`86`，標準偏差が`9.274`である，という情報を読み取ることはできませんよね，，，データを標準偏差で割ることで，これらの情報が失われたということです．

### 偏差値（T-score）
さきほど例に挙げた，平均が50，標準偏差が10になるように正規化したデータです．標準化したデータに`10`を掛けて`50`を足せばOK．

```python:偏差値を求める
scores_df['偏差値'] = 50 + 10 * scores_df['Zスコア']
scores_df[['点数', '偏差値']]
```
|生徒|点数|偏差値|
|:-:|--:|--:|
|A|42|35\.982|
|B|69|65\.097|
|C|56|51\.078|
|D|41|34\.903|
|E|57|52\.157|
|F|48|42\.452|
|G|65|60\.783|
|H|49|43\.530|
|I|65|60\.783|
|J|58|53\.235|

標準化されたデータが，皆さんに馴染みのある**偏差値**に変換されましたね！

## 視覚化
次に，1次元データを図表で視覚化して，データの特徴を明らかにする方法に関して記載します．50人分の英語の点数を使用します．

```python:使用するデータ
english_scores = df['英語']
english_scores.describe().to_frame()
```
||英語|
|---|--:|
|count|50\.0|
|mean|58\.38|
|std|9\.80|
|min|37\.0|
|25%|54\.0|
|50%|57\.5|
|75%|65\.0|
|max|79\.0|

### 度数分布表（Frequency table）
データの分布を知るために，値の範囲を一定の区間に分割し，データを分類してまとめたものが`度数分布表`です．分割する区間を**階級（Class）**，階級に属している数を**度数（Frequency**）と言います．

NumPyの`histogram`に使用して度数分布表を作成します．
```python:度数分布のDataFrame作成
freq, _ = np.histogram(english_scores, bins=10, range=(0, 100))
freq_class = [f'{i}〜{i + 10}' for i in range(0, 100, 10)]

freq_dist_df = pd.DataFrame({'度数': freq}, index=pd.Index(freq_class, name='階級'))
freq_dist_df
```
|階級|度数|
|---|--:|
|0〜10|0|
|10〜20|0|
|20〜30|0|
|30〜40|2|
|40〜50|8|
|50〜60|16|
|60〜70|18|
|70〜80|6|
|80〜90|0|
|90〜100|0|

### 階級値（Class）
各階級を代表する値で，階級の中央の値が使われます．

```python:階級値を求める
class_values = [(i + (i + 10)) // 2 for i in range(0, 100, 10)]
class_values
```
```:実行結果
[5, 15, 25, 35, 45, 55, 65, 75, 85, 95]
```

### 相対度数（Relative frequency）
全データに対してその階級が占める割合が**相対度数**です．

```python:相対度数を求める
rel_freq = freq / freq.sum()
rel_freq
```
```:実行結果
array([0.  , 0.  , 0.  , 0.04, 0.16, 0.32, 0.36, 0.12, 0.  , 0.  ])
```
相対度数を合計すると`1.0`になります．

### 累積相対度数（Cumulative relative frequency）
相対度数の累積和です．

```python:相対度数の累積和
cum_rel_freq = np.cumsum(rel_freq)
cum_rel_freq
```
```:実行異結果
array([0.  , 0.  , 0.  , 0.04, 0.2 , 0.52, 0.88, 1.  , 1.  , 1.  ])
```

各指標を表にまとめて表示します．
```python:各指標を表に列を追加
freq_dist_df['階級値'] = class_values
freq_dist_df['相対度数'] = rel_freq
freq_dist_df['累積相対度数'] = cum_rel_freq
freq_dist_df
```
|階級|度数|階級値|相対度数|累積相対度数|
|---|--:|--:|--:|--:|
|0〜10|0|5|0\.0|0\.0|
|10〜20|0|15|0\.0|0\.0|
|20〜30|0|25|0\.0|0\.0|
|30〜40|2|35|0\.04|0\.04|
|40〜50|8|45|0\.16|0\.2|
|50〜60|16|55|0\.32|0\.52|
|60〜70|18|65|0\.36|0\.88|
|70〜80|6|75|0\.12|1\.0|
|80〜90|0|85|0\.0|1\.0|
|90〜100|0|95|0\.0|1\.0|

pandasの`cut`や`qcut`などで同じようなことを実現していましたが，NumPyでも楽に作ることができるんですね，，，

### ヒストグラム（Histogram）
作成した度数分布表のデータを基にグラフ描画してみます．

点数の度数分布のグラフ
```python:ヒストグラムを作成
import matplotlib.pyplot as plt
import japanize_matplotlib

plt.figure(figsize=(10, 6))
plt.hist(english_scores, bins=10, range=(1, 100))
plt.grid(True)
plt.xlabel('点数')
plt.ylabel('人数')
plt.xticks(np.linspace(0, 100, 10 + 1))
plt.yticks(np.arange(0, freq.max() + 1))
plt.show()
```
![スクリーンショット 2022-07-18 15.33.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/1cca221e-8eed-ff17-3c04-09e3c3ab8e1d.png)

相対度数と累積相対度数のグラフ
```python:相対度数と累積相対度数
fig = plt.figure(figsize=(10, 6))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()

weights = np.ones_like(english_scores) / english_scores.shape[0]
rel_freq, _, _ = ax1.hist(english_scores, bins=25, range=(0, 100), weights=weights, alpha=0.5)

cum_rel_freq = np.cumsum(rel_freq)
class_values = [(i + (i + 4)) // 2 for i in range(0, 100, 4)]
ax2.plot(class_values, cum_rel_freq, ls='--', marker='o', color='green', alpha=0.7)
ax2.grid(False)

ax1.set_xlabel('点数')
ax1.set_ylabel('相対度数')
ax2.set_ylabel('累積相対度数')
ax1.set_xticks(np.linspace(0, 100, 25 + 1))

ax1.grid(True)
plt.show()
```
![スクリーンショット 2022-07-18 15.58.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/91181259-6b32-5fe7-47e1-12a39aae4cef.png)

### 箱ひげ図（Box plot）
データの分布や外れ値を表現することができます．

- `Q1`・・・第一四分位数（`0.25`の分位数）
- `Q3`・・・第三四分位数（`0.75`の分位数）
- `IQR`・・・四分位範囲

上記としたとき，
- 箱・・・`IQR`の範囲
- ひげ・・・以下の`A`〜`B`の範囲
    - `A`・・・`MAX(Q1 - 1.5 * IQR, 最小値)`
    - `B`・・・`MIN(Q3 + 1.5 * IQR, 最大値)`
- 点・・・外れ値

となります．

```python:matplotlibによる箱ひげ図
plt.figure(figsize=(4, 6))
plt.boxplot(english_scores, labels=['英語'])
plt.show()
```
![スクリーンショット 2022-07-18 16.15.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/105f5f99-7dc9-748d-8e38-a6c720dc84d0.png)

seabornでは，外れ値以外の値をプロットしたグラフがよく使われている印象．

```python:seabornのstripplotによる箱ひげ図
import seaborn as sns

english_scores_df = english_scores.to_frame().reset_index()

plt.figure(figsize=(4, 6))
sns.boxplot(y='英語', data=english_scores_df, fliersize=12)
sns.stripplot(y='英語', data=english_scores_df, jitter=True, color='black', alpha=0.4)
plt.show()
```
![スクリーンショット 2022-07-18 16.32.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/0d9ecbe4-5416-7450-efec-a99c01ec4c43.png)

箱ひげ図については以下を参照

https://bellcurve.jp/statistics/course/5222.html

ライブラリで視覚化された箱ひげ図が「外れ値検出のある箱ひげ図」として紹介されており，通常の箱ひげ図は，5数要約をシンプルに視覚化したもの，とされています．

# 2次元データ
ここまで1次元データ（英語の点数データ）での様々な指標や視覚化について記載してきました．ここからは，2次元データ（英語と数学の点数データ）を使用して，2つ以上のデータ間の関係性を知る方法を示します．

これまでも使用してきた点数データの最初の10件を使用します．
```python:読み込んで10件分を使用
df = pd.read_csv('data/ch2_scores_em.csv', index_col='生徒番号')

scores_df = df[:10].copy()
scores_df.index = pd.Index(list('ABCDEFGHIJ'), name='生徒')
scores_df
```
|生徒|英語|数学|
|:-:|--:|--:|
|A|42|65|
|B|69|80|
|C|56|63|
|D|41|63|
|E|57|76|
|F|48|60|
|G|65|81|
|H|49|66|
|I|65|78|
|J|58|82|

## 相関（Correlation）
2つのデータの関係性を表す指標が，**相関**です．相関の有無と正負に関して以下にまとめました．

- **正の相関**がある
    - 英語の点数が高い人は数学の点数が高い
    - 英語の点数が低い人は数学の点数が低い
- **負の相関**がある
    - 英語の点数が高い人は数学の点数が低い
    - 英語の点数が低い人は数学の点数が高い
- **無相関**である
    - 英語の点数が数学の点数に直接影響しない

上記のような言葉でも相関関係を表現できますが，以降は相関関係を定量的に表現する指標について，説明していきます．

### 共分散（Covariance）
データの偏差を求めて2乗して平均したものが分散ですが，2つのデータの偏差の積（掛け算）を平均したものを**共分散**といいます．偏差は，点数が平均より高いと正に，平均より低いと負になります．従って，英語と数学の偏差の積は以下のようになります．

|英語|数学|偏差の積|
|:-:|:-:|:-:|
|低|低|負 ✕ 負 = 正|
|低|高|負 ✕ 正 = 負|
|高|低|正 ✕ 負 = 負|
|高|高|正 ✕ 正 = 正|

偏差の積が先にまとめた相関関係と一致していることが分かります！否定排他的論理和 (XNOR) ですね．書籍ではこの辺りを図を交えて解りやすく説明していましたが，私にはこれが限界でした..

実際に偏差の積を求めてみます．
```pyhton:偏差の積を求める
scores_df['英語の偏差'] = scores_df['英語'] - scores_df['英語'].mean()
scores_df['数学の偏差'] = scores_df['数学'] - scores_df['数学'].mean()
scores_df['偏差の積'] = scores_df['英語の偏差'] * scores_df['数学の偏差']
scores_df
```
|生徒|英語|数学|英語の偏差|数学の偏差|偏差の積|
|:-:|--:|--:|--:|--:|--:|
|A|42|65|-13\.0|-6\.4|83\.2|
|B|69|80|14\.0|8\.6|120\.4|
|C|56|63|1\.0|-8\.4|-8\.4|
|D|41|63|-14\.0|-8\.4|117\.6|
|E|57|76|2\.0|4\.6|9\.2|
|F|48|60|-7\.0|-11\.4|79\.8|
|G|65|81|10\.0|9\.6|96\.0|
|H|49|66|-6\.0|-5\.4|32\.4|
|I|65|78|10\.0|6\.6|66\.0|
|J|58|82|3\.0|10\.6|31\.8|

偏差の積を平均して共分散を求めます．
```pyhton:共分散を求める
scores_df['偏差の積'].mean()
```
```:実行結果
62.800
```
正の相関があると言えそうな結果になりました．

### 共分散行列（Covariance matrix）
Numpyの`cov`関数で共分散を求めます．
```python:covで共分散を求める
cov_mat = np.cov(scores_df['英語'], scores_df['数学'], ddof=0)
cov_mat = pd.DataFrame(cov_mat, columns=['英語', '数学'], index=['英語', '数学'])
cov_mat
```
||英語|数学|
|:-:|--:|--:|
|英語|86\.0|62\.8|
|数学|62\.8|68\.44|

NumPyの`cov`関数で得られる配列は，共分散行列です．マトリクスになっており，英語と英語，数学と数学のセルにはそれぞれの分散が入ります．英語と数学，数学と英語のセルには，両方とも以前求めた共分散の値`62.8`が入っていることがわかります．

### 相関係数（Correlation coefficient）
共分散は偏差の積から求められるので，異なる単位の共分散を評価するとき，共分散の値同士を比較することが難しくなってしまいます．単位に依存しない指標を作るために，各偏差を標準偏差で割った上で積の平均をとったものが**相関係数**です．標準化のときと同じ操作ですね！

```python:相関係数を求める
en_s = scores_df['英語の偏差'] / np.std(scores_df['英語'])
ma_s = scores_df['数学の偏差'] / np.std(scores_df['数学'])
np.mean(en_s * ma_s)
```
```:実行結果
0.819
```
最大値が`1.0`である相関係数で`0.819`なので，英語と数学の点数はかなり相関が高いと言えそうです．

相関係数の目安については以下を参照．

https://www.nsspirt-cashf2.com/entry/2018/12/29/294/

### 相関行列（Correlation matrix）
NumPyの`corrcoef`でそのまま相関行列を得ることができます．共分散行列のときと戻り値の配列の構成は同じです！
```python:Numpyのcorrcoefによる相関行列
corr_mat = np.corrcoef(scores_df['英語'], scores_df['数学'])
corr_mat = pd.DataFrame(corr_mat, columns=['英語', '数学'], index=['英語', '数学'])
corr_mat
```
pandasの`corr`は2つ以上のデータから一発で相関行列を得ることができます．実行結果は両方とも同じです．
```python:pandasのcorrによる相関行列
scores_df[['英語', '数学']].corr()
```
||英語|数学|
|:-:|--:|--:|
|英語|1\.0|0\.819|
|数学|0\.819|1\.0|

英語と英語，数学と数学はすべて同一の値なので，相関係数は共に`1.0`になっています．

## 視覚化
ここからは2次元データの関係性を視覚化する方法について説明します．元の50件のデータを使用して描画していきます．

### 散布図（Scatter plot）
X軸に英語の点数，Y軸に数学の点数を取って，**散布図**を描画します．

```python:matplotlibによる散布図
import matplotlib.pyplot as plt
import japanize_matplotlib

plt.figure(figsize=(6, 6))
plt.scatter(df['英語'], df['数学'])
plt.xlabel('英語')
plt.ylabel('数学')
plt.grid(True)
plt.show()
```
![スクリーンショット 2022-07-19 20.58.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/efe9e0cd-df07-f0d6-7053-ec6e219f8766.png)

### 回帰直線（Regression line）
2つのデータの関係性を最も上手く表す直線で表現を**回帰直線**と言います．matplotlibでは楽に描画できないため，以下はseabornを用いました．

```python:seabornによる回帰直線と散布図
import seaborn as sns

plt.figure(figsize=(6, 6))
sns.regplot(x='英語', y='数学', data=df, fit_reg=True)
plt.grid(True)
plt.show()
```
![スクリーンショット 2022-07-19 21.08.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/fd4771c8-67c3-344c-7e78-713048466b66.png)

回帰直線に関する詳細は以下．

https://www.sentankyo.jp/articles/246905e7-8a8c-46f0-93f1-d28fe1bf3dfb

### ヒートマップ（Heatmap）
2次元データのヒストグラムや，マトリクスデータを色によって表現するグラフです．今回は`hist2d`関数を使用して2次元ヒストグラムを描画します．

```python:ヒートマップ
fig = plt.figure(figsize=(8, 7))
c = plt.hist2d(df['英語'], df['数学'], bins=[9, 8], range=[(35, 80), (55, 95)])
plt.xticks(c[1])
plt.yticks(c[2])
plt.xlabel('英語')
plt.ylabel('数学')
plt.colorbar(c[3])
plt.show()
```
![スクリーンショット 2022-07-19 21.30.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44273/9d36a240-f5db-86f2-d3d4-3c3b6a47a27e.png)

ヒストグラムは散布図と似た表現なので，マトリクスのデータに対してヒートマップを用いることが多く，個人的にはseabornの`heatmap`を使用することが多い，，，

seabornの`heatmap`の詳細は以下

https://seaborn.pydata.org/generated/seaborn.heatmap.html

# おわりに
長くなりましたが，データ編は以上です．

今回のデータ編については，基礎的な内容のため知っていることも多かったのですが，細かい部分で理解できていなかったことや，知識をそれぞれの点としてではなく，全体を線をなぞるように確認し直せたのは良かったと思っています．少しでも皆さんの参考になれば幸いです．最後まで読んで頂きありがとうございました！

次回，2つ目の確率編はこちらです！

https://qiita.com/o93/items/1b3f379da3e62c6252d8