Pandas 객체 소개

가장 Pandas 객체는 행과 열을 단순한 정수 인덱스가 아닌 레이블로 식별하는, 한층 강화된 NumPy 구조화 배열이라고 볼 수 있습니다. 이번 장에서 살펴보겠지만 Pandas는 기본 데이터 구조 외에도 다양한 유용한 도구와 메서드를 제공합니다. 하지만 그 전에 이러한 구조가 무엇인지 정확히 이해해야 합니다. 본격적으로 진행하기에 앞서 Pandas의 세 가지 기본 데이터 구조인 ‘Series’, ‘DataFrame’, ’Index’를 살펴보겠습니다.

먼저 표준 NumPy와 Pandas를 임포트하여 세션을 시작하겠습니다.

import numpy as np
import pandas as pd

Pandas Series 객체

Pandas ’Series’는 인덱싱된 데이터의 1차원 배열입니다. 다음과 같이 리스트나 배열로 생성합니다.

data = pd.Series([0.25, 0.5, 0.75, 1.0])
data
0    0.25
1    0.50
2    0.75
3    1.00
dtype: float64

‘Series’는 일련의 값과 명시적인 인덱스를 결합한 형태이며, ’values’와 ’index’ 속성으로 접근합니다. ’values’는 우리에게 익숙한 NumPy 배열입니다.

data.values
array([0.25, 0.5 , 0.75, 1.  ])

indexpd.Index 타입의 배열 형태 객체입니다. 이에 대해서는 잠시 후 자세히 설명하겠습니다.

data.index
RangeIndex(start=0, stop=4, step=1)

NumPy 배열과 마찬가지로, 익숙한 Python 대괄호 표기법을 사용하여 관련 인덱스로 데이터에 접근합니다.

data[1]
0.5
data[1:3]
1    0.50
2    0.75
dtype: float64

앞으로 보겠지만 Pandas ’Series’는 모방 대상인 1차원 NumPy 배열보다 훨씬 범용적이고 유연합니다.

일반화된 NumPy 배열로서의 Series

지금까지 살펴본 바로는 Series 객체가 1차원 NumPy 배열과 호환되는 것처럼 보일 수 있습니다. 근본적인 차이점은 NumPy 배열이 값에 접근할 때 암시적으로 정의된 정수 인덱스를 사용하는 반면, Pandas Series는 값과 연결된 명시적으로 정의된 인덱스를 가진다는 점입니다.

이러한 명시적 인덱스 정의 덕분에 Series 객체는 추가 기능을 제공합니다. 예를 들어 인덱스가 꼭 정수일 필요는 없으며, 원하는 타입의 값으로 구성합니다. 원한다면 문자열을 인덱스로 사용할 수도 있습니다.

data = pd.Series([0.25, 0.5, 0.75, 1.0], index=["a", "b", "c", "d"])
data
a    0.25
b    0.50
c    0.75
d    1.00
dtype: float64

항목 접근 역시 예상대로 잘 작동합니다.

data["b"]
0.5

불연속적이거나 비순차적인 인덱스를 사용할 수도 있습니다.

data = pd.Series([0.25, 0.5, 0.75, 1.0], index=[2, 5, 3, 7])
data
2    0.25
5    0.50
3    0.75
7    1.00
dtype: float64
data[5]
0.5

특화된 딕셔너리로서의 Series

이런 관점에서 Pandas ’Series’는 Python 딕셔너리를 특수화한 것과 비슷하다고 생각합니다. 딕셔너리가 임의의 키를 임의의 값 집합에 매핑하는 구조라면, ’Series’는 타입이 지정된 키를 타입이 지정된 값 집합에 매핑합니다. 타입 지정은 성능 면에서 중요합니다. NumPy 배열의 타입별 컴파일 코드가 특정 연산에서 Python 리스트보다 효율적이듯, Pandas Series의 타입 정보도 특정 연산에서 Python 딕셔너리보다 훨씬 효율적입니다.

Python 딕셔너리에서 직접 ‘Series’ 객체를 구성해 보면 ’Series’를 딕셔너리에 비유하는 것이 더욱 명확해집니다. 다음은 2020년 인구 조사 기준 미국에서 가장 인구가 많은 5개 주입니다.

population_dict = {
    "California": 39538223,
    "Texas": 29145505,
    "Florida": 21538187,
    "New York": 20201249,
    "Pennsylvania": 13002700,
}
population = pd.Series(population_dict)
population
California      39538223
Texas           29145505
Florida         21538187
New York        20201249
Pennsylvania    13002700
dtype: int64

여기서 일반적인 딕셔너리 스타일로 항목에 접근합니다.

population["California"]
39538223

하지만 딕셔너리와 달리 Series는 슬라이싱 같은 배열 스타일의 작업도 지원합니다.

population["California":"Florida"]
California    39538223
Texas         29145505
Florida       21538187
dtype: int64

데이터 인덱싱 및 선택에서 Pandas 인덱싱과 슬라이싱의 몇 가지 주의사항을 자세히 다루겠습니다.

Series 객체 생성하기

