Pandas 提供了 to_json 方法,可以將 DataFrame 轉換成 JSON 格式,其中 orient 引數扮演著關鍵角色,它決定了 JSON 輸出的結構。orient 提供了多種選項,例如 columnsrecordssplitindexvaluestable 等,每種選項都適用於不同的場景。選擇正確的 orient 值,可以有效地控制 JSON 輸出的結構,方便後續的資料處理和應用。在資料科學領域,處理 JSON 資料是常見的需求,Pandas 的 to_json 方法提供了一個便捷的工具,可以將 DataFrame 轉換成各種 JSON 格式,方便資料交換和儲存。理解 orient 引數的用法,可以提升資料處理的效率和靈活性。

JSON 資料格式化:pandas DataFrame 的不同表現方式

在實務上,將表格資料表示為 JSON 格式的方法有許多種。不同的使用者可能會希望以不同的方式呈現資料,例如將 DataFrame 的每一行表示為 JSON 陣列,或者將每一列表示為陣列。有時候,使用者可能希望將行標籤、列標籤及資料分別表示為獨立的 JSON 物件,或者完全不關心行列標籤。為了滿足這些需求,pandas 提供了 orient 引數,讓使用者可以根據需求選擇 JSON 的佈局方式。

以下是 orient 的各種選項及其特性:

  1. columns(預設):產生 JSON 物件,其中鍵是列標籤,值是另一個物件,這個物件將行標籤對應到資料點。
  2. records:每一行 DataFrame 都以 JSON 陣列表示,包含將欄位名稱對應到資料點的物件。
  3. split:對映到 {"columns": [...], "index": [...], "data": [...]}。欄位和索引值都是標籤的陣列,而資料包含陣列中的陣列。
  4. index:類別似於 columns,但將行和欄位標籤作為鍵的使用方式相反。
  5. values:將 DataFrame 的資料對映到陣列中的陣列。行和欄位標籤被丟棄。
  6. table:遵循 JSON Table Schema。

這些 orient 選項之間存在權衡,涉及資料損失、冗長度以及最終使用者需求。例如,orient="table" 是最少損失且產生最大負載的選項,而 orient="values" 則完全處於另一端。

實際範例

以下是一個簡單的 DataFrame 範例:

import pandas as pd

beatles = {
    'first': ['Paul', 'John', 'Richard', 'George'],
    'last': ['McCartney', 'Lennon', 'Starkey', 'Harrison'],
    'birth': [1942, 1940, 1940, 1943]
}
df = pd.DataFrame(beatles, index=["row 0", "row 1", "row 2", "row 3"])
df = df.convert_dtypes(dtype_backend="numpy_nullable")

columns 型態

當使用 orient="columns" 時,資料會以 {"column":{"row": value, "row": value, ...}, ...} 的形式儲存。

serialized = df.to_json(orient="columns")
print(f'Length of orient="columns": {len(serialized)}')
print(serialized[:100])

records 型態

當使用 orient="records" 時,每一行都會以 [{"col": value, "col": value, ...}, ...] 的形式儲存。

serialized = df.to_json(orient="records")
print(f'Length of orient="records": {len(serialized)}')
print(serialized[:100])

split 型態

當使用 orient="split" 時,行標籤、欄位標籤和資料會分別儲存。

serialized = df.to_json(orient="split")
print(f'Length of orient="split": {len(serialized)}')
print(serialized[:100])

index 型態

當使用 orient="index" 時,與 columns 型態類別似,但將行和欄位標籤作為鍵的使用方式相反。

serialized = df.to_json(orient="index")
print(f'Length of orient="index": {len(serialized)}')
print(serialized[:100])

values 型態

當使用 orient="values" 時,既不保留行也不保留欄位標籤。

serialized = df.to_json(orient="values")
print(f'Length of orient="values": {len(serialized)}')
print(serialized[:100])

table 型態

當使用 orient="table" 時,這是最冗長但唯一被標準支援(JSON Table Schema)的形式。

serialized = df.to_json(orient="table")
print(f'Length of orient="table": {len(serialized)}')
print(serialized[:100])

各種 orient 的比較

| orient | 特性 | 說明 | |



–|


















-|


















