【问题标题】:Numpy vectorization messes up data type (2)Numpy 向量化搞乱了数据类型 (2)
【发布时间】:2020-04-22 03:09:38
【问题描述】:

np.vectorize 产生了不想要的行为,也就是说,它改变了进入原始函数的参数的数据类型。我的original question is about the general case,我将使用这个新问题来询问更具体的案例。

(为什么是第二个问题?我创建了这个问题关于更具体的案例以说明问题 - 从具体到更一般总是更容易。而且我'我单独创建了这个问题,因为我认为保留一般情况以及对它的一般答案(应该找到)自己而不是“污染”考虑解决问题是有用的任何特殊问题。)

所以,一个具体的例子。我住的地方,星期三是彩票日。所以,让我们从一个包含今年所有星期三的日期列的 pandas 数据框开始:

df = pd.DataFrame({'date': pd.date_range('2020-01-01', freq='7D', periods=53)})

我想看看我会在哪些可能的日子里玩。每个月的月初和月底我都不会觉得特别幸运,有些月份我觉得特别不吉利。因此我使用这个函数来查看一个日期是否符合条件:

def qualifies(dt, excluded_months = []):
    #Date qualifies, if...
    #. it's on or after the 5th of the month; and
    #. at least 5 days remain till the end of the month (incl. date itself); and
    #. it's not in one of the months in excluded_months.
    if dt.day < 5:
        return False
    if (dt + pd.tseries.offsets.MonthBegin(1) - dt).days < 5:
        return False
    if dt.month in excluded_months:
        return False
    return True

我希望你意识到这个例子仍然有些做作;)但它更接近我想要做的事情。我尝试通过两种方式应用此功能:

df['qualifies1'] = df['date'].apply(lambda x: qualifies(x, [3, 8]))
df['qualifies2'] = np.vectorize(qualifies, excluded=[1])(df['date'], [3, 8])

据我所知,两者都应该工作,我更喜欢后者,因为前者很慢而且frowned upon编辑:我了解到第一个也是不赞成大声笑的。

但是,只有第一个成功,第二个失败并返回 AttributeError: 'numpy.datetime64' object has no attribute 'day'。所以我的问题是,如果有办法在这个函数qualifies 上使用np.vectorize,它需要一个日期时间/时间戳作为参数。

非常感谢!

PS:有兴趣的朋友,这里是df

In [15]: df
Out[15]: 
         date  qualifies1
0  2020-01-01       False
1  2020-01-08        True
2  2020-01-15        True
3  2020-01-22        True
4  2020-01-29       False
5  2020-02-05        True
6  2020-02-12        True
7  2020-02-19        True
8  2020-02-26       False
9  2020-03-04       False
10 2020-03-11       False
11 2020-03-18       False
12 2020-03-25       False
13 2020-04-01       False
14 2020-04-08        True
15 2020-04-15        True
16 2020-04-22        True
17 2020-04-29       False
18 2020-05-06        True
19 2020-05-13        True
20 2020-05-20        True
21 2020-05-27        True
22 2020-06-03       False
23 2020-06-10        True
24 2020-06-17        True
25 2020-06-24        True
26 2020-07-01       False
27 2020-07-08        True
28 2020-07-15        True
29 2020-07-22        True
30 2020-07-29       False
31 2020-08-05       False
32 2020-08-12       False
33 2020-08-19       False
34 2020-08-26       False
35 2020-09-02       False
36 2020-09-09        True
37 2020-09-16        True
38 2020-09-23        True
39 2020-09-30       False
40 2020-10-07        True
41 2020-10-14        True
42 2020-10-21        True
43 2020-10-28       False
44 2020-11-04       False
45 2020-11-11        True
46 2020-11-18        True
47 2020-11-25        True
48 2020-12-02       False
49 2020-12-09        True
50 2020-12-16        True
51 2020-12-23        True
52 2020-12-30       False

【问题讨论】:

  • 1) Vectorize 也不受欢迎。无论哪种方式,您都在运行 vanilla python 代码并放弃了实际矢量化的所有好处。
  • 2) Vectorize 将尝试将第一个参数转换为数组。 np.array(df['date'])df['date'].values 都有 dtype='datetime64[ns]'
  • @MadPhysicist,看起来指定otypes=['boo'] 会阻止这种转换。好像np.array(...) 转换仅在执行隐含的otype 计算时才适用。当我测试一个只打印 dt 类型的简单函数时,我也看到了这种行为。
  • @MadPhysicist,在进行otypes 计算时,vectorize 使用np.asarray(...).ravel()[0] 得到第一个测试值。但是对于主要的loop,它会执行np.array(..., dtype=object) 以准备将args 发送到frompyfunc。也许我们应该为此提交issue
  • @hpaulj。绝对看起来很值得,尽管我想不出更好的解决方案来进行测试运行。也许将类型检查推迟到以后会很有用。

