Pandas 将上一期的数据设置为新的 DataFrame 列

Posted

技术标签:

【中文标题】Pandas 将上一期的数据设置为新的 DataFrame 列【英文标题】:Pandas Set Data From the Last Period As New DataFrame Column 【发布时间】:2016-05-18 13:29:24 【问题描述】:

我有一个 Pandas 数据框:

import pandas as pd

df = pd.DataFrame([['A', '2014-01-01', '2014-01-07', 1.2],
                   ['B', '2014-01-01', '2014-01-07', 2.5],
                   ['C', '2014-01-01', '2014-01-07', 3.],
                   ['A', '2014-01-08', '2014-01-14', 13.],
                   ['B', '2014-01-08', '2014-01-14', 2.],
                   ['C', '2014-01-08', '2014-01-14', 1.],
                   ['A', '2014-01-15', '2014-01-21', 10.],
                   ['A', '2014-01-21', '2014-01-27', 98.],
                   ['B', '2014-01-21', '2014-01-27', -5.],
                   ['C', '2014-01-21', '2014-01-27', -72.],
                   ['A', '2014-01-22', '2014-01-28', 8.],
                   ['B', '2014-01-22', '2014-01-28', 25.],
                   ['C', '2014-01-22', '2014-01-28', -23.],
                   ['A', '2014-01-22', '2014-02-22', 8.],
                   ['B', '2014-01-22', '2014-02-22', 25.],
                   ['C', '2014-01-22', '2014-02-22', -23.],
                  ], columns=['Group', 'Start Date', 'End Date', 'Value'])

输出如下所示:

   Group  Start Date    End Date  Value
0      A  2014-01-01  2014-01-07    1.2
1      B  2014-01-01  2014-01-07    2.5
2      C  2014-01-01  2014-01-07    3.0
3      A  2014-01-08  2014-01-14   13.0
4      B  2014-01-08  2014-01-14    2.0
5      C  2014-01-08  2014-01-14    1.0
6      A  2014-01-15  2014-01-21   10.0
7      A  2014-01-21  2014-01-27   98.0
8      B  2014-01-21  2014-01-27   -5.0
9      C  2014-01-21  2014-01-27  -72.0
10     A  2014-01-22  2014-01-28    8.0
11     B  2014-01-22  2014-01-28   25.0
12     C  2014-01-22  2014-01-28  -23.0
13     A  2014-01-22  2014-02-22    8.0
14     B  2014-01-22  2014-02-22   25.0
15     C  2014-01-22  2014-02-22  -23.0

我正在尝试添加一个新列,其中包含上一时期同一组的数据(如果存在)。所以,输出应该是这样的:

   Group  Start Date    End Date  Value   Last Period Value
0      A  2014-01-01  2014-01-07    1.2                 NaN
1      B  2014-01-01  2014-01-07    2.5                 NaN
2      C  2014-01-01  2014-01-07    3.0                 NaN
3      A  2014-01-08  2014-01-14   13.0                 1.2
4      B  2014-01-08  2014-01-14    2.0                 2.5   
5      C  2014-01-08  2014-01-14    1.0                 3.0
6      A  2014-01-15  2014-01-21   10.0                13.0 
7      A  2014-01-21  2014-01-27   98.0                 NaN
8      B  2014-01-21  2014-01-27   -5.0                 NaN
9      C  2014-01-21  2014-01-27  -72.0                 NaN
10     A  2014-01-22  2014-01-28    8.0                10.0     
11     B  2014-01-22  2014-01-28   25.0                 NaN
12     C  2014-01-22  2014-01-28  -23.0                 NaN
13     A  2014-01-22  2014-02-22    8.0                 NaN   
14     B  2014-01-22  2014-02-22   25.0                 NaN   
15     C  2014-01-22  2014-02-22  -23.0                 NaN   

请注意,具有 NaN 的行在同一组中没有对应的值,并且是在最后一个时期。因此,跨越 7 天(一周)的行需要与前一周的同一组的同一行匹配。

【问题讨论】:

“前期”是如何定义的?周期是否等同于日历周或可以有任意周期?如果它们始终等于一周,则将期间开始日期转换为周数可能会有所帮助。 ***.com/questions/31181295/… 周期可以是可变的(由天数定义)。因此,行索引#3 是 7 天,最后 7 天(同组)正好在行索引 #0 之前。因此,组必须相同,天数必须相同,并且两个周期必须连续(当前周期的开始日期是上一个周期的结束日期的一天后)。跨度> 在使用周数时,周数是在不断增加还是从 1 月 1 日的 1 开始?同样,每个周期的长度是可变的,所以我不确定这是否可行。 【参考方案1】:

