[メモ]機械学習の前処理のあれこれ

※signate cloudを勉強する中で覚えたことをメモ書きします。

---

pandasでデータ整形する手順を備忘用にメモ書きします。


## 公式ドキュメント

```
# 標準ライブラリ リファレンス（日本語）
https://docs.python.org/ja/3/library/index.html#library-index

# 公式ドキュメント DataFrame
https://pandas.pydata.org/pandas-docs/stable/reference/frame.html
```

## dataframeの行、列表示リミットを変更

```
pd.set_option("display.max_rows",50)
pd.set_option("display.max_columns",50)
```


## ライブラリ読み込み

```python
# ライブラリpandasを略称名pdとしてインポート
import pandas as pd 

# ライブラリnumpyを略称名npとしてインポート
import numpy as np

# ライブラリmatplotlibのpyplotモジュールのみを略称名pltとしてインポート
from matplotlib import pyplot as plt 
```

## データの読み込み
```python
# DataFrameと呼ばれる表形式構造でデータが読み込まれる

## CSVファイル
df = pd.read_csv('data.csv')
## TSVファイル
df = pd.read_csv('data.tsv', sep = "\t")
```

## ファイルへの書き込み

```python
## indexなしでファイルへ書き込み
DataFrame.to_csv('ファイル名', index=None)
```

## ファイルへの書き込み（指定したカラムだけを書き出す）

```python
DataFrame.to_csv('ファイル名', index=None, columns=['カラム名'])
```


## DataFrameの行数・列数の確認
```python
df.shape

# 行数だけ取得
df.shape[0]
# 列数だけ取得
df.shape[1]
```

## 値を取得する。
```python
# 加藤さんの身長を取得する。
sample[df["名前"]=="kato"]["身長"]
```

## 各列のデータを確認

### loc：行名（インデックスラベル）もしくは列名（カラムラベル）を指定することで特定の値を抽出する。

- 特定行を確認する。

```python
df.loc["1行", :]
```

- 特定の複数行を確認する。

```python
df.loc[["111行", "222行", "333行"], :]
```

### iloc：indexを指定することで特定の値を抽出する。

- 特定行を確認する。

```python
# df.iloc[行, 列]
df_receipt.iloc[0,0]
df_receipt.iloc[0]

# 先頭5行を出力
df.iloc[:5]
df_receipt.iloc[0:5,:]
```

- 特定の複数行を確認する。

```python
df.iloc[[1, 2, 3], :]
```



## DataFrameの要約情報の確認

- DataFrameの各カラムのデータ型を確認する
```python
print(df.info())
```

## カラム名一覧の確認

```python
print(df.columns)
```


## データ型の確認

```python
print(type(data))
```

## 各カラムのデータ型の確認

```python
print(df.dtypes)
```

## 数値型の変数に関する基本統計量の確認
```python
df.describe()
```

- count:データの個数
- mean:平均値
- std:標準偏差
- min:最小値
- 25%:第一四分位数
- 50%:第二四分位数(中央値)
- 75%:第三四分位数
- max:最大値

## オブジェクト型の変数に関する基本統計量の確認
```python
# ※アルファベットのオー
df.describe(include=['O'])
```

- count : データの個数
- unique : ユニークな要素の個数
- top : 最も多く出現する要素（最頻値）
- freq : topで返された要素の出現回数


## 数値型の変数とオブジェクト型の変数に関する情報を表示する。

```python
df.describe(include='all')
```


## カテゴリごとのデータ数の確認
```python
# 頻度が多い順に表示する
df["AAA"].value_counts()
# 頻度が少ない順に表示する
df["AAA"].value_counts(ascending=True)
# 値の順に表示する
df["AAA"].value_counts().sort_index()
```

## 特定のカラムの値のユニーク件数を確認

```python
print(df_receipt['columns1'].nunique())

print(len(df_receipt["columns1"].unique()))
```


## 欠損値の数を確認
```python
# isnull関数は、欠損値が含まれていればTrue、含まれていなければFalseを返す。
print(df.isnull().sum())
```

## 欠損値を含む行を抽出
```python
#any(axis=1)は、一行毎にTrueの存在の有無を調べるコマンド
print(df[df.isnull().any(axis=1)])
```

## 特定の値の行だけを抽出

### データフレームの再帰代入

```python
df[df["AAA"] == 0]
```

### query関数

```python
df.query("AAA == 0")
```

## 複数の条件を満たす行だけを抽出

