清理一列字符串并添加新列的更有效方法

More efficient way to clean a column of strings and add a new column

我有一个包含列 ['metric_type', 'metric_value'] 的数据框 df。对于每一行,我想确保我有一个名称等于 'metric_type' 的列,该列的值等于 'metric_value'.

我的一个问题是 'metric_type' 有我想删除的虚假空格。

考虑数据框 df:

df = pd.DataFrame([
        ['a ', 1],
        [' b', 2],
        [' c ', 3]
    ], columns=['metric_type', 'metric_value'])

print(df)

  metric_type  metric_value
0          a              1
1           b             2
2          c              3

请注意 'metric_type' 的每个值在不同的地方都有空格。

我创建了一个函数来使用 apply,但它需要很长时间。

def assign_metric_vals(row):
    row[row['metric_type'].replace(" ", "")] = row['metric_value']
    return row

当我使用它时,我得到这个:

       a    b    c metric_type  metric_value
0 1.0000  nan  nan          a              1
1    nan 2.00  nan           b             2
2    nan  nan 3.00          c              3

是否有更好的(阅读,"faster")方法来完成同样的任务?

使用 metric_type 设置索引并取消堆叠会更好。

df.set_index(df.metric_type.str.replace(' ', ''), append=True).metric_value.unstack()

演示

df = pd.DataFrame([
        ['a ', 1],
        [' b', 2],
        [' c ', 3]
    ], columns=['metric_type', 'metric_value'])

print(df)

  metric_type  metric_value
0          a              1
1           b             2
2          c              3

print(df.apply(assign_metric_vals, 1))

       a    b    c metric_type  metric_value
0 1.0000  nan  nan          a              1
1    nan 2.00  nan           b             2
2    nan  nan 3.00          c              3

或者我的方式

idx = df.metric_type.str.replace(' ', '')
d1 = df.set_index(idx, append=True).metric_value.unstack()
print(pd.concat([d1, df], axis=1))

       a    b    c metric_type  metric_value
0 1.0000  nan  nan          a              1
1    nan 2.00  nan           b             2
2    nan  nan 3.00          c              3

时间

使用更大的 df
df1 = pd.concat([df] * 30000, ignore_index=True)

%%timeit
idx = df1.metric_type.str.replace(' ', '')
d1 = df1.set_index(idx, append=True).metric_value.unstack()
pd.concat([d1, df1], axis=1)

10 loops, best of 3: 77.3 ms per loop

%%timeit
df1.apply(assign_metric_vals, 1)

1 loop, best of 3: 57.4 s per loop

这是一个比@piRSquared 快大约 20% 并给出相同答案的替代方案。我不会建议它是好是坏(一般来说),但是赏金是在那个答案被接受后发布的,所以我会把它作为一个额外的选项提供。

%%timeit
idx = df1.metric_type.str.replace(' ', '')
d1 = df1.set_index(idx, append=True).metric_value.unstack()
result1 = pd.concat([d1, df1], axis=1)
10 loops, best of 3: 97.6 ms per loop

%%timeit 
df1.metric_type = df1.metric_type.str.strip()
d1 = df1.pivot(columns='metric_type', values='metric_value')
result2 = pd.concat([d1, df1], axis=1)
10 loops, best of 3: 77.2 ms per loop

大约 1/3 的速度提升来自使用 strip 而不是 replace,2/3 来自使用 pivot 而不是 unstack。 (concat 步骤是一样的,而且非常快)。

看看最终数据帧的创建方式,与目前提到的其他方法相比,就整体性能而言,字符串列的单热编码确实不是一个坏主意.

步骤:

  1. metric_type 系列上使用 pd.get_dummies,从分类变量创建虚拟变量。这部分加上str.strip是最耗时的。

  2. 我们可以在计算 get_dummies 部分而不是直接在系列对象上剥离 leading/trailing 空白字符,因为某些分类变量很有可能确实在系列中重复出现,稍后将在虚拟创建期间共享同一列。重复的变量越多,过滤掉这些额外空间所花费的时间就越少。仅对获得的虚拟变量DF 的列执行str.strip。这种方法可以节省大量时间。

  3. 对获得的这些列进行排序,以便它们按字典顺序排序,并且重复的列(如果存在)将彼此相邻放置。允许根据这些列组合修改 DF
  4. 使用 np.uniquereturn_index=True 参数来提取存在的唯一列及其对应的索引。
  5. 我们需要找到一种方法将相同的列组合成一个单一的有益列。为此,我们可以使用 np.add.reduceat,它的工作方式类似于 groupby 操作(等效 - df.groupby(df.columns.tolist(), axis=1).sum()),但它的特点是非常快。要配对的索引由 np.uniqueidx 提供。值的减少发生在这些切片上,并且它们的 运行 总和是跨列计算的 (axis=1)。
  6. 返回的 dtypebool,这有助于我们使用 np.where,因为它的功能类似于布尔掩码,其中 1's/0 被映射到 True/False 分别。然后这些 1 由 metric_value 系列中的值填充,0 由 NaN.
  7. 中的值填充
  8. 我们的 DF 现已准备就绪,需要将其与原始起始 DF 按列连接,从而产生最终清理后的数据帧。

解法:

def dummies_strip_concat(df):
    one_hot_enc = pd.get_dummies(df.metric_type)
    one_hot_enc.columns = one_hot_enc.columns.str.strip()
    one_hot_enc.sortlevel(axis=1, inplace=True)
    a, idx = np.unique(one_hot_enc.columns.values, return_index=True)
    out = np.where(np.add.reduceat(one_hot_enc.values, idx, axis=1, dtype=np.bool), 
                   df.metric_value.values[:, None], 
                   np.nan)
    return (pd.concat([pd.DataFrame(out, df.index, a), df], axis=1))

时间:

def pir(df):
    idx = df.metric_type.str.replace(' ', '')
    d1 = df.set_index(idx, append=True).metric_value.unstack()
    return pd.concat([d1, df], axis=1)

def johne(df):
    df.metric_type = df.metric_type.str.strip()
    d1 = df.pivot(columns='metric_type', values='metric_value')
    return pd.concat([d1, df], axis=1)

对于包含几千行的 DF 与 OP 的想法相当:

df1 = pd.concat([df] * 30000, ignore_index=True)
df1.shape
(90000, 2)

# Check whether they produce the same outcome
dummies_strip_concat(df1).equals(pir(df1))
True

%timeit pir(df1)
10 loops, best of 3: 97.5 ms per loop

%timeit johne(df1)
10 loops, best of 3: 76.5 ms per loop

%timeit dummies_strip_concat(df1)
100 loops, best of 3: 13.2 ms per loop