熊猫矢量化:计算满足条件的每个组的比例

时间:2018-12-09 05:26:14

标签: python pandas group-by vectorization apply

假设我们有一张客户及其消费表。

 TextArea {
     text: "Printer IP: " + printers.model[printers.currentIndex].ip +
         "\nPrinter Port: " + printers.model[printers.currentIndex].port
 }

对于每个客户,我们可以使用import pandas as pd df = pd.DataFrame({ "Name": ["Alice", "Bob", "Bob", "Charles"], "Spend": [3, 5, 7, 9] }) LIMIT = 6 方法来计算他的支出中大于6的部分:

apply

但是,df.groupby("Name").apply( lambda grp: len(grp[grp["Spend"] > LIMIT]) / len(grp) ) Name Alice 0.0 Bob 0.5 Charles 1.0 方法is just a loop,如果有很多客户,这会很慢。

问题:有没有一种更快的方法,大概使用向量化?

从0.23.4版开始,SeriesGroupBy不支持比较运算符:

apply

下面的代码将导致Alice为空值:

(df.groupby("Name") ["Spend"] > LIMIT).mean()

TypeError: '>' not supported between instances of 'SeriesGroupBy' and 'int'

下面的代码给出了正确的结果,但是它要求我们修改表或进行复制以避免修改原始表。

df[df["Spend"] > LIMIT].groupby("Name").size() / df.groupby("Name").size()

Name
Alice      NaN
Bob        0.5
Charles    1.0

1 个答案:

答案 0 :(得分:0)

Groupby不使用矢量化,但具有使用Cython优化的聚合功能。

您可以取平均值:

(df["Spend"] > LIMIT).groupby(df["Name"]).mean()

df["Spend"].gt(LIMIT).groupby(df["Name"]).mean()

或使用div将NaN替换为0:

df[df["Spend"] > LIMIT].groupby("Name").size() \
.div(df.groupby("Name").size(), fill_value = 0)

df["Spend"].gt(LIMIT).groupby(df["Name"]).sum() \
.div(df.groupby("Name").size(), fill_value = 0)

以上每种都会产生

Name
Alice      0.0
Bob        0.5
Charles    1.0
dtype: float64

性能

取决于行数和每个条件过滤的行数,因此最好对真实数据进行测试。

np.random.seed(123)

N = 100000
df = pd.DataFrame({
    "Name":  np.random.randint(1000, size = N),
    "Spend": np.random.randint(10, size = N)
})
LIMIT = 6

In [10]: %timeit df["Spend"].gt(LIMIT).groupby(df["Name"]).mean()
6.16 ms ± 332 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [11]: %timeit df[df["Spend"] > LIMIT].groupby("Name").size().div(df.groupby("Name").size(), fill_value = 0)
6.35 ms ± 95.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [12]: %timeit df["Spend"].gt(LIMIT).groupby(df["Name"]).sum().div(df.groupby("Name").size(), fill_value = 0)
9.66 ms ± 365 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

# RafaelC comment solution
In [13]: %timeit df.groupby("Name")["Spend"].apply(lambda s: (s > LIMIT).sum() / s.size)
400 ms ± 27.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

In [14]: %timeit df.groupby("Name")["Spend"].apply(lambda s: (s > LIMIT).mean())
328 ms ± 6.12 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

此NumPy解决方案是矢量化的,但有点复杂:

In [15]: %%timeit
    ...: i, r = pd.factorize(df["Name"])
    ...: a = pd.Series(np.bincount(i), index = r)
    ...: 
    ...: i1, r1 = pd.factorize(df["Name"].values[df["Spend"].values > LIMIT])
    ...: b = pd.Series(np.bincount(i1), index = r1)
    ...: 
    ...: df1 = b.div(a, fill_value = 0)
    ...: 
5.05 ms ± 82.7 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)