最简单的方法(虽然具有二次复杂度)如下:

import datetime as dt
df.sd = pd.to_datetime(df['Start Date'])
df.ed = pd.to_datetime(df['End Date'])

def find_previous_period(row):
  prev_sd = row.sd - dt.timedelta(days=7)
  prev_ed = row.ed - dt.timedelta(days=7)
  prev_period = df[(df.sd == prev_sd) & (df.ed == prev_ed) & (df.Group == row.Group)]
  if prev_period.size > 0:
    return prev_period.irow(0).Value

df['Last Period Value'] = df.apply(find_previous_period, axis=1)

如果您有大量数据,可能需要一些更优雅的解决方案。


更新天数需要相同的要求(来自 cmets):

def find_previous_period(row):
  delta = row.ed - row.sd + dt.timedelta(days=1)
  prev_sd = row.sd - delta
  prev_ed = row.ed - delta
  prev_period = df[(df.sd == prev_sd) & (df.ed == prev_ed) & (df.Group == row.Group)]
  if prev_period.size > 0:
    return prev_period.irow(0).Value

【讨论】:

确实,我有很多数据,所以我希望找到比 n-squared 性能更优雅、更快的解决方案。【参考方案2】:

如果我正确理解了您对“句号”的定义,这将起作用并且应该很快。

  df['sd'] = pd.to_datetime(df['Start Date'])
  df['sd2'] = df.sd - dt.timedelta(days=1)
  df['ed2'] = df.ed - dt.timedelta(days=1)

  df2 = pd.merge(df, df[['sd2','ed2','Value', 'Group']], left_on=['sd','Group', 'ed'], 
           right_on=['sd2','Group', 'ed2'], how='outer', copy=False)

您必须清理列名/删除多余的列。

【讨论】:

这很接近。两个期间(当前和最后一个)内的天数也必须相同。这是另外两个时期的连续性。 每一行的 timedelta 天数应等于 (end - start + 1)。否则,这些时期将是重叠的,而不是连续的和连续的(即相邻的几周)。【参考方案3】:

假设我们为每一行计算StartEnd 之间的持续时间:

df['duration'] = df['End']-df['Start']

假设我们还根据该持续时间计算之前的 Start 值:

df['Prev'] = df['Start'] - df['duration'] - pd.Timedelta(days=1)

然后我们可以将所需的 DataFrame 表示为 df 和它自身之间的 merge 的结果,我们合并 GroupdurationPrev 的行(在一个 DataFrame 中)匹配 GroupdurationStart(在另一个 DataFrame 中):

import pandas as pd

df = pd.DataFrame([['A', '2014-01-01', '2014-01-07', 1.2],
                   ['B', '2014-01-01', '2014-01-07', 2.5],
                   ['C', '2014-01-01', '2014-01-07', 3.],
                   ['A', '2014-01-08', '2014-01-14', 3.],
                   ['B', '2014-01-08', '2014-01-14', 2.],
                   ['C', '2014-01-08', '2014-01-14', 1.],
                   ['A', '2014-01-15', '2014-01-21', 10.],
                   ['A', '2014-01-21', '2014-01-27', 98.],
                   ['B', '2014-01-21', '2014-01-27', -5.],
                   ['C', '2014-01-21', '2014-01-27', -72.],
                   ['A', '2014-01-22', '2014-01-28', 8.],
                   ['B', '2014-01-22', '2014-01-28', 25.],
                   ['C', '2014-01-22', '2014-01-28', -23.],
                   ['A', '2014-01-22', '2014-02-22', 8.],
                   ['B', '2014-01-22', '2014-02-22', 25.],
                   ['C', '2014-01-22', '2014-02-22', -23.],
                  ], columns=['Group', 'Start', 'End', 'Value'])
for col in ['Start', 'End']:
    df[col] = pd.to_datetime(df[col])

df['duration'] = df['End']-df['Start']
df['Prev'] = df['Start'] - df['duration'] - pd.Timedelta(days=1)

result = pd.merge(df, df[['Group','duration','Start','Value']], how='left',
                  left_on=['Group','duration','Prev'], 
                  right_on=['Group','duration','Start'], suffixes=['', '_y'])
