关于人工智能:pandas高效实现条件逻辑

5次阅读

共计 5720 个字符,预计需要花费 15 分钟才能阅读完成。

作者 |Louis Chan
编译 |VK
起源 |Towards Data Science

Python 能够说是当今最酷的编程语言(多亏了机器学习和数据迷信),但与最好的编程语言之一 C 相比,它的效率并不是很高。

在开发机器学习模型时,很常见的状况是,咱们须要依据从统计分析或上一次迭代的后果导出的硬编码规定,而后以编程形式更新。抵赖这一点并不耻辱:我始终在用 Pandas apply 编写代码,直到有一天我对嵌套十分腻烦,于是决定钻研(又称 Google)其余更可保护、更高效的办法

演示数据集

咱们将要应用的数据集是 iris 数据集,你能够通过 pandas 或 seaborn 收费取得它。

import pandas as pd
iris = pd.read_csv('https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv')

# import seaborn as sns
# iris = sns.load_dataset("iris")

iris 数据集的前 5 行

数据统计信息

假如在初始剖析之后,咱们心愿用以下逻辑标记数据集:

  • 如果萼片长度(sepal length)< 5.1,则标签为 0;
  • 否则,如果萼片宽度(sepal width)> 3.3 和萼片长度 < 5.8,则标签为 1;
  • 否则,如果萼片宽度 > 3.3,花瓣长度(petal length)> 5.1,则标签为 2;
  • 否则,如果萼片宽度 > 3.3,花瓣长度 < 1.6 且萼片长度 < 6.4 或花瓣宽度 < 1.3,则标签 3;
  • 否则,如果萼片宽度 >3.3 且萼片长度 < 6.4 或花瓣宽度 < 1.3,则标签为 4;
  • 否则,如果萼片宽度 > 3.3,则标签为 5;
  • 否则标签 6

在深入研究代码之前,让咱们疾速地将一个新的 label 列设置为 None:

iris['label'] = None

Pandas.iterrows+ 嵌套 If Else 块

如果你还在用这个,这篇博文相对是适宜你的中央!

%%timeit
for idx, row in iris.iterrows():
  if row['sepal_length'] < 5.1:
    iris.loc[idx, 'label'] = 0
  elif row['sepal_width'] > 3.3:
    if row['sepal_length'] < 5.8:
      iris.loc[idx, 'label'] = 1
    elif row['petal_length'] > 5.1:
      iris.loc[idx, 'label'] = 2
    elif (row['sepal_length'] < 6.4) or (row['petal_width'] < 1.3):
      if row['petal_length'] < 1.6:
        iris.loc[idx, 'label'] = 3
      else:
        iris.loc[idx, 'label'] = 4
    else:
      iris.loc[idx, 'label'] = 5
  else:
    iris.loc[idx, 'label'] = 6
1min 29s ± 8.91 s per loop (mean ± std. dev. of 7 runs, 1 loop each)

工夫挺长…好吧,咱们持续…

Pandas .apply

Pandas.apply 间接用于沿数据帧的轴或 Series 来利用函数。例如,如果咱们有一个函数 f,它能够是一个数列的和(例如,能够是一个 list, np.array, tuple 等),并将其传递给如下数据帧,咱们将跨行求和:

def f(numbers):
    return sum(numbers)
    
df['Row Subtotal'] = df.apply(f, axis=1)

在 axis= 1 上利用函数。默认状况下,apply 参数 axis=0,即逐行利用函数;而 axis= 1 将逐列利用函数。

当初咱们曾经对 pandas.apply 有了根本的理解,当初让咱们编写调配标签的逻辑代码,看看它运行多长时间:

%%timeit
def rules(row):
  if row['sepal_length'] < 5.1:
    return 0
  elif row['sepal_width'] > 3.3:
    if row['sepal_length'] < 5.8:
      return 1
    elif row['petal_length'] > 5.1:
      return 2
    elif (row['sepal_length'] < 6.4) or (row['petal_width'] < 1.3):
      if row['petal_length'] < 1.6:
        return 3
      return 4
    return 5
  return 6