-| | columns | 預設格式 | 每個欄位以物件形式呈現,包含所有行 | | records | 每行以物件形式呈現 | 不保留行索引 | | split | 權衡呈現方式 | 分別保留欄位、索引和資料 | | index | 行和欄位互換 | 與 columns 類別似但角色互換 | | values | 最簡單呈現方式 | 不保留任何索引 | | table | 最完整且標準化呈現方式 | 採用 JSON Table Schema |

各種 orient 在實務上的應用

不同的 orient 型態在實務上有不同的應用場景:

  • columns 和 index:適合需要保留詳細索引資訊的情況。
  • records:適合需要快速讀取每行資料的情況。
  • split:適合需要完整重建 DataFrame 的情況。
  • values:適合只關心資料本身而不在乎索引的情況。
  • table:適合需要遵循 JSON Table Schema 標準的情況。
內容解密:

這段程式碼展示瞭如何在 pandas 中將 DataFrame 轉換成 JSON 格式,並介紹了不同的 orient 引數及其特性。透過實際範例展示瞭如何使用各種 orient 引數來轉換 DataFrame。這些方法在不同的應用場景中有不同的優勢和劣勢,玄貓建議根據具體需求來選擇最合適的方法。

將 JSON 資料轉換為資料框架

在處理 JSON 資料時,我們通常會面臨如何將其轉換為資料框架(DataFrame)的挑戰。Pandas 提供了多種方法來完成這一任務,但每種方法都有其特定的應用場景和優缺點。以下玄貓將詳細介紹如何使用不同的 orient 引數來讀取和寫入 JSON 資料,並解釋其背後的技術原理。

使用 JSON 表格格式

JSON 表格格式(Table Schema)之所以更為詳細,是因為它儲存了關於被序列化資料的後設資料,這與 Apache Parquet 格式類別似(儘管功能上不如 Parquet)。當使用其他 orient 引數時,Pandas 需要在讀取資料的過程中推斷資料型別,而 JSON 表格格式則保留了這些資訊。

假設我們有一個 DataFrame,並且已經使用 Pandas 擴充套件型別進行了型別轉換:

import pandas as pd
import io

# 假設我們有一個 DataFrame
data = {
    "birth": [1990, 1995, 2000]
}
df = pd.DataFrame(data)

# 轉換為 UInt16Dtype
df["birth"] = df["birth"].astype(pd.UInt16Dtype())

# 序列化為 JSON 表格格式
serialized = df.to_json(orient="table")

# 認真讀取 JSON 表格格式
df_read = pd.read_json(
    io.StringIO(serialized),
    orient="table",
)

# 檢查資料型別
print(df_read.dtypes)

內容解密:

在此範例中,我們首先建立了一個簡單的 DataFrame,並將其中的一列轉換為 UInt16Dtype。接著,我們使用 to_json 方法將 DataFrame 序列化為 JSON 表格格式。這樣做的好處是,我們不需要在讀取時指定 dtype_backend 引數,因為 JSON 表格格式已經保留了資料型別資訊。

當我們再次讀取這些序列化的資料時,Pandas 能夠準確地還原原始的資料型別。這是因為 JSON 表格格式在序列化過程中保留了關於資料型別的後設資料。

排除不必要的鍵值

在某些情況下,JSON 資料中可能包含不必要的鍵值,這些鍵值可能會干擾到我們的資料處理。例如,假設我們從一個 REST API 取得了以下 JSON 資料:

data = {
    "records": [{
        "name": "human",
        "characteristics": {
            "num_leg": 2,
            "num_eyes": 2
        }
    }, {
        "name": "dog",
        "characteristics": {
            "num_leg": 4,
            "num_eyes": 2
        }
    }, {
        "name": "horseshoe crab",
        "characteristics": {
            "num_leg": 10,
            "num_eyes": 10
        }
    }],
    "type": "animal",
    "pagination": {
        "next": "23978sdlkusdf97234u2io",
        "has_more": 1
    }
}

在這個例子中,pagination 鍵值對於我們的報告沒有什麼價值,反而可能會影響到 JSON 的序列化過程。我們實際上只關心 records 鍵值所對應的陣列。

排除 pagination 鍵值

我們可以使用 pd.json_normalize 函式來處理這個問題。這個函式可以將 JSON 資料轉換為表格格式,並排除不必要的鍵值:

# 忽略 pagination 鍵值
normalized_data = pd.json_normalize(
    data,
    record_path="records"
).convert_dtypes(dtype_backend="numpy_nullable")