result = result[['Group', 'Start', 'End', 'Value', 'Value_y']]
result = result.rename(columns='Value_y':'Prev Value')
print(result)

产量

   Group      Start        End  Value  Prev Value
0      A 2014-01-01 2014-01-07    1.2         NaN
1      B 2014-01-01 2014-01-07    2.5         NaN
2      C 2014-01-01 2014-01-07    3.0         NaN
3      A 2014-01-08 2014-01-14    3.0         1.2
4      B 2014-01-08 2014-01-14    2.0         2.5
5      C 2014-01-08 2014-01-14    1.0         3.0
6      A 2014-01-15 2014-01-21   10.0         3.0
7      A 2014-01-21 2014-01-27   98.0         NaN
8      B 2014-01-21 2014-01-27   -5.0         NaN
9      C 2014-01-21 2014-01-27  -72.0         NaN
10     A 2014-01-22 2014-01-28    8.0        10.0
11     B 2014-01-22 2014-01-28   25.0         NaN
12     C 2014-01-22 2014-01-28  -23.0         NaN
13     A 2014-01-22 2014-02-22    8.0         NaN
14     B 2014-01-22 2014-02-22   25.0         NaN
15     C 2014-01-22 2014-02-22  -23.0         NaN

在 cmets 中,Artur Nowak 询问了 pd.merge 的时间复杂度。我相信它正在做一个O(N + M) 哈希连接,其中N 是哈希表的大小,M 是查找表的大小。下面是一些代码,用于根据经验测试 pd.merge 的性能作为 DataFrame 大小的函数。

import collections
import string
import timeit 
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt

timing = collections.defaultdict(list)

def make_df(ngroups, ndur, ndates):
    groups = list(string.uppercase[:ngroups])
    durations = range(ndur)
    start = pd.date_range('2000-1-1', periods=ndates, freq='D')

    index = pd.MultiIndex.from_product([start, durations, groups], 
                                       names=['Start', 'duration', 'Group'])
    values = np.arange(len(index))
    df = pd.DataFrame('Value': values, index=index).reset_index()
    df['End'] = df['Start'] + pd.to_timedelta(df['duration'], unit='D')
    df = df.drop('duration', axis=1)
    df = df[['Group', 'Start', 'End', 'Value']]

    df['duration'] = df['End']-df['Start']
    df['Prev'] = df['Start'] - df['duration'] - pd.Timedelta(days=1)
    return df

def using_merge(df):
    result = pd.merge(df, df[['Group','duration','Start','Value']], how='left',
                      left_on=['Group','duration','Prev'], 
                      right_on=['Group','duration','Start'], suffixes=['', '_y'])
    return result

Ns = np.array([10**i for i in range(5)])
for n in Ns:
    timing['merge'].append(timeit.timeit(
        'using_merge(df)',
        'from __main__ import using_merge, make_df; df = make_df(10, 10, )'.format(n),
        number=5))

print(timing['merge'])
slope, intercept, rval, pval, stderr = stats.linregress(Ns, timing['merge'])
print(slope, intercept, rval, pval, stderr)

plt.plot(Ns, timing['merge'], label='merge')
plt.plot(Ns, slope*Ns + intercept)
plt.legend(loc='best')
plt.show()

这表明对于数万行的 DataFrame,pd.merge 的速度大致是线性的。

【讨论】:

出于好奇,您知道merge 操作的计算复杂度是多少吗?我无法找到此信息,我想知道它与按组和持续时间(如您的答案中定义)拆分数据、按期间开始排序然后按顺序浏览数据相比如何。 @ArturNowak:我相信pd.merge 执行hash join which is O(N+M)。作为一个实际问题,我认为总是有必要在接近实际用例的数据上对两个版本进行基准测试,以确定哪个更快(对于那个用例)。我添加了一些timeit 代码来调查pd.merge 的性能作为DataFrame 大小的函数。如果您要添加代码来进行拆分/排序/顺序处理,我们可以进行一些经验测试。

以上是关于Pandas 将上一期的数据设置为新的 DataFrame 列的主要内容,如果未能解决你的问题,请参考以下文章

Pandas二次学习- 回炉重造(进阶)

如何在pandas dataframe中为新列添加值?

Pandas的concat方法

如何将 1 和 0 的行转换为新的 int 列

如何在熊猫数据框中找到标准匹配上方和下方的x行,然后将它们保存为新的df?

pandas_处理异常值缺失值重复值数据差分