TransWikia.com

Pandasデータフレームで特定行を削除する方法

スタック・オーバーフロー Asked by ogawa on January 21, 2021

(購買データの前処理に関するご質問です)
同「id」「購入日」「店舗」「商品」「売上タイプ」ごとに連番を振った、下記データフレームがあるのですが、

df = pd.DataFrame({'id': ['111', '111', '111', '111', '111', '222', '222', '222', '333', '333', '333', '333', '333'],
                   '購入日': ['1/5', '1/5', '1/5', '1/5','1/5', '2/3', '2/3', '2/3', '3/5', '3/5', '4/1', '4/1', '4/1'],
                   '店舗': ['東京', '東京', '東京', '東京','東京', '千葉', '千葉', '千葉', '東京', '東京', '千葉', '千葉', '千葉'],
                   '商品': ['A', 'A', 'A', 'A','A', 'B', 'B', 'B', 'C', 'C', 'D', 'D', 'D'],
                   '売上タイプ': ['売上', '売上', '売上', '返品','返品', '売上', '返品', '返品', '売上', '返品', '売上', '売上', '返品'],
                   'cnt': [1, 2, 3, 1, 2, 1, 1, 2, 1, 1, 1, 2, 1]})

このうち、
同「id」「購入日」「店舗」「商品」の、”売上” に対応する "返品" があるレコードは削除したいです。<対応する>とは、"売上"のcnt "1" があり、かつ、"返品" のcnt"1" がある場合です。"売上"のcnt "1" ・"返品" のcnt "1" のレコードを共に削除したい、ということです。(・・・売上cnt"2"・返品cnt"2"同士は削除、売上cnt"3"・返品cnt"3"同士は削除・・・。対応がないレコードは残す)

上記のdfに対してこの処理を行った場合、下記のデータフレームが返るイメージです。

ans = pd.DataFrame({'id': ['111', '222', '333'],
                    '購入日': ['1/5', '2/3', '4/1'],
                    '店舗': ['東京', '千葉', '千葉'],
                    '商品': ['A', 'B', 'D'],
                    '売上タイプ': ['売上', '返品', '売上'],
                    'cnt': [3, 2, 2]})

データ量が2500万件ほどあるので、処理速度も考慮しつつ書きたいと思っております、。(基本のfor文だと遅くなる・・・?)アドバイスいただける方がいらっしゃいましたらお願いいたします。

2 Answers

グループ化した後、それぞれに含まれる「売上タイプ」の個数を agg() で調べます。「売上」だけ、もしくは「返品」だけの場合は 1 になりますので、「売上タイプ」の個数が 1 になるレコードを抽出します。

df.iloc[
  sum((
    df.assign(idx=df.index)
      .groupby(['id', '購入日', '店舗', '商品', 'cnt'], as_index=False)
      .agg({
        'idx': lambda x: list(x) if len(list(x)) == 1 else []
      })
  ).idx, [])].reset_index(drop=True)

# 処理結果

    id 購入日  店舗 商品 売上タイプ  cnt
0  111    1/5  東京    A       売上    3
1  222    2/3  千葉    B       返品    2
2  333    4/1  千葉    D       売上    2

追記

df.iloc[ sum(●●●).idx, [])]

ilocは、数値で行と列の位置を指定するものという認識ですが(df.iloc[行, 列])、この部分のコードについて補足して下さい

こちらについては処理の途中経過を見て貰うと分かりやすいかと思います。

(df.assign(idx=df.index)
  .groupby(['id', '購入日', '店舗', '商品', 'cnt'], as_index=False)
  .agg({
    'idx': lambda x: list(x) if len(list(x)) == 1 else []
  }))
=>
    id 購入日  店舗 商品  cnt   idx
0  111    1/5  東京    A    1    []
1  111    1/5  東京    A    2    []
2  111    1/5  東京    A    3   [2]
3  222    2/3  千葉    B    1    []
4  222    2/3  千葉    B    2   [7]
5  333    3/5  東京    C    1    []
6  333    4/1  千葉    D    1    []
7  333    4/1  千葉    D    2  [11]

ここで、idx カラムには元のデータフレーム(df)のインデックス値が入ります。条件に適合しない行(レコード)の場合は空リスト([])にしていますが、これは後の sum() のためです。

上述のデータフレーム.idx
=> [[], [], [2], [], [7], [], [], [11]]

sum(上述のデータフレーム.idx, [])
=> [2, 7, 11]

sum(lst, []) で「上述のデータフレーム.idx」(リストのリスト)を平坦化(flatten)しています。

df.iloc[上述の sum() の結果].reset_index(drop=True)

sum() の結果はリストで、これは抽出条件に適合するレコードのインデックス値(元のデータフレームの行番号)になります。このリストを df.iloc[] に渡す事によって目的の「行」を選択・抽出しています。

Answered by metropolis on January 21, 2021

概念的にはこれでいけるはずです。
ただし性能は考慮していません。

droprows = []
grouped = df.groupby(['id','購入日','店舗','商品','cnt'])
for i,dfw in grouped:
    r = len(dfw.index)
    if r >= 2:    ## 上記条件が同一で2つ以上の記録有り
        ## 以下は売上と返品が対になっていることの確認とその対応処理
        sale = []
        void = []
        subg = dfw.groupby('売上タイプ')
        for t,dfs in subg:
            if t == '売上':
                sale.extend(dfs.index)
            else:
                void.extend(dfs.index)
        
        slen = len(sale)
        vlen = len(void)
        blen = min(slen,vlen)
        if slen == vlen:  ## 売上と返品の記録が同件数
            droprows.extend(dfw.index)
        elif blen > 0:  ## 記録件数は違うが売上と返品が対になっているのが0ではない
            ## リストに先に現れた順番に削除する。対になっていない分は残す
            droprows.extend(sale[:blen])
            droprows.extend(void[:blen])
        ## else: どちらかが0件なら対になっていないので削除リストには追加しない

droprows.sort() ## 削除リストを昇順でソートしておく

ans = df.drop(index=droprows)
ans.reset_index(drop=True, inplace=True)

コメント対応追記:

pandas.core.groupby.DataFrameGroupByの大元の構造については私も情報が無いですが、単一変数で受けるfor x in grouped:とか、上記回答で書いたfor i,dfw in grouped:のループでprint(x)とか、print(i), print(dfw)すると内容が見えてきます。

単一変数で受けるとグループ選択に使った値(複数条件だとそのtuple)と、対応するデータフレームの選択された部分がtupleになっています。
受ける変数を分けると、それらが別々の変数に格納されます。

データフレームの選択された部分は、それ単独がデータフレームとして変更したり情報を取得したりと言った各種操作が出来ます。(変更は出来なかったかも、以下の件があるので未確認です)
しかし何か操作をしてそのデータフレーム自身を書き換えても、それはループの中だけでループ外には持ち出せません。
そのため別途変数(ここではdroprows)を用意して情報を保存しています。

Answered by kunif on January 21, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP