Pandas - 查找和索引与行序列模式匹配的行

Posted

技术标签:

【中文标题】Pandas - 查找和索引与行序列模式匹配的行【英文标题】:Pandas - Find and index rows that match row sequence pattern 【发布时间】:2018-07-20 12:23:21 【问题描述】:

我想在数据框中的分类变量中找到一个模式,该模式沿行向下。我可以看到如何使用 Series.shift() 向上/向下查找并使用布尔逻辑来查找模式,但是,我想使用分组变量来执行此操作,并标记作为模式一部分的所有行,而不仅仅是起始行。

代码:

import pandas as pd
from numpy.random import choice, randn
import string

# df constructor
n_rows = 1000
df = pd.DataFrame('date_time': pd.date_range('2/9/2018', periods=n_rows, freq='H'),
                   'group_var': choice(list(string.ascii_uppercase), n_rows),
                   'row_pat': choice([0, 1, 2, 3], n_rows),
                   'values': randn(n_rows))

# sorting 
df.sort_values(by=['group_var', 'date_time'], inplace=True)
df.head(10)

返回这个:

我可以通过这个找到模式的开始(虽然没有分组):

# the row ordinal pattern to detect
p0, p1, p2, p3 = 1, 2, 2, 0 

# flag the row at the start of the pattern
df['pat_flag'] = \
df['row_pat'].eq(p0) & \
df['row_pat'].shift(-1).eq(p1) & \
df['row_pat'].shift(-2).eq(p2) & \
df['row_pat'].shift(-3).eq(p3)

df.head(10)

我想不通的是,如何仅使用“group_var”来执行此操作,而不是为模式的开头返回 True,而是为属于模式的所有行返回 true。

感谢有关如何解决此问题的任何提示!

谢谢...

【问题讨论】:

【参考方案1】:

我认为您有两种方法 - 更简单、更慢的解决方案或更快更复杂的解决方案。

使用Rolling.apply 和测试模式 将0s 替换为NaNs 为mask 使用bfilllimit(与fillnamethod='bfill' 相同)重复使用1 然后fillna NaNs 到0 astype 最后一次转换为布尔值
pat = np.asarray([1, 2, 2, 0])
N = len(pat)


df['rm0'] = (df['row_pat'].rolling(window=N , min_periods=N)
                          .apply(lambda x: (x==pat).all())
                          .mask(lambda x: x == 0) 
                          .bfill(limit=N-1)
                          .fillna(0)
                          .astype(bool)
             )

如果是重要的性能,请使用strides,link 的解决方案已修改:

使用rolling window 方法 与 pattaern 比较并返回 Trues 以匹配 all 通过np.mgrid 和索引获取首次出现的索引 使用列表理解创建所有索引 比较numpy.in1d并创建新列
def rolling_window(a, window):
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    c = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
    return c

arr = df['row_pat'].values
b = np.all(rolling_window(arr, N) == pat, axis=1)
c = np.mgrid[0:len(b)][b]

d = [i  for x in c for i in range(x, x+N)]
df['rm2'] = np.in1d(np.arange(len(arr)), d)

另一种解决方案,谢谢@divakar:

arr = df['row_pat'].values
b = np.all(rolling_window(arr, N) == pat, axis=1)

