Pandas 将所有列中的行降到低于其他行

Pandas drop rows lower then others in all colums

我有一个包含很多行和数字列的数据框,例如:

A B C D
12 7 1 0
7 1 2 0
1 1 1 1
2 2 0 0

我需要通过删除那些具有另一行且所有值都更大的行来减小数据框的大小。 在前面的示例中,我需要删除最后一行,因为第一行的所有值都更大(在重复行的情况下,我需要保留其中一个)。 还有 return 这个:

A B C D
12 7 1 0
7 1 2 0
1 1 1 1

我更快的解决方案如下:

    def complete_reduction(df, columns):
        def _single_reduction(row):
            df["check"] = True
            for col in columns:
                df["check"] = df["check"] & (df[col] >= row[col])
            drop_index.append(df["check"].sum() == 1)
        df = df.drop_duplicates(subset=columns)
        drop_index = []
        df.apply(lambda x: _single_reduction(x), axis=1)
        df = df[numpy.array(drop_index).astype(bool)]
        return df

有更好的主意吗?


更新:

这里找到了一个新的解决方案 但我希望事情更快。

我们可以做numpy板投

s = df.values
out = df[np.sum(np.all(s>=s[:,None],-1),1)==1]
Out[44]: 
    A  B  C  D
0  12  7  1  0
1   7  1  2  0
2   1  1  1  1

我们取数据框中每列的累积最大值。

我们希望保留所有具有等于最大值的列值的行。然后我们使用 pandas drop_duplicates

删除重复项
In [14]: df = pd.DataFrame(
    ...:     [[12, 7, 1, 0], [7, 1, 2, 0], [1, 1, 1, 1], [2, 2, 0, 0]],
    ...:     columns=["A", "B", "C", "D"],
    ...: )

In [15]: df[(df == df.cummax(axis=0)).any(axis=1)].drop_duplicates()
Out[15]: 
    A  B  C  D
0  12  7  1  0
1   7  1  2  0
2   1  1  1  1

比目前提出的解决方案更多 memory-efficient 和更快的解决方案是使用 Numba。无需使用 Numba 创建巨大的临时数组。此外,很容易编写一个 并行实现 来利用所有 CPU 个内核。这是实现:

import numba as nb

@nb.njit
def is_dominated(arr, k):
    n, m = arr.shape
    for i in range(n):
        if i != k:
            dominated = True
            for j in range(m):
                if arr[i, j] < arr[k, j]:
                    dominated = False
            if dominated:
                return True
    return False

# Precompile the function to native code for the most common types
@nb.njit(['(i4[:,::1],)', '(i8[:,::1],)'], parallel=True, cache=True)
def dominated_rows(arr):
    n, m = arr.shape
    toRemove = np.empty(n, dtype=np.bool_)
    for i in nb.prange(n):
        toRemove[i] = is_dominated(arr, i)
    return toRemove

# Special case
df2 = df.drop_duplicates()

# Main computation
result = df2[~dominated_rows(np.ascontiguousarray(df.values))]

基准

