文章目录
想法
日常刷刷知乎,看到这个问题:pandas 在使用时语法感觉很乱,有什么学习的技巧吗?
想来自己也经常使用pandas,但是每次使用都会忘记一些函数、方法,而且df太多,各种变量名层出不穷,不会管理内存,导致表一多,容易爆…下面我们看看知乎大佬是如何使用pandas的?
原文:https://www.zhihu.com/question/289788451/answer/2495499460
提示:以下是本篇文章正文内容,下面案例可供参考
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个数量级以上,而且可读性完全不一样。以算股票收益率为例。
作者:peter
链接:https://www.zhihu.com/question/289788451/answer/2495499460
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
# 以下是数据准备。
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广播机制,比较列和行的问题。
如何用 pandas 处理这个问题?
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可以避免,这个方法的好处是可以给定任意条件,并匹配对应的值。满足条件1,赋值v1;满足条件2,赋值v2。。。如果条件多了,也能一次完成赋值操作。
np.select(condlist=[con1, con2, con3, ...], choicelist=[v1, v2, v3, ...], other=0)
df.assign(c=np.select([(df.a>0.5)&(df.b<0.9), df.a<0, df.b<0],
[df.b, 0, -np.inf],
default=np.nan))
更重要的是select方法比用apply一个复杂的if else自定义函数效率要快几个数量级