标签: python pandas numpy date


【解决方案1】:

就像原来的问题一样,我可以通过在函数的第一个 if 语句之前添加 dt = pd.to_datetime(dt) 来“解决”问题 by forcing the incoming argument 成为 pandas 日期时间对象。

老实说,这感觉就像在修补损坏的、不应该使用的东西。我将改用.apply 来降低性能。任何觉得有更好解决方案的人都非常欢迎分享:)

【讨论】:

  • 这种方法在性能方面真的比.apply 更快吗?在我的测试中,它实际上更慢
  • 我刚刚尝试过,有趣的是,它不是。我试过短(53)和长(5300行)数据帧,时间最多相差几个%。这很有趣,因为如果您查看我对原始问题的回答(链接在此答案中),您会发现当要矢量化的函数仅采用一个参数时,它的速度是原来的两倍。
  • 我也设法通过添加dt = pd.Timestamp(dt) 来解决这个问题,因为day 属性来自Timestamp 类。似乎vectorize 悄悄地访问df["date"].values 什么的。它可能值得打开一个 github 问题。
  • 我认为问题可能在于 numpy vectorize 只能使用 numpy 的数据类型,并且由于 Timestamp 对象不是,所以它不能工作。
  • np.vectorize 是一个numpy 函数。它不知道pandas
【解决方案2】:

我认为@rpanai 在original post 上的回答仍然是最好的。在这里我分享我的测试:

def qualifies(dt, excluded_months = []):
    if dt.day < 5:
        return False
    if (dt + pd.tseries.offsets.MonthBegin(1) - dt).days < 5:
        return False
    if dt.month in excluded_months:
        return False
    return True

def new_qualifies(dt, excluded_months = []):
    dt = pd.Timestamp(dt)
    if dt.day < 5:
        return False
    if (dt + pd.tseries.offsets.MonthBegin(1) - dt).days < 5:
        return False
    if dt.month in excluded_months:
        return False
    return True

df = pd.DataFrame({'date': pd.date_range('2020-01-01', freq='7D', periods=12000)})

申请方法:

%%timeit
df['qualifies1'] = df['date'].apply(lambda x: qualifies(x, [3, 8]))

每个循环 385 毫秒 ± 21.6 毫秒(平均值 ± 标准偏差,7 次运行,每个循环 1 个)


转换方法:

%%timeit
df['qualifies1'] = df['date'].apply(lambda x: new_qualifies(x, [3, 8]))

每个循环 389 毫秒 ± 12.6 毫秒(平均值 ± 标准偏差,7 次运行,每个循环 1 个)


矢量化代码:

%%timeit
df['qualifies2'] =  np.logical_not((df['date'].dt.day<5).values | \
    ((df['date']+pd.tseries.offsets.MonthBegin(1)-df['date']).dt.days < 5).values |\
    (df['date'].dt.month.isin([3, 8])).values)

每个循环 4.83 ms ± 117 µs(平均值 ± 标准偏差,7 次运行,每次 100 个循环)

【讨论】:

  • 感谢您回复@Adrea。这肯定是一个很大的性能提升!我发现它的可读性要差很多,但我想这是性能和可读性之间的权衡。
  • @ElRudi。你只是还没有学会如何阅读它。不过你会通过足够的练习。
  • 我同意,可读性要差得多。也许可以尝试通过将问题拆分为 3 个部分并使用三个不同的列来解决它,但 .apply 方法仍然更优雅(至少更 Pythonic)
【解决方案3】:

总结

如果使用np.vectorize,最好指定otypes。在这种情况下,错误是由vectorize 在未指定otypes 时使用的试算引起的。另一种方法是将 Series 作为对象类型数组传递。

np.vectorize 有性能免责声明。 np.frompyfunc 可能更快,甚至是列表理解。

测试矢量化

让我们定义一个更简单的函数 - 一个显示参数类型的函数:

In [31]: def foo(dt, excluded_months=[]): 
    ...:     print(dt,type(dt)) 
    ...:     return True 

还有一个更小的数据框:

In [32]: df = pd.DataFrame({'date': pd.date_range('2020-01-01', freq='7D', perio
    ...: ds=5)})                                                                
In [33]: df                                                                     
Out[33]: 
        date
0 2020-01-01
1 2020-01-08
2 2020-01-15
3 2020-01-22
4 2020-01-29

测试vectorize。 (vectorize 文档说使用excluded 参数会降低性能,所以我使用lambdaapply 一起使用):

