著者のgithubのコードを都度さまよっていたので、すぐ使えるようまとめておく。
元ネタ
前処理大全[データ分析のためのSQL/R/Python実践テクニック]
以下githubよりPython部分を抽出。詳しい解説は書籍参照。
また、これらのソースコードは BSD 3 条項でライセンスされています。
BSD 3-Clause License
Copyright (c) 2018, Tomomitsu Motohashi
All rights reserved.
データ読み込み
# ライブラリより読み込み
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')
型確認/変換
# 確認
reserve_tb.dtypes
print(type(reserve_tb))
# 変換
reserve_tb['people_num'] = reserve_tb['people_num'].astype('float64')
列の抽出
# 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']]
列の削除
# axisを1にすることによって、列の削除を指定
# inplaceをTrueに指定することによって、reserve_tbの書き換えを指定
reserve_tb.drop(['people_num', 'total_price'], axis=1, inplace=True)
条件指定による行の抽出
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)]
サンプリング
# reserve_tbから50%サンプリング
reserve_tb.sample(frac=0.5)
集約
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]
# 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']
分散、標準偏差
# 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関数)
# round関数で四捨五入した後に、mode関数で最頻値を算出
reserve_tb['total_price'].round(-3).mode()
rank関数
# 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
# 結合処理前には、できる限りデータを小さくする
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'というラベルをつけたいケース。「列は名前、行は添字で指定したい」という場合ちょっと困るのでメモ
df['data_cat'] = 'train' ### 初期化
df.iloc[4*len(df)//5:, df.columns.get_loc('data_cat')] = 'valid' #ilocで行、列ともに添字で指定する。
時系列シフト結合:shift
# 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関数は、自身のデータ行を基準にした選択しかできない。
時系列データにおけるモデル検証用のデータ分割
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
# 交差検定の結果をまとめる
数値変換
# apply関数
reserve_tb['total_price_log'] = \
reserve_tb['total_price'].apply(lambda x: np.log(x / 1000 + 1))
外れ値の除去
# メジャーな手法をまとめたパッケージは存在せず、自ら実装する必要がある
reserve_tb = reserve_tb[
(abs(reserve_tb['total_price'] - np.mean(reserve_tb['total_price'])) /
np.std(reserve_tb['total_price']) <= 3)
].reset_index()
日付型(時系列分析では不可避)
# 日付型として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
年/月/日/時刻/分/秒/曜日への変換
# 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')
日時差の取得
# 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]')
# メモ
# timedelta64[D/h/m/s]型によって差分を日/時/分/秒単位に変換した場合、小数点以下は「切り上げ」た結果が返ってくる(SQLやRとは異なる)
# たとえば、差分が2日と3時間の時に日単位に変換すると、3(日)が返ってくる
# ???
# pandasのサイトを見ても、切り捨てられているように見える。
# https://pandas.pydata.org/docs/user_guide/timedeltas.html
日付型の増減
# 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する。
# 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']
)
参考