Pythonを使ったデータ加工 〜Pandasによる主要な前処理〜

スポンサーリンク


データの取得から担当者への結果報告というデータ分析の一連のプロセスで最も時間がかかるのはデータの前処理です。平均や標準偏差などの何かしらのデータ集計を行うにしろ、機械学習モデルを作成するにしろ、それらを行う前にデータの前処理が悠然と壁になっています。データの前処理をスムーズに行えるようになることで、分析作業の効率が格段に上がるでしょう。

Pythonを使ってデータの前処理を行う際に万人が使うのはPandasです。Pandasの扱い方に関してはすでにいくつも資料がありますが、実用で使用する主要な処理に関してまとめてあるものはあまりないので、独自の視点で重宝する処理方法を取り上げました。特に、効率的ににデータフレームを扱うために、処理を連続して行える方法を重視しています。今回のコードはこちらです。
 


 

必要なモジュールのインポート


今回はPandasとNumpyだけを使います。

import pandas as pd
import numpy as np


データの作成


今回前処理の対象となるデータは、以下のように自作したものです。前処理結果がわかりやすいようにいたずらに行数が多くならないようにしています。

data = pd.DataFrame({
    'cate1': ['a', 'a', 'a', 'b', 'b', 'c'],
    'cate2': ['A', 'B', 'B', 'C', 'C', 'C'],
    'value1': [1, 0, 1, 0, 1, 0],
    'value2': [10, 3, -4, -1, 0, 1]
})
data
cate1 cate2 value1 value2
0 a A 1 10
1 a B 0 3
2 a B 1 -4
3 b C 0 -1
4 b C 1 0
5 c C 0 1


列の追加/演算


データ処理において最も基本と言って良い、演算や、その結果を新しい列として追加するなどの処理を行います。 下の例では、新しい列名を 「value_new」 としているので、演算後の結果が新たな列になっていますが、既存の列名にするとその列の値が演算後の結果になります。

target_data = data.assign(
    value_new=lambda x: x.value1 + x.value2
)
target_data[['value1', 'value2', 'value_new']]
value1 value2 value_new
0 1 10 11
1 0 3 3
2 1 -4 -3
3 0 -1 -1
4 1 0 1
5 0 1 1


また、「assign」関数は処理結果がデータフレームであるため、連続して処理を行えます。

target_data = data.assign(
    value_new=lambda x: x.value1 + x.value2
).assign(value_new=lambda x: x.value_new + 5)
target_data[['value1', 'value2', 'value_new']]
value1 value2 value_new
0 1 10 16
1 0 3 8
2 1 -4 2
3 0 -1 4
4 1 0 6
5 0 1 6


IFELSE


条件によって列の値を変えたいときに使うのがIFELSE処理です。 「assign」 の内部のlambdaで指定した 「x.value2」 はベクトル(Series)なので、ベクトル全体に対する処理は行なえますが、値一つ一つに対しての処理は行なえません。そこで、 下の例では「map」を使って値一つ一つに対して処理を行っており、値が0より大きい場合は1、それ以外の場合は0としています。

target_data = data.assign(
    value_new=lambda x: x.value2.map(lambda y: 1 if y > 0 else 0)
)
target_data[['value2', 'value_new']]
value2 value_new
0 10 1
1 3 1
2 -4 0
3 -1 0
4 0 0
5 1 1


複数列によるIFELSE


先程のやり方では、一つの列に対してIFELSE処理を行えますが、複数列を使った条件式のIFELSE処理は行なえません。これを行うために、「apply」を使います。

target_data = data.copy()
target_data['value_new'] = target_data.apply(
    lambda x: x['cate2'] if x['cate1'] == 'a' and x['value2'] > 0 else '◯',
    axis=1
)
target_data[['cate1', 'cate2', 'value2', 'value_new']]
cate1 cate2 value2 value_new
0 a A 10 A
1 a B 3 B
2 a B -4
3 b C -1
4 b C 0
5 c C 1


複数列によるIFELSE(高速化)


先程のやり方で複数列を条件としたIFELSE処理はできましたが、データ数が多くなったときに処理時間がかかります。この処理を高速化するためにNumpyを使います。

target_data = data.copy()
target_data['value_new'] = np.where(
    (target_data['cate1'].values == 'a') * (target_data['value2'].values > 0),
    target_data['cate2'].values,
    '◯'
)
target_data[['cate1', 'cate2', 'value2', 'value_new']]
cate1 cate2 value2 value_new
0 a A 10 A
1 a B 3 B
2 a B -4
3 b C -1
4 b C 0
5 c C 1


