pandas 点阵矢量化

iswrvxsc  于 6个月前  发布在  其他
关注(0)|答案(2)|浏览(60)

在我下面的代码中,我创建了一个DataFrame df,其中包含包含value和时间戳的示例数据。此外,我还添加了一个新列'value_timespan',并将其初始化为-1。然后,我遍历DataFrame以计算'value'列中连续正值之间的时间跨度。
需要注意两点,第一是即使有多个连续的正值,也只计算连续的正值形成对的时间差,不计算后续的正值不形成对的时间差(见下面的例子)。
第二,在连续的正值之间可以有任意数量的零。

import pandas as pd
from datetime import datetime

# Sample data
data = {    
    'datetime': [
        datetime(2023, 11, 11, 8, 0, 0),
        datetime(2023, 11, 11, 8, 5, 0),
        datetime(2023, 11, 11, 8, 10, 0),
        datetime(2023, 11, 11, 8, 15, 0),
        datetime(2023, 11, 11, 8, 20, 0),
        datetime(2023, 11, 11, 8, 25, 0),
        datetime(2023, 11, 11, 8, 30, 0),
        datetime(2023, 11, 11, 8, 35, 0),
        datetime(2023, 11, 11, 8, 40, 0),
        datetime(2023, 11, 11, 8, 45, 0),
        datetime(2023, 11, 11, 8, 50, 0),
    ],
    'value': [1,  3, 4, 2, -1, 1, 0, 2, -3, 0, -3],                   
}

# Create the DataFrame
df = pd.DataFrame(data)

df['value_timespan'] = -1
# Initialize variables to keep track of the last positive value and its timestamp
last_positive_value = None
last_positive_timestamp = None
# Iterate through the DataFrame
for index, row in df.iterrows():
    if row['value'] > 0:
        if last_positive_value is not None:
            # Calculate the time span between the current positive value and the last positive value
            time_difference = (row['datetime'] - last_positive_timestamp).total_seconds()
            df.at[index, 'value_timespan'] = time_difference
            last_positive_value = None
            last_positive_timestamp = None
        else:
            last_positive_value = row['value']
            last_positive_timestamp = row['datetime']
    if row['value'] < 0:
        last_positive_value = None
        last_positive_timestamp = None        
print(df)

字符串
它打印出如下结果,(1,3),(4,2),(1,2)被认为是对

datetime        | value | value_timespan
        --------------------------------------------
    0   2023-11-11 08:00:00 |   1   | -1
    1   2023-11-11 08:05:00 |   3   | 300
    2   2023-11-11 08:10:00 |   4   | -1
    3   2023-11-11 08:15:00 |   2   | 300
    4   2023-11-11 08:20:00 |  -1   | -1
    5   2023-11-11 08:25:00 |   1   | -1
    6   2023-11-11 08:30:00 |   0   | -1
    7   2023-11-11 08:35:00 |   2   | 600
    8   2023-11-11 08:40:00 |  -3   | -1
    9   2023-11-11 08:45:00 |   0   | -1
    10  2023-11-11 08:50:00 |  -3   | -1


现在,我想对我的代码进行矢量化。我如何正确地进行矢量化?

更新2023/12/07

例如,对于'value':[1,3,1,-2,-1,1,0,0,3,0,-3],
正确的结果是,因为(1,3),(1,3)形成一对

datetime    | value | timespan
-----------------------------------------
0   2023-11-11 08:00:00 |  1    | -1.0
1   2023-11-11 08:05:00 |  3    | 300.0
2   2023-11-11 08:10:00 |  1    | -1.0
3   2023-11-11 08:15:00 | -2    | -1.0
4   2023-11-11 08:20:00 | -1    | -1.0
5   2023-11-11 08:25:00 |  1    | -1.0
6   2023-11-11 08:30:00 |  0    | -1.0
7   2023-11-11 08:35:00 |  0    | -1.0
8   2023-11-11 08:40:00 |  3    | 900.0
9   2023-11-11 08:45:00 |  0    | -1.0
10  2023-11-11 08:50:00 | -3    | -1.0


我希望我的要求是明确的。

l2osamch

l2osamch1#

编辑新版本,仅使用numpy进行完全矢量化:26 ms,100万行

矢量化解决方案

简洁而快速,在这里。我将在下面提供解释。

