學習資料科學:農產品交易資料清理實戰
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 的資料結構,可以讓我們像操作表格一樣,輕鬆地整理、清理、分析資料。pd
是 pandas 的簡稱,之後我們用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)
: 如果資料格式驗證通過,這行就用 pandas 的DataFrame()
函數,把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
當作1
,False
當作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[...]
: 把這個布林值序列放到 DataFramedf
的中括號[...]
裡面, pandas 就會根據True
/False
來篩選資料,只留下布林值為True
的資料 (也就是 ‘市場名稱’ 是 ‘台北一’ 的資料)。filtered_df = ...
: 篩選後的結果會被存在新的 DataFramefiltered_df
裡面。
-
print(filtered_df.head())
: 印出篩選後的 DataFramefiltered_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 = ...
: 排序後的結果會被存在新的 DataFramesorted_df
裡面。
-
print(sorted_df.head())
: 印出排序後的 DataFramesorted_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 = ...
: 分組聚合後的結果會被存在新的 DataFramegrouped_df
裡面。
-
grouped_df.columns = ['市場名稱', '平均價格']
: 重新命名grouped_df
的欄位,把預設的欄位名稱改成 ‘市場名稱’ 和 ‘平均價格’,讓資料更易於理解。 -
print(grouped_df)
: 印出分組聚合後的 DataFramegrouped_df
,讓我們看看每個市場的平均價格。
-
-
else: print(" -- '平均價' 欄位非數值類型,無法進行分組聚合計算平均值。請先進行資料清理。")
: 如果 ‘平均價’ 欄位不是數值類型,就印出訊息,提醒我們需要先進行資料清理,才能計算平均值。
總結 (Summary)
這段程式碼展示了如何使用 Python 和 pandas 進行資料分析的基本流程:
- 資料讀取 (Data Acquisition): 從農業部的 API 讀取農產品交易資料。
- 資料清理 (Data Cleaning): 清理數值欄位中的雜訊、處理缺失值,確保資料品質。
- 資料探索 (Data Exploration): 使用
df.info()
,df.head()
,df.describe()
了解資料的基本資訊。 - 資料轉換 (Data Transformation): 進行資料篩選、排序、分組聚合等操作,從不同角度分析資料。