実務で使えそうなPandas関連操作の自分用メモ

# 0. はじめに
最近、[データサイエンス100本ノック（構造化データ加工編）](https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess)を一通り解いてPandasの勉強をしました。
まずは答えを見ずにわからない箇所は自分で調べて全問解いた後、答え合わせをして勉強したのですが、今回の勉強を通じて、普段実務で使えそうだと思った処理を自分用にまとめました。

※あくまで自分用のまとめですので、出力結果や説明は割愛しており、以下の順番も100本ノックの順と無関係です。模範解答にはない処理もあります。

# 1. データセットの読み込み〜前処理
## ファイル入出力
```
# ファイル読み込み。文字コードはUTF-8、Tab区切り
df = pd.read_csv('data/d.csv', header=0, encoding='utf-8', sep='\t')
```
```
# ファイル書き込み。ヘッダーあり、文字コードはUTF-8、カンマ区切り
df.to_csv('data/d.csv', header=True, encoding='utf-8', sep=',')
```
```
# ファイル書き込み。ヘッダーなし、文字コードは文字コードはCP932、Tab区切り
df.to_csv('data/d.csv', header=False, encoding='cp932', sep='\t') 
```

## データフレームのコピー
```
df2 = df.copy()
```

## キーを使ったデータフレームの結合
```
pd.merge(df_receipt, df_store[['store_cd', 'store_name']], on='store_cd', how='inner')
#inner:内部結合, left:左外部結合, right:右外部結合, outer:完全外部結合
```

## データフレームの連結
```
pd.concat([df1, df2], axis=1)
#mergeと違い、リストで渡す。axis=0とすれば縦方向、axis=1とすれば横方向に結合される。
```

## データフレームの件数（行数）
```
len(df)
```

## ユニーク数
```
len(df['id'].unique())
```

## ユニークな要素の値とその出現回数
```
df['id'].value_counts()
```

## 欠損値の処理
```
# 各列の欠損数の確認
df.isnull().sum()
```

```
# 欠損値が一つでも含まれるレコードを削除
df.dropna()
```

```
# fillnaは辞書型で一度に指定できる
df.fillna({'price': mean(df['price']),
           'cost': median(df['cost'])}) 
```

## 重複の削除
```
#subsetで対象とする列を設定して重複削除
df.drop_duplicates(subset=['name', 'cd'], keep='first', inplace=True)
```

## 列名の変更
```
# renameで任意の列名を変更
df.rename(columns={'ymd':'sales_date'}, inplace=True)
```
```
# リストで直接書き換える
df.columns = ['category', 'price', 'cost']
```

## 型キャスト
```
# 文字列型に変換
df['sales_date'].astype(str)
```
```
# True, Falseを1, 0に変換する
(df['sales']>2000).astype(int)
```

## 値の置換
```
code_dict = {
    'A': 11,
    'B': 22,
    'C': 33,
    'D': 44
}
df['name'].replace(code_dict) #一致しないときはそのまま
df['name'].map(code_dict) #一致しないときはNAN
```

## 条件を満たす行にアクセスし、値を代入
```
df.loc[df['age10']=='60代', 'age10'] = '60代以上'
```

## 条件を満たすか否かの0-1フラグを立てる
```
(df['sales'] != 0).apply(int) #salesが0でなければ1のフラグが立つ
```

## 列に関数を適用して変換
```
# 四捨五入する
df['price'].apply(lambda x: np.round(x))
```
```
# 1.1倍して、小数点以下を切り捨てる
df['price'].apply(lambda x: np.floor(x * 1.1))
```
```
# 常用対数化（底=10）する
df['sales'].apply(lambda x: math.log10(x))
```

## 複数の列に関数を適用して変換
```
# 2つの列の月の差を出す
df[['end_date', 'start_date']].apply(\
    lambda x: relativedelta(x[0], x[1]).years * 12 + relativedelta(x[0], x[1]).months, axis=1)
```
```
# 欠損値ならx[1]（メジアン）。そうでなければそのまま
df[['price', 'median_price']].apply(\
    lambda x: np.round(x[1]) if np.isnan(x[0]) else x[0], axis=1)
```

## 値を標準化（平均0、標準偏差1）
```
# 1行で書く場合
df['sales_ss'] = preprocessing.scale(df['sales'])
```
```
from sklearn import preprocessing
scaler = preprocessing.StandardScaler()
scaler.fit(customer[['sales']])
customer['sales_ss'] = scaler.transform(customer[['sales']])
```

## 値を正規化（最小値0、最大値1）
```
from sklearn import preprocessing
scaler = preprocessing.MinMaxScaler()
scaler.fit(customer[['sales']])
customer['sales_mm'] = scaler.transform(customer[['sales']])
```

## ダミー変数を作成
```
pd.get_dummies(df, columns=['cd'])
# columnsを指定すると特定の列だけ適用でき、object型でなくてもダミー化できる
```

## 文字列操作
```
# 先頭3文字の抽出
df['name'].str[0:3]
```
```
# 文字列の連結
df['gender'].str.cat((df['age10']).astype(str)) # gender列とage10列の文字列を結合する
```

## 数値条件による行の抽出
```
# queryメソッドによるデータ抽出。複数条件を指定するときにシンプルに書ける
df[['sales_date', 'id', 'cd', 'quantity', 'amount']]\
    .query('id == "XXXX" & (amount >= 1000 | quantity >=5)')
```
```
# （参考）queryメソッドを使わない場合、条件が複雑だと煩雑になる
target = (df['id']=="XXXX") & ((df['amount']>=1000) | (df['quantity']>=5))
df[target][['sales_date', 'id', 'cd', 'quantity', 'amount']]
```

