从Pandas列列表中获取每行的第一个非空值

42

如果我在pandas中有一个类似下面的DataFrame:

    A   B   C
0   1 NaN   2
1 NaN   3 NaN
2 NaN   4   5
3 NaN NaN NaN

如何从每行中获取第一个非空值?例如,对于上面的示例,我想获得:[1, 3, 4, None] (或等效的Series)。

9个回答

72

使用fillna从左侧填充NaN值,然后获取最左侧的列:

df.fillna(method='bfill', axis=1).iloc[:, 0]

5
非常棒的解决方案,谢谢。 我该如何获取第一个非空值所在列的列名? - RajeshM
1
@RajeshM 要获取第一个非空值的列名,请参考此答案 - Asclepius

14

这种方法非常混乱,首先使用first_valid_index获取有效列,将返回的系列转换为数据框以便我们可以逐行调用apply并使用它来索引回原始数据框:

In [160]:
def func(x):
    if x.values[0] is None:
        return None
    else:
        return df.loc[x.name, x.values[0]]
pd.DataFrame(df.apply(lambda x: x.first_valid_index(), axis=1)).apply(func,axis=1)
​
Out[160]:
0     1
1     3
2     4
3   NaN
dtype: float64

编辑

稍微更干净的方法:

In [12]:
def func(x):
    if x.first_valid_index() is None:
        return None
    else:
        return x[x.first_valid_index()]
df.apply(func, axis=1)

Out[12]:
0     1
1     3
2     4
3   NaN
dtype: float64

在这里将df作为全局变量使用让我有点不舒服。实际上,你需要结合@yangjie的答案和这个答案。我将发布一个组合,但如果你认为有更好的部分,可以随意借鉴。 - LondonRob
@LondonRob 我认为它应该只在 df 上迭代一次。 - EdChum
现在你说的话才是正解。这真的很好。+1(为什么不直接摧毁你的第一次尝试呢?) - LondonRob
1
@LondonRob 我喜欢展示我的思考过程,这展现了一种逐步改进答案的方法。有时仅展示最终答案并不能演示如何调整你的方法和对反馈的响应。 - EdChum

14

我认为这比所有提出的方法都快,我会在这里发表意见。 argmin以向量化的方式给出了np.isnan结果中每行第一个False值的索引,这是最困难的部分。它仍然依赖于Python循环来提取值,但查找非常快速:

def get_first_non_null(df):
    a = df.values
    col_index = np.isnan(a).argmin(axis=1)
    return [a[row, col] for row, col in enumerate(col_index)]

编辑: 这里有一个完全向量化的解决方案,根据输入形状,速度可能会更快。以下是更新后的基准测试结果。

def get_first_non_null_vec(df):
    a = df.values
    n_rows, n_cols = a.shape
    col_index = np.isnan(a).argmin(axis=1)
    flat_index = n_cols * np.arange(n_rows) + col_index
    return a.ravel()[flat_index]

如果一行完全为空,则相应的值也将为空。这是与unutbu的解况对比:

df = pd.DataFrame(np.random.choice([1, np.nan], (10000, 1500), p=(0.01, 0.99)))
#%timeit df.stack().groupby(level=0).first().reindex(df.index)
%timeit get_first_non_null(df)
%timeit get_first_non_null_vec(df)
1 loops, best of 3: 220 ms per loop
100 loops, best of 3: 16.2 ms per loop
100 loops, best of 3: 12.6 ms per loop
In [109]:


df = pd.DataFrame(np.random.choice([1, np.nan], (100000, 150), p=(0.01, 0.99)))
#%timeit df.stack().groupby(level=0).first().reindex(df.index)
%timeit get_first_non_null(df)
%timeit get_first_non_null_vec(df)
1 loops, best of 3: 246 ms per loop
10 loops, best of 3: 48.2 ms per loop
100 loops, best of 3: 15.7 ms per loop


df = pd.DataFrame(np.random.choice([1, np.nan], (1000000, 15), p=(0.01, 0.99)))
%timeit df.stack().groupby(level=0).first().reindex(df.index)
%timeit get_first_non_null(df)
%timeit get_first_non_null_vec(df)
1 loops, best of 3: 326 ms per loop
1 loops, best of 3: 326 ms per loop
10 loops, best of 3: 35.7 ms per loop

1
在所有的答案中,这个是最快的,速度是其他答案的5到10倍。 - user1367204
在return语句中需要进行小修正 = [a[row, col] for row, col in enumerate(col_index)] - user1367204
谢谢@JoeCondron - 我自己尝试过,确实更快了,感谢您的添加。 - Dave Challis
很好。你能把它改成被接受的答案吗?我不确定该怎么做。 - JoeCondron
一个打字错误:%timeit get_frst_non_null(df) - yangjie
两个拼写错误.. 我需要更加小心。谢谢你指出来。 - JoeCondron

13

以下是另一种方法:

In [183]: df.stack().groupby(level=0).first().reindex(df.index)
Out[183]: 
0     1
1     3
2     4
3   NaN
dtype: float64

这里的想法是使用 stack 将列转换为行索引层:


In [184]: df.stack()
Out[184]: 
0  A    1
   C    2