データフレームで指定した列を 「values」 でNumpyに変換し、Numpyのarray型(ベクトル)で条件式の処理をしています。AND式を行うためにBoolean型のベクトルの掛け算をしています。また、OR式の場合は足し算です。

(data['cate1'].values == 'a') * (data['value2'].values > 0)
array([ True,  True, False, False, False, False])


高速化の検証


%%timeit
target_data['value_new'] = data.apply(
    lambda x: x['cate2'] if x['cate1'] == 'a' and x['value2'] > 0 else '◯',
    axis=1
)
586 µs ± 21.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%%timeit
target_data['value_new'] = np.where(
    (data['cate1'].values == 'a') * (data['value2'].values > 0),
    data['cate2'].values,
    '◯'
)
 73.6 µs ± 2.99 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

平均値ではだいたい8倍ほど速くなっており、標準偏差も小さく安定しています。

フィルター

「query」関数を使って指定した条件に当てはまる行を抽出できます。

target_data = data.query('value1 > 0', engine='python')
target_data
cate1 cate2 value1 value2
0 a A 1 10
2 a B 1 -4
4 b C 1 0


target_data = data.query('cate1 == "a"', engine='python')
target_data
cate1 cate2 value1 value2
0 a A 1 10
1 a B 0 3
2 a B 1 -4


Nullを条件とした抽出


「isnull」 関数を使いNaNの行を抽出し、「notnull」 関数を使ってNaN以外の行を抽出することができます。また、NaNの行の削除は「dropna」 関数を使ってもできます。

target_data = data.assign(
    value1=lambda x: x.value1.map(lambda y: np.NaN if y > 0 else y)
).query('value1.isnull()', engine='python')
target_data
cate1 cate2 value1 value2
0 a A NaN 10
2 a B NaN -4
4 b C NaN 0


target_data = data.assign(
    value1=lambda x: x.value1.map(lambda y: np.NaN if y > 0 else y)
).query('value1.notnull()', engine='python')
target_data
cate1 cate2 value1 value2
1 a B 0.0 3
3 b C 0.0 -1
5 c C 0.0 1


target_data = data.assign(
    value1=lambda x: x.value1.map(lambda y: np.NaN if y > 0 else y)
).dropna(subset=['value1'], axis=0)
target_data
cate1 cate2 value1 value2
1 a B 0.0 3
3 b C 0.0 -1
5 c C 0.0 1


指定した文字を含む行を抽出


「query」関数では文字列の条件として、指定した文字列を含んでいているかどうかも扱えます。下の例では、文字列「C」を含む行を取り出しています。

target_data = data.copy()
target_data['cate_new'] = target_data.apply(
    lambda x: x['cate1'] + x['cate2'], axis=1
)
target_data = target_data.query('cate_new.str.contains("C")', engine='python')
target_data
cate1 cate2 value1 value2 cate_new
3 b C 0 -1 bC
4 b C 1 0 bC
5 c C 0 1 cC


Grouping


統計演算を行う際に必ず出てくるのがGroupingです。これに慣れれば、どんな指標もサクッと作れます。 下の例では、「cate1」 と 「cate2」 の2つの列を合わせた組み合わせが同じ行において、「value2」 の平均値を算出しています。 「mean」 関数を変えれば、異なる集計ができます。

target_data = data.groupby(['cate1', 'cate2'])['value2'].mean().reset_index()
target_data
cate1 cate2 value2
0 a A 10.0
1 a B -0.5
2 b C -0.5
3 c C 1.0


同一列に複数演算


Groupingをする際に一つの列に対して複数の統計処理を行いたいときがあります。これは、「agg」によりできます。
下の例では、「value2」 に対して、件数、平均値、標準偏差を計算しています。

target_data = data.groupby(['cate1', 'cate2']).agg({
    'value2': ['count', 'mean', 'std']
}).reset_index()
target_data
cate1 cate2 value2
count mean std
0 a A 1 10.0 NaN
1 a B 2 -0.5 4.949747
2 b C 2 -0.5 0.707107
3 c C 1 1.0 NaN


自作関数


自作した関数を使ってのGroupingもできます。先程のGroupingでは、件数が1件の条件に関して、標準偏差がNaNになってしまいました。NaNの場合には0とするという関数を作って、その関数を使ってGroupingします。