In [34]: np.vectorize(lambda x:foo(x,[3,8]))(df['date'])                        
2020-01-01T00:00:00.000000000 <class 'numpy.datetime64'>
2020-01-01 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-08 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-15 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-22 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-29 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
Out[34]: array([ True,  True,  True,  True,  True])

第一行是datetime64,它给出了问题。其他行是原始的 pandas 对象。如果我指定otypes,这个问题就会消失:

In [35]: np.vectorize(lambda x:foo(x,[3,8]), otypes=['bool'])(df['date'])       
2020-01-01 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-08 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-15 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-22 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-29 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
Out[35]: array([ True,  True,  True,  True,  True])

申请:

In [36]: df['date'].apply(lambda x: foo(x, [3, 8]))                             
2020-01-01 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-08 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-15 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-22 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-29 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
Out[36]: 
0    True
1    True
2    True
3    True
4    True
Name: date, dtype: bool

datetime64 dtype 是通过将系列包装在 np.array 中来生成的。

In [37]: np.array(df['date'])                                                   
Out[37]: 
array(['2020-01-01T00:00:00.000000000', '2020-01-08T00:00:00.000000000',
       '2020-01-15T00:00:00.000000000', '2020-01-22T00:00:00.000000000',
       '2020-01-29T00:00:00.000000000'], dtype='datetime64[ns]')

显然np.vectorize 在执行初始试算时会进行这种包装,但在进行主要迭代时不会。指定 otypes 会跳过该试算。这种试算在其他 SO 中引起了问题,尽管这是一个比较模糊的案例。

在过去,当我测试 np.vectorize 时,它比更明确的迭代要慢。它确实有一个明确的性能免责声明。当函数需要多个输入并且需要广播的好处时,它最有价值。仅使用一个论点时很难证明其合理性。

np.frompyfuncvectorize 的基础,但返回一个对象 dtype。通常它比数组上的显式迭代快 2 倍,尽管速度与列表上的迭代相似。在创建和使用 numpy 对象数组时,它似乎最有用。在这种情况下我还没有得到它的工作。

矢量化代码

np.vectorize 代码位于np.lib.function_base.py

如果没有指定otypes,代码会这样做:

        args = [asarray(arg) for arg in args]
        inputs = [arg.flat[0] for arg in args]
        outputs = func(*inputs)

它将每个参数(这里只有一个)放入一个数组中,并获取第一个元素。然后将其传递给func。正如Out[37] 所示,这将是一个datetime64 对象。

来自pyfunc

要使用frompyfunc,我需要转换df['date']的dtype:

In [68]: np.frompyfunc(lambda x:foo(x,[3,8]), 1,1)(df['date'])                  
1577836800000000000 <class 'int'>
1578441600000000000 <class 'int'>
...

没有它,它会将int 传递给函数,有了它,它就会传递熊猫时间对象:

In [69]: np.frompyfunc(lambda x:foo(x,[3,8]), 1,1)(df['date'].astype(object))   
2020-01-01 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
2020-01-08 00:00:00 <class 'pandas._libs.tslibs.timestamps.Timestamp'>
...

所以qualifies 的这种用法有效:

In [71]: np.frompyfunc(lambda x:qualifies(x,[3,8]),1,1)(df['date'].astype(object))                                                                     
Out[71]: 
0    False
1     True
2     True
3     True
4    False
Name: date, dtype: object

对象数据类型

对于主迭代,np.vectorize

      ufunc = frompyfunc(_func, len(args), nout)
      # Convert args to object arrays first
        inputs = [array(a, copy=False, subok=True, dtype=object)
                  for a in args]
        outputs = ufunc(*inputs)

这解释了为什么 vectorizeotypes 有效 - 它使用 frompyfunc 和对象 dtype 输入。与Out[37]对比:

In [74]: np.array(df['date'], dtype=object)                                     
Out[74]: 
array([Timestamp('2020-01-01 00:00:00'), Timestamp('2020-01-08 00:00:00'),
       Timestamp('2020-01-15 00:00:00'), Timestamp('2020-01-22 00:00:00'),
       Timestamp('2020-01-29 00:00:00')], dtype=object)

指定otypes 的替代方法是确保将对象dtype 传递给vectorize

In [75]: np.vectorize(qualifies, excluded=[1])(df['date'].astype(object), [3, 8])                                                                      
Out[75]: array([False,  True,  True,  True, False])

这似乎是最快的版本:

np.frompyfunc(lambda x: qualifies(x,[3,8]),1,1)(np.array(df['date'],object))    

或者更好的是,一个简单的 Python 迭代:

[qualifies(x,[3,8]) for x in df['date']] 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-19
    • 2017-07-12
    • 1970-01-01
    相关资源
    最近更新 更多