从熊猫行中删除重复项

时间:2020-08-25 16:44:25

标签: python pandas dataframe

问题

如何在Pandas数据框中分别考虑每一行(并可能用NaN代替)从每一行中删除重复的单元格?

如果我们可以将所有新创建的NaN移到每一行的末尾,那就更好了。


相关但不同的帖子

关于如何删除被视为重复的整行的帖子:

发布如何从熊猫列中的列表中删除重复项

此处给出的答案将返回一系列字符串,而不是数据帧。


可重复的设置

import pandas as pd

让我们创建一个dataframe

df = pd.DataFrame({'a': ['A', 'A', 'C', 'B'],
                   'b': ['B', 'D', 'B', 'B'],
                   'c': ['C', 'C', 'C', 'A'],
                   'd': ['D', 'D', 'B', 'A']},
                   index=[0, 1, 2, 3])

df已创建:

+----+-----+-----+-----+-----+
|    | a   | b   | c   | d   |
|----+-----+-----+-----+-----|
|  0 | A   | B   | C   | D   |
|  1 | A   | D   | C   | D   |
|  2 | C   | B   | C   | B   |
|  3 | B   | B   | A   | A   |
+----+-----+-----+-----+-----+

(使用this打印。)


解决方案

从每行中删除重复项的一种方法,分别考虑每行:

df = df.apply(lambda row: pd.Series(row).drop_duplicates(keep='first'),axis='columns')

使用apply()lambda函数,pd.Series()Series.drop_duplicates()

使用Shift NaNs to the end of their respective rows将所有的NaN推到每一行的末尾:

df.apply(lambda x : pd.Series(x[x.notnull()].values.tolist()+x[x.isnull()].values.tolist()),axis='columns') 

输出:

+----+-----+-----+-----+-----+
|    | 0   | 1   | 2   | 3   |
|----+-----+-----+-----+-----|
|  0 | A   | B   | C   | D   |
|  1 | A   | D   | C   | nan |
|  2 | C   | B   | nan | nan |
|  3 | B   | A   | nan | nan |
+----+-----+-----+-----+-----+

就如我们所愿。


问题

有没有更有效的方法?也许带有一些内置的Pandas功能?

5 个答案:

答案 0 :(得分:23)

您可以先stack,然后再drop_duplicates。然后,我们需要借助cumcount级别进行透视。 stack保留值在行中出现的顺序,而cumcount确保NaN将出现在末尾。

df1 = df.stack().reset_index().drop(columns='level_1').drop_duplicates()

df1['col'] = df1.groupby('level_0').cumcount()
df1 = (df1.pivot(index='level_0', columns='col', values=0)
          .rename_axis(index=None, columns=None))

   0  1    2    3
0  A  B    C    D
1  A  D    C  NaN
2  C  B  NaN  NaN
3  B  A  NaN  NaN

时间

假设有4列,让我们看看随着行数的增加,这些方法的比较情况。 mapapply解决方案在事物较小时具有很好的优势,但是它们比更复杂的stack + drop_duplicates + pivot解决方案要慢一些随着DataFrame变得更长。无论如何,它们对于大型DataFrame都需要花费一些时间。

import perfplot
import pandas as pd
import numpy as np

def stack(df):
    df1 = df.stack().reset_index().drop(columns='level_1').drop_duplicates()

    df1['col'] = df1.groupby('level_0').cumcount()
    df1 = (df1.pivot(index='level_0', columns='col', values=0)
              .rename_axis(index=None, columns=None))
    return df1

def apply_drop_dup(df):
    return pd.DataFrame.from_dict(df.apply(lambda x: x.drop_duplicates().tolist(),
                                           axis=1).to_dict(), orient='index')

def apply_unique(df):
    return pd.DataFrame(df.apply(pd.Series.unique, axis=1).tolist())


def list_map(df):
    return pd.DataFrame(list(map(pd.unique, df.values)))


