二行まとめ
- Pandas.DataFrame でありがちな
df = df[df['都道府県']=='北海道']
みたいな処理を代替するモジュールを作成した - 自作モジュールにしては便利なので紹介する
背景
Python でデータの前処理をする際に、ほぼ毎回 Pandas にお世話になっている。
しかし、時には少し面倒だと感じる処理もある。私にとっては以下がその例だ。
df = df[df['都道府県']=='北海道'] # ここの df は pandas.DataFrame のインスタンスとする
つまり、pandas.DataFrame
のある列の値を参照してデータの絞り込みをする場面だ。
こういう処理は本当に頻繁に必要になるのだが、煩雑な点がいくつかある。
- 一回の除外処理のために
df
を 3 回も書く必要があり、コードが冗長になる - データが何件除外されたかを知るには前後で
print(len(df))
などを使う必要があり、面倒
なんとかしたい。
なんとかした
たとえば、日本の市区町村のデータ1から北海道のデータを絞り込むと、除外された件数が自動でprint
される。2
import pandas as pd
import removerows as rmrows
df = pd.read_csv('SSDSE-市区町村データ前処理済み.csv')
df = rmrows.select_by_word(df, col='都道府県', word='北海道')
このremoverows
が今回の主題であり、実際のコードは記事末尾に記載する。3
おまけ
興味を持ってくれた方のために、他にどんな機能があるかも示しておく。
特に理由はないが、東北地方の市区町村で「〇〇町」がいくつあるかを調べてみる.
df = pd.read_csv('SSDSE-市区町村データ前処理済み.csv')
df = rmrows.select_by_word(df, '項目名', '総人口') # 文字列による除外
df = rmrows.select_by_date(df, '年度', '>=', '2020-01-01') # 日付による除外
df = rmrows.select_by_number(df, '人口', '>', 0) # 数字と不等号による除外
touhoku = ['青森県', '秋田県', '岩手県', '宮城県', '山形県', '福島県']
df = rmrows.select_by_list(df, '都道府県', touhoku, title='東北地方') # list にある項目を残す
df = rmrows.select_by_regexp(df, '市区町村', '町$') # 正規表現による除外
df.groupby('都道府県').agg({'市区町村': 'count'}).rename(columns={'市区町村': '町の数'})
除外条件ごとに弾かれる件数がわかるので、重ねて除外を行う際には特に便利である。4
今回はデータの欠損、重複がないデータセットだったが、
データに不備がないかどうかを明示的に確認することもできる。
df = rmrows.remove_isna(df, '市区町村')
df = rmrows.remove_dupulicated(df, ['都道府県', '市区町村'])
df = rmrows.remove_by_regexp(df, '市区町村', '市$')
# 除外前の行数: 115
# [市区町村]の列について、値が入っていない行を除外する
# 除外した行数: 0
# 除外後の行数: 115
# 除外前の行数: 115
# [['都道府県', '市区町村']]列の重複を除外する(NaNの行は除外しない)
# 除外した行数: 0
# 除外後の行数: 115
# 除外前の行数: 115
# [市区町村]の列について、[市$]のパターンに一致する行を除外する
# 除外した行数: 0
# 除外後の行数: 115
便利ですね〜。
removerows.py
書き方が拙い部分もあると思いますが、個人用として特に不便がなければよしとしてます。
利用は自己責任でお願いします。
import re
from datetime import datetime
import pandas as pd
def check_len_removed_rows(func):
"""除外した行数確認用のデコレータ関数"""
def wrapper(*args, **kwargs):
if 'df' in kwargs.keys():
len_before = len(kwargs['df'])
elif isinstance(args[0], pd.DataFrame):
len_before = len(args[0])
else:
raise Exception('args[0] must be pandas.Dataframe')
print(f'除外前の行数: {len_before}')
result = func(*args, **kwargs)
print(f'除外した行数: {len_before-len(result)}\n除外後の行数: {len(result)}\n')
return result
return wrapper
@check_len_removed_rows
def select_by_word(df:pd.DataFrame, col:str, word:str):
print(f'[{col}]列が[{word}]と一致する行を残し、他を全て除外する')
return df[df[col]==word]
@check_len_removed_rows
def remove_by_word(df:pd.DataFrame, col:str, word:str,):
print(f'[{col}]列が[{word}]と一致する行を除外する')
return df[df[col]!=word]
@check_len_removed_rows
def select_by_bool(df:pd.DataFrame, col:str, bool_:bool):
print(f'[{col}]の列について、{bool_}に一致する行を残し、他の行を除外する')
return df[df[col]==bool_]
@check_len_removed_rows
def remove_dupulicated(
df: pd.DataFrame, col: str | list[str],
keep='first',sort_col=None, ascending=True
):
print(f'[{col}]列の重複を除外する(NaNの行は除外しない)')
if sort_col:
asc_text = '昇順' if ascending else '降順'
keep_text_dict = {
'first':'最初の行を残して、それ以外',
'last':'最後の行を残して、それ以外',
False:'全て'
}
print(f'[{sort_col}]の値を{asc_text}に並び替え、{keep_text_dict[keep]}を除外する')
df = df.sort_values(sort_col, ascending=ascending)
if isinstance(col, str):
return df[~(df.duplicated(subset=col, keep=keep)) | (df[col].isna())]
elif isinstance(col, list):
return df[~(df.duplicated(subset=col, keep=keep)) | (df[col].isna().all(axis=1))]
@check_len_removed_rows
def select_by_list(df:pd.DataFrame, col:str, list_:list, title:str=''):
print(f'[{col}]の列について、{title}リストに含まれる行を残し、他の行を除外する')
return df[df[col].apply(lambda x: True if x in list_ else False)]
@check_len_removed_rows
def remove_by_list(df:pd.DataFrame, col:str, list_:list, title:str=''):
print(f'[{col}]の列について、{title}リストに含まれる行を除外する')
return df[df[col].apply(lambda x: False if x in list_ else True)]
@check_len_removed_rows
def remove_notna(df:pd.DataFrame, col:str):
print(f'[{col}]の列について、値が入っている行を除外する')
return df[df[col].isna()]
@check_len_removed_rows
def remove_isna(df:pd.DataFrame, col:str):
print(f'[{col}]の列について、値が入っていない行を除外する')
return df[df[col].notna()]
@check_len_removed_rows
def select_by_number(
df:pd.DataFrame, col:str, symbol:str, number:float,
):
symbol_dict = {'>':'より大きい', '>=':'以上の', '=':'に等しい', '<=':'以下の', '<':'より小さい'}
print(f'[{col}]の列について、{number} {symbol_dict[symbol]}行のみに絞る')
if symbol == '>':
df = df[df[col] > number]
elif symbol == '>=':
df = df[df[col] >= number]
elif symbol == '=':
df = df[df[col] == number]
elif symbol == '<=':
df = df[df[col] <= number]
elif symbol == '<':
df = df[df[col] < number]
return df
@check_len_removed_rows
def select_by_regexp(df:pd.DataFrame, col:str, pattarn:str):
print(f'[{col}]の列について、[{pattarn}]のパターンに一致する行を残し、それ以外を除外する')
df = df.copy()
df = df[df[col].apply(
lambda x: True if re.search(pattern=pattarn, string=x) else False
)]
return df
@check_len_removed_rows
def remove_by_regexp(df:pd.DataFrame, col:str, pattarn:str):
print(f'[{col}]の列について、[{pattarn}]のパターンに一致する行を除外する')
df = df.copy()
df = df[df[col].apply(
lambda x: False if re.search(pattern=pattarn, string=x) else True
)]
return df
@check_len_removed_rows
def select_by_date(df:pd.DataFrame, col:str, symbol:str, date: str):
df = df.copy()
symbol_dict = {'>':'より後の', '>=':'以降の', '=':'に等しい', '<=':'以前の', '<':'より前の'}
df[col] = pd.to_datetime(df[col])
print(f'[{col}]の列について、{date} {symbol_dict[symbol]}行のみに絞る')
date = datetime.strptime(date, '%Y-%m-%d')
if symbol == '>':
df = df[df[col] > date]
elif symbol == '>=':
df = df[df[col] >= date]
elif symbol == '=':
df = df[df[col] == date]
elif symbol == '<=':
df = df[df[col] <= date]
elif symbol == '<':
df = df[df[col] < date]
return df
@check_len_removed_rows
def anti_join(df, anti_df, on, title=''):
right_data = f'[{title}]' if title else '右の Dataframe '
print(f'[{on}]列をキーにして{right_data}と共通する行を除外する')
anti_df = anti_df[on]
merged_df = df.merge(anti_df, on=on, how='left', indicator=True)
anti_joined = merged_df[merged_df['_merge']=='left_only'].copy() # エラー回避のcopy() c.f. https://qiita.com/FukuharaYohei/items/b3aa7113d08858676910
anti_joined.drop('_merge', axis=1, inplace=True)
return anti_joined
-
データの出典: 独立行政法人統計センター のSSDSE-市区町村(SSDSE-A)
データの前処理については以下の記事を参照
https://qiita.com/guneco/items/c3e60e04bdc2e8dced08 ↩ -
画像は Visual Studio Code の Interactive Window で実行したもの ↩
-
日本語にしか対応させていないので、今のところ pip などで配布の予定はない。 ↩
-
当初人口が 0 人の市区町村が存在する想定はしていなかったが、実際に人口が 0 人の町が存在することをこのとき知りました。Wikipedia - 双葉町 ↩