LoginSignup
1
0

Pandas でありがちな df = df[df['都道府県']=='北海道']] みたいな除外処理を代替するモジュール

Posted at

二行まとめ

  • 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='北海道')

image.png

この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={'市区町村': '町の数'})

image.png

除外条件ごとに弾かれる件数がわかるので、重ねて除外を行う際には特に便利である。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

  1. データの出典: 独立行政法人統計センター のSSDSE-市区町村(SSDSE-A)
    データの前処理については以下の記事を参照
    https://qiita.com/guneco/items/c3e60e04bdc2e8dced08

  2. 画像は Visual Studio Code の Interactive Window で実行したもの

  3. 日本語にしか対応させていないので、今のところ pip などで配布の予定はない。

  4. 当初人口が 0 人の市区町村が存在する想定はしていなかったが、実際に人口が 0 人の町が存在することをこのとき知りました。Wikipedia - 双葉町

1
0
0

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
1
0