Pandas を使ってcsv 任意の列で重複するキーの組み合わせを求める。

サンプルのCSV

品名 産地 単価 数量
キャベツ 群馬県 219 120
コシヒカリ 新潟県 3350 20
小松菜 茨城県 198 40
キャベツ 群馬県 209 80
りんご 長野県 224 100
小松菜 茨城県 188 90
キャベツ 神奈川県 210 70
キャベツ 群馬県 224 100

'品名''産地'が重複するパターンのみを取得したい。
SQLで言う GROUP BY '品名','産地' HAVING COUNT(*) > 1 の結果の
'品名'と'産地'の組み合わせのみを求めたい。
期待値が、

キャベツ 群馬県
小松菜 茨城県

を抽出できていれば良い、単価や数量はどうでもいい。
重複するものがどんな値なのか把握せずに、
大量のCSV、任意の複数列のみ(しかも指定列も数が多い)の重複を探すのは、
結構、目視で探すとなるとたいへんなはずだ。

csv の読込み、Pandas を使って求める。

import pandas as pd

df = pd.read_csv('data.csv', encoding='utf-8', skipinitialspace=True)

groupby() by で複数列ヘッダを指定して、len() グループ数 > 1 でフィルタする。

res = df.groupby(by=['品名', '産地']).filter(lambda x: len(x) > 1)
print(res)

結果

     品名   産地   単価   数量
0  キャベツ  群馬県  219  120
2   小松菜  茨城県  198   40
3  キャベツ  群馬県  209   80
5   小松菜  茨城県  188   90
7  キャベツ  群馬県  224  100

左端がインデックス、重複行が全て抽出される。
これを更に、size() で、pandas.core.series.Series を取得すると

res = df.groupby(by=['品名', '産地'])
            .filter(lambda x: len(x) > 1)
            .groupby(by=['品名', '産地']).size()
print(res)

結果

品名    産地 
キャベツ  群馬県    3
小松菜   茨城県    2
dtype: int64

これで目的は果たせるのだが、右端にグループ数、最後に pandas のデータタイプが出力される

DataFrame の duplicated を使ってみる。
duplicated で抽出されるのは重複発生してからの後続の行、
よって更に重複行が抽出される可能性がある。

res = df[df.duplicated(subset=['品名', '産地'])]
print(res)

結果

     品名   産地   単価   数量
3  キャベツ  群馬県  209   80
5   小松菜  茨城県  188   90
7  キャベツ  群馬県  224  100

これでは、「キャベツ」の行が2つ出てしまって目的を果たさない。
duplicated 抽出後に、再度 drop_duplicates で余分な抽出行を取り除く。

res = df[df.duplicated(subset=['品名', '産地'])]
              .drop_duplicates(subset=['品名', '産地'])
print(res)

結果

     品名   産地   単価  数量
3  キャベツ  群馬県  209  80
5   小松菜  茨城県  188  90

subset で2回指定する記述がカッコ悪いけど指定キー以外の列の値が出るが、
インデックスは、重複してからの前方が出力される。

上と違うアプローチ
groupby groups pandas.io.formats.printing.PrettyDict
グループ化した タプルのkey と、pandas.core.indexes.base.Index の dict を求める

pdict = df.groupby(by=['品名', '産地']).groups
print(pdict)

結果、以下 dict が取得できる。

{('りんご', '長野県'): [4],
 ('キャベツ', '神奈川県'): [6],
 ('キャベツ', '群馬県'): [0, 3, 7],
 ('コシヒカリ', '新潟県'): [1],
 ('小松菜', '茨城県'): [2, 5]}

pandas.core.indexes.base.Index は、tolist() でリスト化できる。

for k, v in df.groupby(by=['品名', '産地']).groups.items():
    print(f'key={k}      tolist={v.tolist()}')

pandas.io.formats.printing.PrettyDict の展開結果

key=('りんご', '長野県')      tolist=[4]
key=('キャベツ', '神奈川県')      tolist=[6]
key=('キャベツ', '群馬県')      tolist=[0, 3, 7]
key=('コシヒカリ', '新潟県')      tolist=[1]
key=('小松菜', '茨城県')      tolist=[2, 5]

フィルタで、len() グループ数 > 1 を適用すれば、

pdict = df.groupby(by=['品名', '産地']).filter(lambda x: len(x) > 1).groupby(by=['品名', '産地']).groups
for k, v in pdict.items():
    print(f'key={k}      tolist={v.tolist()}')

結果は、目的を果たせる

key=('キャベツ', '群馬県')      tolist=[0, 3, 7]
key=('小松菜', '茨城県')      tolist=[2, 5]