資料科學 教學 農業資料分析 開放資料

學習資料科學:農產品交易資料清理實戰

1. 導入函式庫 (Import Libraries)

import requests
import json
import pandas as pd
  • import requests: 就像準備好了一支「網路請求工具」。 requests 是一個 Python 的函式庫,專門用來發送網路請求,例如我們今天要用的 GET 請求,去跟資料來源 (農業部的 API) 要資料。想像一下,你要去餐廳點餐, requests 就是幫你把菜單送到廚房的工具。

  • import json: 這個是「JSON 資料處理工具」。JSON 是一種常見的資料格式,很像字典或列表,但它是文字格式,方便在網路傳輸。API 回傳的資料通常都是 JSON 格式。 json 函式庫可以幫我們把 JSON 格式的文字轉換成 Python 可以讀懂的字典或列表,反之亦然。就像翻譯機,幫我們把外文翻譯成中文。

  • import pandas as pd: 這是「資料分析的瑞士刀」! pandas 是 Python 最強大的資料分析函式庫之一。它提供了一個叫做 DataFrame 的資料結構,可以讓我們像操作表格一樣,輕鬆地整理、清理、分析資料。 pdpandas 的簡稱,之後我們用 pd 就可以代表 pandas 這個函式庫了。

2. 設定 API 連結、查詢參數與 Header (API Request Setup)

# API 連結
api_url = "[https://data.moa.gov.tw/api/v1/AgriProductsTransType/](https://data.moa.gov.tw/api/v1/AgriProductsTransType/)"

# 查詢參數
params = {
    "Start_time": "114.01.01",
    "End_time": "114.03.09",
    "CropName": "甘藍" # 甘藍就是高麗菜
}

# 設定 header
headers = {
    "accept": "application/json"
}
  • api_url = "https://data.moa.gov.tw/api/v1/AgriProductsTransType/": 這行定義了我們要請求資料的網址,也就是農業部開放資料平台的「農產品交易行情 API」的網址。這就像餐廳的地址。

  • params = { ... }: 這裡設定了「查詢參數」。API 就像餐廳的廚房,我們點餐時要告訴廚房我們要什麼菜,有哪些要求。查詢參數就是告訴 API 我們想要哪些資料的條件。

    • "Start_time": "114.01.01": 我們設定要從民國 114 年 1 月 1 日開始的資料。
    • "End_time": "114.03.09": 結束時間設定為民國 114 年 3 月 9 日。
    • "CropName": "甘藍": 我們指定要查詢的農產品名稱是「甘藍」,也就是高麗菜。
  • headers = { "accept": "application/json" }: 這裡設定了「Header」。Header 就像信封上的標籤,告訴伺服器一些額外的資訊。

    • "accept": "application/json": 這個 header 告訴伺服器,我們希望它回傳的資料格式是 JSON。就像跟餐廳說,請用盤子裝菜,不要用碗。

3. 發送 GET 請求 (Sending the API Request)

# 發送 GET 請求
response = requests.get(api_url, params=params, headers=headers)
  • response = requests.get(api_url, params=params, headers=headers): 這行程式碼真正發送了我們的請求!requests.get(): 使用 requests 函式庫的 get() 方法發送 GET 請求。GET 請求是一種常見的網路請求方式,通常用於從伺服器取得資料。
    • api_url: 我們要請求的 API 網址 (餐廳地址)。
    • params=params: 附帶我們設定的查詢參數 (點餐的條件)。
    • headers=headers: 附帶我們設定的 Header (對資料格式的要求)。
    • response: 伺服器收到我們的請求後,會回傳一個「回應」,這個回應就存在 response 變數裡面。 response 包含了伺服器回傳的資料、狀態碼等等。

4. 檢查請求是否成功並轉換 JSON 資料 (Data Handling and DataFrame Creation)

# 檢查請求是否成功
if response.status_code == 200:
    # 將 JSON 格式的回應轉換成 Python 字典或列表
    json_data = response.json() # 儲存 JSON 資料到 json_data 變數

    # 確認資料為 list 類型,且 list 內元素為 dict
    if isinstance(json_data, list) and all(isinstance(item, dict) for item in json_data):
        # 將 list of dict 轉換成 pandas DataFrame
        df = pd.DataFrame(json_data)

        print("資料擷取成功,並轉換為 pandas DataFrame。\n")

        # ... 後續資料處理程式碼 ...

    else:
        print("API 回應資料格式錯誤,預期為 list of dict。")
        print("請檢查 API 回應內容:\n", json.dumps(json_data, indent=2, ensure_ascii=False))

