【问题标题】:Modifying timestamps in pandas to make index unique修改 pandas 中的时间戳以使索引唯一
【发布时间】:2017-04-08 16:54:40
【问题描述】:

我正在处理不定期记录的财务数据。一些时间戳是重复的,这使得分析变得棘手。这是一个数据示例 - 注意有四个 2016-08-23 00:00:17.664193 时间戳:

In [167]: ts
Out[168]: 
                               last  last_sz      bid      ask
datetime                                                      
2016-08-23 00:00:14.161128  2170.75        1  2170.75  2171.00
2016-08-23 00:00:14.901180  2171.00        1  2170.75  2171.00
2016-08-23 00:00:17.196639  2170.75        1  2170.75  2171.00
2016-08-23 00:00:17.664193  2171.00        1  2170.75  2171.00
2016-08-23 00:00:17.664193  2171.00        1  2170.75  2171.00
2016-08-23 00:00:17.664193  2171.00        2  2170.75  2171.00
2016-08-23 00:00:17.664193  2171.00        1  2170.75  2171.00
2016-08-23 00:00:26.206108  2170.75        2  2170.75  2171.00
2016-08-23 00:00:28.322456  2170.75        7  2170.75  2171.00
2016-08-23 00:00:28.322456  2170.75        1  2170.75  2171.00

