返回文章列表

Pandas Series 資料結構完全解析:從基礎到進階應用實務

深入探討 Pandas Series 資料結構的核心概念與實務應用,涵蓋 Series 建立方式、索引機制、切片操作、與 DataFrame 整合,以及 loc 與 iloc 索引器的使用技巧,提供台灣資料科學從業人員完整的 Pandas 基礎知識

資料科學 Python 程式設計 資料分析

在 Python 資料分析生態系統中,Pandas 函式庫扮演著舉足輕重的角色,而 Series 作為 Pandas 的基礎資料結構,是理解整個 Pandas 體系的關鍵起點。Series 巧妙地結合了 Python 字典的靈活性與 NumPy 陣列的高效能,創造出一種既直觀又強大的一維資料容器。對於台灣的資料科學從業人員而言,無論是處理股市交易資料、氣象觀測記錄,或是電商使用者行為分析,Series 都是不可或缺的工具。

Series 的設計哲學在於提供標籤化的資料存取能力。相較於傳統陣列僅能透過數值位置索引,Series 允許開發者使用有意義的標籤來識別資料元素,這種特性讓程式碼更具可讀性與維護性。舉例而言,在處理台灣各縣市人口統計資料時,使用「台北市」、「高雄市」等縣市名稱作為索引,遠比使用 0、1、2 等數值索引更為直觀。同時,Series 繼承了 NumPy 的向量化運算能力,能夠對整個資料序列執行高效的數學運算,避免了傳統迴圈處理的效能瓶頸。

在實務應用中,Series 經常作為 DataFrame 的組成元件出現。DataFrame 可以視為多個 Series 的集合,每個欄位都是一個獨立的 Series 物件。理解 Series 的運作機制,不僅有助於掌握單一序列資料的處理技巧,更是深入學習 DataFrame 操作的必要基礎。本文將從 Series 的建立方式開始,逐步探討索引機制的奧妙、切片操作的靈活性、遮罩篩選的實用性,以及與 DataFrame 整合的實務技巧,為讀者建構完整的 Pandas 知識體系。

Pandas Series 的核心概念與建立方式

Pandas Series 本質上是一個帶有標籤索引的一維陣列,這個看似簡單的定義背後隱含著豐富的設計思維。與 Python 內建的串列(List)相比,Series 提供了標籤索引能力與向量化運算支援。與字典(Dictionary)相比,Series 保證了元素的順序性並支援陣列式的批次操作。與 NumPy 陣列相比,Series 增加了標籤索引功能,讓資料存取更具語意。這種多重特性的結合,使 Series 成為資料分析工作流程中極為靈活的工具。

從 Python 字典建立 Series 是最直觀的方式之一。當我們需要處理台灣各縣市的統計資料時,字典的鍵值對結構自然地對應到 Series 的索引與值。字典的鍵會自動成為 Series 的索引標籤,而字典的值則成為 Series 的資料元素。這種轉換過程完全自動化,Pandas 會保持字典中鍵值對的對應關係,並根據鍵的自然順序(在 Python 3.7 以後字典保持插入順序)建立 Series。在實務應用中,這種建立方式特別適合處理具有明確識別標籤的資料集。

