Pandas аналог оператора SQL MINUS / EXCEPT, с использованием нескольких столбцов - PullRequest
2 голосов
/ 21 мая 2019

Я ищу самый быстрый и идиоматический аналог оператора SQL MINUS (AKA EXCEPT) .

Вот что я имею в виду - учитывая два кадра данных панд следующим образом:

In [77]: d1
Out[77]:
   a  b  c
0  0  0  1
1  0  1  2
2  1  0  3
3  1  1  4
4  0  0  5
5  1  1  6
6  2  2  7

In [78]: d2
Out[78]:
   a  b   c
0  1  1  10
1  0  0  11
2  1  1  12

Как найти результат d1 MINUS d2 с учетом только столбцов "a" и "b", чтобы получить следующий результат:

In [62]: res
Out[62]:
   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7

MVCE:

d1 = pd.DataFrame({
    'a': [0, 0, 1, 1, 0, 1, 2], 
    'b': [0, 1, 0, 1, 0, 1, 2], 
    'c': [1, 2, 3, 4, 5, 6, 7]
})

d2 = pd.DataFrame({
    'a': [1, 0, 1], 
    'b': [1, 0, 1], 
    'c': [10, 11, 12]
})

Что я пробовал:

In [65]: tmp1 = d1.reset_index().set_index(["a", "b"])

In [66]: idx = tmp1.index.difference(d2.set_index(["a","b"]).index)

In [67]: res = d1.loc[tmp1.loc[idx, "index"]]

In [68]: res
Out[68]:
   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7

это дает мне правильные результаты, но у меня есть ощущение, что должен быть более идиоматичный и более приятный / чистый способ добиться этого.

PS Метод DataFrame.isin () в этом случае не поможет, так как даст неправильный набор результатов

Ответы [ 4 ]

2 голосов
/ 21 мая 2019

Сравнение времени выполнения для больших наборов данных:

In [100]: df1 = pd.concat([d1] * 10**5, ignore_index=True)

In [101]: df2 = pd.concat([d2] * 10**5, ignore_index=True)

In [102]: df1.shape
Out[102]: (700000, 3)

In [103]: df2.shape
Out[103]: (300000, 3)

pd.concat().drop_duplicates() подход:

In [10]: %%timeit
    ...: res = pd.concat([d1, pd.concat([d2]*2)]).drop_duplicates(['a', 'b'], keep=False)
    ...:
    ...:
2.59 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

многоиндексный подход НЕ ВХОДИТ:

In [11]: %%timeit
    ...: res = df1[~df1.set_index(["a", "b"]).index.isin(df2.set_index(["a","b"]).index)]
    ...:
    ...:
484 ms ± 18.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

многоиндексный подход:

In [12]: %%timeit
    ...: tmp1 = df1.reset_index().set_index(["a", "b"])
    ...: idx = tmp1.index.difference(df2.set_index(["a","b"]).index)
    ...: res = df1.loc[tmp1.loc[idx, "index"]]
    ...:
    ...:
1.04 s ± 20.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

merge(how="outer") подход - дает мне MemoryError:

In [106]: %%timeit
     ...: res =  (df1.reset_index()
     ...:         .merge(df2, on=['a','b'], indicator=True, how='outer', suffixes=('','_'))
     ...:         .query('_merge == "left_only"')
     ...:         .set_index('index')
     ...:         .rename_axis(None)
     ...:         .reindex(df1.columns, axis=1))
     ...:
     ...:
---------------------------------------------------------------------------
MemoryError                               Traceback (most recent call last)

Сравнение метода каскадных строк:

In [13]: %%timeit
    ...: res = df1[~df1[['a','b']].astype(str).sum(axis=1).isin(df2[['a','b']].astype(str).sum(axis=1))]
    ...:
    ...:
2.05 s ± 65.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2 голосов
/ 21 мая 2019

Я думаю, что здесь немного похоже на Excel:

d1[~d1[['a','b']].astype(str).sum(axis=1).isin(d2[['a','b']].astype(str).sum(axis=1))]

   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7
2 голосов
/ 21 мая 2019

Одно возможное решение с merge и indicator=True:

df = (d1.reset_index()
        .merge(d2, on=['a','b'], indicator=True, how='outer', suffixes=('','_'))
        .query('_merge == "left_only"')
        .set_index('index')
        .rename_axis(None)
        .reindex(d1.columns, axis=1))
print (df)
   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7

Решение с isin:

df = d1[~d1.set_index(["a", "b"]).index.isin(d2.set_index(["a","b"]).index)]
print (df)
   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7
1 голос
/ 21 мая 2019

Мы можем использовать pandas.concat с drop_duplicates здесь и передать ему аргумент для отбрасывания всех дубликатов с keep=False:

pd.concat([d1, d2]).drop_duplicates(['a', 'b'], keep=False)

   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7

Редактировать после комментария с помощью OP

Если вы хотите убедиться, что уникальные строки в df2 не приняты во внимание, мы можем продублировать df:

pd.concat([d1, pd.concat([d2]*2)]).drop_duplicates(['a', 'b'], keep=False)

   a  b  c
1  0  1  2
2  1  0  3
6  2  2  7
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...