我有不需要的行为来自np.vectorize
,即它改变了进入原始函数的参数的数据类型。我最初的问题是关于一般情况的,我将使用这个新问题来提出更具体的情况。
(为什么是第二个问题?我创建了一个关于更具体案例的问题,以说明问题 - 从具体到更一般总是更容易。我单独创建了这个问题,因为我认为保持一般情况以及一般答案(如果找到一个)是有用的,而不是被思考解决任何特定问题所"污染"。
所以,一个具体的例子。在我住的地方,星期三是彩票日。因此,让我们从一个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])
据我所知,两者都应该有效,我更喜欢后者,因为前者速度慢且不受欢迎。编辑:我了解到,第一个也是皱眉的哈哈。
但是,只有第一个成功,第二个失败并AttributeError: 'numpy.datetime64' object has no attribute 'day'
.所以我的问题是,如果有办法在这个函数上使用np.vectorize
qualifies
,它以日期时间/时间戳作为参数。
非常感谢!
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
我认为原始帖子上的@rpanai答案仍然是最好的。在这里,我分享我的测试:
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 次循环)
总结
如果使用np.vectorize
最好指定otypes
。 在这种情况下,错误是由未指定otypes
时vectorize
使用的试验计算引起的。 另一种方法是将 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
参数会降低性能,所以我使用lambda
与apply
一起使用):
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
。 其他行是原始的大熊猫对象。 如果我指定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
通过将系列包装在np.array
中来产生datetime64
dtype。
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.frompyfunc
是vectorize
的基础,但返回一个对象 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
对象。
弗洛皮芬克
要使用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
传递给函数,有了它,它会传递 Pandas 时间对象:
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
对象 dtype
对于主迭代,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)
这就解释了为什么vectorize
withotypes
有效 - 它将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']]
就像在原始问题中一样,我可以通过强制传入参数为pandas
日期时间对象来"解决"问题,方法是在函数的第一个if
-语句之前添加dt = pd.to_datetime(dt)
。
老实说,这感觉就像修补一些坏了的东西,不应该使用。我将改用.apply
并承受性能影响。任何认为有更好解决方案的人都非常受邀分享:)