이미 Pandas ’Series’를 처음부터 생성하는 몇 가지 방법을 보았습니다. 기본 형식은 다음과 같습니다.

``python pd.Series(data, index=index) ````

여기서 index는 선택적 인자이며, data는 여러 형태가 될 수 있습니다.

예를 들어 data가 리스트나 NumPy 배열인 경우, index를 지정하지 않으면 정수 시퀀스가 설정됩니다.

pd.Series([2, 4, 6])
0    2
1    4
2    6
dtype: int64

data가 스칼라 값인 경우 지정된 인덱스를 채우기 위해 해당 값이 반복됩니다.

pd.Series(5, index=[100, 200, 300])
100    5
200    5
300    5
dtype: int64

딕셔너리일 수도 있는데, 이 경우 index는 딕셔너리의 키로 설정됩니다.

pd.Series({2: "a", 1: "b", 3: "c"})
2    a
1    b
3    c
dtype: object

필요한 경우 인덱스를 명시적으로 설정하여 사용할 키를 선택하거나 순서를 제어합니다.

pd.Series({2: "a", 1: "b", 3: "c"}, index=[1, 2])
1    b
2    a
dtype: object

Pandas DataFrame 객체

Pandas의 다음 기본 구조는 ’DataFrame’입니다. 앞서 설명한 Series 객체와 마찬가지로, DataFrame 역시 NumPy 배열의 일반화 또는 Python 딕셔너리의 특수화로 생각합니다. 각 관점을 차례로 살펴보겠습니다.

일반화된 NumPy 배열로서의 DataFrame

‘Series’가 명시적 인덱스가 있는 1차원 배열과 비슷하다면, ’DataFrame’은 명시적 행 및 열 인덱스가 있는 2차원 배열과 비슷합니다. 2차원 배열을 정렬된 1차원 열들의 시퀀스로 보듯, ’DataFrame’은 정렬된 ’Series’ 객체들의 시퀀스로 생각합니다. 여기서 “정렬”이란 동일한 인덱스를 공유한다는 뜻입니다.

이를 확인하기 위해 먼저 앞서 다룬 5개 주의 면적(km²)을 나열하는 새로운 ’Series’를 구성해 보겠습니다.

area_dict = {
    "California": 423967,
    "Texas": 695662,
    "Florida": 170312,
    "New York": 141297,
    "Pennsylvania": 119280,
}
area = pd.Series(area_dict)
area
California      423967
Texas           695662
Florida         170312
New York        141297
Pennsylvania    119280
dtype: int64

이제 area와 기존 population 시리즈가 준비되었으므로, 딕셔너리를 사용하여 이 정보를 포함하는 단일 2차원 객체를 생성합니다.

states = pd.DataFrame({"population": population, "area": area})
states
population area
California 39538223 423967
Texas 29145505 695662
Florida 21538187 170312
New York 20201249 141297
Pennsylvania 13002700 119280

Series 객체와 마찬가지로 DataFrame도 인덱스 레이블에 접근할 수 있는 index 속성을 가집니다.

states.index
Index(['California', 'Texas', 'Florida', 'New York', 'Pennsylvania'], dtype='object')

또한 DataFrame에는 열 레이블을 담고 있는 Index 객체인 columns 속성이 있습니다.

states.columns
Index(['population', 'area'], dtype='object')

따라서 DataFrame은 행과 열 모두 데이터 접근을 위한 범용 인덱스를 가진 2차원 NumPy 배열의 확장판으로 생각합니다.

특수화된 딕셔너리로서의 DataFrame

마찬가지로 DataFrame을 딕셔너리의 특수 형태로 볼 수도 있습니다. 딕셔너리가 키를 값에 매핑하듯, DataFrame은 열 이름을 해당 열 데이터를 담은 Series 객체에 매핑합니다. 예를 들어 ‘area’ 속성을 요청하면 앞서 살펴본 면적 데이터가 포함된 ‘Series’ 객체가 반환됩니다.

states["area"]
California      423967
Texas           695662
Florida         170312
New York        141297
Pennsylvania    119280
Name: area, dtype: int64

여기서 혼동하기 쉬운 점이 있습니다. 2차원 NumPy 배열에서 data[0]은 첫 번째 을 반환하지만, DataFramedata['col0']은 첫 번째 을 반환합니다. 따라서 DataFrame은 범용 배열보다는 범용 딕셔너리로 이해하는 것이 더 명확합니다. 물론 두 관점 모두 유용합니다. 데이터 인덱싱 및 선택에서 DataFrame을 더 유연하게 인덱싱하는 방법을 알아보겠습니다.

DataFrame 객체 생성하기

Pandas DataFrame은 다양한 방법으로 생성합니다. 몇 가지 예시를 살펴보겠습니다.

단일 Series 객체로 생성

DataFrameSeries 객체들의 집합이며, 단일 Series로도 한 개 열을 가진 DataFrame을 만들 수 있습니다.

pd.DataFrame(population, columns=["population"])
population
California 39538223
Texas 29145505
Florida 21538187
New York 20201249
Pennsylvania 13002700

딕셔너리 리스트로 생성