def std_fillna(x):
    return np.nan_to_num(np.std(x, ddof=1), 0)

target_data = data.groupby(['cate1', 'cate2']).agg({
    'value2': ['count', 'mean', std_fillna]
}).reset_index()
target_data
cate1 cate2 value2
count mean std_fillna
0 a A 1 10.0 0.000000
1 a B 2 -0.5 4.949747
2 b C 2 -0.5 0.707107
3 c C 1 1.0 0.000000


SummarizeせずにGrouping結果列に追加


先程の例では、Groupingに指定した変数の重複でまとめらましたが、Groupingによる結果を新たな列として元のデータに追加したい場合があります。これは「transorm」により可能です。
下の例では、指定したGroupingの件数を元ののデータに新しい列として追加しています。

target_data = data.copy()
target_data['count'] = target_data.groupby(['cate1', 'cate2'])['value2'].transform('count')
target_data
cate1 cate2 value1 value2 count
0 a A 1 10 1
1 a B 0 3 2
2 a B 1 -4 2
3 b C 0 -1 2
4 b C 1 0 2
5 c C 0 1 1


また、指定したGroup単位で順番に番号を付与するといったこともできます。

target_data = data.assign(number=1).copy()
target_data['number'] = target_data.groupby(['cate1', 'cate2'])['number'].transform('cumsum')
target_data
cate1 cate2 value1 value2 number
0 a A 1 10 1
1 a B 0 3 1
2 a B 1 -4 2
3 b C 0 -1 1
4 b C 1 0 2
5 c C 0 1 1


重複削除


「drop_duplicates」関数を使って指定した列で重複している値の行を消せます。

target_data = data.drop_duplicates(['cate1', 'cate2'])[['cate1', 'cate2']]
target_data
cate1 cate2
0 a A
1 a B
3 b C
5 c C


ソート


「sort_values」を使って指定した列の値による並び替えができます。 下の例では、先に 「value1」 で昇順、次に 「value2」 で降順に並び替えています。「sort_values」 内の 「ascending」 で昇順か降順を指定しています。

target_data = data.sort_values(['value1', 'value2'], ascending=[True, False])
target_data
cate1 cate2 value1 value2
1 a B 0 3
5 c C 0 1
3 b C 0 -1
0 a A 1 10
4 b C 1 0
2 a B 1 -4


ソートしてから重複削除の処理は多用します。

target_data = data.sort_values(
    ['value1', 'value2'], ascending=[True, False]
).drop_duplicates(['cate1', 'cate2'])
target_data
cate1 cate2 value1 value2
1 a B 0 3
5 c C 0 1
3 b C 0 -1
0 a A 1 10


列名の変更


「rename」関数により列名を変更できます。

target_data = data.rename(columns={'cate1': 'cate_new', 'value1': 'value_new'})
target_data
cate_new cate2 value_new value2
0 a A 1 10
1 a B 0 3
2 a B 1 -4
3 b C 0 -1
4 b C 1 0
5 c C 0 1


先程、Groupingしたときに列名がmulti_indexになってしまい、データフレームとしては扱いづらくなってしまいました。列名を書き換えることで、扱いやすい形に戻します。

target_data = data.groupby(['cate1', 'cate2']).agg({
    'value2': ['count', 'mean', 'std']
})
target_data.columns
MultiIndex([('value2', 'count'),
            ('value2',  'mean'),
            ('value2',   'std')],
           )


target_data.columns = list(map(
    lambda x: '{}_{}'.format(x[0], x[1]), target_data.columns
))
target_data.reset_index(inplace=True, drop=False)
target_data
cate1 cate2 value2_count value2_mean value2_std
0 a A 1 10.0 NaN
1 a B 2 -0.5 4.949747
2 b C 2 -0.5 0.707107
3 c C 1 1.0 NaN


target_data.columns
Index(['cate1', 'cate2', 'value2_count', 'value2_mean', 'value2_std'], dtype='object')

列名がmulti_indexではなくなり、データが通常の形のデータフレーム型に戻っています。

値の置換


「replace」関数により値の置換ができます。辞書型で置換元と置換先の値を指定します。

target_data = data.assign(
    cate_new=lambda x: x.cate1.replace({'a': '○'}),
    value_new=lambda x: x.value1.replace({1: -1})
)
target_data
cate1 cate2 value1 value2 cate_new value_new
0 a A 1 10 -1
1 a B 0 3 0
2 a B 1 -4 -1
3 b C 0 -1 b 0
4 b C 1 0 b -1
5 c C 0 1 c 0


