使用列中的日期范围扩展pandas数据框

cla*_*bot 15 python pandas

我有一个pandas数据帧,其日期和字符串与此类似:

Start        End           Note    Item
2016-10-22   2016-11-05    Z       A
2017-02-11   2017-02-25    W       B
Run Code Online (Sandbox Code Playgroud)

我需要将它扩展/转换为下面的内容,在StartEnd列之间填写几周(W-SAT)并转发填写NoteItems中的数据:

Start        Note    Item
2016-10-22   Z       A
2016-10-29   Z       A
2016-11-05   Z       A
2017-02-11   W       B
2017-02-18   W       B
2017-02-25   W       B
Run Code Online (Sandbox Code Playgroud)

什么是与熊猫一起做到这一点的最好方法?某种多指数适用?

Ted*_*rou 15

您可以迭代每一行并创建一个新的数据帧,然后将它们连接在一起

pd.concat([pd.DataFrame({'Start': pd.date_range(row.Start, row.End, freq='W-SAT'),
               'Note': row.Note,
               'Item': row.Item}, columns=['Start', 'Note', 'Item']) 
           for i, row in df.iterrows()], ignore_index=True)

       Start Note Item
0 2016-10-22    Z    A
1 2016-10-29    Z    A
2 2016-11-05    Z    A
3 2017-02-11    W    B
4 2017-02-18    W    B
5 2017-02-25    W    B
Run Code Online (Sandbox Code Playgroud)

  • 有没有另一种方法来实现这一点,而不是逐行迭代? (6认同)
  • 我想知道没有iterrows .. iterrows对于处理大约300,000行的数据集来说很慢. (3认同)

小智 8

您根本不需要迭代。

df_start_end = df.melt(id_vars=['Note','Item'],value_name='date')

df = df_start_end.groupby('Note').apply(lambda x: x.set_index('date').resample('W').pad()).drop(columns=['Note','variable']).reset_index()
Run Code Online (Sandbox Code Playgroud)

  • 具有 apply 和自定义函数的 Groupby 是迭代,并且通常比仅使用 for 循环慢很多倍。 (2认同)

小智 7

因此,我最近花了一些时间试图找出一种有效的pandas基于 - 的方法来解决这个问题(这对于data.tablein来说非常简单R),并想在这里分享我想到的方法:

df.set_index("Note").apply(
    lambda row: pd.date_range(row["Start"], row["End"], freq="W-SAT").values, axis=1
).explode()
Run Code Online (Sandbox Code Playgroud)

注意:使用.values对性能有很大影响!

这里已经有很多解决方案,我想比较不同行数和周期的速度 - 请参阅下面的结果(以秒为单位):

  • n_rows是初始行数,n_periods是每行的周期数,即窗口大小:展开时,下面的组合始终会产生 100 万行
  • 其他栏目以解决方案的海报命名
  • 请注意,我对 Gen 的方法进行了轻微调整,之后pd.melt(),我将其df.set_index("date").groupby("Note").resample("W-SAT").ffill()标记为 Gen2,它的性能似乎稍好一些,并给出了相同的结果
  • 每个n_rows、n_periods组合运行 10 次,然后对结果取平均值

无论如何,当行数较多且句点较少时, jwdink 的解决方案看起来像是赢家,而我的解决方案在另一端似乎更好,尽管随着行数的减少,仅略微领先于其他解决方案:

n_行 n_周期 尤丁克 特德·佩特鲁 第二代 罗比
250 4000 6.63 0.33 0.64 0.45 0.28
500 2000年 3.21 0.65 1.18 0.81 0.34
1000 1000 1.57 1.28 2.30 1.60 0.48
2000年 500 0.83 2.57 4.68 3.24 0.71
5000 200 0.40 6.10 13.26 9.59 1.43

如果您想对此运行自己的测试,可以在我的GitHub 存储库中找到我的代码- 请注意,我创建了一个DateExpander类对象来包装所有函数,以便更轻松地扩展模拟。

另外,作为参考,我使用了 2 核 STANDARD_DS11_V2 Azure VM - 仅使用了大约 10 分钟,所以这实际上是我在这个问题上给出了 2 美分!


jwd*_*ink 5

如果 的唯一值的数量df['End'] - df['Start']不太大,但数据集中的行数很大,那么以下函数将比循环数据集快得多:

def date_expander(dataframe: pd.DataFrame,
                  start_dt_colname: str,
                  end_dt_colname: str,
                  time_unit: str,
                  new_colname: str,
                  end_inclusive: bool) -> pd.DataFrame:
    td = pd.Timedelta(1, time_unit)

    # add a timediff column:
    dataframe['_dt_diff'] = dataframe[end_dt_colname] - dataframe[start_dt_colname]

    # get the maximum timediff:
    max_diff = int((dataframe['_dt_diff'] / td).max())

    # for each possible timediff, get the intermediate time-differences:
    df_diffs = pd.concat([pd.DataFrame({'_to_add': np.arange(0, dt_diff + end_inclusive) * td}).assign(_dt_diff=dt_diff * td)
                          for dt_diff in range(max_diff + 1)])

    # join to the original dataframe
    data_expanded = dataframe.merge(df_diffs, on='_dt_diff')

    # the new dt column is just start plus the intermediate diffs:
    data_expanded[new_colname] = data_expanded[start_dt_colname] + data_expanded['_to_add']

    # remove start-end cols, as well as temp cols used for calculations:
    to_drop = [start_dt_colname, end_dt_colname, '_to_add', '_dt_diff']
    if new_colname in to_drop:
        to_drop.remove(new_colname)
    data_expanded = data_expanded.drop(columns=to_drop)

    # don't modify dataframe in place:
    del dataframe['_dt_diff']

    return data_expanded
Run Code Online (Sandbox Code Playgroud)