m = (rolling_window(arr, len(pat)) == pat).all(1)
m_ext = np.r_[m,np.zeros(len(arr) - len(m), dtype=bool)]
df['rm1'] = binary_dilation(m_ext, structure=[1]*N, origin=-(N//2))

时间安排

np.random.seed(456) 

import pandas as pd
from numpy.random import choice, randn
from scipy.ndimage.morphology import binary_dilation
import string

# df constructor
n_rows = 100000
df = pd.DataFrame('date_time': pd.date_range('2/9/2018', periods=n_rows, freq='H'),
                   'group_var': choice(list(string.ascii_uppercase), n_rows),
                   'row_pat': choice([0, 1, 2, 3], n_rows),
                   'values': randn(n_rows))

# sorting 
df.sort_values(by=['group_var', 'date_time'], inplace=True)

def rolling_window(a, window):
    shape = a.shape[:-1] + (a.shape[-1] - window + 1, window)
    strides = a.strides + (a.strides[-1],)
    c = np.lib.stride_tricks.as_strided(a, shape=shape, strides=strides)
    return c


arr = df['row_pat'].values
b = np.all(rolling_window(arr, N) == pat, axis=1)

m = (rolling_window(arr, len(pat)) == pat).all(1)
m_ext = np.r_[m,np.zeros(len(arr) - len(m), dtype=bool)]
df['rm1'] = binary_dilation(m_ext, structure=[1]*N, origin=-(N//2))

arr = df['row_pat'].values
b = np.all(rolling_window(arr, N) == pat, axis=1)
c = np.mgrid[0:len(b)][b]

d = [i  for x in c for i in range(x, x+N)]
df['rm2'] = np.in1d(np.arange(len(arr)), d)

print (df.iloc[460:480])

                date_time group_var  row_pat    values    rm0    rm1    rm2
12045 2019-06-25 21:00:00         A        3 -0.081152  False  False  False
12094 2019-06-27 22:00:00         A        1 -0.818167  False  False  False
12125 2019-06-29 05:00:00         A        0 -0.051088  False  False  False
12143 2019-06-29 23:00:00         A        0 -0.937589  False  False  False
12145 2019-06-30 01:00:00         A        3  0.298460  False  False  False
12158 2019-06-30 14:00:00         A        1  0.647161  False  False  False
12164 2019-06-30 20:00:00         A        3 -0.735538  False  False  False
12210 2019-07-02 18:00:00         A        1 -0.881740  False  False  False
12341 2019-07-08 05:00:00         A        3  0.525652  False  False  False
12343 2019-07-08 07:00:00         A        1  0.311598  False  False  False
12358 2019-07-08 22:00:00         A        1 -0.710150   True   True   True
12360 2019-07-09 00:00:00         A        2 -0.752216   True   True   True
12400 2019-07-10 16:00:00         A        2 -0.205122   True   True   True
12404 2019-07-10 20:00:00         A        0  1.342591   True   True   True
12413 2019-07-11 05:00:00         A        1  1.707748  False  False  False
12506 2019-07-15 02:00:00         A        2  0.319227  False  False  False
12527 2019-07-15 23:00:00         A        3  2.130917  False  False  False
12600 2019-07-19 00:00:00         A        1 -1.314070  False  False  False
12604 2019-07-19 04:00:00         A        0  0.869059  False  False  False
12613 2019-07-19 13:00:00         A        2  1.342101  False  False  False

In [225]: %%timeit
     ...: df['rm0'] = (df['row_pat'].rolling(window=N , min_periods=N)
     ...:                           .apply(lambda x: (x==pat).all())
     ...:                           .mask(lambda x: x == 0) 
     ...:                           .bfill(limit=N-1)
     ...:                           .fillna(0)
     ...:                           .astype(bool)
     ...:              )
     ...: 
1 loop, best of 3: 356 ms per loop

In [226]: %%timeit
     ...: arr = df['row_pat'].values
     ...: b = np.all(rolling_window(arr, N) == pat, axis=1)
     ...: c = np.mgrid[0:len(b)][b]
     ...: d = [i  for x in c for i in range(x, x+N)]
     ...: df['rm2'] = np.in1d(np.arange(len(arr)), d)
     ...: 
100 loops, best of 3: 7.63 ms per loop

In [227]: %%timeit
     ...: arr = df['row_pat'].values
     ...: b = np.all(rolling_window(arr, N) == pat, axis=1)
     ...: 
     ...: m = (rolling_window(arr, len(pat)) == pat).all(1)
     ...: m_ext = np.r_[m,np.zeros(len(arr) - len(m), dtype=bool)]
     ...: df['rm1'] = binary_dilation(m_ext, structure=[1]*N, origin=-(N//2))
     ...: 
100 loops, best of 3: 7.25 ms per loop

【讨论】:

将赏金授予@jezrael,因为它为模式的所有成员正确设置了标志,而不仅仅是开始。它还包括 3 种方法,以及每种方法的时间安排。由于在我的情况下可能有 100 万行,因此替代方法将很有用。再次感谢所有参与并提交回复的人! @RandallGoodwin - 谢谢。这是一个非常有趣的问题,很高兴能提供帮助! 每次我需要有关代码/语法的帮助时,我都会参考您的旧答案。它确实有助于我很好地理解熊猫,并以您的回答为基准为其他用户提供解决方案:) @Pygirl - 我也是;)有很多好的答案,但不容易找到;)【参考方案2】:

您可以使用 pd.rolling() 方法,然后简单地将它返回的数组与包含您尝试匹配的模式的数组进行比较。

pattern = np.asarray([1.0, 2.0, 2.0, 0.0])
n_obs = len(pattern)
df['rolling_match'] = (df['row_pat']
                       .rolling(window=n_obs , min_periods=n_obs)
                       .apply(lambda x: (x==pattern).all())
                       .astype(bool)             # All as bools
                       .shift(-1 * (n_obs - 1))  # Shift back
                       .fillna(False)            # convert NaNs to False
                       )

在此处指定最小周期很重要,以确保您只找到完全匹配(因此当形状未对齐时相等性检查不会失败)。 apply 函数在两个数组之间进行成对检查,​​然后我们使用 .all() 来确保所有匹配。我们转换为布尔值,然后在函数上调用 shift 以将其变为“前瞻性”指标,而不是仅在事后发生。

此处提供有关滚动功能的帮助 - https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.rolling.html

【讨论】:

【参考方案3】:

这行得通。 它的工作原理是这样的: a) 对于每个组,它需要一个大小为 4 的窗口并扫描该列,直到它以精确的顺​​序找到组合 (1,2,2,0)。一旦找到序列,它就会用 1 填充新列 'pat_flag' 的相应索引值。 b) 如果没有找到组合,则用 0 填充列。

pattern = [1,2,2,0]
def get_pattern(df):

    df = df.reset_index(drop=True)
    df['pat_flag'] = 0

    get_indexes = [] 
    temp = []

    for index, row in df.iterrows():

        mindex = index +1

        # get the next 4 values
        for j in range(mindex, mindex+4):

            if j == df.shape[0]:
                break
            else:
                get_indexes.append(j)
                temp.append(df.loc[j,'row_pat'])

        # check if sequence is matched
        if temp == pattern:
            df.loc[get_indexes,'pat_flag'] = 1
        else:
            # reset if the pattern is not found in given window
            temp = []
            get_indexes = []

    return df

# apply function to the groups
df = df.groupby('group_var').apply(get_pattern)

## snippet of output 

        date_time       group_var   row_pat     values  pat_flag
41  2018-03-13 21:00:00      C         3       0.731114     0
42  2018-03-14 05:00:00      C         0       1.350164     0
43  2018-03-14 11:00:00      C         1      -0.429754     1
44  2018-03-14 12:00:00      C         2       1.238879     1
45  2018-03-15 17:00:00      C         2      -0.739192     1
46  2018-03-18 06:00:00      C         0       0.806509     1
47  2018-03-20 06:00:00      C         1       0.065105     0
48  2018-03-20 08:00:00      C         1       0.004336     0

【讨论】:

【参考方案4】:

扩展 Emmet02 的答案:对所有组使用滚动功能并将所有匹配模式索引的 match-column 设置为 1:

pattern = np.asarray([1,2,2,0])

# Create a match column in the main dataframe
df.assign(match=False, inplace=True)

for group_var, group in df.groupby("group_var"):

    # Per group do rolling window matching, the last 
    # values of matching patterns in array 'match'
    # will be True
    match = (
        group['row_pat']
        .rolling(window=len(pattern), min_periods=len(pattern))
        .apply(lambda x: (x==pattern).all())
    )

    # Get indices of matches in current group
    idx = np.arange(len(group))[match == True]

    # Include all indices of matching pattern, 
    # counting back from last index in pattern
    idx = idx.repeat(len(pattern)) - np.tile(np.arange(len(pattern)), len(idx))

    # Update matches
    match.values[idx] = True
    df.loc[group.index, 'match'] = match

df[df.match==True]

编辑:没有 for 循环

# Do rolling matching in group clause
match = (
    df.groupby("group_var")
    .rolling(len(pattern))
    .row_pat.apply(lambda x: (x==pattern).all())
)

# Convert NaNs
match = (~match.isnull() & match)

# Get indices of matches in current group
idx = np.arange(len(df))[match]
# Include all indices of matching pattern
idx = idx.repeat(len(pattern)) - np.tile(np.arange(len(pattern)), len(idx))

# Mark all indices that are selected by "idx" in match-column
df = df.assign(match=df.index.isin(df.index[idx]))

【讨论】:

感谢所有全面的回复!我将在周末进行测试,然后根据速度和编程简单性的结合来奖励赏金(我第一次使用赏金......似乎引起了一些关注!:)。【参考方案5】:

您可以通过定义自定义聚合函数,然后在 group_by 语句中使用它,最后将其合并回原始数据框来实现。像这样的:

聚合函数:

def pattern_detect(column):
 # define any other pattern to detect here
 p0, p1, p2, p3 = 1, 2, 2, 0       
 column.eq(p0) & \
 column.shift(-1).eq(p1) & \
 column.shift(-2).eq(p2) & \
 column.shift(-3).eq(p3)
 return column.any()

接下来使用按功能分组:

grp = df.group_by('group_var').agg([patter_detect])['row_pat']

现在将其合并回原始数据框:

df = df.merge(grp, left_on='group_var',right_index=True, how='left')

【讨论】:

以上是关于Pandas - 查找和索引与行序列模式匹配的行的主要内容,如果未能解决你的问题,请参考以下文章

python--pandas切片

Pandas:在多列中查找具有匹配值的行的 Pythonic 方法(分层条件)

Pandas - 在两列中查找具有匹配值的行并在另一列中相乘

Pandas - 在 DataFrame 中的任何位置查找值索引

pandas重新索引

Pandas.DataFrame 的 iterrows()方法详解