def get_timespan(df):
    v = df['value'].to_numpy()
    g = (v < 0).cumsum()
    ipos = np.flatnonzero(v > 0)
    gpx = np.r_[-1, g[ipos]]
    z = gpx[1:] == gpx[:-1]
    chg = np.flatnonzero(~z)
    z[chg[1:]] |= (np.diff(chg) % 2) == 0
    ix = np.flatnonzero(np.bitwise_xor.accumulate(z))
    t = df.iloc[ipos]['t'].to_numpy()
    ts = np.full(len(v), -1)  # or use -1.0 if a float output is desired
    ts[ipos[ix]] = (t[ix] - t[ix - 1]) // 1e9
    return ts

字符串

简化设置

为了测试这一点,我们考虑几个设置:op1是OP提供的第一个示例,op2是第二个示例。然后,gen(n)生成任意大的测试df

def op(value):
    t = pd.date_range('2023-11-11 08:00:00', freq='5min', periods=len(value))
    return pd.DataFrame({'t': t, 'value': value})

op1 = op([1,  3, 4, 2, -1, 1, 0, 2, -3, 0, -3])
op2 = op([1, 3, 1, -2, -1, 1, 0, 0, 3, 0, -3])

def gen(n):
    np.random.seed(0)
    return op(np.random.randint(-10, 100, n))

测试

>>> get_timespan(op1)
array([ -1, 300,  -1, 300,  -1,  -1,  -1, 600,  -1,  -1,  -1])

>>> get_timespan(op2)
array([ -1, 300,  -1,  -1,  -1,  -1,  -1,  -1, 900,  -1,  -1])

# or, to see it as a column of df:
>>> op1.assign(timespan=get_timespan(op1))
                     t  value  timespan
0  2023-11-11 08:00:00      1        -1
1  2023-11-11 08:05:00      3       300
2  2023-11-11 08:10:00      4        -1
3  2023-11-11 08:15:00      2       300
4  2023-11-11 08:20:00     -1        -1
5  2023-11-11 08:25:00      1        -1
6  2023-11-11 08:30:00      0        -1
7  2023-11-11 08:35:00      2       600
8  2023-11-11 08:40:00     -3        -1
9  2023-11-11 08:45:00      0        -1
10 2023-11-11 08:50:00     -3        -1

速度

df = gen(1_000_000)

%timeit get_timespan(df)
25.8 ms ± 118 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

(详细)说明

第一部分是相当习惯的:我们建立连续的非负值组(负值本身属于哪一组并不重要)。我们将使用op1作为例子:

df = op1
v = df['value'].to_numpy()
g = (v < 0).cumsum()
>>> np.c_[v, g].T
array([[ 1,  3,  4,  2, -1,  1,  0,  2, -3,  0, -3],
       [ 0,  0,  0,  0,  1,  1,  1,  1,  2,  2,  3]])


注意前四个值是如何在第1组中组合在一起的,接下来的四个是如何在第2组中组合在一起的(从-1开始)等等。
下一部分是计算这些值中哪些是严格正的。为了可视化索引,我们使用一个小的辅助函数rix,它在索引处产生1,在其他地方产生0(给出一个完整大小的向量):

def rix(ix, v):
    a = np.zeros_like(v)
    a[ix] = 1
    return a


有了这个:

ipos = np.flatnonzero(v > 0)  # 0,1,2,3,5,7
>>> np.c_[v, g, rix(ipos, v)].T
array([[ 1,  3,  4,  2, -1,  1,  0,  2, -3,  0, -3],
       [ 0,  0,  0,  0,  1,  1,  1,  1,  2,  2,  3],
       [ 1,  1,  1,  1,  0,  1,  0,  1,  0,  0,  0]])


从这里开始,我们可以关注gp = g[ipos],即v为正的组值。
现在,一个棘手的部分。我们需要找到ipos的每一个奇数索引,但 * 在每个组 * 内。它是每隔一个ipos,但有时我们必须跳过一个(如果一个组已经改变,我们不能选择第一个)。
为了说明这个问题,假设我们有:

gp = np.array([0,0,1,1,1,2,2,2,2])


在这种情况下,我们想要找到第二个0(索引1),第二个1(索引3),以及第二个和第四个2(索引6,8)。
为此,我们设计了一个掩码,其xor-累加仅在感兴趣的索引处为1
这个掩码z最初是True,除了一个组中的第一个元素之外,gp的所有元素都是True。为了方便起见,我们在开头添加了一个sentinel -1

gpx = np.r_[-1, gp]
z = gpx[1:] == gpx[:-1]
>>> np.c_[gp, z].T
array([[0, 0, 1, 1, 1, 2, 2, 2, 2],
       [0, 1, 0, 1, 1, 0, 1, 1, 1]])