- 複数の条件を指定する場合、&演算子（かつ）もしくは|演算子（または）を利用する。
- 「AかつB　または　AかつC」という条件式は、A & (B | C)のようにまとめて書ける。
- 「3以上5未満」という条件式は、query('3 <= A < 5')のようにまとめて書ける。
- 「A行が5、B行が3以外」という条件式は、query('A == 5 & B != 3')のように書ける。
- 「◯◯◯でない」という条件式は、query('not(◯◯◯)')のように書ける。

### データフレームの再帰代入


```python
# それぞれの条件を()で囲むこと。
df[(df['customer_id']=='CS000001')&(df['amount']>=1000)]


df[(df['customer_id']=='CS000001')&((df['amount']>=1000)|(df['quantity']>=5))]
df[(df['customer_id']=='CS000001')][(df['amount']>=1000)|(df['quantity']>=5)]
```



### query関数

```python
df.query('customer_id == "CS000001" & amount >= 1000'))

df.query('customer_id == "CS000001" & (amount >= 1000 | quantity >=5)')
df.query('customer_id == "CS000001"').query('amount >= 1000 | quantity >= 5')

df.query('customer_id == "CS000001" & 1000 <= amount <= 2000')

print(df.query('not(customer_id == "CS000001")'))
```

## 特定の文字列を含む行を抽出

### str.startswith

- columns1の値が"ほげほげ"で始まるものだけ全項目抽出

```python
df[df["columns1"].str.startswith("ほげほげ")]
```

- query関数でこの文字列メソッドを使う際には、引数にengine='python'が必要

```python
df.query("columns1.str.startswith('ほげほげ')", engine='python')
```

### str.endswith

- columns1の値が"ほげほげ"で終わるものだけ全項目抽出

```python
df[df["columns1"].str.endswith("ほげほげ")]
```

- query関数でこの文字列メソッドを使う際には、引数にengine='python'が必要

```python
df.query("columns1.str.endswith('ほげほげ')", engine='python')
```

### str.contains

:::note info
文字列メソッドstr.contains()は、正規表現も使える。
:::


- columns1の値が"ほげほげ"を含むものだけ全項目抽出

```python
df[df["columns1"].str.contains("ほげほげ")]
```

- query関数でこの文字列メソッドを使う際には、引数にengine='python'が必要

```python
df.query("columns1.str.contains('ほげほげ')", engine='python')
```


## 欠損値の補完（？）
```python
#inplace=Trueで上書き保存する
df["AAA"].fillna(df["BBB"], inplace=True)
```

```python
#欠損値を-99で埋める
select_columns = ['ほげ１','ほげ２','ほげ３','ほげ４','ほげ５']
df[select_columns] = df[select_columns].fillna(-99)
```



## 重複
```python
#重複しているデータの件数
print(df.duplicated().sum())

#重複部分の抽出
print(df[df.duplicated()])

#重複データの削除
#inplace=Trueで上書き保存する
df.drop_duplicates(inplace=True)

#新しいインデックスを取得
#drop=Trueで元のインデックスの削除し、、新しいインデックスを振りなおす
df.reset_index(drop=True, inplace=True)
```

## カラムの削除
```python
# hogeカラムを削除
df = df.drop(columns=["hoge"])
```

### 複数カラムの場合
```python
# hoge1,hoge2カラムを削除
df = df.drop(columns=["hoge1","hoge2"])
```


## 欠損値を含む行の削除
```python
# dropnaの結果を再び代入する
df = df.dropna()

# inplace=Trueで、dfを直接書き換える。
df.dropna(inplace=True)
```


## 分離
```python
# カテゴリ変数の列名をリストで指定
col_categoric = ["AAA", "CCC"]

#数量変数のデータフレームを作成
#axis=0は行を削除するオプション、axis=1は列を削除するオプション
df_numeric = df.drop(col_categoric, axis=1)

#カテゴリ変数のデータフレームを作成
df_categoric = df[col_categoric]
```

## 型変換（datetime型）
```python
df["AAA"] = pd.to_datetime(df["AAA"])
```

## 型変換（int型）
```python
df["AAA"] = df["AAA"].astype('int')
```


## データ整形①

### 例）型を変更する。

```python
#整形用関数
def change_f(x):
    return int(x)
    
#一次元配列に適用可能な関数をapply()の引数に渡す。
df['AAA'] = df['AAA'].apply(change_f)
```

### 例）単位を削除して型を変更する。