else:
    # 如果請求失敗,印出錯誤訊息
    print(f"請求失敗,狀態碼: {response.status_code}")
    print(response.text)
  • if response.status_code == 200:: 這行檢查「狀態碼 (status code)」。狀態碼是伺服器告訴我們請求結果的數字。 200 代表「OK」,表示請求成功!就像餐廳服務生跟你說:「您的餐點準備好了!」如果狀態碼不是 200,就表示請求過程中出錯了,可能是網路問題、API 伺服器錯誤等等。 else 區塊會處理請求失敗的情況,印出錯誤訊息。

  • json_data = response.json(): 如果請求成功 (狀態碼是 200),這行就把伺服器回傳的 JSON 格式資料,用 response.json() 轉換成 Python 可以操作的資料結構 (通常是列表或字典),並存在 json_data 變數裡。這就像把餐廳端上來的菜餚,從盤子裡放到自己的碗裡,方便食用。

  • if isinstance(json_data, list) and all(isinstance(item, dict) for item in json_data):: 這段程式碼做「資料格式驗證」。我們預期 API 回傳的資料應該是一個列表 (list),而且列表裡面的每個元素都應該是一個字典 (dict)。這行程式碼檢查 json_data 是否符合這個預期。

    • isinstance(json_data, list): 檢查 json_data 是否為列表類型。
    • all(isinstance(item, dict) for item in json_data): 檢查 json_data 列表中的所有元素 item 是否都是字典類型。 如果資料格式不符合預期, else 區塊會印出錯誤訊息,並把原始 JSON 資料印出來,方便我們檢查 API 回應的結構。
  • df = pd.DataFrame(json_data): 如果資料格式驗證通過,這行就用 pandasDataFrame() 函數,把 json_data (列表 of 字典) 轉換成一個 DataFrame,並存在 df 變數裡。現在,我們的資料就變成表格形式了,可以用 pandas 強大的功能來分析了!

  • print("資料擷取成功,並轉換為 pandas DataFrame。\n"): 印出訊息,告訴我們資料已經成功擷取並轉換成 DataFrame 了。

5. 資料探索 (初步) (Data Exploration - Initial)

print("**DataFrame 資訊 (df.info()):**")
print(df.info()) # 顯示 DataFrame 的資訊,包含欄位名稱、資料類型、非空值數量等
print("\n")

print("**DataFrame 前幾筆資料 (df.head()):**")
print(df.head()) # 顯示 DataFrame 的前 5 筆資料 (預設)
print("\n")

print("**DataFrame 描述性統計 (df.describe()):**")
print(df.describe()) # 顯示數值欄位的描述性統計資訊 (例如平均值、標準差、最小值、最大值等)
print("\n")
  • print("**DataFrame 資訊 (df.info()):**")print(df.info()): df.info() 就像是 DataFrame 的「健康檢查報告」。它會告訴我們:

    • 欄位名稱 (Columns): 表格有哪些欄位,例如「交易日期」、「市場名稱」、「上價」、「中價」、「下價」、「平均價」等等。
    • 非空值數量 (Non-Null Count): 每個欄位有多少筆資料不是空的。如果有些欄位的 Non-Null Count 比較少,就可能表示有缺失值。
    • 資料類型 (Dtype): 每個欄位儲存的資料是什麼類型,例如 object (字串)、 int64 (整數)、 float64 (浮點數) 等等。資料類型很重要,會影響到我們後續的分析。
  • print("**DataFrame 前幾筆資料 (df.head()):**")print(df.head()): df.head() 就像是 DataFrame 的「封面」。它會顯示 DataFrame 的前幾筆資料 (預設是前 5 筆),讓我們快速看一下資料的長相,了解每個欄位大概是什麼內容。

  • print("**DataFrame 描述性統計 (df.describe()):**")print(df.describe()): df.describe()DataFrame 數值欄位的「統計摘要」。它會計算數值欄位的:

    • count: 非空值數量
    • mean: 平均值
    • std: 標準差 (資料分散程度)
    • min: 最小值
    • 25%: 第一四分位數 (25% 的資料小於這個值)
    • 50%: 中位數 (一半的資料小於這個值)
    • 75%: 第三四分位數 (75% 的資料小於這個值)
    • max: 最大值 df.describe() 可以幫助我們快速了解數值資料的大致分佈和範圍。

6. 資料清理範例 - 處理 ‘上價’ 欄位 (Data Cleaning Example - ‘上價’ Column)

