【问题标题】:Remove all rows between two sentinel strings in a column using pandas (but not the sentinel strings)使用 pandas 删除列中两个标记字符串之间的所有行(但不删除标记字符串)
【发布时间】:2022-01-20 14:55:32
【问题描述】:

给定一个大数据框,我如何删除两个特定字符串之间的所有内容,这里是“开始”和“结束”。昨天,我问了一个类似的问题,但删除了两个字符串(我在下面发布了我的解决方案)。现在我想看看我如何也能保留这些字符串。我一直在尝试使用 itertuples 获取下一行,但我仍然没有弄清楚如何去做。

df1:

   c1     c2
0   1      1
1   2  start
2   3      3
3   4    end
4   5      5
5   6  start
6   7    end
7   8      0

删除字符串和中间内容的代码:

df = pd.DataFrame(
    {'c1': [1, 2, 3, 4, 5, 6, 7, 8], 
     'c2': ['1', 'start', '3', 'end',
            '5', 'start', 'end', 0]})

df2 = copy.copy(df)
flag = False
list_a = []



for j in df.itertuples():
    if j.c2 == 'start':
        flag = True
        list_a.append((j))
    elif j.c2 =='end':
        flag = False
        list_a.append((j))
    elif flag:
        list_a.append((j))

list_a = tuple(list_a) 
to_remove_df = pd.DataFrame(list_a, columns=['index','c1','c2'])
to_remove_df = to_remove_df["c2"]
removed_df = pd.merge(df, to_remove_df, on=["c2"], how="outer", indicator=True).query('_merge != "both"').drop('_merge', 1)

输出df2:

   c1 c2
0   1  1
4   5  5
7   8  0

想要的输出: df1:

   c1     c2
0   1      1
1   2  start
3   4    end
4   5      5
5   6  start
6   7    end
7   8      0

【问题讨论】:

  • Ciao Giorgio,在 start, 2,3,start,end 的情况下应该输出哪一个?
  • Ciao :) 它应该保持不变。它应该总是在最近的开始和结束之间。因此,如果它是“开始,2,3,开始,1,结束”,结果应该是“开始,2,3,开始,结束”。不过要明确一点:在我的数据框中,这不可能发生,开始后总会有结束。
  • 是否总是有一个开始在结束之前和一个结束在开始之后,没有重复? (即不是结束-开始-开始-结束-开始)
  • 从我目前看到的数据来看,总是开始-结束-开始-结束。没有重复,没有丢失的开始或结束。
  • 那么你真的不应该循环使用vectorial solution ;)

标签: python pandas dataframe


【解决方案1】:

假设“开始”和“结束”总是有逻辑顺序(即先开始,后结束,中间没有重复)。您可以简单地使用由 start/end 分隔的奇数块来构建掩码:

mask = df['c2'].isin(['start', 'end']).cumsum().mod(2).astype(bool)

然后使用蒙版进行切片:

包括边界:

df[~(mask & mask.shift())]

输出:

   c1     c2
0   1      1
1   2  start
3   4    end
4   5      5
5   6  start
6   7    end
7   8      0

排除边界:

df[~(mask | mask.shift())]

输出:

   c1 c2
0   1  1
4   5  5
7   8  0

【讨论】:

  • mod(2) 很聪明,但它依赖于每个“开始”标签后面紧跟着一个且只有一个“结束”标签,否则它会中断。
  • @smci 因此我最初的免责声明 ;) 然而这似乎是最合理的
  • @mozway 这看起来很棒,只有两个问题。例如,如果在开始之前有一个“结束”怎么办?如果“开始”和“结束”在 2 个不同的列上,它会如何工作?
  • 1- 那么你需要反转掩码。需要时,您可以以编程方式确定第一个值是什么。 2-只需独立选择开始和结束,然后将它们添加到cumsum
【解决方案2】:

您只需将开始和停止位置的索引添加到a

改变这个:

for j in df.itertuples():
    if j.c2 == 'start':
        flag = True
        list_a.append((j))
    elif j.c2 =='end':
        flag = False
        list_a.append((j))
    elif flag:
        list_a.append((j))

到这里:

for j in df.itertuples():
    if j.c2 == 'start':
        flag = True
    elif j.c2 =='end':
        flag = False
    elif flag:
        list_a.append((j))

【讨论】:

    【解决方案3】:

    这是一个可能的矢量解决方案,应该很容易理解。

    import pandas as pd
    import numpy as np
    # data
    df = pd.DataFrame(
        {'c1': [1, 2, 3, 4, 5, 6, 7, 8], 
         'c2': ['1', 'start', '3', 'end',
                '5', 'start', 'end', 0]})
    

    现在我想标记包含 startend 的序列。为了做到这一点,我首先在有 start 的地方分配 1,在有 end 的地方分配 0。然后我转发填写nan

    diz = {"start": 1, "end": 0}
    df["mask"] = df["c2"].map(diz).ffill()
    

    输出在哪里

       c1     c2  mask
    0   1      1   NaN
    1   2  start   1.0
    2   3      3   1.0
    3   4    end   0.0
    4   5      5   0.0
    5   6  start   1.0
    6   7    end   0.0
    7   8      0   0.0
    

    之后,我在有 end 的地方设置 1,否则我不修改,并用 0 填充 nan

    df["mask"] = np.where(df["c2"].eq("end"), 1, df["mask"])
    df["mask"] = df["mask"].fillna(0)
    

    这里是输出

       c1     c2  mask
    0   1      1   0.0
    1   2  start   1.0
    2   3      3   1.0
    3   4    end   1.0
    4   5      5   0.0
    5   6  start   1.0
    6   7    end   1.0
    7   8      0   0.0
    

    接下来我要删除所有mask 为1 且c2 不是startend 的元素。

    df["to_remove"] = df["mask"].eq(1) &  ~df["c2"].isin(['start', 'end'])
    

    这里是输出

       c1     c2  mask  to_remove
    0   1      1   0.0      False
    1   2  start   1.0      False
    2   3      3   1.0       True
    3   4    end   1.0      False
    4   5      5   0.0      False
    5   6  start   1.0      False
    6   7    end   1.0      False
    7   8      0   0.0      False
    

    最后我删除所有 to_remove True 并删除我们在途中添加的列

    df = df[df["to_remove"].ne(True)]\
        .drop(columns=["mask", "to_remove"])
    

    你得到了预期的输出

       c1     c2
    0   1      1
    1   2  start
    3   4    end
    4   5      5
    5   6  start
    6   7    end
    7   8      0
    

    【讨论】:

      猜你喜欢
      • 2018-06-06
      • 2022-06-23
      • 1970-01-01
      • 2016-12-16
      • 2011-01-09
      • 1970-01-01
      • 2010-09-19
      • 2017-09-06
      相关资源
      最近更新 更多