1  B    3
2  B    4
   C    5
dtype: float64

现在,如果你按照第一行层级进行分组 -- 即原始索引 -- 并从每个组中取第一个值,你基本上会得到所需的结果:

In [185]: df.stack().groupby(level=0).first()
Out[185]: 
0    1
1    3
2    4
dtype: float64

我们需要做的就是重新索引结果(使用原始索引),以便包含完全为NaN的行:

df.stack().groupby(level=0).first().reindex(df.index)

这是一个非常好的指南,教你如何动态地从n列中填充单个列,使其包含非空字符串或数字值-正如OP所要求的那样。 - Lindsay Veazey

5

axis=1中的groupby

如果我们传递一个返回相同值的可调用函数,我们就可以将所有列分组在一起。这使我们可以使用groupby.agg,它提供了简化此过程的first方法。

df.groupby(lambda x: 'Z', 1).first()

     Z
0  1.0
1  3.0
2  4.0
3  NaN

这将返回一个数据帧,其中包含我在可调用函数中返回的列名


lookupnotnaidxmax

df.lookup(df.index, df.notna().idxmax(1))

array([ 1.,  3.,  4., nan])

argmin and slicing

v = df.values
v[np.arange(len(df)), np.isnan(v).argmin(1)]

array([ 1.,  3.,  4., nan])

5

这并不是什么新鲜的东西,它结合了@yangie的方法和列表推导式的最佳部分,以及@EdChum的df.apply方法,我认为这是最容易理解的。

首先,我们想从哪些列中挑选值?

In [95]: pick_cols = df.apply(pd.Series.first_valid_index, axis=1)

In [96]: pick_cols
Out[96]: 
0       A
1       B
2       B
3    None
dtype: object

现在我们如何选择这些值?
In [100]: [df.loc[k, v] if v is not None else None 
    ....:     for k, v in pick_cols.iteritems()]
Out[100]: [1.0, 3.0, 4.0, None]

这个没问题,但我们真正想要的是索引与原始的 DataFrame 相匹配:
In [98]: pd.Series({k:df.loc[k, v] if v is not None else None
   ....:     for k, v in pick_cols.iteritems()})
Out[98]: 
0     1
1     3
2     4
3   NaN
dtype: float64

@AndyHayden 我有点惊讶,居然没有从 pick_cols 到最终结果的方法。你认为这个操作应该叫什么? - LondonRob
我更新了我的答案,这只迭代一次,你必须测试first_valid_index是否为None来处理所有NaN的行。 - EdChum

2

JoeCondron的回答(编辑前!)很棒,但是通过避免非向量化枚举,仍有很大的改进空间:

def get_first_non_null_vect(df):
    a = df.values
    col_index = np.isnan(a).argmin(axis=1)
    return a[np.arange(a.shape[0]), col_index]

如果DataFrame相对平坦,那么改进的幅度就比较小:

In [4]: df = pd.DataFrame(np.random.choice([1, np.nan], (10000, 1500), p=(0.01, 0.99)))

In [5]: %timeit get_first_non_null(df)
10 loops, best of 3: 34.9 ms per loop

In [6]: %timeit get_first_non_null_vect(df)
10 loops, best of 3: 31.6 ms per loop

...但在精简的数据框中可能会很重要:

In [7]: df = pd.DataFrame(np.random.choice([1, np.nan], (10000, 15), p=(0.1, 0.9)))

In [8]: %timeit get_first_non_null(df)
100 loops, best of 3: 3.75 ms per loop

In [9]: %timeit get_first_non_null_vect(df)
1000 loops, best of 3: 718 µs per loop

与JoeCondron的向量化版本相比,运行时间非常相似(对于较小的数据框来说,这仍然稍微快一些,而对于大型数据框来说则稍微慢一些)。


2

这里有一个一行解决方案:

[row[row.first_valid_index()] if row.first_valid_index() else None for _, row in df.iterrows()]

编辑:

这个解决方案迭代了df的每一行。 row.first_valid_index()返回第一个非NA / null值的标签,这将用作索引以获取每行中的第一个非null项。

如果行中没有非null值,则row.first_valid_index()将为None,因此无法用作索引,因此我需要一个if-else语句。

我将所有内容都打包到列表推导式中以简洁明了。


1
这很棒。稍微解释一下正在发生的事情会使这个答案变得非常有用。而且,不得不检查 first_valid_index() 两次真是太遗憾了。也许如果你失去一点简洁性,你会获得可读性和效率。 - LondonRob
是的,我也不喜欢两次使用 first_valid_index()。谢谢你的建议,也许我稍后会更新。 - yangjie

1
df=pandas.DataFrame({'A':[1, numpy.nan, numpy.nan, numpy.nan], 'B':[numpy.nan, 3, 4, numpy.nan], 'C':[2, numpy.nan, 5, numpy.nan]})

df
     A    B    C
0  1.0  NaN  2.0
1  NaN  3.0  NaN
2  NaN  4.0  5.0
3  NaN  NaN  NaN

df.apply(lambda x: numpy.nan if all(x.isnull()) else x[x.first_valid_index()], axis=1).tolist()
[1.0, 3.0, 4.0, nan]

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