我们还跟踪gp变化的索引:

chg = np.flatnonzero(~z)
>>> chg
array([0, 2, 5])


现在,为了纠正z,我们将前一组长度为偶数的组中的第一位设置为1。然后,我们xor-累加这些位:

z[chg[1:]] |= (np.diff(chg) % 2) == 0
m = np.bitwise_xor.accumulate(z)
>>> np.c_[gp, z, m].T
array([[0, 0, 1, 1, 1, 2, 2, 2, 2],
       [0, 1, 1, 1, 1, 0, 1, 1, 1],
       [0, 1, 0, 1, 0, 0, 1, 0, 1]])


这里就是了,m中的是我们想要的索引。所以,总结一下:

ix = np.flatnonzero(np.bitwise_xor.accumulate(z))
>>> ix
array([1, 3, 6, 8])

回到op1的例子,ix[1, 3, 5](相对于ipos)。
最后,在这些位置的时间跨度简单地是t[ix] - t[ix - 1](其中tdf.iloc[ipos]['t'])。

a11xaf1n

a11xaf1n2#

我不认为代码可以完全矢量化,因为需要连续的对处理和渐进的回看-这就是为什么你使用最后一个_值。首先注意itertuplesiterrows快,因为Pandas Series不是从每一行形成的。但是最好只在提取的Series上做必要的循环,而不是在DF行上。下面的代码这证明了这一点。这在大约0.5秒内处理了100万行数据,我想这在一个独立的应用程序中已经足够了。

import pandas as pd
from datetime import datetime

CHANGE = 1
NOCHANGE = -1
MAYBE = 0

# Sample data
data = {    
    'datetime': [
        datetime(2023, 11, 11, 8, 0, 0),
        datetime(2023, 11, 11, 8, 5, 0),
        datetime(2023, 11, 11, 8, 10, 0),
        datetime(2023, 11, 11, 8, 15, 0),
        datetime(2023, 11, 11, 8, 20, 0),
        datetime(2023, 11, 11, 8, 25, 0),
        datetime(2023, 11, 11, 8, 30, 0),
        datetime(2023, 11, 11, 8, 35, 0),
        datetime(2023, 11, 11, 8, 40, 0),
        datetime(2023, 11, 11, 8, 45, 0),
        datetime(2023, 11, 11, 8, 50, 0),
    ],
   'value': [1,  3, 4, 2, -1, 1, 0, 2, -3, 0, -3]
#  'value': [1, 3, 1, -2, -1, 1, 0, 0, 3, 0, -3]
}

df = pd.DataFrame(data)

# form working df with only non-zero 'value' using copy to maintain index for later re-insertion
df2 = df[df['value'].ne(0)].copy()

#create temp column and mark rows with negative values as NOCHANGE and others as MAYBE
df2['markers'] = NOCHANGE
df2['markers'] = df2['markers'].mask(df2['value'].gt(0), MAYBE)

#loop through df column and mark values to be changed with CHANGE, others with NOCHANGE
prev = CHANGE
res = []        #temp store for modified marks
for entry in df2['markers']:
    if entry == NOCHANGE:
        res.append(NOCHANGE)
        prev = NOCHANGE
    elif entry == MAYBE and prev == MAYBE:
        res.append(CHANGE)
        prev = CHANGE
    else:
        res.append(NOCHANGE)
        prev = MAYBE
df2['markers'] = res

#add timespan to rows marked with CHANGE
df2['markers'] = df2['markers'].mask(df2['markers'].eq(CHANGE), (df2['datetime']-df2['datetime'].shift(1)).dt.total_seconds())

#merge timespan results back into original DF using indices then fill rows missing from DF2 (value 0) with -1
df['timespan'] = df2['markers']
df['timespan'] = df['timespan'].fillna(NOCHANGE).astype(int)

print(df)

字符串
其给出:

datetime  value  timespan
0  2023-11-11 08:00:00      1        -1
1  2023-11-11 08:05:00      3       300
2  2023-11-11 08:10:00      4        -1
3  2023-11-11 08:15:00      2       300
4  2023-11-11 08:20:00     -1        -1
5  2023-11-11 08:25:00      1        -1
6  2023-11-11 08:30:00      0        -1
7  2023-11-11 08:35:00      2       600
8  2023-11-11 08:40:00     -3        -1
9  2023-11-11 08:45:00      0        -1
10 2023-11-11 08:50:00     -3        -1

相关问题