iris['label'] = iris.apply(rules, 1)
1.43 s ± 115 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

15 万行只须要 1.43s 比之前的程度有了很大的进步,但依然十分迟缓。

设想一下,如果你须要解决一个由数百万个交易数据或信贷批准组成的数据集,那么每次咱们要利用一组规定并将函数利用在一个列时,它将占用 14 秒以上。运行足够多的列,你一个下午可能就没了。

Pandas.loc[]

如果你相熟 SQL,那么应用.loc[]为新列赋值实际上只是一个带有 WHERE 条件的 UPDATE 语句。因而,这应该比将函数利用于每个行或列要好得多。

%%timeit
iris['label'] = 6
iris.loc[iris['sepal_width'] > 3.3, 'label'] = 5
iris.loc[(iris['sepal_width'] > 3.3) & 
  ((iris['sepal_length'] < 6.4) | (iris['petal_width'] < 1.3)), 
  'label'] = 4
iris.loc[(iris['sepal_width'] > 3.3) & 
  ((iris['sepal_length'] < 6.4) | (iris['petal_width'] < 1.3)) & 
  (iris['petal_length'] < 1.6), 
  'label'] = 3
iris.loc[(iris['sepal_width'] > 3.3) & 
  (iris['petal_length'] > 5.1), 
  'label'] = 2
iris.loc[(iris['sepal_width'] > 3.3) & 
  (iris['sepal_length'] < 5.8), 
  'label'] = 1
iris.loc[(iris['sepal_length'] < 5.1), 
  'label'] = 0
13.3 ms ± 837 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

当初咱们只花了前一次的十分之一的工夫,这意味着当你在家工作的时候,你没有更多的借口来到办公桌。不过,咱们目前只应用 pandas 内置的函数。只管 pandas 为咱们提供了一个十分不便的高级接口来与数据表交互,然而通过层层形象,效率可能会升高。

Numpy.where

Numpy 有一个较低级别的接口,容许与 n 维 iterables(即向量、矩阵、张量等)进行更无效的交互。它的办法通常是基于 C 语言的,当波及到更简单的计算时,它应用了优化的算法,使得它比咱们从新创造的轮子更快。

依据 numpy 的官网文件,np.where()承受以下语法:

np.where(condition, return value if True, return value if False)

实质上,这是一种二分,其中条件将被计算为布尔值并相应地返回值。这里的技巧是条件实际上能够是 iterable(即布尔 ndarray 类型)。这意味着咱们能够将 df[‘feature’]== 1 作为条件,并将 where 逻辑编码为:

np.where(df['feature'] == 1, 
    'It is one', 
    'It is not one'
)

所以你可能会问,咱们如何用一个像 np.where()这样的二分函数来实现上述逻辑呢? 答案很简略,但却令人不安。嵌套 np.where()

%%timeit
iris['label'] = np.where(iris['sepal_length'] < 5.1,
  0,
  np.where(iris['sepal_width'] > 3.3,
    np.where(iris['sepal_length'] < 5.8,
      1,
      np.where(iris['petal_length'] > 5.1,
        2,
        np.where((iris['sepal_length'] < 6.4) | (iris['petal_width'] < 1.3),
          np.where(iris['petal_length'] < 1.6,
            3,
            4
          ),
          5
        )
      )
    ),
    6
  )
)
3.6 ms ± 149 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

祝贺你,你挺过去了。我不能通知你我花了多少次来计算右括号,然而嘿,这就实现了!咱们又从 pandas 身上砍下了 10 毫秒。loc[]。然而,这个代码片段是不可保护的,这意味着,它是不可承受的。

Numpy.select

Numpy.select,它与.where 不同,它是用来实现多线程逻辑的函数。

np.select(condlist, choicelist, default=0)

它的语法近似于 np.where,但第一个参数当初是一个条件列表,它的长度应该与选项的长度雷同。应用时要记住一件事 np.select 是在满足第一个条件后立刻抉择一个选项。