從 NumPy 陣列或 Python 串列建立 Series 時,若未明確指定索引,Pandas 會自動產生從零開始的整數索引。然而,大多數情況下,我們會希望賦予資料更有意義的標籤。透過 index 參數,可以在建立 Series 時同時指定索引標籤,這些標籤可以是字串、數值、日期時間,甚至是更複雜的物件。索引的選擇應該反映資料的本質特性,例如處理時間序列資料時使用日期時間索引,處理地理資料時使用地區名稱索引。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pandas Series 基礎操作範例
展示 Series 的各種建立方式、資料存取方法,以及與字典的比較
"""

import pandas as pd
import numpy as np

# ============================================================================
# 範例一:從 Python 字典建立 Series
# ============================================================================

# 建立台灣六都人口資料字典(2023 年統計資料)
# 鍵為縣市名稱,值為人口數(單位:人)
population_dict = {
    '新北市': 4028964,
    '台北市': 2602418,
    '桃園市': 2293996,
    '台中市': 2820787,
    '台南市': 1874917,
    '高雄市': 2744691
}

# 使用 pd.Series() 建立 Series 物件
# 字典的鍵自動成為 Series 的索引,值成為資料元素
population = pd.Series(population_dict)

print("台灣六都人口統計 Series:")
print(population)
print("\n" + "="*60 + "\n")

# 透過索引標籤存取特定縣市的人口資料
# 這種存取方式類似字典,但 Series 提供更多功能
taipei_population = population['台北市']
print(f"台北市人口:{taipei_population:,} 人")

# Series 支援多個索引同時存取,回傳新的 Series
northern_cities = population[['新北市', '台北市', '桃園市']]
print("\n北部三都人口統計:")
print(northern_cities)
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例二:從 NumPy 陣列建立 Series 並指定自訂索引
# ============================================================================

# 建立台灣主要河川長度資料(單位:公里)
river_lengths = np.array([186.6, 171.0, 119.0, 106.0, 86.0])

# 指定河川名稱作為索引標籤
# index 參數接受任何可迭代物件,長度必須與資料陣列相同
river_names = ['濁水溪', '高屏溪', '淡水河', '曾文溪', '大甲溪']

# 建立帶有自訂索引的 Series
rivers = pd.Series(river_lengths, index=river_names)

print("台灣主要河川長度統計:")
print(rivers)
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例三:使用非連續整數索引的 Series
# ============================================================================

# 建立台北捷運主要路線的每日平均運量資料(單位:萬人次)
# 使用路線編號作為索引(非連續整數)
metro_ridership = pd.Series(
    data=[52.3, 38.7, 45.2, 31.8],
    index=[1, 2, 3, 4]  # 分別代表淡水信義線、松山新店線、中和新蘆線、板南線
)

print("台北捷運主要路線每日運量:")
print(metro_ridership)

# 透過整數索引存取資料
# 注意:這裡的索引是明確指定的標籤,不是位置
line_2_ridership = metro_ridership[2]
print(f"\n松山新店線每日運量:{line_2_ridership} 萬人次")
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例四:從純量值建立 Series
# ============================================================================

# 建立固定值的 Series,例如初始化資料時使用
# 所有元素都具有相同的值,但索引可以自訂
default_temperature = pd.Series(
    25.0,  # 純量值,會被複製到所有位置
    index=['台北', '台中', '高雄', '花蓮', '台東']
)

print("預設溫度初始值(攝氏度):")
print(default_temperature)
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例五:Series 與字典的比較
# ============================================================================

# Series 提供類似字典的操作,但功能更強大
print("Series 與字典的相似操作:")

# 檢查索引是否存在(類似字典的 key in dict)
print(f"'台北市' 是否在 population 中:{'台北市' in population}")
print(f"'嘉義市' 是否在 population 中:{'嘉義市' in population}")

# 取得所有索引(類似字典的 keys())
print(f"\n所有縣市索引:{population.index.tolist()}")

# 取得所有數值(類似字典的 values())
print(f"所有人口數值:{population.values.tolist()}")

# 取得索引-數值配對(類似字典的 items())
print("\n索引-數值配對:")
for city, pop in population.items():
    print(f"  {city}: {pop:,} 人")

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例六:Series 的向量化運算能力
# ============================================================================

# Series 最大的優勢之一是支援向量化運算
# 可以對整個 Series 進行數學運算,無需使用迴圈

# 計算人口密度(假設面積資料,單位:平方公里)
city_areas = pd.Series({
    '新北市': 2052.57,
    '台北市': 271.80,
    '桃園市': 1220.95,
    '台中市': 2214.90,
    '台南市': 2191.65,
    '高雄市': 2951.85
})

# 向量化運算:人口 / 面積,自動對齊索引
population_density = population / city_areas

print("台灣六都人口密度(人/平方公里):")
print(population_density.round(2))  # 四捨五入到小數點後兩位

# 找出人口密度最高的縣市
max_density_city = population_density.idxmax()
max_density_value = population_density.max()
print(f"\n人口密度最高:{max_density_city}{max_density_value:.2f} 人/平方公里)")

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例七:Series 的屬性與方法
# ============================================================================

print("Series 的基本屬性:")
print(f"資料型別(dtype):{population.dtype}")
print(f"元素數量(size):{population.size}")
print(f"形狀(shape):{population.shape}")
print(f"維度(ndim):{population.ndim}")

print("\n統計資訊:")
print(f"平均人口:{population.mean():,.0f} 人")
print(f"總人口:{population.sum():,} 人")
print(f"最大值:{population.max():,} 人({population.idxmax()})")
print(f"最小值:{population.min():,} 人({population.idxmin()})")

Series 與 Python 字典之間存在微妙的關係。表面上看,兩者都提供鍵值對的資料存取方式,但 Series 在幾個關鍵方面超越了字典。首先是型別一致性,Series 要求所有元素具有相同的資料型別,這個限制換來了更高的運算效率與記憶體使用效率。其次是順序性保證,Series 明確維護元素的順序,而字典在 Python 3.7 之前並不保證順序。最重要的是向量化運算能力,Series 可以對整個資料序列執行數學運算而無需撰寫迴圈,這在處理大量資料時帶來顯著的效能提升。

在實務應用中,Series 的不可變索引特性是一個重要的設計考量。索引物件一旦建立就無法修改,這個限制確保了資料的完整性與一致性。當多個 Series 需要合併或對齊時,不可變的索引提供了穩定的參照基準。然而,這並不意味著 Series 的結構完全固定,我們仍然可以透過重新索引(reindex)方法建立具有不同索引的新 Series,或使用 reset_index 方法將索引轉換為普通欄位。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 150

package "Pandas Series 架構" {
  
  frame "Series 核心組成" as Core {
    rectangle "索引物件\n(Index)" as Index {
      card "標籤序列" as Labels
      card "不可變性" as Immutable
      card "順序性" as Ordered
    }
    
    rectangle "資料陣列\n(Values)" as Values {
      card "NumPy ndarray" as Array
      card "統一資料型別" as Dtype
      card "向量化運算" as Vectorized
    }
  }
  
  frame "建立方式" as Creation {
    usecase "Python 字典" as Dict
    usecase "NumPy 陣列" as NumpyArr
    usecase "Python 串列" as List
    usecase "純量值" as Scalar
  }
  
  frame "核心特性" as Features {
    component "標籤索引存取" as LabelAccess
    component "位置索引存取" as PosAccess
    component "切片操作" as Slicing
    component "遮罩篩選" as Masking
    component "數學運算" as MathOps
  }
  
  frame "與其他結構比較" as Compare {
    database "Python 字典" as DictComp {
      [鍵值對存取]
      [無序性]
      [型別混合]
    }
    
    database "NumPy 陣列" as NumpyComp {
      [位置索引]
      [向量化運算]
      [無標籤]
    }
    
    database "Pandas Series" as SeriesComp {
      [標籤 + 位置]
      [向量化運算]
      [順序保證]
    }
  }
}

Dict --> Core
NumpyArr --> Core
List --> Core
Scalar --> Core

Core --> Features

Index --> LabelAccess
Index --> PosAccess
Values --> Vectorized
Values --> MathOps

LabelAccess --> Slicing
PosAccess --> Slicing
Values --> Masking

note right of Index
  索引物件提供標籤化的
  資料存取能力,一旦建立
  即不可修改,確保資料
  參照的穩定性
end note

note right of Values
  底層使用 NumPy 陣列
  儲存資料,繼承向量化
  運算的高效能特性
end note

note bottom of SeriesComp
  Series 整合了字典的
  標籤存取與陣列的高效
  運算,是資料分析的
  理想工具
end note

@enduml

Series 索引機制的深入理解

Pandas Series 的索引機制是其最強大也最容易造成混淆的特性。每個 Series 實際上擁有兩套索引系統:明確索引(Explicit Index)與隱式索引(Implicit Index)。明確索引是我們建立 Series 時指定的標籤,例如縣市名稱、日期時間或自訂的識別碼。隱式索引則是 Pandas 內部維護的位置索引,從零開始的整數序列,類似於 Python 串列的索引方式。理解這兩套索引系統的運作方式與使用時機,是掌握 Series 操作的關鍵。

當 Series 的明確索引本身就是整數時,情況變得特別微妙。假設我們使用 [2, 5, 3, 7] 作為索引標籤建立 Series,此時若執行 series[2] 操作,Pandas 會優先將 2 視為明確索引標籤而非位置索引。這種行為在直觀上合理,因為我們明確指定了整數作為標籤,理應使用這些標籤來存取資料。然而,這也意味著無法直接透過位置索引存取資料,例如無法用 series[0] 來取得第一個元素(因為 0 不在明確索引中)。

為了解決這個潛在的混淆,Pandas 提供了 loc 與 iloc 兩個專用的索引器屬性。loc 索引器明確表示使用標籤索引,無論標籤是什麼型別,loc 都會將索引值視為標籤來處理。iloc 索引器則明確表示使用位置索引,無論明確索引是什麼,iloc 都會將索引值視為整數位置來處理。這種明確的區分消除了歧義,讓程式碼的意圖更加清晰。在台灣的資料分析實務中,建議養成使用 loc 與 iloc 的習慣,即使在不會產生歧義的情況下,這種明確性也能提升程式碼的可讀性與維護性。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pandas Series 索引機制詳解
展示明確索引與隱式索引的差異,以及 loc 與 iloc 的使用方式
"""