在这个例子中,只有少数重复,但在某些情况下,有数百个连续的行,都共享相同的时间戳。我的目标是通过为每个重复添加 1 个额外的纳秒来解决这个问题(因此,在 4 个连续相同时间戳的情况下,我会在第二个时间戳中添加 1ns,在第三个中添加 2ns,在第四个中添加 3ns。例如,上面的数据将被转换为:

In [169]: make_timestamps_unique(ts)
Out[170]:
                                  last  last_sz      bid     ask
newindex                                                        
2016-08-23 00:00:14.161128000  2170.75        1  2170.75  2171.0
2016-08-23 00:00:14.901180000  2171.00        1  2170.75  2171.0
2016-08-23 00:00:17.196639000  2170.75        1  2170.75  2171.0
2016-08-23 00:00:17.664193000  2171.00        1  2170.75  2171.0
2016-08-23 00:00:17.664193001  2171.00        1  2170.75  2171.0
2016-08-23 00:00:17.664193002  2171.00        2  2170.75  2171.0
2016-08-23 00:00:17.664193003  2171.00        1  2170.75  2171.0
2016-08-23 00:00:26.206108000  2170.75        2  2170.75  2171.0
2016-08-23 00:00:28.322456000  2170.75        7  2170.75  2171.0
2016-08-23 00:00:28.322456001  2170.75        1  2170.75  2171.0

我一直在努力寻找一个好的方法来做到这一点 - 我目前的解决方案是进行多次传递,每次检查重复项,并将 1ns 添加到除了一系列相同时间戳中的第一个之外的所有时间戳。代码如下:

def make_timestamps_unique(ts):
    mask = ts.index.duplicated('first')
    duplicate_count = np.sum(mask)
    passes = 0

    while duplicate_count > 0:
        ts.loc[:, 'newindex'] = ts.index
        ts.loc[mask, 'newindex'] += pd.Timedelta('1ns')
        ts = ts.set_index('newindex')
        mask = ts.index.duplicated('first')
        duplicate_count = np.sum(mask)
        passes += 1

    print('%d passes of duplication loop' % passes)
    return ts

这显然是非常低效的 - 它通常需要数百次通过,如果我在 200 万行数据帧上尝试它,我会得到 MemoryError。有什么更好的方法来实现这一点吗?

【问题讨论】:

    标签: python pandas


    【解决方案1】:

    这是一个更快的 numpy 版本(但可读性稍差),其灵感来自 SO article。这个想法是在重复的时间戳值上使用cumsum,同时在每次遇到np.NaN 时重置累积和:

    # get duplicated values as float and replace 0 with NaN
    values = df.index.duplicated(keep=False).astype(float)
    values[values==0] = np.NaN
    
    missings = np.isnan(values)
    cumsum = np.cumsum(~missings)
    diff = np.diff(np.concatenate(([0.], cumsum[missings])))
    values[missings] = -diff
    
    # print result
    result = df.index + np.cumsum(values).astype(np.timedelta64)
    print(result)
    
    DatetimeIndex([   '2016-08-23 00:00:14.161128',
                      '2016-08-23 00:00:14.901180',
                      '2016-08-23 00:00:17.196639',
                   '2016-08-23 00:00:17.664193001',
                   '2016-08-23 00:00:17.664193002',
                   '2016-08-23 00:00:17.664193003',
                   '2016-08-23 00:00:17.664193004',
                      '2016-08-23 00:00:26.206108',
                   '2016-08-23 00:00:28.322456001',
                   '2016-08-23 00:00:28.322456002'],
                  dtype='datetime64[ns]', name='datetime', freq=None)
    

    计时此解决方案会产生10000 loops, best of 3: 107 µs per loop,而@DYZ groupby/apply 方法(但更具可读性)在使用100 loops, best of 3: 5.3 ms per loop 的虚拟数据上要慢大约 50 倍。

    当然,最后你必须重新设置索引:

    df.index = result
    

    【讨论】:

    • 请注意 - 我将keep=False 更改为keep='first'(在第一行),以便获得与我的示例和@DYZ 解决方案相同的结果
    • @strongvigilance 你是对的 - 这个修改会产生你想要的结果。
    • 另请注意,您的数据框需要在应用它之前按索引排序 - 可能很明显,但我花了一段时间才弄清楚出了什么问题。
    【解决方案2】:

    您可以按索引对行进行分组,然后将一系列顺序时间增量添加到每个组的索引中。我不确定这是否可以直接使用索引来完成,但是您可以先将索引转换为普通列,将操作应用于列,然后再次将列设置为索引:

    newindex = ts.reset_index()\
                 .groupby('datetime')['datetime']\
                 .apply(lambda x: x + np.arange(x.size).astype(np.timedelta64))
    df.index = newindex
    

    【讨论】:

      【解决方案3】:

      让我们从矢量化基准开始,因为您要处理 1M+ 行,这应该是一个优先事项:

      %timeit do
      10000000 loops, best of 3: 20.5 ns per loop
      

      让我们做一些测试数据,因为没有提供任何数据:

      rng = pd.date_range('1/1/2011', periods=72, freq='H')
      
      df = pd.DataFrame(dict(time = rng))
      

      复制时间戳:

      df =pd.concat((df, df))
      df =df.sort()
      
      df
      Out [296]:
                        time
      0  2011-01-01 00:00:00
      0  2011-01-01 00:00:00
      1  2011-01-01 01:00:00
      1  2011-01-01 01:00:00
      2  2011-01-01 02:00:00
      2  2011-01-01 02:00:00
      3  2011-01-01 03:00:00
      3  2011-01-01 03:00:00
      4  2011-01-01 04:00:00
      4  2011-01-01 04:00:00
      5  2011-01-01 05:00:00
      5  2011-01-01 05:00:00
      6  2011-01-01 06:00:00
      6  2011-01-01 06:00:00
      7  2011-01-01 07:00:00
      7  2011-01-01 07:00:00
      8  2011-01-01 08:00:00
      8  2011-01-01 08:00:00
      9  2011-01-01 09:00:00
      9  2011-01-01 09:00:00
      

      找出与上一行时间差为0秒的位置

      mask = (df.time-df.time.shift()) == np.timedelta64(0,'s')
      
      mask
      Out [307]:
      0     False
      0      True
      1     False
      1      True
      2     False
      2      True
      3     False
      3      True
      4     False
      4      True
      5     False
      

      偏移这些位置:在这种情况下,我选择毫秒

      df.loc[mask,'time'] = df.time[mask].apply(lambda x: x+pd.offsets.Milli(5))
      
      Out [309]:
                            time
      0  2011-01-01 00:00:00.000
      0  2011-01-01 00:00:00.005
      1  2011-01-01 01:00:00.000
      1  2011-01-01 01:00:00.005
      2  2011-01-01 02:00:00.000
      2  2011-01-01 02:00:00.005
      3  2011-01-01 03:00:00.000
      3  2011-01-01 03:00:00.005
      4  2011-01-01 04:00:00.000
      4  2011-01-01 04:00:00.005
      5  2011-01-01 05:00:00.000
      

      编辑:使用连续的时间戳[假设为 4]

      consect = 4 
      for i in range(4):
          mask = (df.time-df.time.shift(consect)) == np.timedelta64(0,'s')
          df.loc[mask,'time'] = df.time[mask].apply(lambda x: x+pd.offsets.Milli(5+i))
          consect -= 1
      

      【讨论】:

      • 这在我的例子中肯定行不通,比如我的例子中有两个以上连续相同的时间戳?
      • 迭代修改班次,以便它分批完成所有类似的班次,刚刚测试了 4 个连续班次,仍然可以正常工作
      猜你喜欢
      • 2018-05-26
      • 2022-06-15
      • 2016-02-11
      • 1970-01-01
      • 2014-12-17
      • 2012-12-20
      • 1970-01-01
      • 2020-03-21
      • 2015-03-30
      相关资源
      最近更新 更多