在pandas DataFrame中,保留每行前n个非NaN单元格

4
我有一个Pandas数据框,每行至少有4个非NaN值,但这些值位于不同的列中:
Index       Col1     Col2      Col3         Col4     Col5  Col6  Col7  Col8 
1991-12-31  100.000 100.000    100.000     89.123   NaN    NaN   NaN   NaN                     
1992-01-31  98.300  101.530    100.000     NaN      92.342 NaN   NaN   NaN                     
1992-02-29  NaN     100.230    98.713      97.602   NaN    NaN   NaN   NaN                     
1992-03-31  NaN     NaN        102.060     93.473   98.123 NaN   NaN   NaN                     
1992-04-30  NaN     102.205    107.755     94.529   94.529 NaN   NaN   NaN

(我只显示前8列)我想将其转换为数据框,每行有4列。从左到右阅读的第一个日期的行应仅包含前四个非 NaN 值。

编辑:

每行上的顺序很重要。


1
每行内的顺序是否重要?如果不重要,可能可以提供高性能的解决方案。 - cs95
确实很重要(这段评论的其余部分是为了达到字符限制) - user189035
3个回答

5

方法1: 这里是一个使用NumPy的解决方案,使用justify函数 -

pd.DataFrame(justify(df.values, invalid_val=np.nan, axis=1, side='left')[:,:4])

示例运行 -


In [211]: df
Out[211]: 
             Col1     Col2     Col3    Col4    Col5  Col6  Col7  Col8
Index                                                                
1991-12-31  100.0  100.000  100.000  89.123     NaN   NaN   NaN   NaN
1992-01-31   98.3  101.530  100.000     NaN  92.342   NaN   NaN   NaN
1992-02-29    NaN  100.230   98.713  97.602     NaN   NaN   NaN   NaN
1992-03-31    NaN      NaN  102.060  93.473  98.123   NaN   NaN   NaN
1992-04-30    NaN  102.205  107.755  94.529  94.529   NaN   NaN   NaN

In [212]: pd.DataFrame(justify(df.values, invalid_val=np.nan, axis=1, side='left')[:,:4])
Out[212]: 
         0        1        2       3
0  100.000  100.000  100.000  89.123
1   98.300  101.530  100.000  92.342
2  100.230   98.713   97.602     NaN
3  102.060   93.473   98.123     NaN
4  102.205  107.755   94.529  94.529

方法二:使用定制的掩码函数 -

def app2(df, N=4):
    a = df.values
    out = np.empty_like(a)
    mask = df.isnull().values
    mask_sorted = np.sort(mask,1)
    out[~mask_sorted] = a[~mask]
    return pd.DataFrame(out[:,:N])

保持顺序的工作解决方案的运行时测试 -

# Using df from posted question to recreate a bigger one :
df = df.set_index('Index')
df = pd.concat([df] * 10000, ignore_index=1)

In [298]: %timeit app2(df)
100 loops, best of 3: 4.06 ms per loop

In [299]: %timeit pd.DataFrame(justify(df.values, invalid_val=np.nan, axis=1, side='left')[:,:4])
100 loops, best of 3: 4.78 ms per loop

In [300]: %timeit df.apply(sorted, key=np.isnan, axis=1).iloc[:, :4]
1 loop, best of 3: 4.05 s per loop

好的,太棒了。让我在我的答案中添加时间。 - cs95
@Divakar,你在这里的解决方案https://stackoverflow.com/questions/46326140/how-to-sort-a-numpy-array-with-key-as-isnan 也会更快。 - Bharath M Shetty
我在np.isnan(a)这一行遇到了TypeError: ufunc 'isnan' not supported for the input types错误。有什么想法吗? - cs95
@colspeed 我认为你可以尝试使用 pd.isnull 代替(只需将其放入替换中,它应该可以工作)。 - user189035
啊,没事了。是我没有设置索引的错误。顺便说一下,这个解决方案比你之前的慢了几毫秒。 - cs95
@Divakar:谢谢你的答复。我最终使用了COLDSPEED的方法,但所有答案都很棒,这确实是一个主观决定。 - user189035

4
如果顺序不重要,您可以沿第一个轴调用np.sort
df = df.set_index('Index')   # ignore if `Index` already is the index

pd.DataFrame(np.sort(df.values, axis=1)[:, :4], 
           columns=np.arange(1, 5)).add_prefix('Col')

     Col1     Col2     Col3     Col4