import pandas as pd
import numpy as np

# ============================================================================
# 範例一:理解明確索引與隱式索引
# ============================================================================

# 建立一個使用字串標籤的 Series
# 明確索引:縣市名稱
# 隱式索引:0, 1, 2, 3, 4(自動產生,內部使用)
cities_temp = pd.Series(
    [28.5, 27.3, 29.1, 26.8, 30.2],
    index=['台北', '台中', '高雄', '花蓮', '台東']
)

print("台灣各地氣溫(攝氏度):")
print(cities_temp)
print("\n" + "="*60 + "\n")

# 使用明確索引(標籤)存取資料
taipei_temp = cities_temp['台北']
print(f"使用明確索引存取 - 台北氣溫:{taipei_temp}°C")

# 隱式索引的存取方式(不建議直接使用,應使用 iloc)
# 直接使用整數時,Pandas 會優先嘗試作為明確索引
# 若明確索引中沒有該整數,才會視為位置索引
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例二:整數索引的歧義問題
# ============================================================================

# 建立一個使用整數作為明確索引的 Series
# 這是最容易產生混淆的情況
taiwan_districts = pd.Series(
    ['中正區', '大同區', '中山區', '松山區', '大安區'],
    index=[100, 103, 104, 105, 106]  # 使用郵遞區號作為索引
)