NaNの置換


「fillna」関数によりNaNを置換できます。

target_data = data.assign(
    value_new=lambda x: x.value2.map(lambda y: np.NaN if y >= 0 else y)
).assign(value_new=lambda x: x.value_new.fillna(0))
target_data[['value2', 'value_new']]
value2 value_new
0 10 0.0
1 3 0.0
2 -4 -4.0
3 -1 -1.0
4 0 0.0
5 1 0.0


複数列のNaNの置換が必要なときは一気にやると楽です。

target_data = data.assign(
    value_new1=lambda x: x.value2.map(lambda y: np.NaN if y >= 0 else y),
    value_new2=lambda x: x.value2.map(lambda y: np.NaN if y < 0 else y),
)
target_data.fillna(0, inplace=True)
target_data[['value2', 'value_new1', 'value_new2']]
value2 value_new1 value_new2
0 10 0.0 10.0
1 3 0.0 3.0
2 -4 -4.0 0.0
3 -1 -1.0 0.0
4 0 0.0 0.0
5 1 0.0 1.0


結合


「merge」関数を使って、異なるデータを列の値を基準としてつなぎ合わるといった結合処理ができます。
下の例では、元のデータに、「cate1」の値ごとの件数のデータを結合しています。結合方法は「inner」であり、結合キーの列の値が両方のデータにある行だけ結合されます。

target_data = pd.merge(
    data[['cate1', 'cate2', 'value1']],
    data.groupby('cate1')['value1'].count().reset_index().rename(
        columns={'value1': 'count'}
    ),
    on='cate1', how='inner'
)
target_data
cate1 cate2 value1 count
0 a A 1 3
1 a B 0 3
2 a B 1 3
3 b C 0 2
4 b C 1 2
5 c C 0 1


結合方法を「left」にすると、指定したキーの列の値が結合先にある場合だけ結合されます。

target_data = pd.merge(
    data[['cate1', 'cate2', 'value1']],
    data.query('cate1 == "a"').assign(flag=1)[['cate1', 'flag']].drop_duplicates('cate1'),
    on='cate1', how='left'
)
target_data
cate1 cate2 value1 flag
0 a A 1 1.0
1 a B 0 1.0
2 a B 1 1.0
3 b C 0 NaN
4 b C 1 NaN
5 c C 0 NaN


semi_join


「semi_join」 は、結合されるデータから行を抽出する際に、結合するデータの指定した列の値が同じ行だけを抽出する処理です。
R言語のdplyrにはsemi_joinはありますが、Pandasにはありません(たぶん)。なので、以下のように自作しました。

def semi_join(data1, data2, by):

    if isinstance(by, str):
        by = [by]

    return pd.merge(data2[by].drop_duplicates(), data1, how='inner', on=by)

target_data = semi_join(
    data,
    data.query('cate1 == "a"'),
    by='cate1'
)
target_data
cate1 cate2 value1 value2
0 a A 1 10
1 a B 0 3
2 a B 1 -4


anti_join

「anti_join」 は、結合されるデータから行を抽出する際に、結合するデータの指定した列の値が異なる行だけを抽出する処理です。

def anti_join(data1, data2, by):

    joined_data = data1.copy()
    target_data = data2.copy()
    target_data['flag_tmp'] = 1

    if isinstance(by, str):
        by = [by]

    joined_data = pd.merge(
        joined_data, target_data[by + ['flag_tmp']].drop_duplicates(),
        on=by, how='left'
    ).query('flag_tmp.isnull()', engine='python').drop(
        columns='flag_tmp'
    ).reset_index(drop=True)

    return joined_data

target_data = anti_join(
    data,
    data.query('cate1 == "a"'),
    by='cate1'
)
target_data
cate1 cate2 value1 value2
0 b C 0 -1
1 b C 1 0
2 c C 0 1


連結


結合とは異なり、データを縦や横に繋げる場合があります。
「concat」関数を使い連結し、縦横の指定は引数の「axis」でできます。

縦の結合


target_data = pd.concat([data, data], axis=0).reset_index(drop=True)
target_data
cate1 cate2 value1 value2
0 a A 1 10
1 a B 0 3
2 a B 1 -4
3 b C 0 -1
4 b C 1 0
5 c C 0 1
6 a A 1 10
7 a B 0 3
8 a B 1 -4
9 b C 0 -1
10 b C 1 0
11 c C 0 1