```python
#後ろ2文字を削除して、float型に変更する。
def change_f(x):
    return float(x[:-2])
    
#一次元配列に適用可能な関数をapply()の引数に渡す。
df['面積'] = df['面積'].apply(change_f)
```

### 例）文字列を含むカラムを数値型に変更する。

```python
#新築なら0をそれ以外なら年数を数値型で返す。
def change_f(x):
    if x == '新築':
        return 0
    else:
        return int(x.split('年')[0])
    
#一次元配列に適用可能な関数をapply()の引数に渡す。
df['築年数'] = df['築年数'].apply(change_f)
```

### 例）正規表現を活用した整形

- 最寄駅からの距離の情報が複数入っているアクセス情報を整形し、駅からの最短の所要時間を取得する。（常磐線快速\t南千住駅\t徒歩2分\t\t都電荒川線\t三ノ輪橋駅\t徒歩10分...）

```python
def change_f(x):
    time_list = re.findall(r'徒歩(\d+)分',str(x))
    
    # time_listを数値型に変換
    time_list = map(int, time_list)
    
    # time_listの最小値をmin_valueに代入
    min_value = min(time_list)
    
    return min_value

df['所要時間'] = df['アクセス'].apply(change_f)
```


## データ整形②

書き方：DafaFrame["カラム名"].apply(lambda x: xに関する処理)

```python
# 天気（weather）のうち、数の少ない雪と雷電は雨にまとめるようにweatherを更新
df["weather"] = df["weather"].apply(lambda x: "雨" if x == "雪" or x == "雷電" else x)
```

```python
# 「東京都〇〇区...」という形式の所在地から、区の情報のみのカラムを作成する。
df['区'] = df["所在地"].apply(lambda x : x[x.find("都")+1:x.find("区")])
```


### 複数のカラムを条件にデータ整形する場合

:::note warn
apply()は、デフォルトでは各列に対して適用され、引数axis=1とすると各行に対して適用される。
:::

```python
# remarksが「お楽しみメニュー」かつnameが「カレー」を含めば1、それ以外は0で、remarksを更新
df["remarks"] = df.apply(lambda x: 1 if x["remarks"] == "お楽しみメニュー" and "カレー" in x["name"] else 0, axis=1)
```


## ［補足］無名関数

定義と呼び出しを同時に記述することができる。

書き方：lambda 引数: 戻り値


```python
lambda x: x * 2
```

以下のように記述したのと同じ

```python
def func(x):
    retrun x * 2
```

## ［補足］三項演算子

書き方：条件式が真のときに返す値 if 条件式 else 条件式が偽のときに返す値

```python
# 「雪」と「雷電」の場合は「雨」、それ以外の場合はそのままの値をresultへ返す。
x = "雷電"
result = "雨" if x == "雪" or x == "雷電" else x
```

## 階級率のカラムを作成する。

- 同じ3階でも、3階建ての3階と30階建ての3階では価値は異なると考えられる。階数を最上階で割った相対的な階数を表す階数率カラムを作成する。

```python
df['階数率'] = df['階数'] / df['最上階']
```

## YYYY-MM-DDのカラムから、新しいYYYYとMMのカラムを作成する。

```python
df["year"]  = df["datetime"].apply(lambda x: x.split("-")[0])
df["month"] = df["datetime"].apply(lambda x: x.split("-")[1])

# 整数に変換
df["year"] = df["year"].astype(int)
df["month"] = df["month"].astype(int)

       datetime  year  month 
0    2013-11-18  2013     11 
1    2013-11-19  2013     11 
2    2013-11-20  2013     11 
3    2013-11-21  2013     11 
4    2013-11-22  2013     11 
```

## 連続する日付データを生成する

```python
df["DATE"] = pd.date_range(df_start, df_end)
```


## ソート

- 引数ascendingのデフォルトはTrue（昇順）

```python
df.sort_values(by='columns1', ascending=True)
```

## ユーザ毎に売上金額（amount）と売上数量（quantity）の合計を求める。

```python
df.groupby('user_custom')["amount","quantity"].apply(sum)

df.groupby('user_custom')[['amount','quantity']].sum()
```

## ユーザ毎に時系列順に並べる
```python
# user_customカラムでgroupbyする
df.groupby("user_custom").apply(lambda x: x.sort_values("time_custom"))
```

## 売上順にランキングを付与し、TOP10を取得する。

### rank

