擴充套件 pandas#
雖然 pandas 提供了豐富的函式、容器和資料型別,但您的需求可能無法完全滿足。pandas 提供了幾種擴充套件 pandas 的選項。
註冊自定義訪問器#
庫可以使用裝飾器 pandas.api.extensions.register_dataframe_accessor()、pandas.api.extensions.register_series_accessor() 和 pandas.api.extensions.register_index_accessor() 來為 pandas 物件新增額外的“名稱空間”。所有這些都遵循類似的約定:您用裝飾器修飾一個類,提供要新增的屬性的名稱。類的 __init__ 方法接收被裝飾的物件。例如
@pd.api.extensions.register_dataframe_accessor("geo")
class GeoAccessor:
def __init__(self, pandas_obj):
self._validate(pandas_obj)
self._obj = pandas_obj
@staticmethod
def _validate(obj):
# verify there is a column latitude and a column longitude
if "latitude" not in obj.columns or "longitude" not in obj.columns:
raise AttributeError("Must have 'latitude' and 'longitude'.")
@property
def center(self):
# return the geographic center point of this DataFrame
lat = self._obj.latitude
lon = self._obj.longitude
return (float(lon.mean()), float(lat.mean()))
def plot(self):
# plot this array's data on a map, e.g., using Cartopy
pass
現在使用者可以透過 geo 名稱空間訪問您的函式
>>> ds = pd.DataFrame(
... {"longitude": np.linspace(0, 10), "latitude": np.linspace(0, 20)}
... )
>>> ds.geo.center
(5.0, 10.0)
>>> ds.geo.plot()
# plots data on a map
這是一種方便的擴充套件 pandas 物件而不對其進行子類化的方法。如果您編寫了一個自定義訪問器,請提交一個 pull request 將其新增到我們的 生態系統 頁面。
我們強烈建議驗證您訪問器 __init__ 中的資料。在我們的 GeoAccessor 中,我們驗證資料是否包含預期的列,並在驗證失敗時引發 AttributeError。對於 Series 訪問器,如果訪問器僅適用於某些 dtype,則應驗證 dtype。
擴充套件型別#
注意
在 pandas 1.5 之前,pandas.api.extensions.ExtensionDtype 和 pandas.api.extensions.ExtensionArray API 是實驗性的。從 1.5 版本開始,未來的更改將遵循 pandas 棄用策略。
pandas 定義了一個介面,用於實現擴充套件 NumPy 型別的型別和陣列。pandas 本身使用擴充套件系統來處理一些 NumPy 未內建的型別(分類、週期、區間、帶時區的日期時間)。
庫可以定義自定義陣列和資料型別。當 pandas 遇到這些物件時,它們將被正確處理(即不會轉換為物件 ndarray)。許多函式,如 pandas.isna(),將分派到擴充套件型別的實現。
如果您正在構建一個實現該介面的庫,請在 生態系統頁面 上進行宣傳。
該介面包含兩個類。
ExtensionDtype#
一個 pandas.api.extensions.ExtensionDtype 類似於 numpy.dtype 物件。它描述了資料型別。實現者負責一些獨特的項,如名稱。
一個特別重要的項是 type 屬性。這應該是您資料的標量型別的類。例如,如果您正在為 IP 地址資料編寫擴充套件陣列,這可能是 ipaddress.IPv4Address。
有關介面定義的更多資訊,請參閱 擴充套件 dtype 原始碼。
可以將 pandas.api.extensions.ExtensionDtype 註冊到 pandas,以便透過字串 dtype 名稱進行建立。這允許人們例項化 Series 和使用註冊的字串名稱進行 .astype(),例如 'category' 是 CategoricalDtype 的註冊字串訪問器。
有關如何註冊 dtype 的更多資訊,請參閱 擴充套件 dtype 原始碼。
ExtensionArray#
此類提供了所有類陣列的功能。ExtensionArrays 僅限於 1 維。ExtensionArray 透過 dtype 屬性連結到 ExtensionDtype。
pandas 對如何透過 __new__ 或 __init__ 建立擴充套件陣列沒有任何限制,並且對如何儲存資料也沒有任何限制。我們要求您的陣列可轉換為 NumPy 陣列,即使這相對昂貴(如 Categorical)。
它們可以由零個、一個或多個 NumPy 陣列支援。例如,pandas.Categorical 是由兩個陣列支援的擴充套件陣列,一個用於程式碼,一個用於類別。IPv6 地址陣列可以由一個包含兩個欄位(一個用於低 64 位,一個用於高 64 位)的 NumPy 結構化陣列支援。或者它們可以由其他儲存型別支援,例如 Python 列表。
有關介面定義的更多資訊,請參閱 擴充套件陣列原始碼。文件字串和註釋包含有關正確實現介面的指導。
ExtensionArray 運算子支援#
預設情況下,ExtensionArray 類沒有定義運算子。有兩種方法可以為您的 ExtensionArray 提供運算子支援
在您的
ExtensionArray子類上定義每個運算子。使用 pandas 中一個依賴於 ExtensionArray 底層元素(標量)上已定義的運算子的運算子實現。
注意
無論採用哪種方法,如果您希望在與 NumPy 陣列進行二元運算時呼叫您的實現,您可能需要設定 __array_priority__。
對於第一種方法,您定義選定的運算子,例如 __add__、__le__ 等,您希望您的 ExtensionArray 子類支援這些運算子。
第二種方法假定 ExtensionArray 的底層元素(即標量型別)已經定義了單獨的運算子。換句話說,如果您的名為 MyExtensionArray 的 ExtensionArray 實現方式是每個元素都是 MyExtensionElement 類的例項,那麼如果為 MyExtensionElement 定義了運算子,第二種方法將自動為 MyExtensionArray 定義運算子。
混合類 ExtensionScalarOpsMixin 支援第二種方法。如果您正在開發一個 ExtensionArray 子類,例如 MyExtensionArray,您可以簡單地將 ExtensionScalarOpsMixin 作為 MyExtensionArray 的父類,然後呼叫 _add_arithmetic_ops() 和/或 _add_comparison_ops() 方法將運算子鉤接到您的 MyExtensionArray 類中,如下所示
from pandas.api.extensions import ExtensionArray, ExtensionScalarOpsMixin
class MyExtensionArray(ExtensionArray, ExtensionScalarOpsMixin):
pass
MyExtensionArray._add_arithmetic_ops()
MyExtensionArray._add_comparison_ops()
注意
由於 pandas 會自動逐個呼叫底層運算子,這可能不如直接在 ExtensionArray 上實現自己的運算子版本高效。
對於算術運算,此實現將嘗試使用元素操作的結果重建一個新的 ExtensionArray。這是否成功取決於操作返回的結果是否對 ExtensionArray 有效。如果無法重建 ExtensionArray,則返回包含標量結果的 ndarray。
為了便於實現並與 pandas 和 NumPy ndarrays 之間的操作保持一致,我們建議在您的二元運算中不要處理 Series 和 Indexes。相反,您應該檢測這些情況並返回 NotImplemented。當 pandas 遇到類似 op(Series, ExtensionArray) 的操作時,pandas 會
從
Series中解包陣列(Series.array)呼叫
result = op(values, ExtensionArray)將結果重新裝入
Series
NumPy 通用函式#
Series 實現 __array_ufunc__。作為實現的一部分,pandas 會從 Series 中解包 ExtensionArray,應用 ufunc,並在必要時重新裝入。
如果適用,我們強烈建議您在擴充套件陣列中實現 __array_ufunc__,以避免強制轉換為 ndarray。有關示例,請參閱 NumPy 文件。
作為實現的一部分,我們要求您在 inputs 中檢測到 pandas 容器(Series、DataFrame、Index)時,將呼叫委託給 pandas。如果其中任何一個存在,您應該返回 NotImplemented。pandas 將負責從容器中解包陣列,並使用解包後的輸入重新呼叫 ufunc。
測試擴充套件陣列#
我們提供了一個測試套件,用於確保您的擴充套件陣列滿足預期行為。要使用該測試套件,您必須提供幾個 pytest 夾具並繼承自基測試類。所需的夾具可以在 pandas-dev/pandas 中找到。
要使用測試,請繼承它
from pandas.tests.extension import base
class TestConstructors(base.BaseConstructorsTests):
pass
有關所有可用測試的列表,請參閱 pandas-dev/pandas。
與 Apache Arrow 的相容性#
透過實現兩個方法:ExtensionArray.__arrow_array__ 和 ExtensionDtype.__from_arrow__,ExtensionArray 可以支援與 pyarrow 陣列的雙向轉換(從而支援例如序列化為 Parquet 檔案格式)。
ExtensionArray.__arrow_array__ 確保 pyarrow 知道如何將特定的擴充套件陣列轉換為 pyarrow.Array(即使它被包含為 pandas DataFrame 中的列)。
class MyExtensionArray(ExtensionArray):
...
def __arrow_array__(self, type=None):
# convert the underlying array values to a pyarrow Array
import pyarrow
return pyarrow.array(..., type=type)
然後,ExtensionDtype.__from_arrow__ 方法控制從 pyarrow 轉換回 pandas ExtensionArray。此方法僅接收一個 pyarrow Array 或 ChunkedArray 作為引數,並應返回適合該 dtype 和傳入值的 pandas ExtensionArray。
class ExtensionDtype:
...
def __from_arrow__(self, array: pyarrow.Array/ChunkedArray) -> ExtensionArray:
...
有關更多資訊,請參閱 Arrow 文件。
這些方法已為 pandas 中包含的可空整數和字串擴充套件 dtype 實現,並確保了與 pyarrow 和 Parquet 檔案格式的往返轉換。
子類化 pandas 資料結構#
本節介紹如何子類化 pandas 資料結構以滿足更具體的需求。有兩點需要注意
覆蓋建構函式屬性。
定義原始屬性
注意
您可以在 geopandas 專案中找到一個很好的例子。
覆蓋建構函式屬性#
每個資料結構都有幾個建構函式屬性,用於將新資料結構作為操作的結果返回。透過覆蓋這些屬性,您可以在 pandas 資料操作中保留子類。
子類中可以定義 3 個建構函式屬性
DataFrame/Series._constructor:當操作結果的維度與原始維度相同時使用。DataFrame._constructor_sliced:當DataFrame(子)類的操作結果應為Series(子)類時使用。Series._constructor_expanddim:當Series(子)類的操作結果應為DataFrame(子)類時使用,例如Series.to_frame()。
以下示例展示瞭如何透過覆蓋建構函式屬性來定義 SubclassedSeries 和 SubclassedDataFrame。
class SubclassedSeries(pd.Series):
@property
def _constructor(self):
return SubclassedSeries
@property
def _constructor_expanddim(self):
return SubclassedDataFrame
class SubclassedDataFrame(pd.DataFrame):
@property
def _constructor(self):
return SubclassedDataFrame
@property
def _constructor_sliced(self):
return SubclassedSeries
>>> s = SubclassedSeries([1, 2, 3])
>>> type(s)
<class '__main__.SubclassedSeries'>
>>> to_framed = s.to_frame()
>>> type(to_framed)
<class '__main__.SubclassedDataFrame'>
>>> df = SubclassedDataFrame({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
>>> df
A B C
0 1 4 7
1 2 5 8
2 3 6 9
>>> type(df)
<class '__main__.SubclassedDataFrame'>
>>> sliced1 = df[["A", "B"]]
>>> sliced1
A B
0 1 4
1 2 5
2 3 6
>>> type(sliced1)
<class '__main__.SubclassedDataFrame'>
>>> sliced2 = df["A"]
>>> sliced2
0 1
1 2
2 3
Name: A, dtype: int64
>>> type(sliced2)
<class '__main__.SubclassedSeries'>
定義原始屬性#
為了讓原始資料結構具有附加屬性,您應該讓 pandas 知道添加了哪些屬性。 pandas 將未知屬性對映到資料名稱,透過覆蓋 __getattribute__ 來實現。定義原始屬性可以透過以下兩種方式之一完成
為臨時屬性定義
_internal_names和_internal_names_set,這些屬性不會傳遞到操作結果。為普通屬性定義
_metadata,這些屬性將傳遞到操作結果。
以下示例展示瞭如何定義兩個原始屬性:“internal_cache”作為一個臨時屬性,“added_property”作為一個普通屬性
class SubclassedDataFrame2(pd.DataFrame):
# temporary properties
_internal_names = pd.DataFrame._internal_names + ["internal_cache"]
_internal_names_set = set(_internal_names)
# normal properties
_metadata = ["added_property"]
@property
def _constructor(self):
return SubclassedDataFrame2
>>> df = SubclassedDataFrame2({"A": [1, 2, 3], "B": [4, 5, 6], "C": [7, 8, 9]})
>>> df
A B C
0 1 4 7
1 2 5 8
2 3 6 9
>>> df.internal_cache = "cached"
>>> df.added_property = "property"
>>> df.internal_cache
cached
>>> df.added_property
property
# properties defined in _internal_names is reset after manipulation
>>> df[["A", "B"]].internal_cache
AttributeError: 'SubclassedDataFrame2' object has no attribute 'internal_cache'
# properties defined in _metadata are retained
>>> df[["A", "B"]].added_property
property
繪圖後端#
pandas 可以透過第三方繪圖後端進行擴充套件。主要思想是允許使用者選擇一個不同於基於 Matplotlib 的預設繪圖後端。例如
>>> pd.set_option("plotting.backend", "backend.module")
>>> pd.Series([1, 2, 3]).plot()
這或多或少等同於
>>> import backend.module
>>> backend.module.plot(pd.Series([1, 2, 3]))
然後,後端模組可以使用其他視覺化工具(Bokeh、Altair 等)來生成圖表。
實現繪圖後端功能的庫應該使用 入口點 來使其後端可被 pandas 發現。關鍵是 "pandas_plotting_backends"。例如,pandas 註冊了預設的“matplotlib”後端,如下所示。
# in setup.py
setup( # noqa: F821
...,
entry_points={
"pandas_plotting_backends": [
"matplotlib = pandas:plotting._matplotlib",
],
},
)
有關如何實現第三方繪圖後端的更多資訊,請參閱 pandas-dev/pandas。
與第三方型別的算術運算#
為了控制自定義型別和 pandas 型別之間的算術運算方式,請實現 __pandas_priority__。類似於 NumPy 的 __array_priority__ 語義,DataFrame、Series 和 Index 物件的算術方法將委託給 other,前提是它具有一個值更高的屬性 __pandas_priority__。
預設情況下,pandas 物件會嘗試與其他物件進行運算,即使它們不是 pandas 已知的型別。
>>> pd.Series([1, 2]) + [10, 20]
0 11
1 22
dtype: int64
在上例中,如果 [10, 20] 是一個可以被理解為列表的自定義型別,pandas 物件仍將以相同的方式與它進行運算。
在某些情況下,將運算委託給其他型別很有用。例如,考慮我實現了一個自定義列表物件,並且我希望將我的自定義列表與 pandas Series 相加的結果是我的列表例項,而不是像前一個示例中看到的 Series。透過定義我的自定義列表的 __pandas_priority__ 屬性並將其設定為比我想要與之運算的 pandas 物件更高的值,現在就可以實現這一點。
DataFrame、Series 和 Index 的 __pandas_priority__ 分別是 4000、3000 和 2000。基礎 ExtensionArray.__pandas_priority__ 為 1000。
class CustomList(list):
__pandas_priority__ = 5000
def __radd__(self, other):
# return `self` and not the addition for simplicity
return self
custom = CustomList()
series = pd.Series([1, 2, 3])
# Series refuses to add custom, since it's an unknown type with higher priority
assert series.__add__(custom) is NotImplemented
# This will cause the custom class `__radd__` being used instead
assert series + custom is custom