print(normalized_data)

內容解密:

在這裡,我們使用 record_path 引數來指定要處理的鍵值路徑。這樣做可以忽略掉 pagination 鍵值,但也會丟失掉 type 鍵值所包含的後設資料。如果我們需要保留 type 鍵值中的資訊,可以使用 meta 引數:

# 忽略 pagination 鍵值並保留 type 鍵值
normalized_data_with_meta = pd.json_normalize(
    data,
    record_path="records",
    meta="type"
).convert_dtypes(dtype_backend="numpy_nullable")

print(normalized_data_with_meta)

內容解密:

在此範例中,我們除了指定 record_path 外,還使用了 meta 引數來保留 type 鍵值中的資訊。這樣做可以確保我們在轉換過程中不會丟失重要的後設資料。

從網頁抓取與處理表格資料

擷取特定表格

在實務應用中,常常需要從網頁中抓取特定的表格資料。以「The Beatles」的音樂專輯為例,這些資料通常會以表格形式呈現在Wikipedia上。我們可以使用Pandas的read_html函式來擷取這些表格。

首先,我們需要指定要擷取的表格,這可以透過match引數來達成。match引數可以是字串或正規表示式,用來比對HTML中的<caption>標籤內容。例如,Wikipedia頁面上的「The Beatles」音樂專輯列表標題為「List of studio albums」,我們可以將這個字串作為match引數的值。

以下是擷取「The Beatles」音樂專輯列表的完整程式碼:

import pandas as pd

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match=r"List of studio albums",
    dtype_backend="numpy_nullable",
)
print(f"Number of tables returned was: {len(dfs)}")
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

內容解密:

  1. 擷取特定表格:使用pd.read_html函式並指定match=r"List of studio albums"來擷取包含「List of studio albums」標題的表格。
  2. 指定資料型態:透過dtype_backend="numpy_nullable"來確保資料型態處理正確。
  3. 篩選特定欄位:使用.filter(regex=r"Title|UK|AUS|CAN")來篩選出我們感興趣的欄位(專輯標題、英國、澳洲及加拿大的排行榜位置)。
  4. 顯示前五筆資料:使用.head()方法顯示篩選後的前五筆資料。

處理多層次欄位名稱

在Wikipedia的表格中,常見的是多層次的欄位名稱,例如「Peak chart positions」下方有不同國家的欄位名稱。Pandas會將這些多層次的欄位名稱轉換成pd.MultiIndex。為了讓表格更易讀,我們可以使用header=1引數來忽略第一層的多層次欄位名稱。

以下是修改後的程式碼:

import pandas as pd

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match="List of studio albums",
    header=1,
    dtype_backend="numpy_nullable",
)
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

內容解密:

  1. 忽略第一層欄位名稱:透過header=1引數來忽略第一層的多層次欄位名稱,使得表格結構更簡單。
  2. 篩選與顯示:與之前相同,使用.filter(regex=r"Title|UK|AUS|CAN")篩選出我們感興趣的欄位,並使用.head()顯示前五筆資料。

處理缺失值

Wikipedia常用「—」來表示缺失值。我們可以使用Pandas的na_values引數來將這些符號轉換成缺失值。

以下是處理缺失值的程式碼:

import pandas as pd

url = "https://en.wikipedia.org/wiki/The_Beatles_discography"
dfs = pd.read_html(
    url,
    match="List of studio albums",
    header=1,
    na_values=["—"],
    dtype_backend="numpy_nullable",
)
dfs[0].filter(regex=r"Title|UK|AUS|CAN").head()

內容解密:

  1. 指定缺失值符號:透過na_values=["—"]引數來指定「—」為缺失值。
  2. 篩選與顯示:與之前相同,使用.filter(regex=r"Title|UK|AUS|CAN")篩選出我們感興趣的欄位,並使用.head()顯示前五筆資料。

非程式碼主題:資料來源與信任度

在處理外部資料時,來源的信任度至關重要。Wikipedia是一個開放編輯的平台,雖然其內容通常經過社群檢視和修正,但仍可能存在誤誤或過時資訊。因此,在使用Wikipedia資料時,應該結合其他可靠來源進行交叉驗證。此外,如果這些資料將用於正式報告或研究,應該考慮使用更正式和可靠的資料來源。