# **資料清理範例 - 處理 '上價' 欄位**
if '上價' in df.columns:
    print("**資料清理範例 - 處理 '上價' 欄位:**")
    print("  -- 原始 '上價' 欄位資料類型:", df['上價'].dtype)

    # 移除 '上價' 欄位中的非數值字元 (例如 '$', ',', 空格等),並轉換為數值 (float)
    df['上價'] = df['上價'].astype(str).str.replace(r'[^\d\.]', '', regex=True)
    df['上價'] = pd.to_numeric(df['上價'], errors='coerce')

    print("  -- 清理後 '上價' 欄位資料類型:", df['上價'].dtype)
    print("  -- '上價' 欄位缺失值數量 (df['上價'].isnull().sum()):", df['上價'].isnull().sum())
    df['上價'].fillna(df['上價'].mean(), inplace=True)
    print("  -- 填補缺失值後 '上價' 欄位缺失值數量:", df['上價'].isnull().sum())
    print("\n")
  • if '上價' in df.columns:: 先檢查 DataFrame 中是否真的有 ‘上價’ 這個欄位,避免程式出錯。

  • print("**資料清理範例 - 處理 '上價' 欄位:**")print(" -- 原始 '上價' 欄位資料類型:", df['上價'].dtype): 印出訊息,並顯示原始 ‘上價’ 欄位的資料類型。有時候從 API 拿到的數值資料,可能會被當成字串讀進來,這時候就需要轉換資料類型。

  • df['上價'] = df['上價'].astype(str).str.replace(r'[^\d\.]', '', regex=True): 這行程式碼做了兩件事:

    • df['上價'].astype(str): 先把 ‘上價’ 欄位的所有資料都轉換成字串類型。
    • .str.replace(r'[^\d\.]', '', regex=True): 然後用字串的 replace() 方法,搭配「正規表示式 (regular expression)」 r'[^\d\.]', 把字串中所有「不是數字 ( \d) 也不是小數點 ( \.)」的字元都移除,替換成空字串 ''。這一步通常用於清理數值欄位中可能存在的貨幣符號、千分位符號、空格等等。
  • df['上價'] = pd.to_numeric(df['上價'], errors='coerce'): 把清理過後的字串,用 pd.to_numeric() 轉換成數值類型。 errors='coerce' 的意思是,如果有些字串真的無法轉換成數字 (例如轉換後還是空字串),就把它們變成「缺失值 (NaN - Not a Number)」。

  • print(" -- 清理後 '上價' 欄位資料類型:", df['上價'].dtype): 印出清理後 ‘上價’ 欄位的資料類型,確認是否已經變成數值類型。

  • print(" -- '上價' 欄位缺失值數量 (df['上價'].isnull().sum()):", df['上價'].isnull().sum()): 印出 ‘上價’ 欄位中缺失值的數量。 df['上價'].isnull() 會產生一個布林值 (True/False) 序列,True 表示是缺失值,False 表示不是。 .sum() 會把 True 當作 1False 當作 0,所以加總起來就是缺失值的總數。

  • df['上價'].fillna(df['上價'].mean(), inplace=True): 處理缺失值。 fillna() 方法用來填補缺失值。這裡我們用 df['上價'].mean() 計算 ‘上價’ 欄位的平均值,然後用平均值來填補缺失值。 inplace=True 表示直接修改原始的 DataFrame

  • print(" -- 填補缺失值後 '上價' 欄位缺失值數量:", df['上價'].isnull().sum()): 再次印出缺失值數量,確認缺失值是否已經被填補。

7. 資料統計範例 - 計算 ‘平均價’ 欄位統計指標 (Statistical Analysis Examples - ‘平均價’ Column)