print("台北市行政區(以郵遞區號為索引):")
print(taiwan_districts)
print("\n" + "="*60 + "\n")

# 使用整數索引時的行為
# taiwan_districts[100] 會被視為明確索引(郵遞區號 100)
district_100 = taiwan_districts[100]
print(f"郵遞區號 100 的行政區:{district_100}")

# 若想存取第一個元素(位置 0),直接使用 [0] 會出錯
# 因為明確索引中沒有 0,而 Pandas 優先使用明確索引
try:
    taiwan_districts[0]
except KeyError as e:
    print(f"\n嘗試使用 [0] 存取時發生錯誤:{e}")
    print("這是因為 Pandas 將 0 視為明確索引標籤,而非位置索引")

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例三:使用 loc 進行明確的標籤索引
# ============================================================================

print("使用 loc 索引器(明確的標籤索引):\n")

# loc 明確表示使用標籤索引
# 無論標籤是字串、整數或其他型別,loc 都會將其視為標籤

# 單一標籤索引
district_104 = taiwan_districts.loc[104]
print(f"使用 loc[104] 取得:{district_104}")

# 多個標籤索引(回傳 Series)
selected_districts = taiwan_districts.loc[[100, 104, 106]]
print(f"\n使用 loc 選取多個標籤:")
print(selected_districts)

# loc 的切片操作(包含結束標籤)
# 這是 loc 與 Python 切片的重要差異
slice_districts = taiwan_districts.loc[100:105]
print(f"\n使用 loc[100:105] 切片(注意:包含 105):")
print(slice_districts)

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例四:使用 iloc 進行位置索引
# ============================================================================

