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


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

[code]

    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]})
    
[/code]

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

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

[code]

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

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




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

[code]

    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
    
[/code]

**追記**

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

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

[code]

    (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]
    
[/code]

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

[code]

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

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

[code]

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

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

