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)

bool

BOOL

(u)int{8,16,32,64}

(U)INT{8,16,32,64}

float32

FLOAT

float64

DOUBLE

str / unicode

STRING

pd.Categorical

DICTIONARY

pd.Timestamp

TIMESTAMP(unit=ns)

datetime.date

DATE

datetime.time

TIME64

Arrow -> pandas 轉換#

來源類型 (Arrow)

目標類型 (pandas)

BOOL

bool

BOOL 含空值

object (值為 TrueFalseNone)

(U)INT{8,16,32,64}

(u)int{8,16,32,64}

(U)INT{8,16,32,64} 可為空值

float64

FLOAT

float32

DOUBLE

float64

STRING

字串 (str)

DICTIONARY

pd.Categorical

TIMESTAMP(單位=*)

pd.Timestamp (np.datetime64[ns])

DATE

object (包含 datetime.date 物件)

TIME64

object (包含 datetime.time 物件)

類別型別#

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 time64Time64Array

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 轉換#

在某些狹隘的情況下,從 ArrayChunkedArray 到 NumPy 陣列或 pandas Series 的零複製轉換是可能的

  • Arrow 資料儲存在整數 (帶號或不帶號 int8int64) 或浮點類型 (float16float64) 中。這包括許多數值類型以及時間戳記。

  • Arrow 資料沒有空值 (因為這些空值是使用點陣圖表示的,而 pandas 不支援點陣圖)。

  • 對於 ChunkedArray,資料由單個區塊組成,即 arr.num_chunks == 1。由於 pandas 的連續性要求,多個區塊始終需要複製。

在這些情況下,to_pandasto_numpy 將為零複製。在所有其他情況下,都需要複製。

減少 Table.to_pandas 中的記憶體使用量#

在撰寫本文時,pandas 應用了一種稱為「合併」的資料管理策略,以在二維 NumPy 陣列中收集類似類型的 DataFrame 欄位,在內部稱為「區塊」。我們已盡力建構精確的「合併」區塊,以便在我們將資料移交給 pandas.DataFrame 之後,pandas 不會執行任何進一步的配置或複製。此合併策略的明顯缺點是它會強制「記憶體加倍」。

為了盡可能限制 Table.to_pandas 期間「記憶體加倍」的潛在影響,我們提供了幾個選項

  • split_blocks=True,啟用時,Table.to_pandas 會為每個欄位產生一個內部 DataFrame「區塊」,跳過「合併」步驟。請注意,許多 pandas 操作無論如何都會觸發合併,但尖峰記憶體使用量可能小於完全記憶體加倍的最壞情況。由於此選項,我們能夠在與 ArrayChunkedArray 進行零複製的相同情況下,對欄位進行零複製轉換。

  • 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,在整個表格轉換完成之前,也無法釋放任何記憶體。