优化 Pandas 中的值插值

Optimizing interpolation of values in Pandas

我一直在努力解决 Pandas 的优化问题。

我开发了一个脚本来对相对较小的 DataFrame 的每一行应用计算(大约几千行,几十列)。 我严重依赖 apply() 函数,这在大多数情况下显然是一个糟糕的选择。

经过一轮优化,我只有一个方法需要时间,我还没有找到一个简单的解决方案:

基本上我的数据框包含一个视频观看统计列表,其中包含每个四分位数观看视频的人数(有多少人观看了 0%、25%、50% 等),例如:

video_name video_length video_0 video_25 video_50 video_75 video_100
video_1 6 1000 500 300 250 5
video_2 30 1000 500 300 250 5

我正在尝试对统计数据进行插值,以便能够回答“如果视频持续 X 秒,有多少人会观看视频的每个四分位数”

现在我的函数接受数据帧和一个“new_length”参数,并在每一行调用 apply()。

处理每一行的函数计算每个四分位数的时间标记(因此 0、7.5、15、22.5 和 30 用于 30 秒的视频),以及给定新长度的每个四分位数的时间标记(以便减少30 秒视频到 6 秒,新的时间标记将是 0、1.5、3、4.5 和 6)。 我构建了一个数据框,其中包含时间标记作为索引,统计数据作为第一列中的值:

index (time marks) view_stats
0 1000
7.5 500
15 300
22.5 250
30 5
1.5 NaN
3 NaN
4.5 NaN

然后我调用 DataFrame.interpolate(method="index") 来填充 NaN 值。

它有效并给了我预期的结果,但它花费了 3k 行数据帧高达 11 秒,我相信这与使用 apply() 方法结合创建一个新的用于为每一行插入数据的数据框。

是否有一种明显的方法可以“就地”获得相同的结果,例如,通过避免直接在原始数据帧上应用/新数据帧方法?

编辑: 使用 6 作为新长度参数调用函数时的预期输出为:

video_name video_length video_0 video_25 video_50 video_75 video_100 new_video_0 new_video_25 new_video_50 new_video_75 new_video_100
video_1 6 1000 500 300 250 5 1000 500 300 250 5
video_2 6 1000 500 300 250 5 1000 900 800 700 600

第一行将保持不变,因为视频已经有 6 秒长了。 在第二行中,视频将从 30 秒缩短为 6 秒,因此新的四分位数将位于 0、1.5、3、4.5、6 秒,统计数据将在 1000 和 500 之间插值,这是旧 0% 的值和 25% 的时间标记

EDIT2:我不在乎是否需要添加临时列,时间是个问题,内存不是。

作为参考,这是我的代码:

def get_value(marks, asset, mark_index) -> int:
  value = marks["count"][asset["new_length_marks"][mark_index]]
  if isinstance(value, pandas.Series):
    res = value.iloc(0)
  else:
    res = value
  return math.ceil(res)

def length_update_row(row, assets, **kwargs):
  asset_name = row["asset_name"]
  asset = assets[asset_name]
  # assets is a dict containing the list of files and the old and "new" video marks
  # pre-calculated

  marks = pandas.DataFrame(data=[int(row["video_start"]), int(row["video_25"]), int(row["video_50"]), int(row["video_75"]), int(row["video_completed"])],
                            columns=["count"],
                            index=asset["old_length_marks"])
    
  marks = marks.combine_first(pandas.DataFrame(data=NaN, columns=["count"], index=asset["new_length_marks"][1:]))
  marks = marks.interpolate(method="index")
    
  row["video_25"] = get_value(marks, asset, 1)
  row["video_50"] = get_value(marks, asset, 2)
  row["video_75"] = get_value(marks, asset, 3)
  row["video_completed"] = get_value(marks, asset, 4)
  
  return row
  

def length_update_stats(report: pandas.DataFrame,
                 assets: dict) -> pandas.DataFrame:
  new_report = new_report.apply(lambda row: length_update_row(row, assets), axis=1)
  return new_report

IIUC,你可以使用 np.interp:

# get the old x values
xs = df['video_length'].values[:, None] * [0, 0.25, 0.50, 0.75, 1]

# the corresponding y values
ys = df.iloc[:, 2:].values

# note that 6 is the new value
nxs = np.repeat(np.array(6), 2)[:, None] * [0, 0.25, 0.50, 0.75, 1]

res = pd.DataFrame(data=np.array([np.interp(nxi, xi, yi) for nxi, xi, yi in zip(nxs, xs, ys)]), columns="new_" + df.columns[2:] )

print(res)

输出

   new_video_0  new_video_25  new_video_50  new_video_75  new_video_100
0       1000.0         500.0         300.0         250.0            5.0
1       1000.0         900.0         800.0         700.0          600.0

然后跨第二个轴连接:

output = pd.concat((df, res), axis=1)
print(output)

输出 (连接)

  video_name  video_length  video_0  ...  new_video_50  new_video_75  new_video_100
0    video_1             6     1000  ...         300.0         250.0            5.0
1    video_2            30     1000  ...         800.0         700.0          600.0

[2 rows x 12 columns]