- 引数methodは、デフォルトが'average'（平均）になっている。
    - method='min'とすると最小値が順位となる。1位、2位タイ、2位タイ、4位のように値が等しい場合は同一順位となる。
    - method='first'とすると同一値（重複値）は登場順に順位付けされる。同順位を別順位とすることができる。
- 引数ascendingは、デフォルトがTrueで昇順の順位付けになる。

```python
# rankingカラムを作成
df["ranking"] = df.rank(method='min', ascending=False)["amount"]
# ソート
df = df.sort_values(by=["ranking"], ascending=True)
# TOP10取得
print(df[["customer_id","amount","ranking"]].head(10))
```

## 売上金額（amount）の平均を計算し、降順のTOP10を取得する。

```python
df.groupby('customer_id')[["amount"]].mean().reset_index().sort_values('amount', ascending=False).head(10)
```

## 商品毎のユーザ数を数える。
```python
# 商品毎のユニークユーザー数
tmp = data.groupby("product_custom").apply(lambda x: len(x["user_custom"].unique()))
```

## 商品毎のアクション数を数える。
```python
# カートに入れられた回数
df.groupby("product_custom").apply(lambda x: (x['event_custom']==123).sum())
```

## グルーピング agg()メソッド：複数種類の集約処理

- グループ型.agg()メソッドを用いると、集約処理を2種類同時に実行することができる。

- 使い方は、agg()メソッドの引数に、「集約関数を要素とするリスト」を渡す。

```
グループ型.agg(['集約関数1', '集約関数2', ...])
```

- 集約関数を示す文字列として使用できるもの

    - 'mean': グループ内の平均値を算出する
    - 'min': グループ内の最小値を算出する
    - 'max': グループ内の最大値を算出する
    - 'median': グループ内の中央値を算出する
    - 'std': グループ内の標準偏差を算出する

```python
# 顧客ID（customer_id）ごとに最も新しい売上日（sales_ymd）と古い売上日を求める
df_tmp = df.groupby('customer_id').agg({'sales_ymd':['max','min']})
df_tmp.columns = ["_".join(pair) for pair in df_tmp.columns]
# 両者が異なるデータを10件出力する
print(df_tmp.query('sales_ymd_max != sales_ymd_min').head(10))
```


## カラム名を変更する。

```python
# カラム名を変更
df.columns  = ['EEE', 'BBB', 'CCC', 'DDD']
```

```python
# カラム名を変更
df["EEE"]  = df["AAA"]
df  = df[['EEE', 'BBB', 'CCC', 'DDD']]
```

```python
# カラム名を変更
df = df.rename(columns={"AAA": "EEE"})
```



## ユーザ毎・商品毎に時系列順に並べる
```python
# 複数カラムでgroupbyする場合
tmp = df.groupby(["user_custom", "product_custom"]).apply(lambda x: x.sort_values("time_custom"))

# 指定した条件のデータを参照
tmp.loc[("user_000001", "product_000001")]
```


## インデックスをカラムにする

```python
df.reset_index()
```

## 2つのDataFrameを縦方向へ結合する。

```python

# 結合する前に、のちのち、どこまでがdf1でどこからdf2かかわかるようにするため
# まずflagというカラムを新しく作り、dfは1、df2は0を代入してお
#
# 新しいカラム「flag」を作成
df1["flag"] = 1
df2"flag"] = 0

# 縦方向に結合
data = pd.concat([df1, df2])

# 結合したデータは元々のインデックス番号を保持してしまい連番ではなくなってしまうので
# reset_index関数でインデックスを連番にする。
#
# インデックスのリセット
data = data.reset_index(drop=True)

     datetime    y week  flag
0  2013-11-18   90    月        1
1  2013-11-19  101    火        1   
2  2013-11-20  118    水        1 
3  2013-11-21  120    木        0 
4  2013-11-22  130    金        0
```

:::note warn
 reset_index関数を使用する時は、drop=Trueオプションを付けないと古いインデックスが新しいカラムとして追加されてしまうので注意!!
:::

```python
# ↓ drop=Trueオプションを付けないで実行した場合 ↓
data = data.reset_index()

   index    datetime    y week  flag
0      0  2013-11-18   90    月        1    
1      1  2013-11-19  101    火        1    
2      2  2013-11-20  118    水        1    
3      0  2013-11-21  120    木        0 
4      1  2013-11-22  130    金        0
```


##  2つのDataFrameを横方向へ結合する。

- indexが一致する行同士が結合される。indexが一致しない場合はNaNで補完される。

```python
df_new = pd.concat([df_a, df_b], axis=1)
```

