pandas 点阵矢量化

iswrvxsc  于 2024-01-04  发布在  其他
关注(0)|答案(2)|浏览(148)

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

  1. import pandas as pd
  2. from datetime import datetime
  3. # Sample data
  4. data = {
  5. 'datetime': [
  6. datetime(2023, 11, 11, 8, 0, 0),
  7. datetime(2023, 11, 11, 8, 5, 0),
  8. datetime(2023, 11, 11, 8, 10, 0),
  9. datetime(2023, 11, 11, 8, 15, 0),
  10. datetime(2023, 11, 11, 8, 20, 0),
  11. datetime(2023, 11, 11, 8, 25, 0),
  12. datetime(2023, 11, 11, 8, 30, 0),
  13. datetime(2023, 11, 11, 8, 35, 0),
  14. datetime(2023, 11, 11, 8, 40, 0),
  15. datetime(2023, 11, 11, 8, 45, 0),
  16. datetime(2023, 11, 11, 8, 50, 0),
  17. ],
  18. 'value': [1, 3, 4, 2, -1, 1, 0, 2, -3, 0, -3],
  19. }
  20. # Create the DataFrame
  21. df = pd.DataFrame(data)
  22. df['value_timespan'] = -1
  23. # Initialize variables to keep track of the last positive value and its timestamp
  24. last_positive_value = None
  25. last_positive_timestamp = None
  26. # Iterate through the DataFrame
  27. for index, row in df.iterrows():
  28. if row['value'] > 0:
  29. if last_positive_value is not None:
  30. # Calculate the time span between the current positive value and the last positive value
  31. time_difference = (row['datetime'] - last_positive_timestamp).total_seconds()
  32. df.at[index, 'value_timespan'] = time_difference
  33. last_positive_value = None
  34. last_positive_timestamp = None
  35. else:
  36. last_positive_value = row['value']
  37. last_positive_timestamp = row['datetime']
  38. if row['value'] < 0:
  39. last_positive_value = None
  40. last_positive_timestamp = None
  41. print(df)

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

  1. datetime | value | value_timespan
  2. --------------------------------------------
  3. 0 2023-11-11 08:00:00 | 1 | -1
  4. 1 2023-11-11 08:05:00 | 3 | 300
  5. 2 2023-11-11 08:10:00 | 4 | -1
  6. 3 2023-11-11 08:15:00 | 2 | 300
  7. 4 2023-11-11 08:20:00 | -1 | -1
  8. 5 2023-11-11 08:25:00 | 1 | -1
  9. 6 2023-11-11 08:30:00 | 0 | -1
  10. 7 2023-11-11 08:35:00 | 2 | 600
  11. 8 2023-11-11 08:40:00 | -3 | -1
  12. 9 2023-11-11 08:45:00 | 0 | -1
  13. 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)形成一对

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


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

l2osamch

l2osamch1#

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

矢量化解决方案

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

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

字符串

简化设置

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

  1. def op(value):
  2. t = pd.date_range('2023-11-11 08:00:00', freq='5min', periods=len(value))
  3. return pd.DataFrame({'t': t, 'value': value})
  4. op1 = op([1, 3, 4, 2, -1, 1, 0, 2, -3, 0, -3])
  5. op2 = op([1, 3, 1, -2, -1, 1, 0, 0, 3, 0, -3])
  6. def gen(n):
  7. np.random.seed(0)
  8. return op(np.random.randint(-10, 100, n))

测试

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

速度

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

(详细)说明

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

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


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

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


有了这个:

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


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

  1. 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

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


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

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


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

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


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

  1. ix = np.flatnonzero(np.bitwise_xor.accumulate(z))
  2. >>> ix
  3. 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万行数据,我想这在一个独立的应用程序中已经足够了。

  1. import pandas as pd
  2. from datetime import datetime
  3. CHANGE = 1
  4. NOCHANGE = -1
  5. MAYBE = 0
  6. # Sample data
  7. data = {
  8. 'datetime': [
  9. datetime(2023, 11, 11, 8, 0, 0),
  10. datetime(2023, 11, 11, 8, 5, 0),
  11. datetime(2023, 11, 11, 8, 10, 0),
  12. datetime(2023, 11, 11, 8, 15, 0),
  13. datetime(2023, 11, 11, 8, 20, 0),
  14. datetime(2023, 11, 11, 8, 25, 0),
  15. datetime(2023, 11, 11, 8, 30, 0),
  16. datetime(2023, 11, 11, 8, 35, 0),
  17. datetime(2023, 11, 11, 8, 40, 0),
  18. datetime(2023, 11, 11, 8, 45, 0),
  19. datetime(2023, 11, 11, 8, 50, 0),
  20. ],
  21. 'value': [1, 3, 4, 2, -1, 1, 0, 2, -3, 0, -3]
  22. # 'value': [1, 3, 1, -2, -1, 1, 0, 0, 3, 0, -3]
  23. }
  24. df = pd.DataFrame(data)
  25. # form working df with only non-zero 'value' using copy to maintain index for later re-insertion
  26. df2 = df[df['value'].ne(0)].copy()
  27. #create temp column and mark rows with negative values as NOCHANGE and others as MAYBE
  28. df2['markers'] = NOCHANGE
  29. df2['markers'] = df2['markers'].mask(df2['value'].gt(0), MAYBE)
  30. #loop through df column and mark values to be changed with CHANGE, others with NOCHANGE
  31. prev = CHANGE
  32. res = [] #temp store for modified marks
  33. for entry in df2['markers']:
  34. if entry == NOCHANGE:
  35. res.append(NOCHANGE)
  36. prev = NOCHANGE
  37. elif entry == MAYBE and prev == MAYBE:
  38. res.append(CHANGE)
  39. prev = CHANGE
  40. else:
  41. res.append(NOCHANGE)
  42. prev = MAYBE
  43. df2['markers'] = res
  44. #add timespan to rows marked with CHANGE
  45. df2['markers'] = df2['markers'].mask(df2['markers'].eq(CHANGE), (df2['datetime']-df2['datetime'].shift(1)).dt.total_seconds())
  46. #merge timespan results back into original DF using indices then fill rows missing from DF2 (value 0) with -1
  47. df['timespan'] = df2['markers']
  48. df['timespan'] = df['timespan'].fillna(NOCHANGE).astype(int)
  49. print(df)

字符串
其给出:

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

展开查看全部

相关问题