【问题标题】:How to use df.loc (or some other method) to make a new column based on specific conditions?如何使用 df.loc(或其他方法)根据特定条件创建新列?
【发布时间】:2021-08-27 14:50:54
【问题描述】:

我有一个包含 5 列的数据框,我正在使用 pandas 和 numpy 来编辑和处理数据。

id      calv1      calv2      calv3      calv4 
1  2006-08-29 2007-08-29 2008-08-29 2009-08-29
2         NaT        NaT        NaT        NaT         
3  2006-08-29        NaT        NaT        NaT
4  2006-08-29 2007-08-29 2010-08-29        NaT
5  2006-08-29 2013-08-29        NaT        NaT
6  2006-08-29        NaT 2013-08-29 2013-08-292

我想创建另一个列来计算每个 id 出现的“calv”数量。 但是,如果其他值之间有缺失值,这对我来说很重要,请参见第 6 行。然后我希望有一个 NaN 或其他值表明这不是正确的行。

id      calv1      calv2      calv3      calv4 no_calv
1  2006-08-29 2007-08-29 2008-08-29 2009-08-29       4
2         NaT        NaT        NaT        NaT       0 
3  2006-08-29        NaT        NaT        NaT       1
4  2006-08-29 2007-08-29 2010-08-29        NaT       3
5  2006-08-29 2013-08-29        NaT        NaT       2
6  2006-08-29        NaT 2013-08-29 2013-08-292     NaN    #or some other value

这是我最后一次尝试:

nat = np.datetime64('NaT')

df.loc[
(df["calv1"] == nat) & (df["calv2"] == nat) &
(df["calv3"] == nat) & (df["calv4"] == nat),
"no_calv"] = 0
#1 calvings
df.loc[
(df["calv1"] != nat) & (df["calv2"] == nat) &
(df["calv3"] == nat) & (df["calv4"] == nat),
"no_calv"] = 1
#2 calvings
df.loc[
(df["calv1"] != nat) & (df["calv2"] != nat) &
(df["calv3"] == nat) & (df["calv4"] == nat),
"no_calv"] = 2
#3 calvings
df.loc[
(df["calv1"] != nat) & (df["calv2"] != nat) &
(df["calv3"] != nat) & (df["calv4"] == nat),
"no_calv"] = 3
#4 or more calvings
df.loc[
(df["calv1"] != nat) & (df["calv2"] != nat) &
(df["calv3"] != nat) & (df["calv4"] != nat),
"no_calv"] = 4

但结果是整个“no_calv”列是4.0

我之前尝试过类似

..
(df["calv1"] != "NaT")
..

..
(df["calv1"] != pd.nat)
..

整个列的结果总是 4.0 或只是 NaN。 我似乎找不到告诉 python NaT 值是什么的方法?

对于新的 python 用户有什么提示和技巧吗? 我已经在 SAS 和 Fortran 中使用 if 和 elseif 语句完成了这项工作,但我正在尝试在 Python 中找到最好的方法。

编辑: 我真的很想知道这是否可以通过 if 或 ifelse 语句来完成。

现在我也在想我希望能够在数据框中有其他列,这些列包含额外的信息,但对于这个确切的目的并不需要。一个示例(添加的 yx 列):

id yx       calv1      calv2      calv3      calv4 no_calv
1  27  2006-08-29 2007-08-29 2008-08-29 2009-08-29       4
2  34         NaT        NaT        NaT        NaT       0 
3  89  2006-08-29        NaT        NaT        NaT       1
4  23  2006-08-29 2007-08-29 2010-08-29        NaT       3
5  11  2006-08-29 2013-08-29        NaT        NaT       2
6  43  2006-08-29        NaT 2013-08-29 2013-08-292     NaN    #or some other value

