pandas 在使用時語法感覺很亂,有什麼學習的技巧嗎?

語言: CN / TW / HK

利益相關,我是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廣播機制,比較列和行的問題。

如何用 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可以避免。參考我在另一個問題下的答案。瞬間提高可讀性,效率也會提升。

python DataFrame 列運算 ?

----answer-end-here----

以上是如何解決“語法亂的問題”。關於學習建議,請移步我另外一個回答

如何系統地學習Python 中 matplotlib, numpy, scipy, pandas?

來源:知乎 www.zhihu.com

作者:peter

【知乎日報】千萬使用者的選擇,做朋友圈裡的新鮮事分享大牛。點選下載

此問題還有47 個回答,檢視全部。

延伸閱讀:

在 Pandas 中如何把物件轉換為浮點型?

pandas的shift沒有辦法應用到apply中,該如何解決?