# **資料統計範例 - 計算 '平均價' 欄位統計指標**
if '平均價' in df.columns:
    print("**資料統計範例 - 計算 '平均價' 欄位統計指標:**")
    if pd.api.types.is_numeric_dtype(df['平均價']):
        average_price = df['平均價'].mean()
        median_price = df['平均價'].median() # 計算中位數
        std_price = df['平均價'].std()       # 計算標準差
        sum_price = df['平均價'].sum()       # 計算總和

        print(f"  -- '平均價' 欄位平均值: {average_price:.2f}")
        print(f"  -- '平均價' 欄欄位中位數: {median_price:.2f}")
        print(f"  -- '平均價' 欄位標準差: {std_price:.2f}")
        print(f"  -- '平均價' 欄位總和: {sum_price:.2f}")
    else:
        print("  -- '平均價' 欄位非數值類型,無法直接計算統計指標。請先進行資料清理。")
    print("\n")
  • if '平均價' in df.columns:: 先檢查 DataFrame 中是否有 ‘平均價’ 這個欄位。

  • print("**資料統計範例 - 計算 '平均價' 欄位統計指標:**"): 印出訊息,說明接下來要做統計分析。

  • if pd.api.types.is_numeric_dtype(df['平均價']):: 再次確認 ‘平均價’ 欄位的資料類型是否為數值類型。因為統計指標 (平均值、中位數、標準差、總和) 只能對數值資料計算。

    • average_price = df['平均價'].mean(): 計算 ‘平均價’ 欄位的平均值,用 mean() 方法。

    • median_price = df['平均價'].median(): 計算 ‘平均價’ 欄位的中位數,用 median() 方法。中位數是把資料排序後,中間位置的值,比較不受極端值影響。

    • std_price = df['平均價'].std(): 計算 ‘平均價’ 欄位的標準差,用 std() 方法。標準差衡量資料的分散程度,標準差越大,資料越分散。

    • sum_price = df['平均價'].sum(): 計算 ‘平均價’ 欄位的總和,用 sum() 方法。

    • print(f" -- '平均價' 欄位平均值: {average_price:.2f}") 等等: 印出計算出來的統計指標。 :.2f 是格式化字串,讓數值保留兩位小數。

  • else: print(" -- '平均價' 欄位非數值類型,無法直接計算統計指標。請先進行資料清理。"): 如果 ‘平均價’ 欄位不是數值類型,就印出訊息,提醒我們需要先進行資料清理,才能計算統計指標。

8. 資料篩選範例 - 篩選 ‘市場名稱’ 為特定市場的資料 (Data Filtering Example - ‘市場名稱’ Filtering)

# **資料篩選範例 - 篩選 '市場名稱' 為 '台北一' 的資料**
if '市場名稱' in df.columns:
    print("**資料篩選範例 - 篩選 '市場名稱' 為 '台北一' 的資料:**")
    filtered_df = df[df['市場名稱'] == '台北一'] # 篩選 '市場名稱' 欄位值為 '台北一' 的資料
    print(filtered_df.head()) # 印出篩選後 DataFrame 的前幾筆資料
    print("\n")
  • if '市場名稱' in df.columns:: 先檢查 DataFrame 中是否有 ‘市場名稱’ 這個欄位。

  • print("**資料篩選範例 - 篩選 '市場名稱' 為 '台北一' 的資料:**"): 印出訊息,說明接下來要做資料篩選。

  • filtered_df = df[df['市場名稱'] == '台北一']: 這行程式碼進行資料篩選。

    • df['市場名稱'] == '台北一': 這是一個「布林運算式 (boolean expression)」。它會檢查 DataFrame 的 ‘市場名稱’ 欄位中,每一筆資料是否等於 ‘台北一’。結果會是一個布林值序列 (True/False),True 表示該筆資料的 ‘市場名稱’ 是 ‘台北一’,False 表示不是。
    • df[...]: 把這個布林值序列放到 DataFrame df 的中括號 [...] 裡面, pandas 就會根據 True/False 來篩選資料,只留下布林值為 True 的資料 (也就是 ‘市場名稱’ 是 ‘台北一’ 的資料)。
    • filtered_df = ...: 篩選後的結果會被存在新的 DataFrame filtered_df 裡面。
  • print(filtered_df.head()): 印出篩選後的 DataFrame filtered_df 的前幾筆資料,讓我們看看篩選結果。

9. 資料排序範例 - 依據 ‘交易日期’ 欄位排序 (Data Sorting Example - ‘交易日期’ Sorting)