perfplot.show(
    setup=lambda n: pd.DataFrame(np.random.choice(list('ABCD'), (n, 4)),
                                 columns=list('abcd')), 
    kernels=[
        lambda df: stack(df),
        lambda df: apply_drop_dup(df),
        lambda df: apply_unique(df),
        lambda df: list_map(df),
    ],
    labels=['stack', 'apply_drop_dup', 'apply_unique', 'list_map'],
    n_range=[2 ** k for k in range(18)],
    equality_check=lambda x,y: x.compare(y).empty,  
    xlabel='~len(df)'
)

enter image description here


最后,如果保留每个行中最初出现的值的顺序不重要,则可以使用numpy。要删除重复数据,请排序然后检查差异。然后创建一个输出数组,将值向右移动。因为此方法将始终返回4列,所以在每行少于4个唯一值的情况下,我们需要dropna来匹配其他输出。

def with_numpy(df):
    arr = np.sort(df.to_numpy(), axis=1)
    r = np.roll(arr, 1, axis=1)
    r[:, 0] = np.NaN
    
    arr = np.where((arr != r), arr, np.NaN)
    
    # Move all NaN to the right. Credit @Divakar
    mask = pd.notnull(arr)
    justified_mask = np.flip(np.sort(mask, axis=1), 1)
    out = np.full(arr.shape, np.NaN, dtype=object) 
    out[justified_mask] = arr[mask]
    
    return pd.DataFrame(out, index=df.index).dropna(how='all', axis='columns')

with_numpy(df)
#   0  1    2    3
#0  A  B    C    D
#1  A  C    D  NaN
#2  B  C  NaN  NaN     # B/c this method sorts, B before C
#3  A  B  NaN  NaN

perfplot.show(
    setup=lambda n: pd.DataFrame(np.random.choice(list('ABCD'), (n, 4)),
                                 columns=list('abcd')), 
    kernels=[
        lambda df: stack(df),
        lambda df: with_numpy(df),
    ],
    labels=['stack', 'with_numpy'],
    n_range=[2 ** k for k in range(3, 22)],
    # Lazy check to deal with string/NaN and irrespective of sort order. 
    equality_check=lambda x, y: (np.sort(x.fillna('ZZ').to_numpy(), 1) 
                                 == np.sort(y.fillna('ZZ').to_numpy(), 1)).all(),
    xlabel='len(df)'
)

enter image description here

答案 1 :(得分:11)

尝试一些新东西

df = pd.DataFrame(list(map(pd.unique, df.values)))
Out[447]: 
   0  1     2     3
0  A  B     C     D
1  A  D     C  None
2  C  B  None  None
3  B  A  None  None

答案 2 :(得分:5)

使用apply并通过pd.DataFrame.from_dict和选项orient='index'构造一个新的数据框

df_final = pd.DataFrame.from_dict(df.apply(lambda x: x.drop_duplicates().tolist(),
                                               axis=1).to_dict(), orient='index')

Out[268]:
   0  1     2     3
0  A  B     C     D
1  A  D     C  None
2  C  B  None  None
3  B  A  None  None

注意:None实际上类似于NaN。如果您想要精确的NaN。只需链接其他.fillna(np.nan)

答案 3 :(得分:3)

您可以在row轴上搜索重复项,然后使用特定的键对结果进行排序,以将Nan“推”到行末:

duplicates = df.apply(pd.Series.duplicated, axis=1)
df.where(~duplicates, np.nan).apply(lambda x: pd.Series(sorted(x, key=pd.isnull)), axis=1)

输出

| 0   | 1   | 2   | 3   |
|:----|:----|:----|:----|
| A   | B   | C   | D   |
| A   | D   | C   | NaN |
| C   | B   | NaN | NaN |
| B   | A   | NaN | NaN |

答案 4 :(得分:3)

在每行上应用{..},提取结果并重新构建数据框:

pd.Series.unique