for…in 迭代循环

首先介绍Python中最常用的for…in循环遍历的方式。for…in循环结构用于遍历列表、元组、字典、字符串、集合、文件等。其实for和in是两个独立的语法,for语句是Python内置的迭代器工具,用于从可迭代容器对象(如列表、元组、字典、字符串、集合、文件等)中逐个读取元素,直到容器中没有更多元素为止,工具和对象之间只要遵循可迭代协议即可进行迭代操作。in的存在使得python在操作可迭代对象时变得简单得多,用于配合for使用逐个取可迭代对象的元素。

for语句参与的具体迭代的过程为:可迭代对象通过iter方法返回迭代器,迭代器具有next方法,for循环不断地调用next方法,每次按序返回迭代器中的一个值,直到迭代到最后,没有更多元素时抛出异常StopIteration(Python会自动处理异常)。模拟迭代的过程如下所示:

# 迭代的过程
x = [1,2,3]
its = x.__iter__() #列表是可迭代对象,否则会提示不是迭代对象
print(its)
# 打印结果:
<list_iterator object at 0x100f32198>
print(next(its)) # its包含此方法,说明its是迭代器
# 打印结果:
1
print(next(its)) 
# 打印结果:
2
print(next(its)) 
# 打印结果:
3
print(next(its)) 
# 打印结果:
Traceback (most recent call last):
 	File "<stdin>", line 1, in <module>
StopIteration

迭代的优点是无需把所有元素一次加载到内存中,可以在调用next方法时逐个返回元素,避免出现内存空间不够的情况。

使用for…in循环方式实现单均线突破策略。遍历全部交易日的收盘价数值和Ma20数值,将收盘价数值减去Ma20数值,并使用np.sign()取差值符号,当收盘价在Ma20上方时差值为正,收盘价在Ma20上下方时差值为负,由负转正对应为买点,由正转负对应为卖点。如下所示

def forin_looping(df):
    df['signal'] = 0 #df = df.assign(signal = 0)  #可采用assign新增一列
    for i in np.arange(0,df.shape[0]):
        df.iloc[i,df.columns.get_loc('signal')] = np.sign(df.iloc[i]['Close'] - df.iloc[i]['Ma20'])
    return df
print(forin_looping(df_stockload)[0:5])
"""
              High     Low    Open   Close  Volume  Adj Close    Ma20  signal
Date                                                                         
2018-01-29  3587.0  3510.3  3563.6  3523.0  236000     3523.0  3454.3     1.0
2018-01-30  3523.1  3484.7  3511.5  3488.0  186400     3488.0  3461.3     1.0
2018-01-31  3495.5  3454.7  3470.5  3480.8  207300     3480.8  3466.8     1.0
2018-02-01  3495.1  3424.4  3478.7  3447.0  260500     3447.0  3469.9    -1.0
2018-02-02  3463.2  3388.9  3419.2  3462.1  208100     3462.1  3473.4    -1.0
"""
iterrows()生成器方式

另一种Python中常用的遍历方式为iterrows()生成器方式。所谓生成器其实是一种特殊的迭代器,内部支持了迭代器协议。Python中提供生成器函数和生成器表达式两种方式实现生成器,每次请求返回一个结果,不需要一次性构建一个结果列表,节省了内存空间。

在Python 3中可使用range返回一个迭代器,用来一次一个值地遍历一个范围.

# 生成器函数方式实现生成器
def gensquares(N):
	for i in range(N):
		yield i**2 
        
print(gensquares(5))
#打印结果:
<generator object gensquares at 0x11a35cf48>

for i in gensquares(5):
	print(i) 
# 打印结果:
0
1
4
9
16

其实yield就相当于一个return,只是return返回的是值,但是yield返回的是生成器,除了这点其他都一样,所以return也好yield也好都只能用在函数中。

生成器表达式方式实现生成器就是类似列表解析,按需产生结果的一个对象,例程代码如下所示:

# 生成器表达式方式实现生成器
print(x**2 for x in range(5))
# 打印结果:
<generator object <genexpr> at 0xb3d31fa4>

print(list(x**2 for x in range(5)))
# 打印结果:
[0, 1, 4, 9, 16]

通过iterrows()遍历方式计算股票每个交易日收盘价与Ma20差值,此处iterrows是对dataframe格式数据行进行迭代的一个生成器,它返回每行的索引及包含行本身的对象,代码如下所示:

#iterrows()遍历方式
def iterrows_loopiter(df):
    df['signal'] = 0 #df = df.assign(signal = 0)  #可采用assign新增一列
    for index,row in df.iterrows():
        df.loc[index, 'signal'] = np.sign(row['Close']-row['Ma20'])
    return df
print(iterrows_loopiter(df_stockload)[0:5])

"""
              High     Low    Open   Close  Volume  Adj Close    Ma20  signal
Date                                                                         
2018-01-29  3587.0  3510.3  3563.6  3523.0  236000     3523.0  3454.3     1.0
2018-01-30  3523.1  3484.7  3511.5  3488.0  186400     3488.0  3461.3     1.0
2018-01-31  3495.5  3454.7  3470.5  3480.8  207300     3480.8  3466.8     1.0
2018-02-01  3495.1  3424.4  3478.7  3447.0  260500     3447.0  3469.9    -1.0
2018-02-02  3463.2  3388.9  3419.2  3462.1  208100     3462.1  3473.4    -1.0
"""
apply()循环方式