## 2つのDataFrameを結合する。

```pyth
pd.merge(df1, df2, on = '結合のキーになるカラム', how='結合方法')
```

- デフォルトはhow='inner'
- LEFT JOIN: how='left'
- RIGHT JOIN: how='right'
- OUTER JOIN: how='outer'
- CROSS JOIN: how='cross'


## ダミー変数化（One-Hot表現）

質的データを量的データに変換すること（質的データは数値でないため、説明変数として使用できない。）


使い方：pd.get_dummies(データ)

:::note warn
カテゴリー変数以外は返却されない。
:::

```python
# カテゴリー変数のカラムを含むDataFrameを入れると、カテゴリー変数のみダミー変数化してくれる。
df = pd.get_dummies(df)
```

- n個のカテゴリをダミー化するにはn-1個のダミー変数があれば十分だが、get_dummies()ではn個のダミー変数が作成される。drop_first=Trueとすると、最初のカテゴリーが除外されn-1個のダミー変数が作成される。

```python
# 引数drop_firstのデフォルトはFalse
df = pd.get_dummies(df, drop_first=True)
```


## 訓練データとテストデータの分割

- train_test_splitは、75%を訓練データに25%をテストデータとして分割する。
- 数学の慣例に従い、xが関数への入力、yが出力を表す。
- ２次元配列であるデータには大文字Xを、１次元配列（ベクトル）には小文字のyを用いる。
- random_stateは擬似乱数生成器に同じシードを渡している。これにより同じ結果が得られる。

``` python
X_train,X_test,y_train,y_test = train_test_split(X, y, random_state = 数値)
```



---

以下、データを可視化する手順を備忘用にメモ書きします。

## 相関係数
```python
df.corr()
```

## 相関係数のヒートマップ
```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns

# 相関係数の算出
corr_matrix = df.corr()

# ヒートマップの可視化
sns.heatmap(corr_matrix)

# ヒートマップを表示
plt.show()
```

## 散布図①
```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt

# 横軸にhoge1、縦軸にhoge2の散布図を描画する
plt.scatter(df["hoge1"],df["hoge2"])

# x軸
plt.xlabel('hoge1')
# y軸
plt.ylabel('hoge2')

# 散布図を表示
plt.show()
```

- 散布図の色を設定する。

```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt
import matplotlib.cm as cm

# 各データセットの色を生成する
colors = cm.rainbow(np.linspace(0, 1, len(df["hoge1"].unique())))

for i,j in zip(df["tire"].unique(), colors):

    tmp = df[df["hoge1"] == i]
   
    # 横軸にhoge1、縦軸にhoge2の散布図を描画する
    plt.scatter(tmp["hoge1"],tmp["hoge2"], color=j)

    # x軸
    plt.xlabel('hoge1')
    # y軸
    plt.ylabel('hoge2')

# 散布図を表示
plt.show()
```


## 散布図②

```python
df.plot.scatter( x="hoge1", y="hoge2",  title="hogehoge" )

# x軸とy軸にラベルを付ける
plt.xlabel("hoge1_label")
plt.ylabel("hoge2_label")

# 散布図を表示
plt.show()
```



## ヒストグラム

```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt

# hogeをSeriesとして抽出
series_hoge = df["hoge"]

# ヒストグラムの可視化
# Seriesが代入された変数.plot.hist()
series_hoge.plot.hist(title='タイトル')

# ヒストグラムを表示
plt.show()
```

## 棒グラフ

```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt

# 頻度値を算出
series_hoge = data["hoge"].value_counts()

# 棒グラフの可視化
# Seriesが代入された変数.plot.bar()
series_hoge.plot.bar(title='hogeカラムの頻度値')

# x軸
plt.xlabel('hoge')
# y軸
plt.ylabel('count')

# 棒グラフを表示
plt.show()
```


## 箱ひげ図
```python
# ライブラリのimport
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns

# x='横軸に割り当てたいカラム名', y='縦軸に割り当てたいカラム名', data=分析対象のDataFrame
sns.boxplot(x='hoge1', y='hoge2', data=df)

#x軸の名前を90度回転する
#plt.xticks(rotation=90)

# 箱ひげ図を表示
plt.show()
```

## コロプレス図

- 省略

---

以下、API関連情報を備忘用にメモ書きします。

## 所在地情報（緯度経度）

- Google Maps API (https://cloud.google.com/maps-platform?hl=ja)
- Geocoding (https://www.geocoding.jp/api/)