複数列によるIFELSE(高速化)
-
高速化の検証
今回前処理の対象となるデータは、以下のように自作したものです。前処理結果がわかりやすいようにいたずらに行数が多くならないようにしています。
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]
データ処理において最も基本と言って良い、演算や、その結果を新しい列として追加するなどの処理を行います。
下の例では、新しい列名を 「value_new」 としているので、演算後の結果が新たな列になっていますが、既存の列名にするとその列の値が演算後の結果になります。
target_data = data.assign(
value_new=lambda x: x.value1 + x.value2
target_data[['value1', 'value2', 'value_new']]
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']]
条件によって列の値を変えたいときに使うのが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']]
先程のやり方では、一つの列に対して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']]
先程のやり方で複数列を条件とした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']]
データフレームで指定した列を 「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
「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
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
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
「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
統計演算を行う際に必ず出てくるのがGroupingです。これに慣れれば、どんな指標もサクッと作れます。
下の例では、「cate1」 と 「cate2」 の2つの列を合わせた組み合わせが同じ行において、「value2」 の平均値を算出しています。 「mean」 関数を変えれば、異なる集計ができます。
target_data = data.groupby(['cate1', 'cate2'])['value2'].mean().reset_index()
target_data
Groupingをする際に一つの列に対して複数の統計処理を行いたいときがあります。これは、「agg」によりできます。
下の例では、「value2」 に対して、件数、平均値、標準偏差を計算しています。
target_data = data.groupby(['cate1', 'cate2']).agg({
'value2': ['count', 'mean', 'std']
}).reset_index()
target_data
自作した関数を使っての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
先程の例では、Groupingに指定した変数の重複でまとめらましたが、Groupingによる結果を新たな列として元のデータに追加したい場合があります。これは「transorm」により可能です。
下の例では、指定したGroupingの件数を元ののデータに新しい列として追加しています。
target_data = data.copy()
target_data['count'] = target_data.groupby(['cate1', 'cate2'])['value2'].transform('count')
target_data
target_data = data.assign(number=1).copy()
target_data['number'] = target_data.groupby(['cate1', 'cate2'])['number'].transform('cumsum')
target_data
「drop_duplicates」関数を使って指定した列で重複している値の行を消せます。
target_data = data.drop_duplicates(['cate1', 'cate2'])[['cate1', 'cate2']]
target_data
「sort_values」を使って指定した列の値による並び替えができます。
下の例では、先に 「value1」 で昇順、次に 「value2」 で降順に並び替えています。「sort_values」 内の 「ascending」 で昇順か降順を指定しています。
target_data = data.sort_values(['value1', 'value2'], ascending=[True, False])
target_data
target_data = data.sort_values(
['value1', 'value2'], ascending=[True, False]
).drop_duplicates(['cate1', 'cate2'])
target_data
「rename」関数により列名を変更できます。
target_data = data.rename(columns={'cate1': 'cate_new', 'value1': 'value_new'})
target_data
先程、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
target_data = data.assign(
cate_new=lambda x: x.cate1.replace({'a': '○'}),
value_new=lambda x: x.value1.replace({1: -1})
target_data
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']]
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']]
「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
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
「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
「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
target_data = pd.concat([
data,
data.rename(columns={'cate1': 'cate_new', 'value1': 'value_new'})[['cate_new', 'value_new']]
], axis=1)
target_data
「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
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
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)
1120 if not len(new_mgr):
-> 1121 raise DataError("No numeric types to aggregate")
1123 return new_mgr
DataError: No numeric types to aggregate
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