0  89.123  100.000  100.000  100.000
1  92.342   98.300  100.000  101.530
2  97.602   98.713  100.230      NaN
3  93.473   98.123  102.060      NaN
4  94.529   94.529  102.205  107.755

相比我的第二种方案,这种方法要快得多,所以如果可能的话,请务必考虑这种方法。


如果顺序很重要,请调用sorted+apply,并获取结果的前4列。

df.apply(sorted, key=np.isnan, axis=1).iloc[:, :4]

               Col1     Col2     Col3    Col4
Index                                        
1991-12-31  100.000  100.000  100.000  89.123
1992-01-31   98.300  101.530  100.000  92.342
1992-02-29  100.230   98.713   97.602     NaN
1992-03-31  102.060   93.473   98.123     NaN
1992-04-30  102.205  107.755   94.529  94.529

时间
这里是仅包含我的答案的时间 -

df = pd.concat([df] * 10000, ignore_index=1)

%timeit df.apply(sorted, key=np.isnan, axis=1).iloc[:, :4]
1 loop, best of 3: 8.45 s per loop

pd.DataFrame(np.sort(df.values, axis=1)[:, :4], 
           columns=np.arange(1, 5)).add_prefix('Col')    
100 loops, best of 3: 4.76 ms per loop

排序已经过时了 :) - Bharath M Shetty
好的,马上开始翻译,需要几秒钟。 - cs95
还有来自@Divakar答案中另外两个函数的时间。 - Bharath M Shetty
@Bharath 请随意编辑Divakar和我回答的内容(现在有点忙)。 - cs95

2

您可以使用:

#if necessary
#df = df.set_index('Index')

df = df.apply(lambda x: pd.Series(x.dropna().values), axis=1).iloc[:, :4]
print (df)
                  0        1        2       3
Index                                        
1991-12-31  100.000  100.000  100.000  89.123
1992-01-31   98.300  101.530  100.000  92.342
1992-02-29  100.230   98.713   97.602     NaN
1992-03-31  102.060   93.473   98.123     NaN
1992-04-30  102.205  107.755   94.529  94.529

或者为了更好的性能使用numpy——当每行至少有4个非值时,请按需求操作:

a = df.values
df = pd.DataFrame(a[~np.isnan(a)].reshape(a.shape[0],-1)[:, :4], index=df.index)

时间:

        Index   Col1     Col2     Col3    Col4    Col5  Col6  Col7  Col8
0  1991-12-31  100.0  100.000  100.000  89.123     NaN   NaN   NaN   NaN
1  1992-01-31   98.3  101.530  100.000     NaN  92.342   NaN   NaN   NaN
2  1992-02-29    NaN  100.230   98.713  97.602     NaN   NaN   NaN   1.0
3  1992-03-31    NaN      NaN  102.060  93.473  98.123   NaN   NaN   1.0
4  1992-04-30    NaN  102.205  107.755  94.529  94.529   NaN   NaN   NaN

df = df.set_index('Index')

df = pd.concat([df] * 10000, ignore_index=1)

In [260]: %timeit pd.DataFrame(justify(df.values, invalid_val=np.nan, axis=1, side='left')[:,:4])
100 loops, best of 3: 6.78 ms per loop

In [261]: %%timeit a = df.values
     ...: pd.DataFrame(a[~np.isnan(a)].reshape(a.shape[0],-1)[:, :4], index=df.index)
     ...: 
100 loops, best of 3: 2.11 ms per loop

In [262]: %timeit pd.DataFrame(np.sort(df.values, axis=1)[:, :4], columns=np.arange(1, 5)).add_prefix('Col')
100 loops, best of 3: 5.28 ms per loop

In [263]: %timeit pd.DataFrame(mask_app(df.values)[:,:4])
100 loops, best of 3: 8.68 ms per loop

2
我假设 OP 希望即使超过 4 列不为空,也只保留 4 列。 - John Zwinck
2
这会导致 ValueError: cannot reshape array of size 18 into shape (5,newaxis)... 你能再检查一下吗? - cs95
我不知道你是否可以假设这一点,因为我没有看到 OP 在任何地方提到它... 我错了吗? - cs95
添加了用于计时的输入DataFrame。 - jezrael
@COLDSPEED:我同意,你的代码似乎更安全,因为它可以处理不符合规范的输入。 - user189035
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接