Pandas 整合#
為了與 pandas 介接,PyArrow 提供了各種轉換常式,以使用 pandas 結構並轉換回它們。
注意
雖然 pandas 使用 NumPy 作為後端,但它有足夠的特殊性(例如不同的類型系統,以及對空值的支援),使其成為與 NumPy 整合 分開的主題。
若要遵循本文檔中的範例,請確保執行
In [1]: import pandas as pd
In [2]: import pyarrow as pa
DataFrames#
在 Arrow 中,與 pandas DataFrame 等效的是 Table。兩者都由一組等長的具名欄組成。雖然 pandas 僅支援平面欄,但 Table 也提供巢狀欄,因此它可以表示比 DataFrame 更多的資料,因此並非總是能完全轉換。
從 Table 轉換為 DataFrame 是透過呼叫 pyarrow.Table.to_pandas()
完成的。反向轉換則可透過使用 pyarrow.Table.from_pandas()
來達成。
import pyarrow as pa
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3]})
# Convert from pandas to Arrow
table = pa.Table.from_pandas(df)
# Convert back to pandas
df_new = table.to_pandas()
# Infer Arrow schema from pandas
schema = pa.Schema.from_pandas(df)
預設情況下,pyarrow
嘗試盡可能保留和還原 .index
資料。請參閱以下章節,以瞭解更多關於此內容,以及如何停用此邏輯。
Series#
在 Arrow 中,與 pandas Series 最相似的結構是 Array。它是一個向量,包含與線性記憶體相同類型的資料。您可以使用 pyarrow.Array.from_pandas()
將 pandas Series 轉換為 Arrow Array。由於 Arrow Array 始終可為空值,您可以使用 mask
參數提供可選遮罩,以標記所有空值條目。
處理 pandas Indexes#
諸如 pyarrow.Table.from_pandas()
之類的方法具有 preserve_index
選項,用於定義如何保留(儲存)或不保留(不儲存)對應 pandas 物件的 index
成員中的資料。此資料會使用內部 arrow::Schema
物件中的綱要層級元資料進行追蹤。
preserve_index
的預設值為 None
,其行為如下
RangeIndex
僅作為元資料儲存,不需要任何額外儲存空間。其他索引類型作為結果
Table
中的一個或多個實體資料欄儲存
若要完全不儲存索引,請傳遞 preserve_index=False
。由於儲存 RangeIndex
可能在某些有限情況下(例如在 Parquet 檔案中儲存多個 DataFrame 物件)導致問題,若要強制將所有索引資料序列化到結果表格中,請傳遞 preserve_index=True
。
類型差異#
以 pandas 和 Arrow 目前的設計,無法未經修改地轉換所有欄類型。這裡的主要問題之一是 pandas 不支援任意類型的可空值欄。此外,datetime64
目前固定為奈秒解析度。另一方面,Arrow 可能仍然缺少對某些類型的支援。
pandas -> Arrow 轉換#
來源類型 (pandas) |
目標類型 (Arrow) |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Arrow -> pandas 轉換#
來源類型 (Arrow) |
目標類型 (pandas) |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
類別型別#
Pandas 類別 欄位會轉換為 Arrow 字典陣列,這是一種特殊的陣列型別,針對處理重複且可能值數量有限的情況進行了最佳化。
In [3]: df = pd.DataFrame({"cat": pd.Categorical(["a", "b", "c", "a", "b", "c"])})
In [4]: df.cat.dtype.categories
Out[4]: Index(['a', 'b', 'c'], dtype='object')
In [5]: df
Out[5]:
cat
0 a
1 b
2 c
3 a
4 b
5 c
In [6]: table = pa.Table.from_pandas(df)
In [7]: table
Out[7]:
pyarrow.Table
cat: dictionary<values=string, indices=int8, ordered=0>
----
cat: [ -- dictionary:
["a","b","c"] -- indices:
[0,1,2,0,1,2]]
我們可以檢查已建立表格的 ChunkedArray
,並查看與 Pandas DataFrame 相同的類別。
In [8]: column = table[0]
In [9]: chunk = column.chunk(0)
In [10]: chunk.dictionary
Out[10]:
<pyarrow.lib.StringArray object at 0x7fe3e1705540>
[
"a",
"b",
"c"
]
In [11]: chunk.indices
Out[11]:
<pyarrow.lib.Int8Array object at 0x7fe3e1705900>
[
0,
1,
2,
0,
1,
2
]
日期時間 (Timestamp) 型別#
Pandas Timestamp 在 Pandas 中使用 datetime64[ns]
型別,並轉換為 Arrow TimestampArray
。
In [12]: df = pd.DataFrame({"datetime": pd.date_range("2020-01-01T00:00:00Z", freq="h", periods=3)})
In [13]: df.dtypes
Out[13]:
datetime datetime64[ns, UTC]
dtype: object
In [14]: df
Out[14]:
datetime
0 2020-01-01 00:00:00+00:00
1 2020-01-01 01:00:00+00:00
2 2020-01-01 02:00:00+00:00
In [15]: table = pa.Table.from_pandas(df)
In [16]: table
Out[16]:
pyarrow.Table
datetime: timestamp[ns, tz=UTC]
----
datetime: [[2020-01-01 00:00:00.000000000Z,2020-01-01 01:00:00.000000000Z,2020-01-01 02:00:00.000000000Z]]
在此範例中,Pandas Timestamp 是時區感知的 (在此情況下為 UTC
),此資訊用於建立 Arrow TimestampArray
。
日期型別#
雖然日期可以使用 pandas 中的 datetime64[ns]
型別處理,但有些系統使用 Python 內建 datetime.date
物件的物件陣列
In [17]: from datetime import date
In [18]: s = pd.Series([date(2018, 12, 31), None, date(2000, 1, 1)])
In [19]: s
Out[19]:
0 2018-12-31
1 None
2 2000-01-01
dtype: object
當轉換為 Arrow 陣列時,預設會使用 date32
型別
In [20]: arr = pa.array(s)
In [21]: arr.type
Out[21]: DataType(date32[day])
In [22]: arr[0]
Out[22]: <pyarrow.Date32Scalar: datetime.date(2018, 12, 31)>
若要使用 64 位元的 date64
,請明確指定
In [23]: arr = pa.array(s, type='date64')
In [24]: arr.type
Out[24]: DataType(date64[ms])
當使用 to_pandas
轉換回 pandas 時,會傳回 datetime.date
物件的物件陣列
In [25]: arr.to_pandas()
Out[25]:
0 2018-12-31
1 None
2 2000-01-01
dtype: object
如果您想改用 NumPy 的 datetime64
dtype,請傳遞 date_as_object=False
In [26]: s2 = pd.Series(arr.to_pandas(date_as_object=False))
In [27]: s2.dtype
Out[27]: dtype('<M8[ms]')
警告
從 Arrow 0.13
版本開始,參數 date_as_object
預設為 True
。舊版本必須傳遞 date_as_object=True
才能獲得此行為
時間型別#
Pandas 資料結構內建的 datetime.time
物件將會轉換為 Arrow time64
和 Time64Array
。
In [28]: from datetime import time
In [29]: s = pd.Series([time(1, 1, 1), time(2, 2, 2)])
In [30]: s
Out[30]:
0 01:01:01
1 02:02:02
dtype: object
In [31]: arr = pa.array(s)
In [32]: arr.type
Out[32]: Time64Type(time64[us])
In [33]: arr
Out[33]:
<pyarrow.lib.Time64Array object at 0x7fe3e1705f00>
[
01:01:01.000000,
02:02:02.000000
]
當轉換為 pandas 時,會傳回 datetime.time
物件的陣列
In [34]: arr.to_pandas()
Out[34]:
0 01:01:01
1 02:02:02
dtype: object
可空類型#
在 Arrow 中,所有資料類型皆可為空值,表示它們支援儲存遺失值。然而,在 pandas 中,並非所有資料類型都支援遺失資料。最值得注意的是,預設的整數資料類型不支援,並且在引入遺失值時會轉換為浮點數。因此,當 Arrow 陣列或表格轉換為 pandas 時,如果存在遺失值,整數欄位將會變成浮點數
>>> arr = pa.array([1, 2, None])
>>> arr
<pyarrow.lib.Int64Array object at 0x7f07d467c640>
[
1,
2,
null
]
>>> arr.to_pandas()
0 1.0
1 2.0
2 NaN
dtype: float64
Pandas 具有實驗性的可空資料類型 (https://pandas.dev.org.tw/docs/user_guide/integer_na.html)。Arrow 支援這些類型的往返轉換
>>> df = pd.DataFrame({'a': pd.Series([1, 2, None], dtype="Int64")})
>>> df
a
0 1
1 2
2 <NA>
>>> table = pa.table(df)
>>> table
Out[32]:
pyarrow.Table
a: int64
----
a: [[1,2,null]]
>>> table.to_pandas()
a
0 1
1 2
2 <NA>
>>> table.to_pandas().dtypes
a Int64
dtype: object
此往返轉換之所以有效,是因為關於原始 pandas DataFrame 的中繼資料會儲存在 Arrow 表格中。但是,如果您擁有的 Arrow 資料 (或例如 Parquet 檔案) 並非源自具有可空資料類型的 pandas DataFrame,則預設轉換為 pandas 將不會使用這些可空 dtype。
pyarrow.Table.to_pandas()
方法具有 types_mapper
關鍵字,可用於覆寫結果 pandas DataFrame 使用的預設資料類型。透過這種方式,您可以指示 Arrow 建立使用可空 dtype 的 pandas DataFrame。
>>> table = pa.table({"a": [1, 2, None]})
>>> table.to_pandas()
a
0 1.0
1 2.0
2 NaN
>>> table.to_pandas(types_mapper={pa.int64(): pd.Int64Dtype()}.get)
a
0 1
1 2
2 <NA>
types_mapper
關鍵字預期一個函數,該函數將傳回要使用的 pandas 資料類型,並給定一個 pyarrow 資料類型。透過使用 dict.get
方法,我們可以使用字典建立這樣的函數。
如果您想使用 pandas 目前支援的所有可空 dtype,則此字典會變成
dtype_mapping = {
pa.int8(): pd.Int8Dtype(),
pa.int16(): pd.Int16Dtype(),
pa.int32(): pd.Int32Dtype(),
pa.int64(): pd.Int64Dtype(),
pa.uint8(): pd.UInt8Dtype(),
pa.uint16(): pd.UInt16Dtype(),
pa.uint32(): pd.UInt32Dtype(),
pa.uint64(): pd.UInt64Dtype(),
pa.bool_(): pd.BooleanDtype(),
pa.float32(): pd.Float32Dtype(),
pa.float64(): pd.Float64Dtype(),
pa.string(): pd.StringDtype(),
}
df = table.to_pandas(types_mapper=dtype_mapping.get)
當使用 pandas API 讀取 Parquet 檔案 (pd.read_parquet(..)
) 時,也可以透過傳遞 use_nullable_dtypes
來達成此目的
df = pd.read_parquet(path, use_nullable_dtypes=True)
記憶體使用量和零複製#
當使用各種 to_pandas
方法將 Arrow 資料結構轉換為 pandas 物件時,有時必須注意與效能和記憶體使用量相關的問題。
由於 pandas 的內部資料表示通常與 Arrow 欄狀格式不同,因此零複製轉換 (不需要記憶體配置或計算) 僅在某些有限的情況下才有可能。
在最壞的情況下,呼叫 to_pandas
將導致記憶體中有兩個版本的資料,一個用於 Arrow,另一個用於 pandas,從而產生大約兩倍的記憶體佔用量。我們已針對此情況實作了一些緩解措施,尤其是在建立大型 DataFrame
物件時,我們將在下面說明。
零複製 Series 轉換#
在某些狹隘的情況下,從 Array
或 ChunkedArray
到 NumPy 陣列或 pandas Series 的零複製轉換是可能的
Arrow 資料儲存在整數 (帶號或不帶號
int8
到int64
) 或浮點類型 (float16
到float64
) 中。這包括許多數值類型以及時間戳記。Arrow 資料沒有空值 (因為這些空值是使用點陣圖表示的,而 pandas 不支援點陣圖)。
對於
ChunkedArray
,資料由單個區塊組成,即arr.num_chunks == 1
。由於 pandas 的連續性要求,多個區塊始終需要複製。
在這些情況下,to_pandas
或 to_numpy
將為零複製。在所有其他情況下,都需要複製。
減少 Table.to_pandas
中的記憶體使用量#
在撰寫本文時,pandas 應用了一種稱為「合併」的資料管理策略,以在二維 NumPy 陣列中收集類似類型的 DataFrame 欄位,在內部稱為「區塊」。我們已盡力建構精確的「合併」區塊,以便在我們將資料移交給 pandas.DataFrame
之後,pandas 不會執行任何進一步的配置或複製。此合併策略的明顯缺點是它會強制「記憶體加倍」。
為了盡可能限制 Table.to_pandas
期間「記憶體加倍」的潛在影響,我們提供了幾個選項
split_blocks=True
,啟用時,Table.to_pandas
會為每個欄位產生一個內部 DataFrame「區塊」,跳過「合併」步驟。請注意,許多 pandas 操作無論如何都會觸發合併,但尖峰記憶體使用量可能小於完全記憶體加倍的最壞情況。由於此選項,我們能夠在與Array
和ChunkedArray
進行零複製的相同情況下,對欄位進行零複製轉換。self_destruct=True
,這會在每個欄位Table
物件轉換為 pandas 相容的表示形式時,銷毀其內部的 Arrow 記憶體緩衝區,從而可能在欄位轉換後立即將記憶體釋放給作業系統。請注意,這會使呼叫Table
物件對於進一步使用而言不安全,並且任何進一步呼叫的方法都會導致您的 Python 處理程序崩潰。
一起使用時,呼叫
df = table.to_pandas(split_blocks=True, self_destruct=True)
del table # not necessary, but a good practice
將在某些情況下顯著降低記憶體使用量。如果沒有這些選項,to_pandas
將始終使記憶體加倍。
請注意,self_destruct=True
並不保證節省記憶體。由於轉換是逐欄位發生的,因此記憶體也是逐欄位釋放的。但是,如果多個欄位共用一個底層緩衝區,則在所有這些欄位都轉換之前,不會釋放任何記憶體。特別是,由於實作細節,來自 IPC 或 Flight 的資料容易發生這種情況,因為記憶體將按如下方式佈局
Record Batch 0: Allocation 0: array 0 chunk 0, array 1 chunk 0, ...
Record Batch 1: Allocation 1: array 0 chunk 1, array 1 chunk 1, ...
...
在這種情況下,即使使用 self_destruct=True
,在整個表格轉換完成之前,也無法釋放任何記憶體。