输入测试是两个随机数据帧,形状为 20000x5 和 5000x100,包含小整数(即 [0;100[)。已在 Windows 上具有 16 GiB RAM 的(6 核)i5-9600KF 处理器上完成测试。 @BingWang 的版本是2022-05-24更新的版本之一。以下是迄今为止提出的方法的性能结果:

Dataframe with shape 5000x100
 - Initial code:   114_340 ms
 - BENY:             2_716 ms  (consume few GiB of RAM)
 - Bing Wang:        2_619 ms
 - Numba:              303 ms  <----

Dataframe with shape 20000x5
 - Initial code:    (too long)
 - BENY:             8.775 ms  (consume few GiB of RAM)
 - Bing Wang:          578 ms
 - Numba:               21 ms  <----

这个解决方案分别比最快的解决方案(@BingWang)快 9 到 28 倍。它还具有消耗更少内存 的好处。实际上,@BENY 实现消耗的 RAM 很少 GiB,而这个(和 @BingWang 的那个)只消耗不超过几个 MiB used-case。 @BingWang 实现的速度增益是由于提前停止、并行性和本机执行。

可以看出,这个Numba实现和@BingWang的实现在列数较少的情况下是相当高效的。这对@BingWang 有意义,因为复杂度应该是 O(N(logN)^(d-2)),其中 d 是列数。至于 Numba,它明显更快,因为大多数行都在第二个随机数据集上占主导地位,导致提前停止在实践中非常有效。我认为@BingWang 算法在大多数行未被支配时可能会更快。然而,这种情况在列数少、行数多的数据帧上应该很少见(至少,在均匀随机的数据帧上是这样)。

这是一个基于 Kung et al 1975 的尝试 http://www.eecs.harvard.edu/~htk/publication/1975-jacm-kung-luccio-preparata.pdf

蛮力解法来自

我没有对其进行可靠的测试,但使用这些参数看起来是相同的答案

不能保证它是正确的,或者我什至是在关注论文。请彻底测试。另外,很有可能会有商业方案来计算。

D=5 #dimension, or number of columns
N=2000 #number of data rows
M=1000 #upper bound for random integers

更改为 D=20 和 N=20000,您可以看到 Kung75 在不到 1 分钟内完成,但 Brutal Force 将使用超过 10 倍的时间。 即使在Dimension=1000,Rows=20000,value range 0~999的情况下,仍然可以稍微超过1分钟完成

可以修改成merge sort(强行计算小块,然后用Filter合并),更容易切换到并行计算。 另一种加快速度的方法是在熟悉代码后关闭数组边界检查。这是由于这里的重数组索引。如果你想尝试这条路,我会推荐 C#。

import pandas as pd
import numpy as np
import datetime

#generate fake data
D=1000 #dimension, or number of columns
N=20000 #number of data rows
M=1000 #upper bound for random integers

np.random.seed(12345) #set seed so this is reproducible
data=np.random.randint(0,M,(N,D))
for i in range(0,12):
    print(i,data[i])

#Compare w and v starting dimention d
def Compare(w,v,d=0):
    cmp=3 #0x11, low bit is GE, high bit is LE, together means EQ
    while d<D:
        if w[d]>v[d]:
            cmp&=1
        elif w[d]<v[d]:
            cmp&=2
        if cmp>0:
            d+=1
        else:
            break
    return cmp # 0=uncomparable, 1=GT, 2=LT, 3=EQ
#unit test:
#print(Compare(data[0],data[1]))
#print(Compare(data[0],data[1],4))
#print(Compare(data[1],data[11]))
#print(Compare(data[11],data[1]))
#print(Compare(data[1],data[1]))
def AuxSort(d,ndxArray): #stable sort desc by dimention d
    return [x[1] for x in sorted([(-data[n][d],n) for n in ndxArray])]
#unit test
#print(AuxSort(data,0,[0,4,3]))
#print(AuxSort(data,2,[0,1,2]))
#cumulatively find the pareto front. Time O(N^2), space O(N)
def N2BrutalForce(data,ndxArray=None,d=0):
    if len(data)==0:
        return []
    if not ndxArray: #by default check the entire data
        ndxArray=list(range(len(data)))
    #up to this point ndxArray is not empty
    result={ndxArray[0]:data[ndxArray[0]]}
    for i in range(1,len(ndxArray)):
        dominated=[]
        j=ndxArray[i]
        for k,v in result.items():
            c=Compare(data[j],v,d)
            if c>1:
                break
            elif c==1:
                dominated.append(k)
        else:
            for o in dominated:
                del result[o]
            result[j]=data[j]
    return [r for r in result]
def resultPrinter(res, ShowCountOnly=False):
    if not ShowCountOnly:
        for r in sorted(res):
            print(r,data[r])
    print(len(res),'results found',datetime.datetime.today())
#unit rest
#resultPrinter(N2BrutalForce(data),True)
#resultPrinter(N2BrutalForce(data,list(range(15))))
def FindT(R1,R2,S1,S2,d):
    S1R1=set(Filter(data,d,R1,S1))
    T1=[s for s in S1 if s in S1R1]
    S2R1=Filter(data,d+1,R1,S2)
    S2R2=set(Filter(data,d,R2,S2))
    T2=[s for s in S2R1 if s in S2R2]
    return T1+T2
def BreakAtPseudoMedian(sArray,d):
    sArray=AuxSort(d,sArray) #this could speed up by moving the sort to caller and avoid redo sorting
    if data[sArray[0]][d]==data[sArray[-1]][d]:
        return [],sArray
    L=len(sArray)
    mHigh=mLow=L//2
    while mLow>0 and data[sArray[mLow]][d]==data[sArray[mLow-1]][d]:
        mLow-=1
    if mLow>0:
        return sArray[:mLow],sArray[mLow:]
    while mHigh<L-1 and data[sArray[mHigh]][d]==data[sArray[mHigh+1]][d]:
        mHigh+=1
    return sArray[:mHigh],sArray[mHigh:]
def Filter(data,d,rArray,sArray):
    L=len(rArray)+len(sArray)
    if d==D-1 and rArray:
        R=max(data[r][d] for r in rArray)
        return [s for s in sArray if data[s][d]>R]
    elif len(rArray)*len(sArray)<=30 or len(rArray)<=2 or len(sArray)<=2:
        nonDominated=[]
        for s in sArray:
            for r in rArray:
                c=Compare(data[s],data[r],d)
                if c>1:
                    break
            else:
                nonDominated.append(s)
        return nonDominated
    S1,S2=BreakAtPseudoMedian(sArray,d)
    R1,R2=BreakAtRefValue(rArray,d,data[S2[0]][d])
    if not S1 and not R1:
        return Filter(data,d+1,rArray,sArray)
    return FindT(R1,R2,S1,S2,d)
#Filter(data,0,[0,1,2,3,4,5,6,7,8,9],[11])
def BreakAtRefValue(rArray,d,br):
    rArray=AuxSort(d,rArray)
    if data[rArray[0]][d]<=br:
        return [],rArray
    if data[rArray[-1]][d]>br:
        return rArray,[]
    mLow,mHigh=0,len(rArray)-1
    while mLow<mHigh-1 and data[rArray[mLow]][d]>br and data[rArray[mHigh]][d]<br:
        mid=(mLow+mHigh)//2
        if data[rArray[mid]][d]>br:
            mLow=mid
        elif data[rArray[mid]][d]<br:
            mHigh=mid
        else:
            mLow=mid
            break
    if data[rArray[mLow]][d]>br and data[rArray[mHigh]][d]<br:
        return rArray[:mHigh],rArray[mHigh:]
    if data[rArray[mLow]][d]==br:
        while data[rArray[mLow-1]][d]==br:
            mLow-=1
        return rArray[:mLow],rArray[mLow:]
    while data[rArray[mHigh-1]][d]==br:
        mHigh-=1
    return rArray[:mHigh],rArray[mHigh:]
def Kung75(data,d,ndxArray):
    L=len(ndxArray)
    if L<10:
        return N2BrutalForce(data,ndxArray,d)
    elif d==D-1:
        x,y=-1,-1
        for n in ndxArray:
            if y<0 or data[n][d]>x:
                x,y=data[n][d],n
        return [y]
    if data[ndxArray[0]][d]==data[ndxArray[-1]][d]:
        return Kung75(data,d+1,AuxSort(d+1,ndxArray))
    R,S=BreakAtPseudoMedian(ndxArray,d)
    R=Kung75(data,d,R)
    S=Kung75(data,d,S)
    T=Filter(data,d+1,R,S)
    return R+T
print('started at',datetime.datetime.today())
resultPrinter(Kung75(data,0,AuxSort(0,list(range(len(data))))),True)
df.sort_values(by=['A', 'B', 'C', 'D'], ascending=False, inplace=True)
df = df.iloc[:cutoff]

如果这花费的时间太长,您可以对 df 的子集执行此操作,直到 够小了。