在過去的幾個版本中, NVIDIA cuDF 團隊為用戶定義函數( UDF )添加了幾個新特性,這些特性可以簡化開發過程,同時提高總體性能。在本文中,我將介紹新的 UDF 增強功能,并展示如何在自己的應用程序中利用它們:
- cuDF
Series.apply
API 及其使用方法 - cuDF
DataFrame.apply
API 以及如何根據“行”編寫自定義項 - 使用兩個 apply API 增強對缺失數據的支持
- 帶有計時的真實用例示例
- 實際考慮、限制和未來計劃
為 cuDF 系列應用 API
如果您不熟悉 pandas , series apply 是用于將任意 Python 函數映射到單個數據系列的主要入口點。例如,您可能希望使用已編寫為 Python 函數的公式將攝氏溫度轉換為華氏溫度。
下面是運行此代碼的輸出后的快速刷新:
從技術上講,您可以在函數f
中編寫任何有效的 Python 代碼, pandas 在序列上循環運行函數。這使得apply
在 pandas 環境中非常靈活,因為任何 UDF 都可以成功應用,只要它能夠成功處理所有輸入數據,甚至是依賴于外部庫或期望或返回任意 Python 對象的 UDF 。
但是,這種靈活性是以性能為代價的。由于各種原因,在長循環中運行 Python 函數并不是一種有效的策略(例如,從一開始就對 Python 進行解釋)。因此,如果您的 UDF 更簡單,例如那些對標量值進行純數學運算的 UDF ,那么這種性能約束可能會令人沮喪。
幸運的是,這些用例正是 cuDF 構建的目的。最近在 UDF 范圍內的 cuDF 改進促使引入等效的apply
API :
如果您熟悉 pandas ,您可以生成與使用 pandas 處理數值相同的結果。唯一顯著的區別是,得到的數據總是 cuDF dtype
,而不是object
,這通常是熊貓的情況。
函數f
可以包含由純數學運算或 Python 運算組成的任何 Python UDF 。 cuDF 基于通過 Numba 對函數的檢查推斷出適當的返回dtype
,并在 GPU 上編譯和運行等效函數。
函數也可以編寫為接受任意
雖然在 cuDF 中有其他方法可以使用自定義內核和其他方法實現相同的目標,但這種編寫 UDF 的方法有助于將 GPU 從過程中抽象出來,這可以縮短從事快節奏、真實項目的數據科學家的開發時間。
到目前為止,我只討論了基于Series
的數據的情況。也就是說,我已經向您展示了如何使用單個輸入和輸出編寫 UDF 。然而,許多用例需要多列輸入,這需要稍微不同的思考。
數據框架 UDF 和按行思考
期望多個列作為輸入并生成單個列作為輸出的 UDF 是 pandas DataFrame apply API 支持的函數集。
在這些情況下,第一個函數參數表示一行數據,而不僅僅是單個輸入列中的一個值。我所說的 row 是指某種能夠通過鍵控獲取值的數據結構,其中鍵是列名,值是與該行中這些列的值相對應的標量。從概念上講,這是在熊貓中使用iloc
時得到的結果:
數量的標量參數。在以下代碼示例中,您可以看到支持args=
:
下面的代碼示例顯示了如何在 pandas 中編寫和使用使用此類行對象的 UDF :
cuDF 現在,您可以在不重寫自定義項的情況下完成確切的操作。
在應用這些函數時,需要注意的是,盡管 cuDF API 希望您以行的形式編寫函數,但在執行此函數時,實際上并不涉及任何行。
cuDF 避免使用 for 循環,而是執行“假裝”存在數據行的 CUDA 內核。通過一點魔法, Numba 知道如何編寫一個合適的內核來獲得與 pandas 相同的結果。因為沒有循環,所以通過此 API 執行函數時應該會看到更高的性能。
使用 series 和 DataFrame 應用對缺失值的支持
從歷史上看, cuDF 中的 UDF 并沒有提供對缺失值的完全支持。這是由于 cuDF 內部的架構選擇與 cuDF 記錄哪些元素為空的方式有關,特別是它使用空掩碼來節省內存。
pandas apply
API 的循環設計僅在數據包含空值時有效。如果數據中遇到 null , UDF 將接收特殊值pd.NA.
,如果特殊值未觸發錯誤,則執行將正常進行。然而, cuDF 不是這樣工作的,它需要一些額外的機器來支持相同的功能。如果使用 cuDF apply API ,您應該發現您的 UDF 以自然的方式處理空值:
您甚至可以在cudf.NA
單例上設置條件并獲得預期的答案,或者直接從函數返回:
顯然,這里的情況與行的情況相同: cuDF 實際上并不像 pandas 那樣運行 Python 函數。相反,它使用了更多的 Numba 魔法將此類函數轉換為等效的 CUDA 內核,然后返回結果。
在下一節中,我將查看一個真實的示例,并執行一些粗略的計時。
使用 apply 的真實示例
考慮一下這個場景:一個在線流媒體服務正在調查其訂閱者中哪些部分的訂閱時間最長。此外, leadership 還要求制定一個具體的細分方案,按年齡劃分訂閱者:
- 18 – 19
- 20 – 29
- 30 – 39
- 40 – 49
- 50 – 59
- 60 – 69
- 69 +
提供的數據只有兩個字段:age
和days_subscribed
。
以下是 UDF 如何解決這個問題。首先,編寫應用分組的按行自定義函數。接下來,獲取結果,按組 ID 分組,并平均續訂次數。
在此代碼示例中,數據是隨機生成的,因此您的里程數可能與實際答案不同。然而,它展示了這個過程。對代碼的 UDF 部分進行計時涉及到創建一個變量pdf
到pdf = df.to_pandas
,并使用 IPython 完成粗略比較:
%timeit df.apply(f, axis=1) # 1.64 ms ± 34.2 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) %timeit pdf.apply(f, axis=1) # 19.2 s ± 63.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
雖然這不是官方的基準測試, CUDA 內核在這個特定的情況下平均快了四個數量級以上,它是在 32 GB V100 GPU 上運行的。
實際考慮、限制和未來
雖然這些 cuDF 改進代表了比以前的迭代更廣泛的功能,但總有增長的空間。以下是為 cuDF 中的 apply 編寫自定義項時要考慮的關鍵項目列表:
- JIT compilation. 第一次針對 cuDF 對象執行函數時,您會遇到編譯正確的 CUDA 內核的開銷影響。除非目標數據集的
dtypes
發生更改,否則該函數的后續使用不需要重新編譯。 - dtype support. 到目前為止,
apply
中只支持數字dtypes
。然而,對其他類型的支持已經在路線圖上,從字符串開始。 - External libraries. 常見的模式是在 pandas 中執行數據準備,然后使用外部庫在 UDF 中為每一行進行處理。由于您無法將外部代碼任意映射到 GPU ,因此目前不支持此操作。
總結
UDF 是快速解決特定問題的簡單方法。在設計管道邏輯時,它們可以幫助您從單個數據的角度進行思考。通過這些新的 cuDF UDF 增強,目的是加快涉及 cuDF 的工作流的開發,并允許您快速原型化解決方案,以及重用現有業務邏輯。此外, null 支持允許您明確說明如何處理缺少的值,而不需要額外的處理步驟。
值得注意的是, UDF 是 cuDF 中積極開發的一個領域,目前正在進行更新。如果您選擇像往常一樣嘗試這些新的 UDF 增強功能,我很樂意在評論部分了解您的體驗。
?