print("使用 iloc 索引器(位置索引):\n")

# iloc 明確表示使用位置索引(從 0 開始的整數)
# 無論明確索引是什麼,iloc 都會將索引值視為位置

# 單一位置索引
first_district = taiwan_districts.iloc[0]
print(f"使用 iloc[0] 取得第一個元素:{first_district}")

# 多個位置索引(回傳 Series)
selected_by_position = taiwan_districts.iloc[[0, 2, 4]]
print(f"\n使用 iloc 選取位置 0, 2, 4:")
print(selected_by_position)

# iloc 的切片操作(不包含結束位置)
# 這與 Python 標準切片行為一致
slice_by_position = taiwan_districts.iloc[0:3]
print(f"\n使用 iloc[0:3] 切片(注意:不包含位置 3):")
print(slice_by_position)

# 負數索引(從後往前數)
last_district = taiwan_districts.iloc[-1]
print(f"\n使用 iloc[-1] 取得最後一個元素:{last_district}")

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例五:loc 與 iloc 的切片行為比較
# ============================================================================

# 建立範例 Series 以清楚展示差異
sample_series = pd.Series(
    ['A', 'B', 'C', 'D', 'E'],
    index=[10, 20, 30, 40, 50]
)

print("範例 Series:")
print(sample_series)
print("\n切片行為比較:\n")

# loc 切片:使用標籤,包含結束標籤
loc_slice = sample_series.loc[20:40]
print("loc[20:40](包含標籤 40):")
print(loc_slice)

# iloc 切片:使用位置,不包含結束位置
iloc_slice = sample_series.iloc[1:3]
print("\niloc[1:3](不包含位置 3):")
print(iloc_slice)

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例六:布林索引(遮罩)操作
# ============================================================================

# 建立台灣五大城市的平均氣溫資料
temperature = pd.Series(
    [28.3, 27.5, 29.8, 26.2, 30.5],
    index=['台北', '台中', '高雄', '花蓮', '台東']
)

print("台灣主要城市平均氣溫:")
print(temperature)

# 建立布林遮罩:篩選氣溫高於 28 度的城市
high_temp_mask = temperature > 28.0
print("\n氣溫 > 28°C 的布林遮罩:")
print(high_temp_mask)

# 使用布林遮罩篩選資料
high_temp_cities = temperature[high_temp_mask]
print("\n氣溫高於 28°C 的城市:")
print(high_temp_cities)

# 簡潔寫法:直接在索引中使用條件式
print("\n氣溫介於 27°C 與 29°C 之間的城市:")
moderate_temp = temperature[(temperature >= 27) & (temperature <= 29)]
print(moderate_temp)

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例七:實務建議 - 明確使用 loc 與 iloc
# ============================================================================

# 建立台灣主要科技公司市值資料(假設數據,單位:億台幣)
tech_companies = pd.Series(
    [28500, 15200, 8900, 6700, 5400],
    index=['台積電', '聯發科', '鴻海', '廣達', '日月光']
)

print("台灣主要科技公司市值(億台幣):")
print(tech_companies)
print("\n" + "="*60 + "\n")

# 建議的做法:明確使用 loc 或 iloc,避免歧義

# 取得特定公司市值 - 使用 loc
tsmc_value = tech_companies.loc['台積電']
print(f"使用 loc 取得台積電市值:{tsmc_value:,} 億台幣")

# 取得前三大公司 - 使用 iloc
top_three = tech_companies.iloc[0:3]
print("\n使用 iloc 取得前三大公司:")
print(top_three)

# 取得市值最高與最低的公司
max_company = tech_companies.idxmax()
min_company = tech_companies.idxmin()
print(f"\n市值最高:{max_company}{tech_companies.loc[max_company]:,} 億)")
print(f"市值最低:{min_company}{tech_companies.loc[min_company]:,} 億)")

# 篩選市值超過 1 兆的公司
trillion_companies = tech_companies[tech_companies >= 10000]
print("\n市值超過 1 兆台幣的公司:")
print(trillion_companies)