딕셔너리 리스트를 DataFrame으로 변환합니다. 리스트 컴프리헨션을 사용하여 간단한 데이터를 생성해 보겠습니다.

data = [{"a": i, "b": 2 * i} for i in range(3)]
pd.DataFrame(data)
a b
0 0 0
1 1 2
2 2 4

딕셔너리에서 일부 키가 누락된 경우, Pandas는 해당 부분을 NaN(“Not a Number”) 값으로 채웁니다(누락된 데이터 처리 참고).

pd.DataFrame([{"a": 1, "b": 2}, {"b": 3, "c": 4}])
a b c
0 1.0 2 NaN
1 NaN 3 4.0

Series 객체 딕셔너리로 생성

앞서 보았듯이 DataFrameSeries 객체들을 담은 딕셔너리로도 생성합니다.

pd.DataFrame({"population": population, "area": area})
population area
California 39538223 423967
Texas 29145505 695662
Florida 21538187 170312
New York 20201249 141297
Pennsylvania 13002700 119280

2차원 NumPy 배열로 생성

2차원 데이터 배열이 있으면 열 이름과 인덱스 이름을 지정하여 ’DataFrame’을 만들 수 있습니다. 생략할 경우 각각 정수 인덱스가 기본으로 사용됩니다.

pd.DataFrame(np.random.rand(3, 2), columns=["foo", "bar"], index=["a", "b", "c"])
foo bar
a 0.471098 0.317396
b 0.614766 0.305971
c 0.533596 0.512377

NumPy 구조화 배열로 생성

구조화된 데이터: NumPy의 구조화된 배열에서 다뤘듯, Pandas DataFrame은 구조화 배열과 매우 비슷하게 동작하며 다음과 같이 직접 생성합니다.

A = np.zeros(3, dtype=[("A", "i8"), ("B", "f8")])
A
array([(0, 0.), (0, 0.), (0, 0.)], dtype=[('A', '<i8'), ('B', '<f8')])
pd.DataFrame(A)
A B
0 0 0.0
1 0 0.0
2 0 0.0

Pandas Index 객체

앞서 보았듯이 SeriesDataFrame 객체는 데이터를 참조하고 수정할 수 있는 명시적 인덱스를 포함합니다. 이 ‘Index’ 객체 자체도 흥미로운 구조이며, 불변 배열 또는 순서가 있는 집합(기술적으로는 중복 값을 가질 수 있는 멀티셋)으로 생각합니다. 이러한 관점은 ‘Index’ 객체에서 활용 가능한 연산에 몇 가지 흥미로운 결과를 가져옵니다. 정수 리스트로 Index를 만드는 간단한 예를 살펴보겠습니다.

ind = pd.Index([2, 3, 5, 7, 11])
ind
Int64Index([2, 3, 5, 7, 11], dtype='int64')

불변 배열로서의 Index

많은 면에서 ’Index’는 배열처럼 작동합니다. 표준 Python 인덱싱 표기법을 사용하여 값이나 슬라이스를 검색합니다.

ind[1]
3
ind[::2]
Int64Index([2, 5, 11], dtype='int64')

Index 객체는 NumPy 배열에서 익숙한 많은 속성도 제공합니다.

print(ind.size, ind.shape, ind.ndim, ind.dtype)
5 (5,) 1 int64

Index 객체와 NumPy 배열의 한 가지 차이점은 인덱스가 불변이라는 점입니다. 즉, 일반적인 방법으로는 수정할 수 없습니다.

ind[1] = 0
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/var/folders/xc/sptt9bk14s34rgxt7453p03r0000gp/T/ipykernel_83282/393126374.py in <module>
----> 1 ind[1] = 0

~/.local/share/virtualenvs/python-data-science-handbook-2e-u_kwqDTB/lib/python3.9/site-packages/pandas/core/indexes/base.py in __setitem__(self, key, value)
   4583     @final
   4584     def __setitem__(self, key, value):
-> 4585         raise TypeError("Index does not support mutable operations")
   4586 
   4587     def __getitem__(self, key):

TypeError: Index does not support mutable operations

이러한 불변성 덕분에 여러 DataFrame과 배열 사이에서 인덱스를 안전하게 공유할 수 있으며, 부주의한 수정으로 인한 사이드 이펙트도 방지합니다.

정렬된 집합으로서의 Index

Pandas 객체는 집합 연산의 여러 측면을 활용하는 데이터 조인(Join) 같은 작업을 쉽게 수행하도록 설계되었습니다. Index 객체는 Python 내장 set 데이터 구조의 많은 규칙을 따르므로 합집합, 교집합, 차집합 등을 익숙한 방식으로 계산합니다.

indA = pd.Index([1, 3, 5, 7, 9])
indB = pd.Index([2, 3, 5, 7, 11])
indA.intersection(indB)
Int64Index([3, 5, 7], dtype='int64')
indA.union(indB)
Int64Index([1, 2, 3, 5, 7, 9, 11], dtype='int64')
indA.symmetric_difference(indB)
Int64Index([1, 2, 9, 11], dtype='int64')