【问题标题】:Fastest way to get the mode of a pandas Series with NaN使用 NaN 获得熊猫系列模式的最快方法
【发布时间】:2020-07-21 04:12:48
【问题描述】:

我需要找到 pandas groupby 对象或单个系列的模式/最常见元素,为此我有以下功能:

def get_most_common(srs):
    from collections import Counter
    import numpy as np

    x = list(srs)
    my_counter = Counter(x)
    if np.nan not in my_counter.keys():
        most_common_value = my_counter.most_common(1)[0][0]
    else:
        most_common_value = srs.mode(dropna=False).iloc[0]

    return most_common_value

如果是平局,我不在乎选择哪一个——随机就可以了。

Counter 在没有 NaN 时更快,但在使用 NaN 时会给出错误的结果。 pd.Series.mode 总是正确的,但是当没有 NaN 时它比 Counter 慢。这是一场赌博(当没有 NaN 时速度更快,但由于额外检查 np.nan not in my_counter.keys() 而在有 NaN 时速度较慢)。到目前为止,我的大型数据集的性能令人满意,可能是因为很多情况下没有 NaN。但是有没有办法让它更快?

【问题讨论】:

  • 在这种情况下,您真的不需要 if/else 和 Counter。不管np.nan 是否在您的Series 中,您总是要求最常见的值,它可以是np.nan。不用定义get_most_common,只需使用srs.mode(dropna=False),你会得到同样的结果。
  • 您能否在进行 groupby 之前使用原始数据中没有的值(例如 max + 1 或类似的值)填充,然后仅使用 Counter?如果这个值是 return 你知道它是 nan?
  • @r.ook 我尝试专门使用srs.mode(dropna=False)。它比我上面做的要慢得多。
  • @Ben.T 是的,我尝试过类似srs.fillna('NAN') 的操作,使用Counter,然后如果我得到'NAN' 作为模式,我会将其恢复为np.nan。我在一个带有 timeit 的示例系列上尝试了这个(不是在大型数据集上),它似乎比我上面的函数慢,即使系列中没有 NaN。
  • @Ben.T,系列元素是字符串。我发现用 0 填充比用字符串填充要好,但是从 timeit 来看,具有 NaN 的系列和没有 NaN 的系列的性能似乎同样慢。我上面的函数在没有 NaN 的情况下工作得更好(几乎和单独的 Counter 一样快),这似乎就是为什么它适用于大型数据集。

标签: python pandas


【解决方案1】:

我觉得奇怪的是,您使用 Counter 获得了更好的性能。这是我的测试结果(n=10000):

Using Series.mode on Series with nan: 52.41649858
Using Series.mode on Series without nan: 17.186453438
Using Counter on Series with nan: 269.33117825500005
Using Counter on Series without nan: 134.207576572

#-----------------------------------------------------#

             Series.mode  Counter
             -----------  -------------
With nan     52.42s       269.33s
Without nan  17.19s       134.21s

测试代码:

import timeit

setup = '''
import pandas as pd
from collections import Counter

def get_most_common(srs):
    return srs.mode(dropna=False)[0]

def get_most_common_counter(srs):
    x = list(srs)
    my_counter = Counter(x)
    return my_counter.most_common(1)[0][0]

df = pd.read_csv(r'large.data')
'''

print(f"""Using Series.mode on Series with nan: {timeit.timeit('get_most_common(df["has_nan"])', setup=setup, number=10000)}""")
print(f"""Using Series.mode on Series without nan: {timeit.timeit('get_most_common(df["no_nan"])', setup=setup, number=10000)}""")
print(f"""Using Counter on Series with nan: {timeit.timeit('get_most_common_counter(df["has_nan"])', setup=setup, number=10000)}""")
print(f"""Using Counter on Series without nan: {timeit.timeit('get_most_common_counter(df["no_nan"])', setup=setup, number=10000)}""")

large.data 是从 099 的随机 2 位字符串的 2 x 50000 行 DataFrame,其中 has_nan 具有 modenan=551


如果有的话,您的if np.nan not in my_counter.keys() 条件将始终被触发,因为np.nan 不在my_counter.keys()。所以实际上你从来没有使用过pd.Series.mode,它一直在使用Counter。如另一个问题中所述,由于您的 pandas 对象已经在 Series/DataFrame 中创建了 np.nan 的副本,因此永远不会满足 in 条件。试试看:

np.nan in pd.Series([np.nan, 1, 2]).to_list()
# False

消除if/else 的全部复杂性并坚持使用一种方法。然后比较性能。正如您在另一个问题中提到的,熊猫方法几乎总是比任何外部模块/方法更好的方法。如果您仍然观察到其他情况,请更新您的问题。

【讨论】:

  • 一些令人困惑的行为。请检查这个 sn-p:get_most_common(pd.Series([np.nan, np.nan, np.nan,"hello","hello", "hi","fat"]))。在我的机器中,这通过pd.Series.mode(我为每个条件添加了print 语句),我可以验证这给了我正确的结果(nan)。请注意,我正在处理涉及字符串或 NaN 的系列。当系列由整数和 NaN 组成时,结果会有所不同。
  • 似乎nan 的引用由dtype 确定。对于您给定的情况,dtype 将是 'O' 混合类型,因此似乎保留了引用。对于[np.nan, 1, 2],它将是dtype='float64',因此nan 对象是新创建的。
猜你喜欢
  • 2021-09-12
  • 2018-04-05
  • 2021-02-24
  • 2017-12-15
  • 2019-12-31
  • 2020-02-18
  • 2013-12-12
  • 1970-01-01
  • 2020-04-08
相关资源
最近更新 更多