切片操作是 Series 索引機制中另一個需要特別注意的面向。使用 loc 進行切片時,結束標籤會被包含在結果中,這與 Python 標準切片的「不包含結束位置」行為不同。舉例而言,series.loc[‘A’:‘C’] 會包含標籤 ‘A’、‘B’、‘C’ 三個元素。這種設計在處理標籤索引時更為直觀,因為標籤通常沒有明確的「下一個」概念。相對地,使用 iloc 進行切片時,遵循 Python 的標準行為,結束位置不會被包含。series.iloc[0:3] 只會包含位置 0、1、2 三個元素。

布林索引(Boolean Indexing)或稱遮罩操作(Masking)是 Series 資料篩選的強大工具。透過條件式運算,可以產生與 Series 長度相同的布林序列,True 表示符合條件,False 表示不符合。將這個布林序列作為索引,即可篩選出符合條件的元素。在處理台灣股市資料時,這種技術特別實用,例如篩選收盤價高於某個門檻的股票、選取成交量異常的交易日,或是找出本益比在特定範圍內的標的。布林索引可以與邏輯運算子(&、|、~)結合,建構複雜的篩選條件。

@startuml
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 14
skinparam minClassWidth 140

participant "使用者程式碼" as User
participant "Series 物件" as Series
participant "loc 索引器" as Loc
participant "iloc 索引器" as Iloc
participant "底層資料陣列" as Array

== 明確索引存取(loc)==

User -> Series: series.loc['台北']
activate Series
Series -> Loc: 使用明確索引標籤查詢
activate Loc
Loc -> Loc: 在索引物件中\n查找標籤 '台北'
Loc -> Array: 取得對應位置的資料
activate Array
Array --> Loc: 回傳數值
deactivate Array
Loc --> Series: 回傳查詢結果
deactivate Loc
Series --> User: 回傳資料值
deactivate Series

== 位置索引存取(iloc)==

User -> Series: series.iloc[0]
activate Series
Series -> Iloc: 使用位置索引查詢
activate Iloc
Iloc -> Iloc: 驗證位置索引\n是否在有效範圍
Iloc -> Array: 直接存取位置 0
activate Array
Array --> Iloc: 回傳數值
deactivate Array
Iloc --> Series: 回傳查詢結果
deactivate Iloc
Series --> User: 回傳資料值
deactivate Series

== 切片操作比較 ==

User -> Series: series.loc['A':'C']
activate Series
Series -> Loc: 標籤切片(包含結束)
activate Loc
Loc -> Loc: 找到標籤 'A' 與 'C'\n的位置範圍
Loc -> Array: 取得範圍內所有資料\n(包含 'C')
activate Array
Array --> Loc: 回傳資料子集
deactivate Array
Loc --> Series: 建立新 Series
deactivate Loc
Series --> User: 回傳切片結果
deactivate Series

User -> Series: series.iloc[0:3]
activate Series
Series -> Iloc: 位置切片(不包含結束)
activate Iloc
Iloc -> Array: 取得位置 0, 1, 2\n的資料(不包含 3)
activate Array
Array --> Iloc: 回傳資料子集
deactivate Array
Iloc --> Series: 建立新 Series
deactivate Iloc
Series --> User: 回傳切片結果
deactivate Series

== 布林索引(遮罩)==

User -> Series: series[series > 28]
activate Series
Series -> Series: 評估條件式\nseries > 28
Series -> Series: 產生布林陣列\n[True, False, True, ...]
Series -> Array: 根據布林遮罩\n篩選資料
activate Array
Array --> Series: 回傳符合條件\n的資料子集
deactivate Array
Series --> User: 回傳篩選結果
deactivate Series

note over User, Array
  loc 與 iloc 的關鍵差異:
  - loc 使用標籤索引,切片包含結束標籤
  - iloc 使用位置索引,切片不包含結束位置
  - 明確使用可避免整數索引的歧義問題
end note

@enduml

Series 與 DataFrame 的整合應用

DataFrame 是 Pandas 中的二維資料結構,可以視為多個 Series 的集合,每個欄位都是一個獨立的 Series 物件。理解 Series 與 DataFrame 之間的關係,是掌握 Pandas 資料處理能力的關鍵。當我們從字典建立 DataFrame 時,字典的每個鍵值對都會轉換為一個欄位,而值(通常是 Series 或串列)則成為該欄位的資料。DataFrame 的索引會自動對齊所有 Series 的索引,若不同 Series 的索引不完全相同,Pandas 會進行外部合併(Outer Join),缺失的位置填入 NaN(Not a Number)。