横の結合


target_data = pd.concat([
    data,
    data.rename(columns={'cate1': 'cate_new', 'value1': 'value_new'})[['cate_new', 'value_new']]
], axis=1)
target_data
cate1 cate2 value1 value2 cate_new value_new
0 a A 1 10 a 1
1 a B 0 3 a 0
2 a B 1 -4 a 1
3 b C 0 -1 b 0
4 b C 1 0 b 1
5 c C 0 1 c 0


縦横変形


グラフで扱いやすいデータにするために、複数の列を一つの列にまとめる縦変形や、機械学習モデルに読み込ませるための横変形などがあり、これはよく行います。

縦変形


「stack」関数を使って縦長に変形させます。これにより「seaborn」でグラフ化しやすくなります。

target_data = data.assign(id=1).assign(
    id=lambda x: x.id.cumsum()
).set_index(['id', 'cate1', 'cate2']).stack().reset_index()
target_data.columns = ['id', 'cate1', 'cate2', 'variable', 'value']
target_data
id cate1 cate2 variable value
0 1 a A value1 1
1 1 a A value2 10
2 2 a B value1 0
3 2 a B value2 3
4 3 a B value1 1
5 3 a B value2 -4
6 4 b C value1 0
7 4 b C value2 -1
8 5 b C value1 1
9 5 b C value2 0
10 6 c C value1 0
11 6 c C value2 1


横変形


「pivot_table」関数により横長に変形できます。

stacked_data = data.assign(id=1).assign(
    id=lambda x: x.id.cumsum()
).set_index(['id', 'cate1', 'cate2']).stack().reset_index()
stacked_data.columns = ['id', 'cate1', 'cate2', 'variable', 'value']

target_data = pd.pivot_table(
    data=stacked_data,
    index=['id', 'cate1', 'cate2'],
    columns='variable'
)
target_data.columns = list(map(lambda x: x[1], target_data.columns))
target_data.reset_index(inplace=True, drop=False)
target_data
id cate1 cate2 value1 value2
0 1 a A 1 10
1 2 a B 0 3
2 3 a B 1 -4
3 4 b C 0 -1
4 5 b C 1 0
5 6 c C 0 1


文字列を値とする横変形


「pivot_table」 は横変形するときに値を平均化するのがデフォルトの挙動です。なので、文字列を値とするとエラーになります。

stacked_data = data.assign(id=1).assign(
    id=lambda x: x.id.cumsum()
).set_index(['id', 'cate1', 'cate2']).stack().reset_index()
stacked_data.columns = ['id', 'cate1', 'cate2', 'variable', 'value']

target_data = pd.pivot_table(
    data=stacked_data[['id', 'cate1', 'cate2']].drop_duplicates(),
    index='id',
    columns='cate1',
    fill_value=''
)
target_data.columns = list(map(lambda x: x[1], target_data.columns))
target_data.reset_index(inplace=True, drop=False)
target_data
/usr/local/lib/python3.8/site-packages/pandas/core/groupby/generic.py in _cython_agg_blocks(self, how, alt, numeric_only, min_count)
   1119
   1120         if not len(new_mgr):
-> 1121             raise DataError("No numeric types to aggregate")
   1122
   1123         return new_mgr


DataError: No numeric types to aggregate


 これは、「pivot_table」 内のagg_func引数のデフォルトが 「np.mean」 となっているからであり、この引数を変えることで文字列を値とすることができます。
 下の例では、「agg_func」引数に「lambda x: x」を代入しており、値をそのまま返すようにしています。

stacked_data = data.assign(id=1).assign(
    id=lambda x: x.id.cumsum()
).set_index(['id', 'cate1', 'cate2']).stack().reset_index()
stacked_data.columns = ['id', 'cate1', 'cate2', 'variable', 'value']

target_data = pd.pivot_table(
    data=stacked_data[['id', 'cate1', 'cate2']].drop_duplicates(),
    index='id',
    columns='cate1',
    fill_value='',
    aggfunc=lambda x: x
)
target_data.columns = list(map(lambda x: x[1], target_data.columns))
target_data.reset_index(inplace=True, drop=False)
target_data
id a b c
0 1 A
1 2 B
2 3 B
3 4 C
4 5 C
5 6 C


終わりに


今回は実用的な処理だけを取り上げましたが、MySQLやR言語も含め網羅的に学習するならば、以下の本がおすすめです。