## 文字列条件による行の抽出
```
# "SASS" から始まる行を抽出
df['store'].str.startswith('SASS')
```
```
# 1 で終わる行を抽出
df['id'].str.endswith('1')
```
```
# 札幌　を含む行を抽出
df['address'].str.contains('札幌')
```

```
# 正規表現を使った判定
df['cd'].str.contains('^[A-D]', regex=True) #A〜Dのいずれかから始まる
df['cd'].str.contains('[1-9]$', regex=True) #1〜9のいずれかで終わる
df['cd'].str.contains('^[A-D].*[1-9]$', regex=True) #A〜Dのいずれかから始まり、かつ、1〜9のいずれかで終わる
df['tel'].str.contains('^[0-9]{3}-[0-9]{3}-[0-9]{4}', regex=True) #電話番号が3桁-3桁-4桁
```

# 2. 集計
## ソート
```
# sales列を基準に降順ソート
df.sort_values('sales', ascending=True).head(10) #Falseにすれば昇順
```
```
# 複数列を基準にソート。また各列それぞれ降順、昇順を指定できる
df.sort_values(['sales', 'id'], ascending=[False, True])
```
```
# sales が多い順（ascending=False）にランクを降る。method='min'とすると値が同じ時は同じ数字が振られる
df['sales'].rank(method='min', ascending=False)
```

## groupbyによる集計
```
# 集計する列ごとに適用する集計関数を選ぶ
df.groupby('id', as_index=False).agg({'amount':'sum', 'quantity':'sum'}) 
```
```
# 1つの列に複数の集計を適用
df.groupby('id', as_index=False).agg({'ymd':['max', 'min']})
```
```
# 任意の関数（ここではpd.Series.mode）を指定
df.groupby('id', as_index=False).agg({'cd': pd.Series.mode})
```
```
# 無名関数lambdaを指定
df.groupby('id', as_index=False).agg({'amount': lambda x: np.var(x)})
```

```
# agg関数を使わない書き方も可能
df.groupby('id', as_index=False).max()[['id', 'ymd']]
df.groupby('id', as_index=False).sum()[['id', 'amount', 'quantity']]
```

```
# （参考）groupbyによる最頻値（モード）を集計する場合agg関数を使う
df.groupby('id', as_index=False).mode() #エラーが出る
```

## クロス集計
```
pd.pivot_table(df, index='age10', columns='gender', values='amount', aggfunc='sum')
#index:表側, columns:表頭, values: 対象とする値, aggfunc: 集計方法）
```
```
pd.pivot_table(sales, index='id', columns='year', values='amount', aggfunc='sum', margins=True) 
#margins=True　とすれば、総計・小計を出せる
```

## 4分位点の算出
```
df.quantile([0, 0.25, 0.5, 0.75, 1.0])[['amount']]
```


# 3. 日時関連の処理
## 時間変数の変換
```
# 文字列 → datetime型
pd.to_datetime(df['str_date'])
```
```
# エポック秒 → datetime型
pd.to_datetime(df['epoch'], unit='s')
```
```
# datetime型 → エポック秒
df['date'].astype(np.int64) // 10**9
```
※参考：https://stackoverflow.com/questions/15203623/convert-pandas-datetimeindex-to-unix-time

```
# datetime型 → 文字列　%Y:年4桁, %m:月2桁, %d:日2桁。 ※%大文字と小文字で意味が違うことに留意。たとえば%Mは分を意味する
df['date'].dt.strftime('%Y%m%d')
```

## datitime変数から年・月・日の情報を取り出す
```
# 年情報を取り出す。monthで月、dayで日付を取り出せる
t.dt.year
```
```
# 0埋め2桁の文字列を得るにはstrftimeを使う
t.dt.strftime('%d')
```

## 時間変数の差分
```
# 日数差を出すときはdatetime型を単に引く
df['end_date'] - df['start_date']
```
```
# 月の差を出すときは、relativedeltaを使う
relativedelta(x0, x1).years * 12 + relativedelta(x0, x1).months
```

## 曜日の処理
```
# weekday関数で曜日を数字（月曜日からの日数）で出力
df['月曜からの経過日数'] = df['ymd'].apply(lambda x: x.weekday())
```
```
# その日の週の月曜日の日付を得る
df['当該週の月曜日'] = df['ymd'].apply(lambda x: x - relativedelta(days=x.weekday()))
```

## 時系列の処理
```
# 1時点前との差分
df['sales'].diff(1)
```
```
# 1時点前との変化率
df['sales'].pct_change(1)
```
```
# 1時点前の値
df['sales'].shift(1)
```
```
# 2時点前の値
df['sales'].shift(2)
```

# 4. サンプリング
## ランダムサンプリング
```
# ランダムで10%のデータをサンプリング
df.sample(frac=0.1, random_state=5) 
```
```
# 性別（gender）の割合に基づきランダムに10%のデータを層化抽出
_, test = train_test_split(df, test_size=0.1, stratify=df['gender'], random_state=5)
```
```
# 学習データとテストデータに8:2の割合で分割する
df_train, df_test = train_test_split(df, test_size=0.2, random_state=5) 
```
```
# 1:1となるようにアンダーサンプリングする
r = RandomUnderSampler(random_state=5)
df_sample, _ = r.fit_sample(df, df['flag'])
```

## 参考
勉強するにあたって、以下を参考にさせていただきました。ありがとうございました。
- [データサイエンス100本ノック（構造化データ加工編）](https://github.com/The-Japan-DataScientist-Society/100knocks-preprocess)
- [note.nkmk.me](https://note.nkmk.me/python-pandas-post-summary/)