LoginSignup
92
122

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-09

著者の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']
)

参考

92
122
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
92
122