前処理大全のPythonだけ抜き出し

著者のgithubのコードを都度さまよっていたので、すぐ使えるようまとめておく。

# 元ネタ
[前処理大全［データ分析のためのSQL/R/Python実践テクニック］](https://www.amazon.co.jp/%E5%89%8D%E5%87%A6%E7%90%86%E5%A4%A7%E5%85%A8%EF%BC%BB%E3%83%87%E3%83%BC%E3%82%BF%E5%88%86%E6%9E%90%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AESQL-R-Python%E5%AE%9F%E8%B7%B5%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF%EF%BC%BD-%E6%9C%AC%E6%A9%8B-%E6%99%BA%E5%85%89-ebook/dp/B07C3JFK3V)

https://github.com/ghmagazine/awesomebook

以下githubよりPython部分を抽出。詳しい解説は書籍参照。

また、これらのソースコードは BSD 3 条項で[ライセンス](https://github.com/ghmagazine/awesomebook/blob/master/LICENSE)されています。

> BSD 3-Clause License
>
> Copyright (c) 2018, Tomomitsu Motohashi
> All rights reserved.

# データ読み込み
```py
# ライブラリより読み込み
from preprocess.load_data.data_loader import load_hotel_reserve
customer_tb, hotel_tb, reserve_tb = load_hotel_reserve()

# CSVより読み込み
reserve_tb = pd.read_csv('./data/reserve.csv', encoding='UTF-8')
```

# 型確認/変換
``` py
# 確認
reserve_tb.dtypes
print(type(reserve_tb))

# 変換
reserve_tb['people_num'] = reserve_tb['people_num'].astype('float64')
```
# 列の抽出
```py
# iloc関数による行/列「番号」での指定は、バグの温床になるため避ける。
# ただし、KFoldではilocが使われる。
reserve_tb[['reserve_id','hotel_id','customer_id','reserve_datetime','checkin_date','checkin_time','checkout_date']]
reserve_tb.loc[:, ['reserve_id','hotel_id','customer_id','reserve_datetime','checkin_date','checkin_time','checkout_date']]
```

# 列の削除
```py
# axisを1にすることによって、列の削除を指定
# inplaceをTrueに指定することによって、reserve_tbの書き換えを指定
reserve_tb.drop(['people_num', 'total_price'], axis=1, inplace=True)
```

# 条件指定による行の抽出
```py
reserve_tb.query('"2016-10-13" <= checkout_date <= "2016-10-14"')

# 条件を and でつなげるときは &
# 条件を or でつなげるときは |
# @var_name のように @ のあとに参照したい変数名を書くことによって、Pythonのメモリ上の変数を利用できる。
# query関数は、in のサポートはしていない。

# 顧客IDにサンプリングを行い、抽出した顧客IDに対応するトランザクション（予約）を抽出する

# reserve_tb['customer_id'].unique()は、重複を排除したcustomer_idを返す
# sample関数を利用するためにpandas.Series(pandasのリストオブジェクト)に変換
# sample関数によって、顧客IDをサンプリング
target = pd.Series(reserve_tb['customer_id'].unique()).sample(frac=0.5)

# isin関数によって、customer_idがサンプリングした顧客IDのいずれかに一致した行を抽出
reserve_tb[reserve_tb['customer_id'].isin(target)]
```

# サンプリング
```py
# reserve_tbから50%サンプリング
reserve_tb.sample(frac=0.5)
```

# 集約
```py
 hotel_idごとに集約処理をしたい場合。customer_idごとにユニークカウント処理をしたい場合

# agg関数を利用して、集約処理をまとめて指定
# reserve_idを対象にcount関数を適用
# customer_idを対象にnunique関数を適用
result = reserve_tb \
  .groupby('hotel_id') \
  .agg({'reserve_id': 'count', 'customer_id': 'nunique'})

# reset_index関数によって、列番号を振り直す（inplace=Trueなので、直接resultを更新）
result.reset_index(inplace=True)
result.columns = ['hotel_id', 'rsv_cnt', 'cus_cnt']

# 参考
# やりたいこと：reserve_idとcustomer_idの両方が重複しているレコード数を['dup']列に格納する。
# データフレームの末尾に重複行のカウント数を追加したいとき。transform('count')を用いる

reserve_tb['dup'] = reserve_tb.groupby(['reserve_id','customer_id']).transform('count')

# 集約処理が1つの場合は、agg関数を使わない方が簡潔に書ける。

# 集約単位をhotel_idとpeople_numの組み合わせを指定
# 集約したデータからtotal_priceを取り出し、sum関数に適用することで売上合計金額を算出
result = reserve_tb \
  .groupby(['hotel_id', 'people_num'])['total_price'] \
  .sum().reset_index()

# 売上合計金額の列名がtotal_priceになっているので、price_sumに変更
result.rename(columns={'total_price': 'price_sum'}, inplace=True)
```
groupby -> agg のコンボ

groupby(['store'])['sales'].sum()['total_sales']とすると、店ごとの売り上げを新しいカラム名'total_sales'として作成できる。

# 集計関数の適用[max/min/mean/median/percentile]

```py
# agg関数内では、パーセントタイル値の集計処理を文字列では指定(q=20のこと)できないので、ラムダ式を利用して指定している。

# total_priceを対象にmax/min/mean/median関数を適用
# Pythonのラムダ式をagg関数の集約処理に指定
# ラムダ式にはnumpy.percentileを指定しパーセントタイル値を算出（パーセントは20指定）
result = reserve_tb \
  .groupby('hotel_id') \
  .agg({'total_price': ['max', 'min', 'mean', 'median',
                        lambda x: np.percentile(x, q=20)]}) \
  .reset_index()
result.columns = ['hotel_id', 'price_max', 'price_min', 'price_mean',
                  'price_median', 'price_20per']
```

# 分散、標準偏差
```py
# total_priceに対して、var関数とstd関数を適用し、分散値と標準偏差値を算出
result = reserve_tb \
  .groupby('hotel_id') \
  .agg({'total_price': ['var', 'std']}).reset_index()
result.columns = ['hotel_id', 'price_var', 'price_std']

# データ数が1件だったときは、分散値と標準偏差値がnaになっているので、0に置き換え
# 置き換える範囲は、DataFrame内のすべてのNAなので、関係のない値まで置換されないよう注意
result.fillna(0, inplace=True)
```

# 最頻値の算出(mode関数)
```py
# round関数で四捨五入した後に、mode関数で最頻値を算出
reserve_tb['total_price'].round(-3).mode()
```

# rank関数
``` py
# rank関数で並び替えるために、データ型を文字列からtimestamp型に変換
# （「第10章 日時型」で解説）
reserve_tb['reserve_datetime'] = pd.to_datetime(
  reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S'
)

# log_noを新たな列として追加
# 集約単位の指定はgroup_byを利用
# 顧客ごとにまとめたreserve_datetimeを生成し、rank関数によって順位を生成
# ascendingをTrueにすることで昇順に設定(Falseだと降順に設定)
reserve_tb['log_no'] = reserve_tb \
  .groupby('customer_id')['reserve_datetime'] \
  .rank(ascending=True, method='first')
```

# 結合:merge
```py
# 結合処理前には、できる限りデータを小さくする
pd.merge(reserve_tb.query('people_num == 1'),
         hotel_tb.query('is_business'),
         on='hotel_id', how='inner')
```
## Pandas DataFrameで、ある列の条件に合致する値を書き換える方法
1.loc関数を使う。 df.loc[df['A']<0, 'A'] = -100
2.where関数を使う。 Numpyのwhere関数と異なるので注意!一致しない場合に置換される。
3.replace関数を使う。1つの条件でよい場合（連続値の範囲指定は不可）

## 行の特定の範囲にラベルをつけたい場合
例）時系列データで、DataFrameの前半8割に'train'というラベル、後半2割に'valid'というラベルをつけたいケース。「列は名前、行は添字で指定したい」という場合ちょっと困るのでメモ

```py
df['data_cat'] = 'train' ### 初期化
df.iloc[4*len(df)//5:, df.columns.get_loc('data_cat')] = 'valid' #ilocで行、列ともに添字で指定する。
```

# 時系列シフト結合:shift
```py
# n行シフトして結合したい場合（過去データを特徴量に追加など）
# shift関数:データを上下にn行ずらすことができる関数

# customerごとにreserve_datetimeで並び替え
# groupby関数のあとにapply関数を適用することによって、groupごとに並び替える
# sort_values関数によってデータを並び替え、axisが0の場合は行、1の場合は列を並び替え
result = reserve_tb \
  .groupby('customer_id') \
  .apply(lambda group:
         group.sort_values(by='reserve_datetime', axis=0, inplace=False))

# resultはすでに、customer_idごとにgroup化されている
# customerごとに2つ前のtotal_priceをbefore_priceとして保存
# shift関数は、periodsの引数の数だけデータ行を下にずらす関数
result['before_price'] = \
  pd.Series(result['total_price'].shift(periods=2))
```

## メモ:pythonはWindow関数は不向き
・pythonは、SQLと比較するとコードが長いので、Window関数が必要な処理を行う場合には、SQLを用いた方がよい。
・一定期間の過去データと結合する処理を行う場合には、SQLが圧倒的におすすめ。
・rolling関数は、自身のデータ行を基準にした選択しかできない。

# 時系列データにおけるモデル検証用のデータ分割
```py
from preprocess.load_data.data_loader import load_monthly_index
monthly_index_tb = load_monthly_index()

# 下の行から本書スタート
# train_window_startに、最初の学習データの開始行番号を指定
train_window_start = 1
# train_window_endに、最初の学習データの終了行番号を指定
train_window_end = 24
# horizonに、検証データのデータ数を指定
horizon = 12
# skipにスライドするデータ数を設定
skip = 12

# 年月に基づいてデータを並び替え
monthly_index_tb.sort_values(by='year_month')

while True:
  # 検証データの終了行番号を計算
  test_window_end = train_window_end + horizon

  # 行番号を指定して、元データから学習データを取得
  # train_window_startの部分を1に固定すれば、学習データを増やしていく検証に変更可能
  train = monthly_index_tb[train_window_start:train_window_end]

  # 行番号を指定して、元データから検証データを取得
  test = monthly_index_tb[(train_window_end + 1):test_window_end]

  # 検証データの終了行番号が元データの行数以上になっているか判定
  if test_window_end >= len(monthly_index_tb.index):
    # 全データを対象にした場合終了
    break

  # データをスライドさせる
  train_window_start += skip
  train_window_end += skip

# 交差検定の結果をまとめる
```

# 数値変換
```py
# apply関数
reserve_tb['total_price_log'] = \
  reserve_tb['total_price'].apply(lambda x: np.log(x / 1000 + 1)) 
```


# 外れ値の除去
```py
# メジャーな手法をまとめたパッケージは存在せず、自ら実装する必要がある
reserve_tb = reserve_tb[
  (abs(reserve_tb['total_price'] - np.mean(reserve_tb['total_price'])) /
   np.std(reserve_tb['total_price']) <= 3)
].reset_index()
```

# 日付型(時系列分析では不可避)
```py
# 日付型としてdatetime64[D]という型も指定できるが、datetime64[ns]からdatetime64[D]に変換できないなど不都合なことが多いので、
# datetime64[ns]型に変換したあとに日時要素を取り出す方が便利。

# to_datetime関数で、datetime64[ns]型に変換
pd.to_datetime(reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S')
pd.to_datetime(reserve_tb['checkin_date'] + reserve_tb['checkin_time'],
               format='%Y-%m-%d%H:%M:%S')

# datetime64[ns]型から日付情報を取得
pd.to_datetime(reserve_tb['reserve_datetime'],
               format='%Y-%m-%d %H:%M:%S').dt.date
pd.to_datetime(reserve_tb['checkin_date'], format='%Y-%m-%d').dt.date
```

# 年/月/日/時刻/分/秒/曜日への変換
```py
# reserve_datetimeをdatetime64[ns]型に変換
reserve_tb['reserve_datetime'] = \
  pd.to_datetime(reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S')

# 年を取得
reserve_tb['reserve_datetime'].dt.year

# 月を取得
reserve_tb['reserve_datetime'].dt.month

# 日を取得
reserve_tb['reserve_datetime'].dt.day

# 曜日（0=日曜日、1＝月曜日）を数値で取得
reserve_tb['reserve_datetime'].dt.dayofweek

# 時刻の時を取得
reserve_tb['reserve_datetime'].dt.hour

# 時刻の分を取得
reserve_tb['reserve_datetime'].dt.minute

# 時刻の秒を取得
reserve_tb['reserve_datetime'].dt.second

# 指定したフォーマットの文字列に変換
reserve_tb['reserve_datetime'].dt.strftime('%Y-%m-%d %H:%M:%S')
```

# 日時差の取得
```py
# datetime64[ns]型どうしの引き算

# reserve_datetimeをdatetime64[ns]型に変換
reserve_tb['reserve_datetime'] = \
  pd.to_datetime(reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S')

# checkin_datetimeをdatetime64[ns]型に変換
reserve_tb['checkin_datetime'] = \
  pd.to_datetime(reserve_tb['checkin_date'] + reserve_tb['checkin_time'],
                 format='%Y-%m-%d%H:%M:%S')

# 年の差分を計算（月以下の日時要素は考慮しない）
reserve_tb['reserve_datetime'].dt.year - \
reserve_tb['checkin_datetime'].dt.year

# 月の差分を取得（日以下の日時要素は考慮しない）
(reserve_tb['reserve_datetime'].dt.year * 12 +
 reserve_tb['reserve_datetime'].dt.month) \
 - (reserve_tb['checkin_datetime'].dt.year * 12 +
    reserve_tb['checkin_datetime'].dt.month)

# 日単位で差分を計算
(reserve_tb['reserve_datetime'] - reserve_tb['checkin_datetime']) \
  .astype('timedelta64[D]')

# 時単位で差分を計算
(reserve_tb['reserve_datetime'] - reserve_tb['checkin_datetime']) \
  .astype('timedelta64[h]')

# 分単位で差分を計算
(reserve_tb['reserve_datetime'] - reserve_tb['checkin_datetime']) \
  .astype('timedelta64[m]')

# 秒単位で差分を計算
(reserve_tb['reserve_datetime'] - reserve_tb['checkin_datetime']) \
  .astype('timedelta64[s]')
```

```py
# メモ
# timedelta64[D/h/m/s]型によって差分を日/時/分/秒単位に変換した場合、小数点以下は「切り上げ」た結果が返ってくる（SQLやRとは異なる）
# たとえば、差分が2日と3時間の時に日単位に変換すると、3(日)が返ってくる

# ???
# pandasのサイトを見ても、切り捨てられているように見える。
# https://pandas.pydata.org/docs/user_guide/timedeltas.html
```

# 日付型の増減
```py
# timedelta用にdatetimeライブラリを読み込み
import datetime

# reserve_datetimeをdatetime64[ns]型に変換
reserve_tb['reserve_datetime'] = \
  pd.to_datetime(reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S')

# reserve_datetimeからdateを抽出
reserve_tb['reserve_date'] = reserve_tb['reserve_datetime'].dt.date

# reserve_datetimeに1日加える
reserve_tb['reserve_datetime'] + datetime.timedelta(days=1)

# reserve_dateに1日加える
reserve_tb['reserve_date'] + datetime.timedelta(days=1)

# reserve_datetimeに1時間加える
reserve_tb['reserve_datetime'] + datetime.timedelta(hours=1)

# reserve_datetimeに1分加える
reserve_tb['reserve_datetime'] + datetime.timedelta(minutes=1)

# reserve_datetimeに1秒加える
reserve_tb['reserve_datetime'] + datetime.timedelta(seconds=1)
```

# 季節への変換：変換関数を定義して、applyする。
```py
# reserve_datetimeをdatetime64[ns]型に変換
reserve_tb['reserve_datetime'] = pd.to_datetime(
  reserve_tb['reserve_datetime'], format='%Y-%m-%d %H:%M:%S'
)

# 月の数字を季節に変換する関数
def to_season(month_num):
  season = 'winter'
  if 3 <= month_num <= 5:
    season = 'spring'
  elif 6 <= month_num <= 8:
    season = 'summer'
  elif 9 <= month_num <= 11:
    season = 'autumn'
  
  return season

# 季節に変換
reserve_tb['reserve_season'] = pd.Categorical(
  reserve_tb['reserve_datetime'].dt.month.apply(to_season),
  categories=['spring', 'summer', 'autumn', 'winter']
)
```

# 参考
https://pandas.pydata.org/docs/user_guide/timedeltas.html