【问题讨论】:

    标签: python pandas dataframe


    【解决方案1】:
    # get the columns that has "calv" in their name
    calv_like = df.filter(like="calv")
    
    # get a boolean frame of whether NaN or not
    nan_mask = calv_like.isna().to_numpy()
    
    # get the rows that has NaN in between
    bad_rows = np.any(nan_mask[:, 1:] < nan_mask[:, :-1], axis=1)
    
    # form the new column with numpy's if-else
    df["no_calv"] = np.where(bad_rows,
                             -1,
                             len(calv_like.columns) - nan_mask.sum(axis=1))
    

    在得到类似 calv 的列之后,我们在它上面形成一个 NaN 掩码。然后,为了检测“坏”行,我们寻找那些在连续单元格中 NaN-ness 增加的行。也就是说,如果我们看到从 NaN 到非 NaN 的变化,那么该行是错误的。最后,np.where(numpy 的 if-else)帮助我们形成了新列:它是坏行吗?然后把 -1 放在那里。如果不是,则在每行中输入非 NaN 的数量,

    得到

       id  yx       calv1       calv2       calv3        calv4  no_calv
    0   1  27  2006-08-29  2007-08-29  2008-08-29   2009-08-29        4
    1   2  34         NaT         NaT         NaT          NaT        0
    2   3  89  2006-08-29         NaT         NaT          NaT        1
    3   4  23  2006-08-29  2007-08-29  2010-08-29          NaT        3
    4   5  11  2006-08-29  2013-08-29         NaT          NaT        2
    5   6  43  2006-08-29         NaT  2013-08-29  2013-08-292       -1
    

    【讨论】:

    • 你的 numpy 解决方案总是让我着迷:)
    • @Ank 哦,谢谢,这不像你所说的那样可读:)
    • (我要注意我一直是一个numpy学习者,这里有很多优秀的用户,我不是其中之一:)
    【解决方案2】:

    要测试一个值是否为NaT,请使用pd.isnull,如this answer 所示。 isnull 匹配 NoneNaNNaT

    您可以构建一个函数来执行此检查并将所有值求和,直到它达到空值。例如:

    import io
    import numpy as np
    import pandas as pd
    df = pd.read_fwf(io.StringIO("""calv1      calv2      calv3      calv4 
    2006-08-29 2007-08-29 2008-08-29 2009-08-29
           NaT        NaT        NaT        NaT         
    2006-08-29        NaT        NaT        NaT
    2006-08-29 2007-08-29 2010-08-29        NaT
    2006-08-29 2013-08-29        NaT        NaT
    2006-08-29        NaT 2013-08-29 2013-08-292"""))
    df = df.replace("NaT", pd.NaT)
    
    def count_non_nat(row):
        count = 0
        for i in row:
            if pd.isnull(i):
                if count < len(row.dropna()):
                    return np.nan
                return count
            count += 1
        return count
    
    # Apply this function row-wise (axis=1)
    df['count'] = df.apply(count_non_nat, axis=1)
    

    输出是一个新列:

      calv1      calv2      calv3      calv4       count
    0 2006-08-29 2007-08-29 2008-08-29 2009-08-29  4
    1 NaT        NaT        NaT        NaT         0
    2 2006-08-29 NaT        NaT        NaT         1
    3 2006-08-29 2007-08-29 2010-08-29 NaT         3
    4 2006-08-29 2013-08-29 NaT        NaT         2
    5 2006-08-29 NaT        2013-08-29 2013-08-292 NaN
    

    【讨论】:

    • 谢谢。但我不希望最后一行(我认为是错误行)中的计数为 1,因此它将与只有 calv1 值的“正确”行相同。
    • 糟糕,我第一次错过了这个要求。我添加了一个检查,如果报告的计数小于len(row.dropna()),那么在NaT 之后有报告日期的值。在这种情况下,我们将 np.nan 返回到数据框。
    【解决方案3】:

    使用pd.Series.last_valid_indexpd.DataFrame.count 的另一种方法:

    >>> df2  = df.copy()
    >>> df2.columns = np.arange(df2.shape[1]) + 1
    >>> mask = (df2.apply(pd.Series.last_valid_index, axis=1).fillna(0) == df2.count(axis=1))
    >>> df.loc[mask, 'no_calv'] = df.notna().sum(1)
    >>> df
             calv1       calv2       calv3        calv4  no_calv
    id                                                          
    1   2006-08-29  2007-08-29  2008-08-29   2009-08-29      4.0
    2          NaN         NaN         NaN          NaN      0.0
    3   2006-08-29         NaN         NaN          NaN      1.0
    4   2006-08-29  2007-08-29  2010-08-29          NaN      3.0
    5   2006-08-29  2013-08-29         NaN          NaN      2.0
    6   2006-08-29         NaN  2013-08-29  2013-08-292      NaN
    

    解释:

    pd.Series.last_valid_index 返回序列中最后一个有效数据的位置。将其应用于您的行将告诉列位置最后一个有效数据在哪里(之后是所有NaNs/NaTs)。

    下面我暂时用整数索引替换了列名,然后在每一行上应用pd.Series.last_valid_index

    >>> df2.columns = np.arange(df2.shape[1]) + 1
    >>> df2
                 1           2           3            4
    id                                                 
    1   2006-08-29  2007-08-29  2008-08-29   2009-08-29
    2          NaN         NaN         NaN          NaN
    3   2006-08-29         NaN         NaN          NaN
    4   2006-08-29  2007-08-29  2010-08-29          NaN
    5   2006-08-29  2013-08-29         NaN          NaN
    6   2006-08-29         NaN  2013-08-29  2013-08-292
    
    >>> df2.apply(pd.Series.last_valid_index, axis=1).fillna(0)
    id
    1    4.0
    2    0.0
    3    1.0
    4    3.0
    5    2.0
    6    4.0
    dtype: float64
    

    所以在第 1 行,最后一个有效数据在第 4 列,在第 2 行没有有效数据,依此类推。

    现在让我们数一下。每行的有效数据:

    >>> df2.count(axis=1)
    id
    1    4
    2    0
    3    1
    4    3
    5    2
    6    3
    dtype: int64
    

    因此,第 1 行有 4 个有效值,第 2 行没有有效值,依此类推。现在,如果所有 NaN/NaT 值都在行尾,则计数应该与我们上面计算的最后一个有效数据位置相匹配:

    >>> df2.apply(pd.Series.last_valid_index, axis=1).fillna(0) == df2.count(axis=1)
    id
    1     True
    2     True
    3     True
    4     True
    5     True
    6    False
    dtype: bool
    

    正如所见,它匹配除最后一行之外的所有行,因为 NaT 出现在最后一行有效值的中间。我们可以用它作为掩码,然后填充总和:

    >>> mask = (df2.apply(pd.Series.last_valid_index, axis=1).fillna(0) == df2.count(axis=1))
    >>> df.loc[mask, 'no_calv'] = df.notna().sum(1)
    >>> df
             calv1       calv2       calv3        calv4  no_calv
    id                                                          
    1   2006-08-29  2007-08-29  2008-08-29   2009-08-29      4.0
    2          NaN         NaN         NaN          NaN      0.0
    3   2006-08-29         NaN         NaN          NaN      1.0
    4   2006-08-29  2007-08-29  2010-08-29          NaN      3.0
    5   2006-08-29  2013-08-29         NaN          NaN      2.0
    6   2006-08-29         NaN  2013-08-29  2013-08-292      NaN
    

    【讨论】:

    • 谢谢。我试过了,但它需要很长时间,因为数据框是 400.000 行。如果数据框包含的列多于所描述的列并且我不想将它们包含在 no_calv 计数中,您将如何使用此方法?
    • 可以先在不同的df中选择要操作的列。喜欢df_temp = df.loc[:, ['calv1','calv2','calv3','calv4']]。然后使用df_temp 作为您的主要df 执行上述步骤。最后,只需将df_temp 中的no_calv 列分配回原来的dfdf['no_calv'] = df_temp['no_calv']
    • 我相信缓慢是由于我在计算计数时所做的 df.transpose 。大型dfs会很慢。将其更改为df2.count(axis=1)。现在应该提高性能。
    【解决方案4】:

    您可以尝试以下操作,df.interpolate

    >>> numeric = df.apply(lambda col: col.dt.day, axis=1)
    # convert to something other than datetime
    
        calv1  calv2  calv3  calv4
    id                            
    1    29.0   29.0   29.0   29.0
    2     NaN    NaN    NaN    NaN
    3    29.0    NaN    NaN    NaN
    4    29.0   29.0   29.0    NaN
    5    29.0   29.0    NaN    NaN
    6    29.0    NaN   29.0   29.0
    
    >>> mask = (
            numeric.isna() != numeric.interpolate(limit_area='inside', axis=1).isna()
        ).any(1)
    >>> mask
    id
    1    False
    2    False
    3    False
    4    False
    5    False
    6     True
    dtype: bool
    
    >>> df.loc[~mask, 'no_calv'] = df.notna().sum(1)
    # Or,
    # df['no_calv'] = np.where(mask, np.nan, df.notna().sum(1))
    >>> df
    
            calv1      calv2      calv3      calv4  no_calv
    id                                                     
    1  2006-08-29 2007-08-29 2008-08-29 2009-08-29      4.0
    2         NaT        NaT        NaT        NaT      0.0
    3  2006-08-29        NaT        NaT        NaT      1.0
    4  2006-08-29 2007-08-29 2010-08-29        NaT      3.0
    5  2006-08-29 2013-08-29        NaT        NaT      2.0
    6  2006-08-29        NaT 2013-08-29 2013-08-29      NaN
    

    interpolate(limit_area='inside') 所做的是,只有在任一侧都有有效值时才会填充nans。 例如:

    >>> numeric
        calv1  calv2  calv3  calv4
    id                            
    1    29.0   29.0   29.0   29.0
    2     NaN    NaN    NaN    NaN
    3    29.0    NaN    NaN    NaN
    4    29.0   29.0   29.0    NaN
    5    29.0   29.0    NaN    NaN
    6    29.0    NaN   29.0   29.0
    
    >>> numeric.interpolate(limit_area='inside', axis=1)
        calv1  calv2  calv3  calv4
    id                            
    1    29.0   29.0   29.0   29.0
    2     NaN    NaN    NaN    NaN
    3    29.0    NaN    NaN    NaN
    4    29.0   29.0   29.0    NaN
    5    29.0   29.0    NaN    NaN
    6    29.0   29.0   29.0   29.0
                 ^
       Only this on is filled
    

    因此,如果我们比较来自numeric 的哪些nan 值与插值的numeric 不匹配,我们可以找到在有效值之间存在nan 值的行。

    【讨论】:

      猜你喜欢
      • 2021-08-27
      • 1970-01-01
      • 2021-08-30
      • 1970-01-01
      • 1970-01-01
      • 2021-12-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多