这意味着,如果超集规定呈现在列表中的子集规定之前,那么子集抉择将永远不会被抉择。具体说来:

condlist = [df['A'] <= 1,
    df['A'] < 1
]

choicelist = ['<=1', '<1']

selection = np.select(condlist, choicelist, default='>1')

因为所有命中 df[‘A’]<1 的行也将被 df[‘A’]<= 1 捕捉,因而没有行最终被标记为 '<1’。为了防止这种状况产生,请务必在更具体的规定之前先制订一个不太具体的规定:

condlist = [df['A'] < 1, # < ───┬ 替换
    df['A'] <= 1 # < ───┘
]

choicelist = ['<1', '<=1'] # 记住也要更新这个!

selection = np.select(condlist, choicelist, default='>1')

从下面能够看到,你须要同时更新 condlist 和 choicelsit,以确保代码顺利运行。但说真的,这一步也耗咱们本人的工夫。通过将其更改为字典,咱们将达到大致相同的工夫和内存复杂性,但应用更易于保护的代码片段:

%%timeit
rules = {0: (iris['sepal_length'] < 5.1),
  1: (iris['sepal_width'] > 3.3) & (iris['sepal_length'] < 5.8),
  2: (iris['sepal_width'] > 3.3) & (iris['petal_length'] > 5.1),
  3: ((iris['sepal_width'] > 3.3) & \
    ((iris['sepal_length'] < 6.4) | (iris['petal_width'] < 1.3)) & \
    (iris['petal_length'] < 1.6)
  ),
  4: ((iris['sepal_width'] > 3.3) & \
    ((iris['sepal_length'] < 6.4) | (iris['petal_width'] < 1.3))
  ),
  5: (iris['sepal_width'] > 3.3),
}

iris['label'] = np.select(rules.values(), rules.keys(), default=6)
6.29 ms ± 475 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

大概是 np.where 的一半,但这不仅使你免于对各种嵌套的调试,而且使 choicelist 产生了变动。之前我曾经遗记更新 choicelist 太屡次了,以至于我花了四倍多的工夫来调试我的机器学习模型。置信我,np.select 和 dict。这是十分好的抉择

优良函数

  1. Numpy 的向量化操作:如果你的代码波及循环和计算一元函数、二进制函数或对数字序列进行操作的函数。你应该通过将数据转换为 numpy-ndarray 来重构代码,并充分利用 numpy 的向量化操作来极大地提高脚本的速度。在 Numpy 的官网文档中查看一元函数、二元函数或对数字序列进行操作的函数的示例:https://www.pythonlikeyoumean…
  2. np.vectorize:不要被这个函数的名字愚弄。这只是一个不便的函数,并不会使代码运行得更快。要应用此函数,首先须要将逻辑编码为可调用函数,而后运行 np.vectorize(你的函数)(你的数据系列)。另一个大的毛病是须要将数据帧转换为一维的 iterable,以便传递到“矢量化”函数中。论断:如果不方便使用 np.vectorize,别应用。
  3. numba.njit:当初这是真正的向量化。它试图将任何 numpy 值挪动到尽可能靠近 C 语言,以进步其效率。尽管它能够减速数值计算,但它也将本人限度为数值计算,这意味着没有 pandas 系列,没有字符串索引,只有具备 int、float、datetime、bool 和 category 类型的 numpy 的 ndarray。论断: 如果你可能轻松地应用 Numpy 的 ndarray 并将逻辑转换为数值计算或仅转换为数值计算,那么它将是一个十分优良的抉择。从这里理解更多:https://numba.pydata.org/numb…

结尾

如果可能的话,去争取 numba.njit;否则,应用 np.select 和 dict 就能够帮忙你远航了。记住,每一点改良都会有帮忙!

原文链接:https://towardsdatascience.co…

欢送关注磐创 AI 博客站:
http://panchuang.net/

sklearn 机器学习中文官网文档:
http://sklearn123.com/

欢送关注磐创博客资源汇总站:
http://docs.panchuang.net/

正文完
 0