apply()方法可将函数应用于dataframe特定行或列。函数由lambda方式在代码中内嵌实现,lambda 为匿名函数,可以省去定义函数的过程,让代码更加精简。lambda函数的末尾包含axis参数,用来告知Pandas将函数运用于行(axis = 1)或者列(axis = 0)。apply()方法循环方式实现的代码如下所示:

df_stockload['signal'] = df_stockload.apply(lambda row: (np.sign(row['Close']-row['Ma20'])), axis = 1)
print(df_stockload.head())
"""
              High     Low    Open   Close  Volume  Adj Close    Ma20  signal
Date                                                                         
2018-01-29  3587.0  3510.3  3563.6  3523.0  236000     3523.0  3454.3     1.0
2018-01-30  3523.1  3484.7  3511.5  3488.0  186400     3488.0  3461.3     1.0
2018-01-31  3495.5  3454.7  3470.5  3480.8  207300     3480.8  3466.8     1.0
2018-02-01  3495.1  3424.4  3478.7  3447.0  260500     3447.0  3469.9    -1.0
2018-02-02  3463.2  3388.9  3419.2  3462.1  208100     3462.1  3473.4    -1.0
矢量化遍历方式

此处我们主要处理一维数组之间的计算,那么矢量化方式可使用Pandas series 的矢量化方式和Numpy arrays的矢量化方式两种。
先来看下Pandas series 的矢量化方式。
Pandas的DataFrame、series基础单元数据结构基于链表,因此可将函数在整个链表上进行矢量化操作,而不用按顺序执行每个值。

Pandas包括了非常丰富的矢量化函数库,我们可把整个series(列)作为参数传递,对整个链表进行计算。Pandas series 的矢量化方式实现代码如下:

#Pandas series 的矢量化方式
df_stockload['signal'] = np.sign(df_stockload['Close']-df_stockload['Ma20'])
print(df_stockload.head())
"""
              High     Low    Open   Close  Volume  Adj Close    Ma20  signal
Date                                                                         
2018-01-29  3587.0  3510.3  3563.6  3523.0  236000     3523.0  3454.3     1.0
2018-01-30  3523.1  3484.7  3511.5  3488.0  186400     3488.0  3461.3     1.0
2018-01-31  3495.5  3454.7  3470.5  3480.8  207300     3480.8  3466.8     1.0
2018-02-01  3495.1  3424.4  3478.7  3447.0  260500     3447.0  3469.9    -1.0
2018-02-02  3463.2  3388.9  3419.2  3462.1  208100     3462.1  3473.4    -1.0
"""

对于Numpy arrays的矢量化方式,由于本例的矢量化运算中只使用了series的数值,无需使用索引等信息,因此可将series转换为array类型,节省操作过程中的很多开销。
我们可使用values 方法将链表从Pandas series转换为NumPy arrays,把NumPy array作为参数传递,对整个链表进行计算。Numpy arrays的矢量化方式实现代码如下:

#Numpy arrays的矢量化方式
df_stockload['signal'] = np.sign(df_stockload['Close'].values-df_stockload['Ma20'].values)
print(df_stockload.head())
"""
              High     Low    Open   Close  Volume  Adj Close    Ma20  signal
Date                                                                         
2018-01-29  3587.0  3510.3  3563.6  3523.0  236000     3523.0  3454.3     1.0
2018-01-30  3523.1  3484.7  3511.5  3488.0  186400     3488.0  3461.3     1.0
2018-01-31  3495.5  3454.7  3470.5  3480.8  207300     3480.8  3466.8     1.0
2018-02-01  3495.1  3424.4  3478.7  3447.0  260500     3447.0  3469.9    -1.0
2018-02-02  3463.2  3388.9  3419.2  3462.1  208100     3462.1  3473.4    -1.0
"""
执行效率对比
#使用timeit方法对比方法参考例程如下,需要import timeit模块:
from timeit import timeit
def test1():
    forin_looping(df_stockload)
def test2():
    iterrows_loopiter(df_stockload)
def test3():
    df_stockload['signal'] = df_stockload.apply(lambda row: (np.sign(row['Close'] - row['Ma20'])), axis=1)
def test4():
    df_stockload['signal'] = np.sign(df_stockload['Close']-df_stockload['Ma20'])
def test5():
    df_stockload['signal'] = np.sign(df_stockload['Close'].values - df_stockload['Ma20'].values)

#for..in循环迭代方式
t1 = timeit('test1()', 'from __main__ import test1', number=100)
#iterrows()遍历方式
t2 = timeit('test2()', 'from __main__ import test2', number=100)
#apply()方法循环方式
t3 = timeit('test3()', 'from __main__ import test3', number=100)
#Pandas series 的矢量化方式
t4 = timeit('test4()', 'from __main__ import test4', number=100)
#Numpy arrays的矢量化方式:
t5 = timeit('test5()', 'from __main__ import test5', number=100)

print(t1,t2,t3,t4,t5)
#14.943237108999998 8.827773373 0.5511996379999999 0.02215727200000117 0.012933490000001768

总结

可以看出循环执行的速度是最慢的,iterrows()针对Pandas的dataframe进行了优化,相比直接循环有显著提升。apply()方法也是在行之间进行循环,但由于利用了类似Cython的迭代器的一系列全局优化,其效率要比iterrows高很多。
NumPy arrays的矢量化运行速度最快,其次是Pandas series矢量化。

由于矢量化是同时作用于整个序列的,可以节省更多的时间,相比使用标量操作更好,NumPy使用预编译的C代码在底层进行优化,同时也避免了Pandas series操作过程中的很多开销,例如索引、数据类型等等,因此,NumPy arrays的操作要比Pandas series快得多。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