從實務角度來看,Series 與 DataFrame 的整合最常見於資料清洗與特徵工程階段。當我們需要為 DataFrame 新增計算欄位時,通常會先對現有欄位進行運算產生新的 Series,然後將這個 Series 指派給 DataFrame 的新欄位。舉例而言,在處理台灣房地產資料時,可能需要根據房屋總價與坪數計算每坪單價,這個計算會產生一個新的 Series,然後加入原始 DataFrame 中。Pandas 會自動處理索引對齊,確保計算結果正確對應到各個資料列。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Pandas Series 與 DataFrame 整合應用
展示如何從 Series 建立 DataFrame,以及兩者之間的互動操作
"""

import pandas as pd
import numpy as np

# ============================================================================
# 範例一:從多個 Series 建立 DataFrame
# ============================================================================

# 建立台灣六都的人口與面積 Series
population = pd.Series({
    '新北市': 4028964,
    '台北市': 2602418,
    '桃園市': 2293996,
    '台中市': 2820787,
    '台南市': 1874917,
    '高雄市': 2744691
}, name='人口數')  # Series 的 name 屬性會成為 DataFrame 的欄位名稱

area = pd.Series({
    '新北市': 2052.57,
    '台北市': 271.80,
    '桃園市': 1220.95,
    '台中市': 2214.90,
    '台南市': 2191.65,
    '高雄市': 2951.85
}, name='面積')

# 使用字典將多個 Series 組合成 DataFrame
# 字典的鍵成為欄位名稱,Series 成為欄位資料
cities_df = pd.DataFrame({
    '人口數': population,
    '面積(平方公里)': area
})

print("台灣六都統計資料:")
print(cities_df)
print("\n" + "="*60 + "\n")

# 計算衍生欄位:人口密度
# 對 DataFrame 的欄位進行運算會產生新的 Series
cities_df['人口密度'] = cities_df['人口數'] / cities_df['面積(平方公里)']

print("新增人口密度欄位後:")
print(cities_df.round(2))
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例二:從 DataFrame 提取 Series
# ============================================================================

# 提取單一欄位會得到 Series
population_series = cities_df['人口數']
print("提取人口數欄位(Series):")
print(population_series)
print(f"\n資料型別:{type(population_series)}")
print("\n" + "="*60 + "\n")

# 提取單一列也會得到 Series
taipei_data = cities_df.loc['台北市']
print("提取台北市資料(Series):")
print(taipei_data)
print(f"\n資料型別:{type(taipei_data)}")
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例三:Series 索引對齊
# ============================================================================

# 建立兩個索引不完全相同的 Series
gdp = pd.Series({
    '台北市': 5.2,
    '新北市': 4.8,
    '桃園市': 3.1,
    '台中市': 3.4,
    '高雄市': 3.0
}, name='GDP(兆台幣)')

unemployment = pd.Series({
    '台北市': 3.2,
    '台中市': 3.5,
    '台南市': 3.8,
    '高雄市': 3.6,
    '基隆市': 4.1
}, name='失業率(%)')

# 建立 DataFrame 時會自動對齊索引
# 缺失的資料會填入 NaN
economic_df = pd.DataFrame({
    'GDP(兆)': gdp,
    '失業率': unemployment
})

print("經濟指標 DataFrame(索引自動對齊):")
print(economic_df)
print("\n注意:缺失的資料顯示為 NaN(Not a Number)")
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例四:從字典列表建立 DataFrame
# ============================================================================

# 建立台灣主要河川的資料列表
# 每個字典代表一條河川的資訊
rivers_data = [
    {'河川名稱': '濁水溪', '長度': 186.6, '流域面積': 3156.9},
    {'河川名稱': '高屏溪', '長度': 171.0, '流域面積': 3256.9},
    {'河川名稱': '淡水河', '長度': 158.7, '流域面積': 2726.0},
    {'河川名稱': '曾文溪', '長度': 138.5, '流域面積': 1176.7},
    {'河川名稱': '大甲溪', '長度': 124.2, '流域面積': 1235.8}
]

# 從字典列表建立 DataFrame
rivers_df = pd.DataFrame(rivers_data)

# 設定河川名稱為索引
rivers_df = rivers_df.set_index('河川名稱')

print("台灣主要河川資料:")
print(rivers_df)
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例五:從 NumPy 陣列建立 DataFrame
# ============================================================================

# 產生隨機資料模擬台北市五個行政區的溫度與濕度
# 使用 NumPy 產生 5x2 的隨機資料陣列
np.random.seed(42)  # 設定隨機種子以確保結果可重現
weather_data = np.random.rand(5, 2)

# 轉換為實際的溫度與濕度數值
weather_data[:, 0] = 25 + weather_data[:, 0] * 5  # 溫度:25-30°C
weather_data[:, 1] = 60 + weather_data[:, 1] * 20  # 濕度:60-80%

# 建立 DataFrame 並指定索引與欄位名稱
districts = ['中正區', '大安區', '信義區', '松山區', '中山區']
weather_df = pd.DataFrame(
    weather_data,
    index=districts,
    columns=['溫度(°C)', '濕度(%)']
)

print("台北市各區氣象資料(模擬):")
print(weather_df.round(2))
print("\n" + "="*60 + "\n")

# ============================================================================
# 範例六:DataFrame 的基本操作
# ============================================================================

print("DataFrame 基本資訊:")
print(f"形狀(列數, 欄數):{cities_df.shape}")
print(f"總元素數:{cities_df.size}")
print(f"欄位名稱:{cities_df.columns.tolist()}")
print(f"索引標籤:{cities_df.index.tolist()}")

print("\n統計摘要:")
print(cities_df.describe())

print("\n" + "="*60 + "\n")

# ============================================================================
# 範例七:Series 與 DataFrame 的進階整合
# ============================================================================

# 建立一個新的 Series 並加入現有 DataFrame
# 計算每個縣市佔全國人口的百分比
total_population = cities_df['人口數'].sum()
cities_df['人口佔比(%)'] = (cities_df['人口數'] / total_population * 100).round(2)

print("新增人口佔比欄位:")
print(cities_df)

# 根據特定欄位排序
cities_df_sorted = cities_df.sort_values('人口密度', ascending=False)
print("\n依人口密度排序(由高到低):")
print(cities_df_sorted.round(2))

DataFrame 的欄位存取會回傳 Series 物件,這個特性讓我們可以對個別欄位進行 Series 的所有操作。例如,可以對特定欄位進行統計分析、篩選符合條件的資料列,或是與其他 Series 進行運算。相對地,DataFrame 的列存取也會回傳 Series,但此時 Series 的索引變成原本 DataFrame 的欄位名稱。這種雙向的 Series 視角讓資料操作更加靈活,可以從不同角度審視與處理資料。

在處理不完整資料時,Series 與 DataFrame 的索引對齊機制展現了 Pandas 的智慧設計。當合併索引不完全相同的多個 Series 時,Pandas 會建立所有索引的聯集,缺失資料的位置自動填入 NaN。這種行為確保了資料不會無聲地遺失,同時也提醒開發者注意資料完整性問題。在台灣的商業分析場景中,經常需要整合來自不同來源的資料,這些資料的索引(例如客戶ID、產品編號、時間戳記)可能不完全一致,Pandas 的自動對齊機制大幅簡化了資料整合的複雜度。

Pandas Series 作為資料分析工作流程的基礎構件,其重要性不言而喻。從簡單的一維資料容器到支援複雜索引操作的強大工具,Series 展現了 Pandas 設計的精妙之處。明確索引與隱式索引的雙重系統提供了靈活性,loc 與 iloc 索引器消除了操作的歧義性,向量化運算能力確保了高效的資料處理效能。對於台灣的資料科學從業人員而言,精通 Series 的各種操作技巧,不僅是掌握 Pandas 的第一步,更是建立穩固資料分析能力的基石。隨著資料量的持續增長與分析需求的日益複雜,Series 所代表的結構化資料處理思維,將在未來的資料驅動決策中扮演更加關鍵的角色。