向量化自定义解析函数会产生 ValueError

Vectorizing a custom parsing function gives a ValueError

我写了一个自定义数字解析函数。基本上,我想将 Google Play 商店(5.6M、3M、112K)中给出的应用程序大小信息转换为标准浮点数。

要将此函数应用于我的数据框中的一列数据,我想使用 numpy.vectorize 对其进行矢量化。但是,当我对其进行测试时,出现错误。

这是函数:

import numpy as np
import re

def parse_numbers(x, homo = False):
    if homo == False:
        if bool(re.match("^[0-9.]+[Mm]{1}$", x)):
            new_number = float(re.sub("[^0-9.]", "", x))
            return new_number * 1000000
        elif bool(re.match("^[0-9.]+[Kk]{1}$", x)):
            new_number = float(re.sub("[^0-9.]", "", x))
            return new_number * 1000
        else:
            return(x)
    elif homo == True:
        if bool(re.match("^[0-9.]+[MmKk]{1}$", x)):
            return "parsed_number"
        else:
            return(x)
    else:
        return "invalid setting for homo attribute"

如您所见,如果它收到任何无法解析为数字的输入,它 return 就是原始输入。

当我手动测试时,它工作正常:parse_numbers("3.1M") returns 3100000parse_numbers("not a number") returns not a number.

现在我尝试对函数进行矢量化,我这样测试它:

vparse_numbers = np.vectorize(parse_numbers)
vparse_numbers(["3.1M", "2k", "not a number"])

我收到以下错误:

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_27076/2263610390.py in <module>
     21 
     22 vparse_numbers = np.vectorize(parse_numbers)
---> 23 vparse_numbers(["3.0M", "2k", "not a number"])

c:\programdata\miniconda3\lib\site-packages\numpy\lib\function_base.py in __call__(self, *args, **kwargs)
   2161             vargs.extend([kwargs[_n] for _n in names])
   2162 
-> 2163         return self._vectorize_call(func=func, args=vargs)
   2164 
   2165     def _get_ufunc_and_otypes(self, func, args):

c:\programdata\miniconda3\lib\site-packages\numpy\lib\function_base.py in _vectorize_call(self, func, args)
   2247 
   2248             if ufunc.nout == 1:
-> 2249                 res = asanyarray(outputs, dtype=otypes[0])
   2250             else:
   2251                 res = tuple([asanyarray(x, dtype=t)

ValueError: could not convert string to float: 'not a number'

当我仅使用可解析的数字对其进行测试时:vparse_numbers(["3.1M", "2k", "not a number"]) 它会向我 return 列出正确的数字。

我在这里错过了什么?我没有正确使用 numpy.vectorize 函数吗?

np.vectorize 创建不能混合不同类型数据的 numpy.array - stringfloat - 它会尝试将所有数据转换为 float .您必须 return np.NaN 而不是所有字符串。

import numpy as np
import re
import pandas as pd

def parse_numbers(text, homo=False):
    if homo == False:
        if bool(re.match("^[0-9.]+[Mm]{1}$", text)):
            new_number = float(re.sub("[^0-9.]", "", text))
            return new_number * 1_000_000
        elif bool(re.match("^[0-9.]+[Kk]{1}$", text)):
            new_number = float(re.sub("[^0-9.]", "", text))
            return new_number * 1_000
        else:
            return np.NaN
    elif homo == True:
        if bool(re.match("^[0-9.]+[MmKk]{1}$", text)):
            return "parsed_number"
        else:
            return np.NaN
    else:
        raise Exception(f"invalid setting for homo attribute: {homo}")

vparse_numbers = np.vectorize(parse_numbers)
vparse_numbers(["3.1M", "2k", "not a number"])

但是如果你想使用 DataFrame 那么你应该使用 .apply() 它可以 return 不同类型的数据。

import numpy as np
import re
import pandas as pd

def parse_numbers(text, homo=False):
    if homo == False:
        if bool(re.match("^[0-9.]+[Mm]{1}$", text)):
            new_number = float(re.sub("[^0-9.]", "", text))
            return new_number * 1_000_000
        elif bool(re.match("^[0-9.]+[Kk]{1}$", text)):
            new_number = float(re.sub("[^0-9.]", "", text))
            return new_number * 1_000
        else:
            return text  # np.NaN
    elif homo == True:
        if bool(re.match("^[0-9.]+[MmKk]{1}$", text)):
            return "parsed_number"
        else:
            return text  # np.NaN
    else:
        raise Exception(f"invalid setting for homo attribute: {homo}")

df = pd.DataFrame({"test": ["3.1M", "2k", "not a number"]})

df['result'] = df['test'].apply(parse_numbers)

print(df)

结果:

           test        result
0          3.1M     3100000.0
1            2k        2000.0
2  not a number  not a number

编辑:

我会写parse_numbers有点不同

  • 只有一行 re
  • 使用 {,1} 将字符串也转换为数字 "123"
  • 最后转换为 integer
  • 使用不带 s 的名称 parse_number 因为它只解析一个数字。
import re
import pandas as pd


def parse_number(text, homo=False):
    if not isinstance(homo, bool):
        raise Exception(f"invalid setting for homo attribute: {homo}")

    results = re.findall("^([0-9.]+)([MmKk]{,1})$", text)
    #print(results)

    if results:
        if homo:
            return "parsed_number"
        else:
            number, name = results[0]
            new_number = float(number)
            if name in ('M', 'm'):
                new_number *= 1_000_000
            elif name in ('K', 'k'):
                new_number *= 1_000
            return int(new_number)
    else:
        return text  # np.NaN

        
df = pd.DataFrame({
    "test": ["3.1M", "2k", "123", "not a number"]
})

df['result'] = df['test'].apply(parse_number)
df['homo'] = df['test'].apply(lambda x:parse_number(x, True))

print(df)

结果:

           test        result           homo
0          3.1M       3100000  parsed_number
1            2k          2000  parsed_number
2           123           123  parsed_number
3  not a number  not a number   not a number
In [475]: alist = ["3.1M", "2k", "not a number"]

将您的函数直接应用于列表元素:

In [476]: [parse_numbers(s) for s in alist]
Out[476]: [3100000.0, 2000.0, 'not a number']

np.vectorize 如果您指定正确的 otypes 将起作用:

In [478]: fn = np.vectorize(parse_numbers, otypes=[object])
In [479]: fn(alist)
Out[479]: array([3100000.0, 2000.0, 'not a number'], dtype=object)


In [480]: timeit [parse_numbers(s) for s in alist]
12.9 µs ± 51.3 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [481]: timeit fn(alist)
24.1 µs ± 33.4 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

对于返回对象,frompyfunc 更快一些,没有一些 np.vectorize 开销:

In [482]: fn = np.frompyfunc(parse_numbers, 1,1)
In [483]: fn(alist)
Out[483]: array([3100000.0, 2000.0, 'not a number'], dtype=object)
In [484]: timeit fn(alist)
21.1 µs ± 68.5 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

我对其他 SO 的观察是 pandas apply 慢得多。 pandas 在跟踪索引方面做了很多额外的工作。

对于基本上面向字符串和列表的任务,使用 numpy 几乎没有任何优势。

对于更大的列表,时间差异消失了:

In [485]: alist = alist*1000
In [486]: len(alist)
Out[486]: 3000
In [487]: timeit fn(alist)
12.5 ms ± 70.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [488]: timeit [parse_numbers(s) for s in alist]
12.3 ms ± 319 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [489]: fn = np.vectorize(parse_numbers, otypes=[object])
In [490]: timeit fn(alist)
12.1 ms ± 42.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)