0. はじめに
最近、データサイエンス100本ノック(構造化データ加工編)を一通り解いて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'])
参考
勉強するにあたって、以下を参考にさせていただきました。ありがとうございました。