# **資料排序範例 - 依據 '交易日期' 欄位排序 (由近到遠):**
if '交易日期' in df.columns:
    print("**資料排序範例 - 依據 '交易日期' 欄位排序 (由近到遠):**")
    sorted_df = df.sort_values(by='交易日期', ascending=False) # 依 '交易日期' 欄位降序排序 (ascending=False)
    print(sorted_df.head()) # 印出排序後 DataFrame 的前幾筆資料
    print("\n")
  • if '交易日期' in df.columns:: 先檢查 DataFrame 中是否有 ‘交易日期’ 這個欄位。

  • print("**資料排序範例 - 依據 '交易日期' 欄位排序 (由近到遠):**"): 印出訊息,說明接下來要做資料排序。

  • sorted_df = df.sort_values(by='交易日期', ascending=False): 這行程式碼進行資料排序。

    • df.sort_values(by='交易日期', ascending=False): 使用 sort_values() 方法來排序 DataFrame
      • by='交易日期': 指定要根據 ‘交易日期’ 欄位來排序。
      • ascending=False: 設定排序方式為「降序 (descending)」,也就是由大到小、由近到遠。如果設定 ascending=True,就會變成升序 (ascending),由小到大、由遠到近。
    • sorted_df = ...: 排序後的結果會被存在新的 DataFrame sorted_df 裡面。
  • print(sorted_df.head()): 印出排序後的 DataFrame sorted_df 的前幾筆資料,讓我們看看排序結果。

10. 資料分組聚合範例 - 依據 ‘市場名稱’ 分組,計算各市場的平均 ‘平均價’ (Data Grouping and Aggregation Example - Grouping by ‘市場名稱’ and Aggregating ‘平均價’)

# **資料分組聚合範例 - 依據 '市場名稱' 分組,計算各市場的平均 '平均價':**
if '市場名稱' in df.columns and '平均價' in df.columns:
    print("**資料分組聚合範例 - 依據 '市場名稱' 分組,計算各市場的平均 '平均價':**")
    if pd.api.types.is_numeric_dtype(df['平均價']): # 再次確認 '平均價' 欄位為數值類型
        grouped_df = df.groupby('市場名稱')['平均價'].mean().reset_index() # 依 '市場名稱' 分組,計算 '平均價' 的平均值,並重設索引
        grouped_df.columns = ['市場名稱', '平均價格'] # 重新命名欄位
        print(grouped_df) # 印出分組聚合後的 DataFrame
    else:
        print("  -- '平均價' 欄位非數值類型,無法進行分組聚合計算平均值。請先進行資料清理。")
    print("\n")
  • if '市場名稱' in df.columns and '平均價' in df.columns:: 先檢查 DataFrame 中是否有 ‘市場名稱’ 和 ‘平均價’ 這兩個欄位,因為分組聚合需要用到這兩個欄位。

  • print("**資料分組聚合範例 - 依據 '市場名稱' 分組,計算各市場的平均 '平均價':**"): 印出訊息,說明接下來要做資料分組聚合。

  • if pd.api.types.is_numeric_dtype(df['平均價']):: 再次確認 ‘平均價’ 欄位的資料類型是否為數值類型,因為計算平均值需要數值資料。

    • grouped_df = df.groupby('市場名稱')['平均價'].mean().reset_index(): 這行程式碼進行資料分組聚合。

      • df.groupby('市場名稱'): 使用 groupby() 方法,根據 ‘市場名稱’ 欄位的值,將 DataFrame 分成不同的群組。就像把來自同一個市場的資料放在一起。
      • ['平均價']: 選擇要計算平均值的欄位,也就是 ‘平均價’ 欄位。
      • .mean(): 計算每個群組的 ‘平均價’ 欄位的平均值。
      • .reset_index(): groupby() 操作會把分組的欄位 ( ‘市場名稱’) 變成索引 (index),使用 reset_index() 可以把索引恢復成普通的欄位。
      • grouped_df = ...: 分組聚合後的結果會被存在新的 DataFrame grouped_df 裡面。
    • grouped_df.columns = ['市場名稱', '平均價格']: 重新命名 grouped_df 的欄位,把預設的欄位名稱改成 ‘市場名稱’ 和 ‘平均價格’,讓資料更易於理解。

    • print(grouped_df): 印出分組聚合後的 DataFrame grouped_df,讓我們看看每個市場的平均價格。

  • else: print(" -- '平均價' 欄位非數值類型,無法進行分組聚合計算平均值。請先進行資料清理。"): 如果 ‘平均價’ 欄位不是數值類型,就印出訊息,提醒我們需要先進行資料清理,才能計算平均值。

總結 (Summary)

這段程式碼展示了如何使用 Python 和 pandas 進行資料分析的基本流程:

  • 資料讀取 (Data Acquisition): 從農業部的 API 讀取農產品交易資料。
  • 資料清理 (Data Cleaning): 清理數值欄位中的雜訊、處理缺失值,確保資料品質。
  • 資料探索 (Data Exploration): 使用 df.info(), df.head(), df.describe() 了解資料的基本資訊。
  • 資料轉換 (Data Transformation): 進行資料篩選、排序、分組聚合等操作,從不同角度分析資料。