利益相关,我是pandas早期版本(1.0之前)的贡献者。以下是我的PR
Pull requests · pandas-dev/pandas
这也不是你一个人遇到的问题。工作原因,我经常review一些菜鸟数据分析、数据处理脚本,对我来说感觉就像是深入到了老坛酸菜的土坑生产作坊。幸运的是社区已经总结了一些常见问题并给出了常见操作的操作手册,都在官网文档的Cookbook中。篇幅不长,显浅易懂,都有可执行样例,强烈推荐!!!
Cookbook – pandas 1.4.2 documentation
我有一些best practice可以解决新手实践中常见的问题,立即提高代码可读性
1.临时DataFrame散落在一个notebook各处,(下文缩写为df)
为了帮菜鸟debug一个error经常要trace一个又一个临时DataFrame溯源出错的列到底是怎么来的,df1,df2,df3…, df_temp1, df_temp2等等,真的心累。
还有,许多人为了看了一些网络教程,为避免满屏的setting with copy warning的,建的都是深度拷贝(df.copy(deep=True)。极大浪费了内存。题主所谓的语法乱,应该很大程度上是这样造成的。
可以用pipe方法解决这个问题,pipe即为管道,把前一项输出的DF,作为后一项输入的DF,同时把df操作函数对象作为第一参数,它所需的参args和kwargs传入。这样避免产生中间的df。当参数复杂(比如是巨大的dictionary,或者是一连串函数计算后的结果)、高阶方法多,比直接chaining可读性高。
# 举个例子,每次分析工作完成后,把琐碎的数据清理工作以如下形式放在数据导入后的下一步
dtype_mapping = {'a':np.float32, 'b':np.uint8, 'c':np.float64, 'd':np.int64, 'e':str}
df_cleaned = (df
.pipe(pd.DataFrame.sort_index, ascending=False) #按索引排序
.pipe(pd.DataFrame.fillna,value=0, method='ffill') #缺失值处理
.pipe(pd.DataFrame.astype, dtype_mapping) #数据类型变换
.pipe(pd.DataFrame.clip, lower= -0.05, upper=0.05) #极端值处理
)
# 也可以包装成一个函数
def clean_data(df):
...#上面的pipe操作
return df_cleaned
2 衍生列、辅助列生生成在各个角落
这会导致debug困难,尤其是列还是前后依赖的情况。通常还伴随着setting with copy warning。可以使用assign方法,把一些列生成操作集中在一起。(和直接用df[‘x] = … 不同的是assign方法会生成一个新的df,原始的df不会变 ,不会有setting with copy warning),还有一个好处,就是不会因为生成新的操作而打断函数chaining.
# 官方doc的例子
df = pd.DataFrame(data=25 + 5 * np.random.randn(10), columns=["temp_c"])
df.assign(temp_f=lambda x: x['temp_c'] * 9 / 5 + 32,
temp_k=lambda x: (x['temp_f'] + 459.67) * 5 / 9)
)
3. 多个简单条件组合起来的筛选看上去很复杂
用query解决很多条件的问题筛选的问题。
df = pd.DataFrame(data=np.random.randn(10,3), columns=list("abc"))
#用query
df.query("(a>0 and b<0.05) or c>b")
#普通方法
df.loc[((df['a']>0) & (df['b']<0.05))| (df['c']>df['b'])]
明显query方法简洁,而且条件越多,逻辑判断越多,可读性优势就越明显(前提是单个条件都是简单的判断)。
4.不必要的iloc或者iterrow或者itertuple遍历df
凡是数值操作,用pandas或者numpy原生的函数一般比你自己定义一个函数要快1个数量级以上,而且可读性完全不一样。以算股票收益率为例。
# 以下是数据准备。
import pandas as pd
import numpy as np
import pandas_datareader as web
import datetime
start = datetime.datetime(2021, 6, 1)
end = datetime.datetime(2021, 12, 31)
#选google,testla,neflix,和coke
assets = ['GOOG', 'TSLA', 'NFLX', 'KO']
#读取4个股票在2021年下半年的历史交易数据
df = web.DataReader(assets, 'stooq', start, end) 、
df_cls_price = df.loc[:,'Close'] #只看收盘价
下面是错误的示范,没有耐心的同学可以直接跳过。
#方法一用iloc遍历的方式
def wrong_func():
df_wrong = pd.DataFrame(index=df_cls_price.index,
columns=df_cls_price.columns)
for i in range(df_cls_price.shape[0]):
for s in assets:
if i == 0:
df_wrong.iloc[i][s] = 0
else:
#通过iloc[i-1]和iloc[i]做差
diff = (df_cls_price.iloc[i][s] - df_cls_price.iloc[i-1][s])
denominator = df_cls_price.iloc[i-1][s]
df_wrong.iloc[i][s] = diff/denominator
return df_wrong
#call这个上述方法
wrong_func()
#方法二、用pandas自带方法
df_cls_price.pct_change()
#看下执行效率
在我的机器上时间差了200多倍,而且方便好多。
5 把timeseries数据当成string操作,又慢又难懂
# 还是上面的例子,求股票月度平均价格
# 方法一、用groupby,string来做
(df_cls_price
# 用function作为grouper时,会取日期索引字符串前7位,比如2021-07
.groupby(lambda x: str(x)[:7])
.mean()
)
#方法二、用resample来操作
(df_cls_price
.resample('1M')
.mean()
)
方法2更直观且速度快,而且可复用性变强了,可以随时换到其他时间区间,方法一就不行了。
# 方法二,也可以很容易扩展到其他时间区间
(df_cls_price
.resample('10T') #10天
#.resample('2W') #双周
#.resample('Q) #季度
.mean()
)
# 还可以有分钟、秒,不适用本案例
6.不必要的merge,
常见情况是用了汇总操作,然后把汇总结果merge回原来的数据。然后进行下一步计算。这就可以用transform代替。接上例,这次做一个原价减去月度均价的操作。
#方法一、用agg汇总后再merge到原表
df_wrong = df_cls_price.reset_index() #把datetime64的索引变成列,列名为Date
df_wrong['month'] = df_wrong['Date'].apply(lambda x: str(x)[:7]) # 生成month辅助列
#得到月均价
df_wrong_avgprice = (df_wrong
.groupby('month')
.mean()
)
#把月均价df和原来数据合并
df_wrong_joined = df_wrong.join(df_wrong_avgprice,on='month', rsuffix='_1m_mean')
#计算
df_wrong_joined.assign(
GOOG_demean = df_wrong_joined['GOOG'] - df_wrong_joined['GOOG_1m_mean'],
TSLA_demean = df_wrong_joined['TSLA'] - df_wrong_joined['TSLA_1m_mean'],
NFLX_demean = df_wrong_joined['NFLX'] - df_wrong_joined['NFLX_1m_mean'],
KO_demean = df_wrong_joined['KO'] - df_wrong_joined['KO_1m_mean']
)
#方法二、用grouper加transform
df_cls_price.groupby(pd.Grouper(freq='1M')).transform(lambda x: x- x.mean())
#方法三、熟练用户会直接用‘-’,更快更简洁
df_cls_price - df_cls_price.groupby(pd.Grouper(freq='1M')).transform(np.mean)
#看一下效率
可以看到用transform明显代码简洁,而且没有生成必要的df和不必要的辅助列。而且可以非常容易扩展到其他时间间隔。
7.没有向量化的思维,太多for循环,不会用numpy造作
参考这个回答。这是个典型的利用numpy广播机制,比较列和行的问题。
8.apply函数用非常复杂的条件,很多的if else
比如
def abcd_to_e(x):
if x['a']>1:
return 1
elif x['b']<0:
return x['b']
elif x['a']>0.1 and x['b']<10:
return 10
elif ...:
return 1000
elif ...
else:
return x['c']
df.apply(abcd_to_e, axis=1)
这个用numpy的select可以避免。参考我在另一个问题下的答案。瞬间提高可读性,效率也会提升。
—-answer-end-here—-
以上是如何解决“语法乱的问题”。关于学习建议,请移步我另外一个回答
如何系统地学习Python 中 matplotlib, numpy, scipy, pandas?
来源:知乎 www.zhihu.com
作者:peter
【知乎日报】千万用户的选择,做朋友圈里的新鲜事分享大牛。
点击下载
此问题还有 47 个回答,查看全部。
延伸阅读:
在 Pandas 中如何把对象转换为浮点型?