新的Dataframe列作为其他行(pandas)的通用函数

时间:2018-01-09 18:17:37

标签: python pandas dataframe vectorization

DataFrame 中创建新列的最快(也是最有效)方法是什么? {{1 }

考虑以下示例:

pandas

哪个收益率:

import pandas as pd

d = {
    'id': [1, 2, 3, 4, 5, 6],
    'word': ['cat', 'hat', 'hag', 'hog', 'dog', 'elephant']
}
pandas_df = pd.DataFrame(d)

假设我想创建一个新列 id word 0 1 cat 1 2 hat 2 3 hag 3 4 hog 4 5 dog 5 6 elephant ,其中包含一个值,该值基于使用函数bar的输出,以将当前行中的单词与{{中的其他行进行比较1}}。

foo

这确实会产生正确的输出,但会使用dataframedef foo(word1, word2): # do some calculation return foobar # in this example, the return type is numeric threshold = some_threshold for index, _id, word in pandas_df.itertuples(): value = sum( pandas_df[pandas_df['word'] != word].apply( lambda x: foo(x['word'], word), axis=1 ) < threshold ) pandas_df.loc[index, 'bar'] = value ,这对于大型itertuples()不具备效果。

有没有办法 vectorize (这是正确的术语吗?)这种方法?或者还有另一种更好(更快)的方法吗?

备注/更新:

  1. 在原帖中,我使用编辑距离/ levenshtein距离作为apply()函数。我已经改变了这个问题,试图更加通用。这个想法是要应用的函数是将当前行值与所有其他行进行比较并返回一些聚合值。
  2. 如果DataFramesfoofoo设置为nltk.metrics.distance.edit_distance(如原始帖子中所示),则会生成以下输出:

    threshold
    1. 我也有2 id word bar 0 1 cat 1.0 1 2 hat 2.0 2 3 hag 2.0 3 4 hog 2.0 4 5 dog 1.0 5 6 elephant 0.0 。我认为将它们分成same question是有意义的,所以它们不是太宽泛。但是,我一般会发现类似spark dataframes问题的解决方案有时可以修改为适用于pandas

    2. two posts启发,问我的spark版问题,我尝试在spark中使用this answer。我的速度测试表明这稍微快一点(尽管我怀疑这可能会随着数据的大小而变化)。不幸的是,我仍然无法调用pandas

    3. 示例代码:

      apply()

2 个答案:

答案 0 :(得分:3)

让我们尝试分析问题:

如果您有N行,那么您的相似度函数中需要考虑N*N“对”。在一般情况下,没有逃避评估所有这些(听起来很合理,但我无法证明)。因此,您有至少O(n ^ 2)时间复杂度

然而,您可以尝试使用该时间复杂度的常数因子。 我找到的可能选项是:

1。并行化:

由于你有一些大的DataFrame,并行化处理是最明显的选择。这将使你(几乎)线性改善时间复杂度,所以如果你有16名工人,你将获得(差不多)16倍的改善。

例如,我们可以将df的行划分为不相交的部分,并单独处理每个部分,然后合并结果。 一个非常基本的并行代码可能如下所示:

from multiprocessing import cpu_count,Pool

def work(part):
    """
    Args:
        part (DataFrame) : a part (collection of rows) of the whole DataFrame.

    Returns:
        DataFrame: the same part, with the desired property calculated and added as a new column
    """
     # Note that we are using the original df (pandas_df) as a global variable
     # But changes made in this function will not be global (a side effect of using multiprocessing).
    for index, _id, word in part.itertuples(): # iterate over the "part" tuples
        value = sum(
            pandas_df[pandas_df['word'] != word].apply( # Calculate the desired function using the whole original df
                lambda x: foo(x['word'], word),
                axis=1
            ) < threshold
        )
        part.loc[index, 'bar'] = value
    return part

# New code starts here ...

cores = cpu_count() #Number of CPU cores on your system

data_split = np.array_split(data, cores) # Split the DataFrame into parts
pool = Pool(cores) # Create a new thread pool
new_parts = pool.map(work , data_split) # apply the function `work` to each part, this will give you a list of the new parts
pool.close() # close the pool
pool.join()
new_df = pd.concat(new_parts) # Concatenate the new parts

注意:我试图让代码尽可能接近OP的代码。这只是一个基本的演示代码,并且存在许多更好的替代方案。

2。 “低级别”优化:

另一种解决方案是尝试优化相似度函数计算和迭代/映射。我不认为与前一个选项或下一个选项相比,这会获得更多的加速。

3。功能相关的修剪:

您可以尝试的最后一件事是相似功能相关的改进。这在一般情况下不起作用,但如果您可以分析相似性函数,则可以很好地工作。例如:

假设您正在使用Levenshtein距离(LD),您可以观察到任意两个字符串之间的距离> = =它们的长度之间的差异。即LD(s1,s2) >= abs(len(s1)-len(s2))

您可以使用此观察来修剪可能的类似对以供评估。因此,对于长度为l1的每个字符串,请仅将其与长度为l2 abs(l1-l2) <= limit的字符串进行比较。 (限制是最大可接受的不相似度,在您提供的示例中为2)。

另一个观察是LD(s1,s2) = LD(s2,s1)。这样可以将对数减少2倍。

此解决方案实际上可能会降低O(n)时间复杂度(主要取决于数据) 为什么?你可能会问 那是因为如果我们有10^9行,但平均而言我们只有10^3行,每行的行距离“接近”,那么我们需要评估大约10^9 * 10^3 /2对的函数,而不是10^9 * 10^9对。{但是(再次)取决于数据。如果(在此示例中)您具有长度为3的字符串,则此方法将无用。

答案 1 :(得分:2)

关于预处理的想法(groupby)

因为您正在寻找小于2的编辑距离,所以您可以先按字符串的长度进行分组。如果组之间的长度差异大于或等于2,则无需比较它们。 (这部分与Qusai Alothman在第3节中的答案非常相似.H)

因此,首先要按字符串的长度进行分组。

df["length"] = df.word.str.len() 
df.groupby("length")["id", "word"]

然后,如果长度差异小于或等于2,则计算每两个连续组之间的编辑距离。这与您的问题没有直接关系,但我希望它会有所帮助

潜在的矢量化(在groupby之后)

之后,您还可以尝试通过将每个字符串拆分为字符来对计算进行矢量化。请注意,如果拆分成本高于其带来的矢量化收益,则不应执行此操作。或者在创建数据框时,只需创建一个包含字符而不是单词的数据框。

我们将使用Pandas split dataframe column for every character中的答案将字符串拆分为字符列表。

# assuming we had groupped the df.
df_len_3 = pd.DataFrame({"word": ['cat', 'hat', 'hag', 'hog', 'dog']})
# turn it into chars
splitted = df_len_3.word.apply(lambda x: pd.Series(list(x)))

    0   1   2
0   c   a   t
1   h   a   t
2   h   a   g
3   h   o   g
4   d   o   g

splitted.loc[0] == splitted # compare one word to all words

    0       1       2
0   True    True    True  -> comparing to itself is always all true.
1   False   True    True
2   False   True    False
3   False   False   False
4   False   False   False


splitted.apply(lambda x: (x == splitted).sum(axis=1).ge(len(x)-1), axis=1).sum(axis=1) - 1

0    1
1    2
2    2
3    2
4    1
dtype: int64

splitted.apply(lambda x: (x == splitted).sum(axis=1).ge(len(x)-1), axis=1).sum(axis=1) - 1

的说明

对于每一行,lambda x: (x == splitted)将每行与整个df进行比较,就像上面的splitted.loc[0] == splitted一样。它将生成一个真/假表。

然后,我们使用.sum(axis=1)之后的(x == splitted)水平地对表格求和。

然后,我们想找出哪些词是相似的。因此,我们应用ge函数来检查true的数量超过阈值。在这里,我们只允许差值为1,因此它被设置为len(x)-1

最后,我们必须将整个数组减去1,因为我们在操作中将每个单词与自身进行比较。我们希望排除自我比较。

注意,此向量化部分仅适用于组内相似性检查。我想仍然需要使用编辑距离方法检查具有不同长度的组。