2 pandas 시작하기
2008년 Wes McKinney가 금융 데이터 분석을 위해 pandas를 개발하였습니다. 그는 pandas를 “Python 데이터 도구”로 만들고자 했으며, R과 MATLAB에 의존하던 금융, 통계 사용자들이 데이터 전처리부터 분석까지 모두 Python에서 수행할 수 있게 하는 것을 목표로 삼았습니다. 이후 데이터 과학, 머신러닝, 빅데이터 기반 비즈니스 분석, 딥러닝 등 다양한 분야에서 중요한 도구로 자리잡았습니다. 이와 관련된 내용이 궁금하시면 YouTube에 “2012 PyData Workshop: Data Analysis in Python with pandas”를 검색(https://www.youtube.com/watch?v=MxRMXhjXZos)하여 시청해보시기 바랍니다.
Wes McKinney는 2008년 Quant 헤지펀드(AQR)에서 Python과 R을 병행 사용하던 시기에 시계열과 라벨이 붙은 배열(labeled array)을 다루는 R의 데이터 조작 기능에 큰 불만을 느꼈고, 이를 계기로 pandas를 만들기 시작했습니다. 당시 Python에는 NumPy가 있었지만, 금융 시계열과 패널 데이터를 다루기 위한 풍부한 고수준 데이터 구조가 없었고, NumPy와 도메인 로직 사이의 “missing layer”를 채우는 도구로 pandas를 설계하였습니다.
pandas는 SQL, R, Excel의 장점을 손쉽게 활용할 수 있게 해주며, 무엇보다 표 형태의 데이터(tabular data)를 다루는 데 탁월한 성능을 보입니다. 우리는 Tidy Data의 원칙을 따르면서 데이터를 효율적으로 다루는 방법을 학습하도록 하겠습니다.
먼저, pandas의 핵심 자료구조인 Series와 DataFrame를 시작으로 간단한 예제와 함께 살펴보겠습니다.
2.1 Series와 DataFrame
실무에서 데이터 관련 업무를 진행하면서 느낀 점은, 기본기를 탄탄히 다지는 것이 중요하다는 것입니다. 대부분의 업무에서 도구에 관련된 매뉴얼을 정독하기 보다는 기존에 사용한 경험이나 비슷한 것과의 유사성을 기반으로 도구를 다루기 때문에 도구에서 제공하는 핵심적인 기능을 빠르게 지나쳐 버립니다.
우리는 Series(시리즈)와 DataFrame(데이터 프레임)의 개념을 정확히 이해하고, 인덱싱(indexing)과 필터링(filtering)을 자유롭게 다룰 수 있다면, 복잡한 데이터 분석 문제도 어렵지 않게 해결할 수 있습니다. pandas의 핵심 자료구조를 시작으로, 데이터를 읽고, 선택하고, 집계하고, 시각화하는 단계별 워크플로우를 Gapminder 데이터셋을 가지고 살펴보겠습니다.
먼저 pandas의 자료구조의 원형이 되는 Python의 리스트(list)와 딕셔너리(dict)부터 살펴보겠습니다.
2.1.1 리스트와 딕셔너리의 한계
Python의 기본 자료구조와 pandas의 핵심 자료구조를 비교해보겠습니다. Python의 리스트(list)와 딕셔너리(dict)는 강력한 자료구조이지만, 대규모 데이터 분석에는 약간의 불편함이 있습니다. 리스트(list)는 순서가 있는 값들의 집합으로, 다양한 타입을 섞어서 담을 수 있고, 슬라이싱과 리스트 컴프리헨션 등 강력한 조작 기능을 제공합니다.
그러나 각 원소는 별도의 Python 객체라서 메모리 상에서 연속적이지 않고, 대규모 수치 데이터에 대해 벡터화된 연산을 직접 지원하지 않습니다. 딕셔너리(dict)는 키와 값의 쌍을 저장하는 해시 기반 맵 구조로, 임의 접근과 유연한 구조 표현에 강점이 있습니다. 하지만 ‘열 단위 연산’이나 ’행/열 라벨을 가진 2차원 테이블’ 같은 분석에 특화된 추상화를 제공하지 않기 때문에, 사용자가 키/값을 직접 순회하며 집계/필터링 로직을 반복적으로 구현해야 하는 불편함이 있습니다.
2.1.1.1 리스트를 사용한 데이터 분석
먼저 국가별 GDP 데이터 중 일부분을 Python의 리스트를 사용하여 관리해보겠습니다. 리스트를 사용해서 데이터를 관리하는 대표적인 방법은 길이가 동일한 두 개의 리스트에 각각 데이터를 저장하는 것입니다. 리스트로 데이터를 관리한다는 것이 억스러운 면이 있지만, Python에서 제공하는 대표적인 자료구조이기 때문에 가끔 리스트를 사용해서 간단한 데이터를 처리하기도 합니다.
리스트를 사용해서 데이터를 관리하는 것은 몇 가지 문제가 있습니다. 첫째, gdp 리스트는 데이터 타입이 혼재되어 있어도 에러가 나지 않습니다. 즉, 리스트에 여러 데이터 타입을 저장할 수 있기 때문에, 일관된 데이터를 관리하기 어렵습니다.
둘째, 리스트를 사용하여 원하는 데이터를 손쉽게 찾아내기 쉽지 않습니다. 예를 들어, GDP가 5000 이상인 국가를 찾으려면, 반복문을 사용하여 찾아야 합니다. 반복문이 아니라 하더라도, 결론적으로 해당 리스트를 순회하면서 처리해야 한다는 점은 변함이 없습니다. 리스트 컴프리헨션(list comprehension)을 사용하면 더 간결하게 표현할 수 있습니다.
셋째, 리스트를 사용해서 벡터 연산을 할 수 없습니다. 예를 들어, GDP를 1,000으로 나누어서 달러 단위로 변환하려면, 이 또한 반복문을 사용하여 처리해야 합니다. 리스트의 한계는 Python을 잘 활용할 수 있다면 큰 문제는 아닐 수 있습니다. 하지만 대규모 데이터를 다루게 되는 업무에서는 비효율적인 측면과 함께 코드의 복잡도가 증가하게 됩니다.
2.1.1.2 딕셔너리를 활용한 데이터 분석
딕셔너리도 리스트와 마찬가지로 대규모 데이터를 다루기에는 한계가 있습니다. 아래 예제에서 살펴볼 수 있듯이, 딕셔너리도 모든 국가의 GDP 평균을 구하려면, 반복문을 사용하여 처리해야 합니다.
country_data = {
"Korea": {"gdp": 1673.0,
"population": 51.7,
"continent": "Asia"},
"Japan": {"gdp": 4231.0,
"population": 125.0,
"continent": "Asia"},
"USA": {"gdp": 25462.0,
"population": 331.9,
"continent": "Americas"},
}
gdp_list = [country_data[c]["gdp"] for c in country_data]
avg_gdp = sum(gdp_list) / len(gdp_list)딕셔너리의 경우 특정 열을 추출하는 것도 리스트에 비해서 가독성은 높지만, 반면에 문법의 복잡도가 증가하게 됩니다. 아래 예제에서 살펴볼 수 있듯이, 각 국가의 GDP만 리스트로 추출하려면, 반복문과 유사한 문법을 사용하여 처리해야 합니다.
딕셔너리의 경우 정렬(sorting), 필터링(filtering), 그룹화(grouping) 같은 데이터 분석 작업도 약간의 번거로움이 있습니다. 예를 들어 GDP가 5,000 이상인 국가를 찾으려면, 다음과 같이 코드를 작성해야 합니다.
데이터 관련 업무를 하면서 데이터의 크기가 기가바이트 단위로 급격히 늘어났고, 효율적으로 처리하기 위해 별도의 데이터베이스나 저장소가 필요한 상황이 되었습니다. Python의 리스트와 딕셔너리는 반복문에 의존해야 하므로 대용량 데이터를 효과적으로 다루기에는 한계가 있습니다.
- 성능 문제: 수십만 건 이상의 데이터를 리스트로 처리하면 메모리와 속도 문제가 발생
- 코드 복잡도: 간단한 필터링, 정렬, 집계에도 복잡한 반복문 필요
- 결측치 처리: None, NaN(nan), 빈 문자열 등을 일관되게 처리하기 어려움
- 타입 안정성: 숫자 데이터에 문자열이 섞여 있어도 런타임 전까지 알 수 없음
이러한 문제들을 해결하기 위해서 pandas는 NumPy 기반의 배열을 사용하기로 하였고, 이 배열을 인덱스와 함께 관리하는 자료구조를 만들었습니다. 이 자료구조가 바로 Series입니다. 그리고 Series를 확장한 자료구조가 DataFrame입니다.
2.1.2 Series
Series는 인덱스(index)를 가진 1차원 배열 형태의 자료구조입니다. 리스트의 순차 인덱스(‘0, 1, 2, …’)와 달리 의미 있는 이름표(label)를 붙일 수 있습니다. 별도의 인덱스를 지정하지 않으면 ‘0, 1, 2, …’ 같은 기본 정수 인덱스가 자동으로 생성되지만, 사용자가 의미 있는 이름표(label)를 인덱스로 지정하면 그 라벨을 기준으로 값을 조회할 수 있습니다.
Series 내부 구조를 보면 두 가지 핵심 속성이 있습니다. 하나는 실제 데이터를 담은 값 배열이고, 다른 하나는 각 값에 대응하는 라벨을 담은 Index 객체입니다. 즉 Series는 “값 배열 + 라벨 배열(index)”이라는 이중 구성으로 되어 있으며, 이 둘이 항상 같은 길이를 유지합니다. Index 객체는 단순한 리스트가 아니라 해시 가능한 라벨 타입을 지원하고, 재색인(reindex), 정렬, 조인 등 라벨 기반 연산을 위한 메서드를 제공합니다.
여기서 값 배열이 항상 NumPy 배열인 것은 아니라는 점이 중요합니다. pandas 2.x까지는 “Series = NumPy ndarray + 인덱스”라고 설명해도 큰 무리가 없었지만, 3.x에서는 문자열이 PyArrow 기반의 새 str dtype으로 저장됩니다(PDEP-14). 따라서 값 배열의 실제 타입은 dtype에 따라 달라집니다.
| dtype | 값 배열의 실제 타입 | 비고 |
|---|---|---|
| int64, float64, bool | NumPy ndarray | 종전과 동일 |
| str | ArrowStringArray | NumPy 배열이 아닌 ExtensionArray |
| Int64, Float64 (nullable) | masked ExtensionArray | 결측을 pd.NA로 표현 |
| category | Categorical | 코드 + 범주 목록 구조 |
이 때문에 .values는 dtype에 따라 다른 종류의 객체를 돌려주는 속성이 되었고, 반환 타입을 미리 단정할 수 없습니다. 값 배열이 실제로 무엇인지 알고 싶다면 .array를, NumPy 배열이 확실히 필요하다면 .to_numpy()를 쓰는 편이 안전합니다. 새로 쓰는 코드에서는 .values보다 이 두 가지를 권장합니다.
Series는 한편으로는 배열 연산의 성능을 그대로 활용하고, 다른 한편으로는 dict와 유사한 라벨 기반 접근을 제공하는 하이브리드 구조로 설계되어 있습니다. NumPy 배열처럼 벡터화된 연산을 지원하므로, 전체 Series에 대한 산술 연산과 통계 연산을 빠르게 수행할 수 있습니다. 동시에, 인덱스 라벨을 키처럼 사용해서 의미 있는 이름 기반으로 값을 조회하거나 수정할 수 있으므로, 코드의 의도가 더 명확하게 드러납니다.
pandas 공식 문서도 Series를 “1차원 라벨 배열” 이며, “고정 크기 dict”라고 소개하고 있습니다. 즉 Series는 “순수 NumPy 배열의 위치 기반 접근”과 “dict의 라벨 기반 접근”을 결합한 자료구조로 보는 것이 자연스럽습니다. 이 두 가지 특징이 어떻게 작동하는지 예제를 통해 살펴보겠습니다.
위에서 설명했듯이, Series는 리스트나 딕셔너리와 달리 다음과 같은 특징을 가집니다. 첫째, 라벨 기반 인덱싱(label-based indexing)을 사용합니다. Series는 정수 위치뿐만 아니라 의미 있는 라벨로도 데이터에 접근할 수 있습니다. 내부적으로 인덱스는 해시 테이블과 유사한 구조로 관리되어 빠른 조회가 가능합니다. 라벨 기반 접근은 코드의 가독성을 높이고, 데이터의 의미를 명확하게 표현할 수 있게 해줍니다.
한국 GDP: 1673.0
인덱스 확인: Index(['Korea', 'Japan', 'China', 'USA'], dtype='str')
내부 값 배열: <class 'numpy.ndarray'>
앞서 말한 “dtype에 따라 값 배열이 달라진다”를 직접 확인해봅시다. 숫자 Series와 문자열 Series의 .values를 나란히 출력하면 차이가 분명히 드러납니다.
countries = pd.Series(["Korea", "Japan", "China", "USA"])
print(
f"숫자 Series dtype={gdp.dtype} -> {type(gdp.values).__name__}"
)
print(
f"문자열 Series dtype={countries.dtype} -> {type(countries.values).__name__}"
)
print(f".array -> {type(countries.array).__name__}")
print(
f".to_numpy() -> {type(countries.to_numpy()).__name__}"
)숫자 Series dtype=float64 -> ndarray
문자열 Series dtype=str -> ArrowStringArray
.array -> ArrowStringArray
.to_numpy() -> ndarray
문자열 Series에서 .values가 ArrowStringArray를 돌려주는 것을 볼 수 있습니다. NumPy 배열을 기대하고 .values에 NumPy 전용 연산을 적용하면 이 지점에서 실패합니다. .to_numpy()는 dtype과 무관하게 항상 NumPy 배열을 돌려주므로, NumPy와 연동하는 코드에서는 이쪽을 쓰세요.
둘째, 벡터 연산(vectorized operations)을 사용합니다. Series의 값 배열은 NumPy 배열이므로, 모든 값에 동시에 연산을 적용할 수 있습니다. 이는 Python의 반복문보다 수십 배에서 수백 배 빠르며, 대규모 데이터 처리에서 필수적입니다.
조 단위로 변환: Korea 1.673
Japan 4.231
China 17.963
USA 25.462
dtype: float64
셋째, 불리언 인덱싱(boolean indexing)을 사용합니다. 조건을 만족하는 데이터만 선택할 수 있습니다. 내부적으로는 조건식이 각 요소에 대해 평가되어 불리언 배열을 생성하고, 이 배열을 마스크로 사용하여 해당하는 값만 선택합니다. 이 과정도 벡터화되어 매우 빠르게 수행됩니다.
China 17963.0
USA 25462.0
dtype: float64
넷째, 결측치 표현과 처리에 강점을 가집니다. pandas는 결측치를 NaN(Not a Number)로 표현하며, 이를 일관되게 처리할 수 있습니다. NaN을 사용하여 결측치를 표현하고, 연산 시 자동으로 결측치를 처리합니다. 이는 데이터 분석에서 매우 중요한 기능입니다.
결측치 개수: 1
결측치 위치: Korea False
Japan True
China False
USA False
dtype: bool
평균 (결측치 제외): 15032.67
2.1.2.1 Series 생성 방법
Series는 다양한 입력 소스로부터 생성할 수 있습니다. 각 방법은 특정 상황을 좀 더 효과적으로 처리할 수 있습니다. 아래 예제에서 살펴볼 수 있듯이, 리스트, 딕셔너리, NumPy 배열, 스칼라 값으로부터 Series를 생성할 수 있습니다.
- 리스트로부터 생성: 리스트를 사용하면 기본적으로 0부터 시작하는 정수 인덱스가 자동으로 생성됩니다. 이는 간단한 방법이지만, 인덱스에 의미를 부여하려면 명시적으로 지정해야 합니다.
- 딕셔너리로부터 생성: 딕셔너리의 키가 인덱스가 되고, 값이 Series의 값이 됩니다. 이 방법은 키-값 쌍의 데이터를 Series로 변환할 때 매우 유용합니다. 딕셔너리의 키 순서는 Python 3.7+에서 유지되지만, 명시적인 순서가 중요하다면 OrderedDict를 사용하는 것이 좋습니다.
- NumPy 배열로부터 생성: NumPy 배열을 직접 사용하면 메모리 복사 없이 효율적으로 Series를 생성할 수 있습니다. 대규모 데이터를 다룰 때 특히 유용하며, NumPy의 다양한 배열 생성 함수와 함께 사용할 수 있습니다.
- 스칼라 값으로 초기화: 스칼라 값과 인덱스를 함께 제공하면, 해당 값이 인덱스의 길이만큼 반복됩니다. 이는 동일한 값으로 초기화된 Series를 만들 때 유용합니다.
2.1.2.2 Series 속성과 메서드
Series는 데이터 분석에 필요한 다양한 속성과 메서드를 제공합니다. 이러한 속성들은 Series의 내부 구조를 이해하고 데이터를 효과적으로 다루는 데 도움이 됩니다. 주요 속성은 Series의 현재 상태를 나타내는 값으로, 데이터 타입, 크기, 인덱스 정보 등을 제공합니다. 이러한 속성들은 데이터를 이해하고 적절한 연산을 선택하는 데 중요한 정보를 제공합니다.
- dtype: 데이터 타입
- size: 크기
- index: 인덱스
- values: 값 배열 (dtype에 따라 NumPy 배열 또는 ExtensionArray)
- array: 값 배열 (항상 pandas의 배열 객체)
- name: 이름
데이터 타입: float64
크기: 4
인덱스: ['Korea', 'Japan', 'China', 'USA']
값 배열: [ 1673. 4231. 17963. 25462.]
이름: None
기술통계는 데이터의 전체적인 특성을 요약하는 수치입니다. pandas는 이러한 통계량을 효율적으로 계산하는 메서드들을 제공하며, 내부적으로는 NumPy의 최적화된 함수를 사용합니다.
- mean: 평균
- median: 중앙값
- std: 표준편차
- min: 최소값
- max: 최대값
- sum: 합계
평균: 12332.25
중앙값: 11097.00
표준편차: 11304.06
최소값: 1673.00
최대값: 25462.00
합계: 49329.00
describe() 메서드는 여러 통계량을 한 번에 계산하여 DataFrame으로 반환합니다. 이는 데이터를 처음 접할 때 전체적인 특성을 빠르게 파악하는 데 매우 유용합니다. 기본적으로 평균, 표준편차, 최소값, 25%, 50%, 75% 백분위수, 최대값을 제공합니다.
- count: 개수 (결측치 제외)
- mean: 평균
- std: 표준편차
- min: 최소값
- 25%: 25% 분위수
- 50%: 50% 분위수
- 75%: 75% 분위수
- max: 최대값
2.1.2.3 주의사항
Series를 사용할 때 자주 실수하는 상황과 그 해결 방법을 이해하는 것은 중요합니다. 이러한 실수는 예상치 못한 결과를 초래할 수 있으므로, 각 상황의 동작 원리를 이해하는 것이 필요합니다.
첫째, Series 간 연산 시 pandas는 인덱스를 기준으로 자동으로 데이터를 정렬합니다. 이는 매우 편리한 기능이지만, 인덱스 순서에 의존하는 코드에서는 예상치 못한 결과를 초래할 수 있습니다. 연산 결과의 인덱스는 두 Series의 인덱스의 합집합(union)이 되며, 한쪽에만 존재하는 인덱스는 NaN이 됩니다.
연산 결과:
A 31
B 22
C 13
dtype: int64
둘째, NaN이 포함된 Series에서 연산을 수행하면, 대부분의 집계 함수는 NaN을 자동으로 제외하고 계산합니다. 이는 일반적으로 원하는 동작이지만, 때로는 NaN을 명시적으로 처리해야 할 수도 있습니다. skipna 매개변수를 사용하여 이 동작을 제어할 수 있습니다. 단, NaN을 포함하고 있는 연산은 그 결과가 모두 NaN이 됩니다.
2.1.3 DataFrame
DataFrame은 여러 개의 Series가 모여 만들어진 2차원 표 형태의 자료구조입니다. 엑셀 시트나 SQL 테이블과 유사하지만, 훨씬 강력한 기능을 제공합니다. 열(column)과 행(row)으로 구성되어 있으며, 열은 컬럼(column)이라고 하고, 행은 행(row)이라고 합니다. DataFrame은 내부적으로 여러 개의 Series를 열(column) 단위로 관리합니다. 각 열은 독립적인 Series이며, 모든 열이 동일한 인덱스를 공유합니다. 공식 문서는 DataFrame을 다음과 같이 요약합니다.
- 2차원 라벨 데이터 구조
- 서로 다른 타입의 열(column)을 포함할 수 있음
- 엑셀 시트, SQL 테이블, 또는 Series 객체의 dict로 생각할 수 있음
DataFrame은 두 종류의 이름(label)을 가집니다. 행(row)의 이름은 인덱스(index)라고 부르며, 각 행을 식별합니다. 열(column)의 이름은 열 이름(column labels)으로, 각 변수 및 특성의 이름을 나타냅니다. 대부분의 경우 행과 열의 인덱스는 정수지만, 문자열이나 날짜 등 다양한 값을 사용할 수 있습니다. 특히, 열의 이름은 헤더(header)로도 불립니다.
DataFrame의 열은 하나의 독립적인 Series로 구현되어 있으며, 모든 열이 동일한 행 인덱스를 공유합니다. 개념적으로는 ’딕셔너리의 키가 열 이름이고 값이 Series’인 딕셔너리 데이터 구조로 볼 수 있습니다.
DataFrame은 다음과 같은 특징을 가집니다. 첫째, 앞서 설명한 것처럼 열은 Series입니다. DataFrame의 각 열은 독립적인 Series입니다. 이는 열에 대해 Series의 모든 메서드와 속성을 사용할 수 있다는 의미입니다. 열을 선택하면 Series가 반환되며, 이 Series는 원본 DataFrame과 동일한 인덱스를 공유합니다.
둘째, shape 속성은 (행 개수, 열 개수) 튜플을 반환합니다. 이는 데이터의 전체적인 크기를 빠르게 파악하는 데 유용합니다.
셋째, 인덱스와 열 이름은 Index 객체로 관리됩니다. 이는 데이터 정렬과 조인 연산의 기반이 됩니다.
RangeIndex(start=0, stop=4, step=1)
Index(['country', 'gdp', 'population', 'continent'], dtype='str')
2.1.3.1 DataFrame 생성 방법
DataFrame은 다양한 방법으로 생성할 수 있습니다. 첫째, 간단한 방법은 딕셔너리를 사용하는 것입니다. 딕셔너리의 키가 열 이름이 되고, 값이 열의 값이 됩니다.
둘째, 리스트의 리스트를 사용하는 것입니다. 리스트의 리스트는 각 행을 나타내며, 각 행은 열의 값들을 나타냅니다.
셋째, NumPy 배열을 사용하는 것입니다. NumPy 배열은 2차원 배열이며, 각 행은 열의 값들을 나타냅니다.
넷째, 딕셔너리(dictionary) 리스트를 사용하는 것입니다. 딕셔너리(dict) 리스트는 각 행을 나타내며, 각 행은 열의 값들을 나타냅니다.
2.1.3.2 DataFrame 속성과 메서드
DataFrame은 데이터를 탐색하고 이해하는 데 필요한 다양한 속성과 메서드를 제공합니다. 이러한 도구들을 효과적으로 사용하면 데이터의 구조와 특성을 빠르게 파악할 수 있습니다. 첫째, head()와 tail() 메서드는 데이터의 앞부분과 뒷부분을 확인하여 데이터의 전반적인 형태를 이해하는 데 도움이 됩니다.
처음 2행:
country gdp population continent
0 Korea 1673.0 51.7 Asia
1 Japan 4231.0 125.0 Asia
마지막 2행:
country gdp population continent
2 China 17963.0 1412.0 Asia
3 USA 25462.0 331.9 Americas
둘째, info() 메서드는 DataFrame의 기본 정보를 제공합니다. 열의 개수, 데이터 타입, 결측치 개수, 메모리 사용량 등을 한눈에 확인할 수 있어, 실무에서 데이터를 처음 접할 때 먼저 실행하는 메서드입니다.
<class 'pandas.DataFrame'>
RangeIndex: 4 entries, 0 to 3
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 country 4 non-null str
1 gdp 4 non-null float64
2 population 4 non-null float64
3 continent 4 non-null str
dtypes: float64(2), str(2)
memory usage: 298.0 bytes
None
셋째, describe() 메서드는 숫자형 열에 대해 기본적인 기술통계를 제공합니다. 평균, 표준편차, 백분위수 등을 한 번에 확인할 수 있어, 데이터의 분포와 특성을 빠르게 이해할 수 있습니다.
gdp population
count 4.000000 4.000000
mean 12332.250000 480.150000
std 11304.063322 632.461701
... ... ...
50% 11097.000000 228.450000
75% 19837.750000 601.925000
max 25462.000000 1412.000000
[8 rows x 2 columns]
넷째, 특정 열에 대해 개별적으로 통계를 계산할 수 있습니다. 이는 Series의 메서드를 사용하므로, Series에서 사용할 수 있는 모든 통계 함수를 사용할 수 있습니다.
2.1.3.3 열 추가, 수정, 삭제
DataFrame의 구조를 변경하는 작업은 데이터 전처리에서 매우 중요합니다. 열을 추가, 수정, 삭제하는 방법을 이해하면 데이터를 원하는 형태로 변환할 수 있습니다. 열을 추가하는 간단한 방법은 대괄호 표기법을 사용하는 것입니다. 기존 열을 기반으로 계산된 값을 새 열으로 추가할 수 있으며, 이는 벡터 연산을 통해 효율적으로 수행됩니다.
0 32359.767892
1 33848.000000
2 12721.671388
3 76715.878277
Name: gdp_per_capita, dtype: float64
rename() 메서드를 사용하여 열 이름을 변경할 수 있습니다. 딕셔너리를 사용하여 여러 열의 이름을 한 번에 변경할 수 있으며, inplace=True 매개변수를 사용하면 원본을 직접 수정할 수 있습니다.
| country | GDP (billion USD) | population | continent | gdp_per_capita | |
|---|---|---|---|---|---|
| 0 | Korea | 1673.0 | 51.7 | Asia | 32359.767892 |
| 1 | Japan | 4231.0 | 125.0 | Asia | 33848.000000 |
| 2 | China | 17963.0 | 1412.0 | Asia | 12721.671388 |
| 3 | USA | 25462.0 | 331.9 | Americas | 76715.878277 |
drop() 메서드를 사용하여 열을 삭제할 수 있습니다. columns 매개변수에 삭제할 열 이름을 리스트로 전달하면 됩니다. 기본적으로는 새 DataFrame을 반환하지만, inplace=True를 사용하면 원본을 직접 수정할 수 있습니다.
2.2 데이터 읽어오기
제가 경험했던 대부분의 팀은 CSV 파일보다는 엑셀 파일을 많이 다루고, 데이터분석가에게 데이터베이스 읽기 권한 등을 부여해서 SQL을 사용하는 경우가 많았습니다. 하지만, 교육이나 프로젝트에서 CSV 파일로 데이터를 주고받는 경우가 많습니다. 그래서 먼저 CSV를 기반으로 데이터를 읽는데 많이 활용되는 pandas의 read_csv() 함수를 먼저 다루도록 하겠습니다.
pandas에 read_sql() 함수가 있습니다. 이 함수를 사용하면 SQL 쿼리를 사용하여 데이터를 읽어올 수 있습니다. 그래서 SQLite 데이터베이스로 데이터 모델링을 연습하고 분석 및 시각화를 pandas로 연습하는 과정도 좋은 학습 방법입니다. SQLite가 Old School이라 생각하신다면, DuckDB를 사용하는 것도 좋은 방법입니다. 그리고 우리는 DuckDB를 사용해서 연습해볼 예정입니다.
pandas에서 데이터를 읽어올 때 자동으로 다음과 같은 dtype이 적용됩니다.
- 정수: int64
- 실수: float64
- 문자열: str
날짜처럼 보이는 열은 여기에 없다는 점에 주의하세요. read_csv()는 날짜를 자동으로 파싱하지 않습니다. 2024-01-01 같은 값이 들어 있어도 그냥 문자열(str)로 읽히므로, 날짜로 다루려면 parse_dates=["열이름"]을 명시하거나 읽은 뒤 pd.to_datetime()으로 변환해야 합니다. 이때 만들어지는 dtype은 datetime64[us]입니다. pandas 3.x의 기본 시간 해상도가 나노초에서 마이크로초로 바뀌었기 때문인데, 이 변화의 의미는 시계열 장에서 다시 다룹니다.
pandas는 다양한 파일 형식(CSV, Excel, JSON, Parquet 등)을 지원하며, 각 형식에 맞는 최적화된 파서를 사용합니다. read_csv()는 내부적으로 C 기반 파서를 사용하여 대용량 파일도 효율적으로 읽을 수 있습니다. read_csv()는 다양한 매개변수를 제공하여 다양한 형식의 CSV 파일을 읽을 수 있습니다. URL을 사용해서 파일을 가져오는 것도 가능하지만, 대부분의 경우 디스크에 저장된 파일을 주로 사용합니다. CSV 파일은 기보적은 콤마(,)로 구성되어 있지만, tsv 파일을 콤마 대신에 탭("\t")으로 구성되어 있습니다. read_csv() 함수는 이런 몇가지 예외사항을 처리할 수 있도록 ‘sep’ 매개변수를 제공합니다.
<class 'pandas.DataFrame'>
RangeIndex: 1704 entries, 0 to 1703
Data columns (total 6 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 country 1704 non-null str
1 continent 1704 non-null str
2 year 1704 non-null int64
3 lifeExp 1704 non-null float64
4 pop 1704 non-null int64
5 gdpPercap 1704 non-null float64
dtypes: float64(2), int64(2), str(2)
memory usage: 103.7 KB
파일을 읽은 후 info() 메서드를 사용하면 데이터의 전반적인 구조를 확인할 수 있습니다. 위 출력에서 볼 수 있듯이 1704개의 행(row)과 6개의 열(column)을 가진 데이터프레임이 생성되었습니다. 각 열의 데이터 타입은 다음과 같습니다.
- country: 문자열 (str)
- continent: 문자열 (str)
- year: 정수 (int64)
- lifeExp: 실수 (float64)
- pop: 정수 (int64)
- gdpPercap: 실수 (float64)
예를 들어 country는 국가 이름이 포함되어 있음을 알 수 있고, str로 표현되는 것을 보아 문자열로 구성되어 있음을 알 수 있습니다. 그리고 year, pop은 정수이므로, 해당 열의 값은 정수로 구성되어 있음을 알 수 있습니다.
’../data/gapminder.tsv’에서 ...는 상대 경로를 의미합니다. 저는 다음과 같은 구조의 폴더를 만들어서 사용합니다.
.
├── data
│ └── gapminder.tsv # 읽어와야 하는 데이터 파일
├── note
│ ├── ch01.ipynb
│ ├── ch02.ipynb # 작업 중인 노트북 파일
│ ├── ....ipynb
│ └── ch99.ipynb
├── src
│ ├── clenaup.py
│ ├── ....py
│ ├── modeling.py
│ ├── visualization.py
│ └── utils.py
├── report
│ ├── 초안(수정_20260720).docx
│ ├── 팀내회람용(수정_20260720).docx
│ └── 최종보고서_v1.3.docx
├── .gitignore
├── README.md
└── TODO.md위 폴더 구조에서 현재 폴더를 . 형태로 나타냅니다. 그리고, .. 형태로 부모 폴더를 나타냅니다. 현재 작성하고 있는 파일은 note 폴더에 있는 ch02.ipynb 입니다. 따라서 ch02.ipynb 기준에서 gapminder.tsv는 한 단계 위에 있는 data 폴더에 있습니다.
즉, ch02.ipynb가 있는 note 폴더 기준에서 .. 는 부모 폴더를 의미하고, data는 그 폴더 아래의 data 폴더를 의미합니다. 따라서 ../data/gapminder.tsv'는 data 폴더 아래의 gapminder.tsv 파일을 의미합니다. 각자 사용하는 폴더 구조에 맞게 데이터 위치를 수정해서 사용하시기 바랍니다.
2.3 데이터 선택과 필터링 기초
CSV 파일에서 데이터 프레임을 생성했습니다. 이제 데이터 프레임을 사용하여 데이터를 분석할 수 있습니다. 이때 우리에게 필요한 기능은 원하는 데이터만 정확하게 선택하는 것 입니다. 데이터 분석의 기본적이면서도 중요한 기술입니다. pandas는 다양한 방법으로 데이터를 선택할 수 있는 유연한 인덱싱 시스템을 제공합니다. 기본적인 대괄호 표기법부터 강력한 .loc과 .iloc 접근자까지, 각 방법의 특징과 용도를 정확히 이해하면 복잡한 데이터 조작도 효율적으로 수행할 수 있습니다.
2.3.1 인덱싱의 기본 원리
pandas의 인덱싱 시스템은 두 가지 기본 원칙에 기반합니다. 첫째, 라벨 기반 인덱싱은 인덱스의 라벨이나 열(column) 이름을 사용하여 데이터에 접근합니다. 이는 의미 있는 데이터 선택을 가능하게 하며, 데이터의 순서가 바뀌어도 안정적으로 동작합니다. 둘째, 위치 기반 인덱싱은 정수 위치(0부터 시작)를 사용하여 데이터에 접근합니다. 이는 데이터의 물리적 위치에 기반하므로, 순서가 중요한 경우에 유용합니다.
잘못된 인덱싱 방법으로 인한 버그가 자주 발생합니다. 특히 체인 인덱싱(chained indexing)은 초보자뿐만 아니라 경험 있는 분석가도 종종 겪는 함정입니다. 이 함정은 pandas 3.x에서 Copy-on-Write가 기본이 되면서 동작이 오히려 단순해졌지만, 옛 자료가 설명하는 방식과 달라졌기 때문에 새로 짚어둘 필요가 있습니다(“인덱싱 시 주의사항”에서 실행 결과로 확인합니다). 올바른 인덱싱 방법과 함께 흔히 발생하는 실수들을 다루어, 안전하고 효율적인 코드를 작성하는 방법을 배우게 됩니다.
2.3.2 열 선택
열을 선택하는 기본적인 방법은 대괄호 표기법입니다. 단일 열을 선택하면 Series가 반환됩니다.
여러 열을 선택하면 DataFrame이 반환됩니다. 이 때, 대괄호 안에 열 이름을 담은 리스트를 사용합니다. 만약, 리스트 안에 하나의 열 이름을 작성해도 DataFrame이 반환됩니다.
2.3.2.1 점 표기법 vs 대괄호 표기법
점 표기법(df.column)은 Python 객체의 속성 접근과 동일하게 간편하게 사용할 수 있기 때문에 코딩할 때 매우 직관적으로 보입니다. 하지만 pandas에서 점 표기법은 여러 가지 제한과 위험을 내포하고 있습니다. 첫째, 열(column) 이름에 공백이나 특수문자가 포함되어 있다면 점 표기법으로 접근이 불가능합니다. 예를 들어 df.population growth처럼 띄어쓰기가 있는 경우 오류가 발생합니다. 둘째, 만약 열 이름이 DataFrame의 내장 메서드(count, mean 등)와 동일하다면, .count가 메서드를 호출하는 것인지, 열을 의미하는 것인지 혼란이 생길 수 있습니다. 마지막으로, 점 표기법은 문자열을 직접 코드에 적어야만 접근이 가능하기 때문에, 열 이름을 변수로 받거나 동적으로 처리해야 할 때 사용할 수 없습니다.
이러한 한계 때문에, pandas에서는 대괄호 표기법(df["column"])을 기본으로 사용하는 것이 훨씬 안전하고 유연합니다. 대괄호 표기법은 공백, 특수문자, 한글 등 다양한 형태의 열 이름을 모두 지원하며, 문자열 변수로 열 이름을 지정할 수도 있어 코드의 재사용성과 범용성이 높아집니다. 또한 내장 메서드와 이름이 겹치는 열도 문제없이 선택할 수 있습니다. 항상 대괄호 표기법을 기본 접근 방법으로 삼는 것을 권장합니다.
2.3.3 행 선택과 필터링
데이터 분석에서 특정 조건을 만족하는 행을 찾거나, 특정 기준으로 정렬하여 상위 또는 하위 데이터를 확인하는 것은 매우 일반적인 작업입니다. pandas는 이러한 작업을 효율적으로 수행할 수 있는 다양한 방법을 제공합니다.
간단한 방법은 정렬을 사용하는 것 입니다. 정렬(sorting)은 데이터를 특정 기준에 따라 재배열하는 작업입니다. 정렬을 통해 상위 N개나 하위 N개를 쉽게 찾을 수 있으며, 데이터의 패턴을 파악하는 데도 유용합니다. sort_values() 메서드는 지정된 컬럼의 값을 기준으로 행을 정렬하며, 내부적으로는 효율적인 정렬 알고리즘을 사용하여 대규모 데이터에서도 빠르게 동작합니다.
하나의 컬럼을 기준으로 정렬하는 기본적인 방법입니다. ascending 매개변수로 오름차순(True, 기본값) 또는 내림차순(False)을 지정할 수 있습니다. 정렬 후 head()나 tail() 메서드를 사용하면 상위 또는 하위 N개 행을 쉽게 선택할 수 있습니다.
여러 컬럼을 기준으로 정렬할 수 있으며, 리스트로 컬럼 이름을 지정하면 순서대로 정렬이 적용됩니다. 첫 번째 컬럼으로 먼저 정렬하고, 값이 동일한 경우 두 번째 컬럼으로 정렬하는 방식입니다. ascending 매개변수도 리스트로 지정하여 각 컬럼별로 오름차순/내림차순을 다르게 설정할 수 있습니다.
| country | lifeExp | |
|---|---|---|
| year | ||
| 1952 | Norway | 72.670 |
| 1957 | Iceland | 73.470 |
| 1962 | Iceland | 73.680 |
| ... | ... | ... |
| 1997 | Japan | 80.690 |
| 2002 | Japan | 82.000 |
| 2007 | Japan | 82.603 |
12 rows × 2 columns
정렬은 단순히 데이터를 재배열하는 것뿐만 아니라, 다음과 같은 분석 작업에 유용합니다.
상위/하위 N개 추출: 정렬 후
head(N)또는tail(N)으로 상위 또는 하위 N개 행을 추출할 수 있습니다.그룹별 순위 확인: 그룹화 후 정렬하여 각 그룹 내 순위 파악할 수 있습니다.
데이터 품질 검증: 정렬을 통해 이상치나 패턴을 시각적으로 확인할 수 있습니다.
시계열 데이터 처리: 시간 순서대로 정렬하여 시계열 분석 준비할 수 있습니다.
정렬은 데이터를 읽기 쉽게 만들고, 패턴을 발견하는 데 도움이 되지만, 원본 데이터의 순서를 변경하므로 필요에 따라 복사본을 만들어 사용하는 것이 좋습니다.
2.3.3.1 불리언 인덱싱: 조건으로 필터링
특정 조건을 만족하는 데이터만 별도로 확인해야 할 때가 있습니다. 이때 불리언 인덱싱을 사용하면 조건을 만족하는 데이터만 선택할 수 있습니다. 내부적으로는 조건식이 각 행에 대해 평가되어 불리언 Series가 생성되고, 이 Series가 마스크로 사용되어 해당하는 행만 선택됩니다. 이 과정은 벡터화되어 매우 빠르게 수행됩니다.
단일 조건을 사용하면 해당 조건을 만족하는 모든 행이 선택됩니다. 불리언 Series는 원본 DataFrame과 동일한 인덱스를 가지며, True인 위치의 행만 반환됩니다.
여러 조건을 결합할 때는 논리 연산자(‘&’, ‘|’, ‘~’)를 사용합니다. 중요한 점은 각 조건을 괄호로 감싸야 한다는 것입니다. 이는 연산자 우선순위 때문이며, 괄호를 생략하면 예상치 못한 결과가 발생할 수 있습니다. 괄호를 사용하지 않으면 오류가 발생합니다. 반드시 괄호를 사용하세요.
2.3.4 loc과 iloc: 명시적 인덱싱
.loc과 .iloc은 pandas의 명시적 인덱싱 접근자입니다. 이들은 대괄호 표기법보다 더 명확하고 안전한 데이터 선택을 제공하며, 특히 복잡한 조건이나 다중 인덱싱이 필요한 경우에 필수적입니다.
.loc은 행과 열의 라벨(이름)을 사용하여 데이터를 선택합니다. 내부적으로는 인덱스의 해시 테이블을 사용하여 빠른 조회를 수행하며, 불리언 Series를 사용할 때는 인덱스 정렬(alignment)을 통해 정확한 행 선택을 보장합니다. 주요 특징은 첫째, 슬라이싱 시 끝 인덱스가 포함됩니다 (inclusive). 둘째, 불리언 Series를 사용할 수 있으며, 인덱스가 자동으로 정렬됩니다. 셋째, 존재하지 않는 라벨을 사용하면 KeyError가 발생합니다. 넷째, 데이터 수정 시 .loc으로 한 번에 접근하면 체인 대입이 무시되는 문제를 피할 수 있습니다(뒤에서 자세히 다룹니다).
아래 예제는 조건을 만족하는 행을 선택한 후, 특정 열의 값을 수정하는 예제입니다.
.iloc은 정수 위치(0부터 시작)를 사용하여 데이터를 선택합니다. 이는 NumPy 배열의 인덱싱과 유사하며, 인덱스 라벨과 무관하게 물리적 위치에 기반하여 데이터에 접근합니다. 주요 특징은 첫째, 슬라이싱 시 끝 인덱스가 제외됩니다 (exclusive, Python 리스트와 동일). 둘째, 정수나 정수 리스트, 슬라이스만 사용할 수 있습니다. 셋째, 불리언 인덱싱을 사용하려면 .values를 통해 NumPy 배열로 변환해야 합니다. 넷째, 인덱스 라벨과 무관하게 항상 위치 기반으로 동작합니다.
아래 예제는 특정 위치의 값을 선택하는 방법을 설명하는 예제입니다.
아래 예제는 불연속적인 행과 열을 선택하는 방법을 설명하는 예제입니다.
.loc과 .iloc의 선택은 데이터의 특성과 작업의 목적에 따라 달라집니다. 각각의 장단점과 적절한 사용 시나리오를 이해하는 것이 중요합니다. 먼저, .loc을 사용하는 경우는 아래와 같습니다.
- 의미 있는 라벨로 선택: 인덱스가 의미 있는 값(날짜, ID 등)일 때 사용합니다.
- 조건으로 필터링: 불리언 Series를 사용한 조건부 선택할 때 사용합니다.
- 데이터 수정: 값을 변경할 때 (체인 대입이 무시되는 문제를 피하기 위해) 사용합니다.
.iloc을 사용하는 경우는 아래와 같습니다.
위치 기반 선택: 데이터의 물리적 위치가 중요할 때 사용합니다.
반복문에서 사용: 인덱스로 순회할 때 사용합니다.
- 인덱스 라벨이 불필요할 때: 단순히 처음/마지막 N개만 필요할 때 사용합니다. 요약하면 아래와 같습니다.
- 조건 필터링: 항상
.loc사용(안전하고 명확) - 열 선택: 대괄호 표기법(
gap["country"]) 또는.loc사용 - 처음/마지막 N개: head()/tail() 메서드가 더 읽기 쉬움
- 데이터 수정:
.loc을 한 번만 써서 체인 대입 회피 - 반복문:
.iloc사용 (위치 기반이므로)
pandas의 인덱싱 연산은 매우 유연하지만, 다음과 같은 실수를 주의해야 합니다.
- 체인 인덱싱(Chained indexing)의 위험: 체인 인덱싱이란, 중첩된 대괄호 표기법 등으로 여러 번 인덱싱을 이어서 쓰는 방식입니다. 아래 코드가 그 예입니다. 필터링 결과에서
lifeExp열을 꺼낸 다음, 다시[0]으로 값을 하나 꺼내려 하고 있습니다.
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) File ~/Works/!books/pandas-with-tidy/.venv/lib/python3.14/site-packages/pandas/core/indexes/range.py:521, in RangeIndex.get_loc(self, key) 520 try: --> 521 return self._range.index(new_key) 522 except ValueError as err: ValueError: range.index(x): x not in range The above exception was the direct cause of the following exception: KeyError Traceback (most recent call last) Cell In[39], line 1 ----> 1 gap[gap["year"] == 2007]["lifeExp"][0] File ~/Works/!books/pandas-with-tidy/.venv/lib/python3.14/site-packages/pandas/core/series.py:959, in Series.__getitem__(self, key) 954 key = unpack_1tuple(key) 956 elif key_is_scalar: 957 # Note: GH#50617 in 3.0 we changed int key to always be treated as 958 # a label, matching DataFrame behavior. --> 959 return self._get_value(key) 961 # Convert generator to list before going through hashable part 962 # (We will iterate through the generator there to check for slices) 963 if is_iterator(key): File ~/Works/!books/pandas-with-tidy/.venv/lib/python3.14/site-packages/pandas/core/series.py:1046, in Series._get_value(self, label, takeable) 1043 return self._values[label] 1045 # Similar to Index.get_value, but we do not fall back to positional -> 1046 loc = self.index.get_loc(label) 1048 if is_integer(loc): 1049 return self._values[loc] File ~/Works/!books/pandas-with-tidy/.venv/lib/python3.14/site-packages/pandas/core/indexes/range.py:523, in RangeIndex.get_loc(self, key) 521 return self._range.index(new_key) 522 except ValueError as err: --> 523 raise KeyError(key) from err 524 if isinstance(key, Hashable): 525 raise KeyError(key) KeyError: 0
KeyError: 0이 발생합니다. 필터링된 결과는 원본의 인덱스 라벨을 그대로 물려받는데, 2007년 행들의 라벨은 11, 23, 35…이므로 라벨 0이 존재하지 않기 때문입니다. 대괄호에 넣은 0을 위치가 아니라 라벨로 해석한다는 점이 핵심입니다.
값을 읽을 때보다 더 위험한 것은 값을 쓸 때입니다. 체인 인덱싱으로 대입하면 pandas 3.x는 ChainedAssignmentError 경고를 내고 대입 자체를 무시합니다.
False
원본은 전혀 바뀌지 않았습니다. 첫 번째 대괄호가 원본의 뷰가 아니라 새로운 임시 객체를 만들고, 대입은 곧 버려질 그 임시 객체에 적용되기 때문입니다. Copy-on-Write가 기본이 된 3.x에서는 이 동작이 예외 없이 일관됩니다. 참고로 2.x 시절 이 상황에서 나오던 SettingWithCopyWarning은 3.0에서 클래스 자체가 제거되었으므로(pd.errors.SettingWithCopyWarning은 더 이상 존재하지 않습니다), 옛 코드나 옛 자료에서 이 이름을 보게 되면 3.x 기준으로 다시 읽어야 합니다.
체인 인덱싱을 피하려면 .loc(라벨 기반) 또는 .iloc(위치 기반)에서 한 번에 선택하는 것이 좋습니다. 읽기와 쓰기 모두 마찬가지입니다.
- 조건 결합(불리언 인덱싱)에서 괄호 누락: Python의
&,|연산자는 pandas의 Series에서 반드시 조건식을 괄호로 감싸서 사용해야 합니다. 괄호 없이 조건을 결합하면 오류가 발생합니다.
- loc과 iloc의 혼동:
.loc은 라벨 기반 인덱싱,.iloc은 정수 위치 기반 인덱싱입니다.
따라서 첫째, 조건식은 반드시 괄호로 감싸야 합니다. 둘째, 값 수정은 .loc 사용합니다. 셋째, 체인 인덱싱 피하고, 한 번에 .loc 또는 .iloc로 접근합니다. 넷째, 인덱스가 숫자여도, 리셋되지 않는다면 .loc과 .iloc의 의미가 다를 수 있으니 주의합니다.
2.4 기본 통계와 집계 입문
데이터 분석에서 첫 번째 단계는 복잡한 원시 데이터를 쉽고 간결하게 요약해 전체적인 흐름과 주요 특성을 파악하는 것입니다. pandas는 이 작업을 매우 쉽게 해주는데, 특히 기술 통계(평균, 표준편차, 빈도수 등)와 집계(그룹별 요약 등) 기능이 강력하게 내장되어 있습니다. 이처럼 방대한 데이터를 신속하게 요약할 수 있는 능력은 데이터의 패턴 발견, 품질 점검, 이상치 탐지에 필수적입니다.
2.4.1 기본 통계 함수
pandas에서는 .mean(), .median(), .min(), .max(), .std()와 같은 함수로 수치형 데이터의 핵심 요약 통계를 바로 구할 수 있습니다. 예를 들어, gapminder 데이터셋의 기대수명에 대해 다음과 같이 주요 통계를 바로 확인할 수 있습니다. 여러 통계 함수를 제공하기 때문에 평균만 볼 것이 아니라 반드시 중앙값 및 범위도 함께 점검하는 것이 좋습니다. 예를 들어 일부 국가만 기대수명이 매우 낮거나 높다면 평균이 왜곡될 수 있기 때문입니다.
평균 기대수명: 59.47세
중앙값 기대수명: 60.71세
최소 기대수명: 23.60세
최대 기대수명: 82.60세
표준편차: 12.92
- 평균(mean): 전체 기대수명의 합을 데이터 개수로 나눈 것으로, 기대수명의 ‘중심’ 경향을 대표
- 중앙값(median): 데이터 값을 크기순으로 정렬했을 때 가운데 값으로, 극단값(아웃라이어)에 덜 민감
- 최소/최대값(min/max): 관측값의 범위, 데이터의 하위/상위 극단을 보여줌
- 표준편차(std): 데이터의 산포도를 의미하며, 값이 클수록 흩어져 있음을 나타냄
앞선 각 통계 함수를 한 눈에 확인하는 방법은 describe() 함수를 사용하는 것입니다.
count 1704.000000
mean 59.474439
std 12.917107
...
50% 60.712500
75% 70.845500
max 82.603000
Name: lifeExp, Length: 8, dtype: float64
해당 함수는 25%, 50%, 75% 값을 각각 1/4, 2/4(중앙), 3/4 위치의 값(분위수)으로 보여줍니다. 예를 들어, 75%와 25%의 차이(IQR)는 대부분의 데이터가 어디에 몰려있는지 알 수 있습니다. 전체 데이터프레임에서 숫자형 열만 선택해 요약하려면 include="number" 매개변수를 사용할 수 있습니다.
| year | lifeExp | pop | gdpPercap | |
|---|---|---|---|---|
| count | 1704.00000 | 1704.000000 | 1.704000e+03 | 1704.000000 |
| mean | 1979.50000 | 59.474439 | 2.960121e+07 | 7215.327081 |
| std | 17.26533 | 12.917107 | 1.061579e+08 | 9857.454543 |
| ... | ... | ... | ... | ... |
| 50% | 1979.50000 | 60.712500 | 7.023596e+06 | 3531.846988 |
| 75% | 1993.25000 | 70.845500 | 1.958522e+07 | 9325.462346 |
| max | 2007.00000 | 82.603000 | 1.318683e+09 | 113523.132900 |
8 rows × 4 columns
또한, 백분위수를 커스터마이즈할 수도 있습니다. 기본적으로는 25%, 50%, 75% 분위수를 보여주지만, 위처럼 percentiles 인자를 조정하면 10%와 90% 등 관심있는 구간까지 구체적으로 볼 수 있습니다. 극단값의 분포를 확인하거나 중앙값이 치우쳐 있지 않은지 점검할 때 유용합니다.
count 1704.000000
mean 59.474439
std 12.917107
...
50% 60.712500
90% 75.097000
max 82.603000
Name: lifeExp, Length: 8, dtype: float64
숫자가 아닌 범주형 열에도 describe()를 적용할 수 있는데, 고유값(unique) 수, 많이 나온 값(top), 그 빈도수(freq) 등이 출력되어 데이터의 다양성과 불균형 여부, 반복값 등을 점검할 때 활용합니다.
2.4.2 빈도수 확인
범주형 변수(대륙/국가/카테고리 등)의 각 값이 데이터셋에 몇 번씩 등장하는지 파악하는 것이 기본 빈도수 분석입니다. 불균형 분류, 표본 품질 점검 등에 필수로 쓰입니다.
위 결과는 각 대륙이 데이터셋에 몇 번 존재하는지(즉, 표본 불균형이 있는지) 알려줍니다. 예를 들어 아프리카 표본이 많고, 오세아니아는 적음을 알 수 있을 겁니다. ’표본이 지나치게 적은 그룹’이 없는지 점검하는 것이 중요합니다.
normalize=True를 추가하면 비율(%)로 결과를 보여주므로, 데이터셋 전체 중에서 각 카테고리가 차지하는 비중을 볼 수 있습니다.
continent
Africa 0.366197
Asia 0.232394
Europe 0.211268
Americas 0.176056
Oceania 0.014085
Name: proportion, dtype: float64
결측치가 있는 경우, dropna=False 옵션을 주면 결측도 함께 집계해 품질 지표로 삼을 수 있습니다.
2.4.3 그룹별 집계
groupby()는 pandas에서 강력한 도구 중 하나입니다. 데이터를 그룹으로 나누고(Split), 각 그룹에 함수를 적용하고(Apply), 결과를 합치는(Combine) “Split-Apply-Combine” 전략을 구현합니다. Split-Apply-Combine 패턴은 데이터 분석에서 매우 중요한 개념입니다. 이 패턴은 다음과 같이 동작합니다.
Split (분할): 지정된 키(컬럼)를 기준으로 데이터를 여러 그룹으로 나눕니다. 내부적으로는 그룹 키의 해시값을 계산하여 각 행을 해당 그룹에 할당합니다. 기본적으로 그룹 키는 정렬되지만,
sort=False옵션을 사용하면 정렬을 건너뛸 수 있어 성능이 향상될 수 있습니다.Apply (적용): 각 그룹에 대해 지정된 함수(집계, 변환, 필터링 등)를 적용합니다. 각 그룹의 연산은 논리적으로 서로 독립적입니다. 다만 pandas가 이 단계를 여러 CPU 코어로 병렬 실행하지는 않습니다. 내장 집계 함수(sum, mean 등)는 단일 스레드의 최적화된 C 코드로 한 번에 처리되고, 사용자 정의 함수는 그룹을 순회하며 실행됩니다. 병렬 처리가 필요할 정도의 규모라면 이 책 마지막 파트의 DuckDB를 고려하는 편이 낫습니다.
Combine (결합): 각 그룹의 결과를 하나의 데이터 구조로 합칩니다. 집계 함수의 경우 결과는 그룹 키를 인덱스로 하는 Series나 DataFrame이 됩니다.
이 패턴의 장점은 복잡한 그룹별 연산을 간단하고 명확하게 표현할 수 있다는 것입니다. 또한 내부적으로 최적화된 알고리즘을 사용하여 대규모 데이터에서도 효율적으로 동작합니다. groupby의 기본 패턴을 이해하면 다양한 그룹별 연산을 효율적으로 수행할 수 있습니다. 각 패턴의 동작 원리를 이해하는 것이 중요합니다.
2.4.4 단일 그룹, 단일 집계
기본적인 패턴으로, 하나의 컬럼을 기준으로 그룹화하고 하나의 컬럼에 대해 집계를 수행합니다. 결과는 그룹 키를 인덱스로 하는 Series가 됩니다.
2.4.5 다중 그룹
여러 컬럼을 기준으로 그룹화하면 계층적(hierarchical) 인덱스를 가진 결과가 생성됩니다. 이는 다차원 데이터 분석에 유용하며, 결과는 MultiIndex를 가진 Series가 됩니다.
2.4.6 여러 열 집계
하나의 그룹 키에 대해 여러 컬럼을 동시에 집계할 수 있습니다. 결과는 그룹 키를 인덱스로 하고 집계된 컬럼들을 열로 하는 DataFrame이 됩니다.
2.4.7 다양한 집계 함수 동시 적용
agg() 메서드는 여러 집계 함수를 한 번에 적용할 수 있게 해주는 강력한 도구입니다. 이는 여러 번 groupby를 호출하는 것보다 훨씬 효율적이며, 데이터를 한 번만 순회하여 모든 집계를 수행합니다.
2.4.7.1 단일 열에 여러 함수 적용
하나의 컬럼에 대해 여러 통계량을 동시에 계산할 수 있습니다. 결과는 각 통계량을 컬럼으로 하는 DataFrame이 됩니다.
2.4.7.2 서로 다른 열에 서로 다른 함수 적용
딕셔너리를 사용하여 각 컬럼에 대해 다른 집계 함수를 지정할 수 있습니다. 이는 복잡한 요구사항을 만족하는 요약 통계를 생성할 때 매우 유용합니다.
2.4.7.3 커스텀 함수 사용
표준 집계 함수뿐만 아니라 사용자 정의 함수도 사용할 수 있습니다. 이는 특정 도메인에 맞는 집계를 수행할 때 유용합니다. 다만, 커스텀 함수는 내장 함수보다 느릴 수 있으므로, 가능하면 내장 함수를 사용하는 것이 좋습니다.
2.4.7.4 transform과 filter
groupby는 단순한 집계뿐만 아니라 변환(transformation)과 필터링(filtering)도 지원합니다. 이러한 고급 패턴은 데이터 전처리와 분석에서 매우 유용합니다.
transform()은 집계 결과를 원본 DataFrame의 각 행에 매핑합니다. 이는 그룹별 통계를 새로운 컬럼으로 추가하거나, 그룹별로 정규화하는 등의 작업에 유용합니다. 결과는 원본과 동일한 크기와 인덱스를 가집니다.
| country | continent | lifeExp | continent_mean_life | |
|---|---|---|---|---|
| 0 | Afghanistan | Asia | 28.801 | 60.064903 |
| 1 | Afghanistan | Asia | 30.332 | 60.064903 |
| 2 | Afghanistan | Asia | 31.997 | 60.064903 |
| ... | ... | ... | ... | ... |
| 7 | Afghanistan | Asia | 40.822 | 60.064903 |
| 8 | Afghanistan | Asia | 41.674 | 60.064903 |
| 9 | Afghanistan | Asia | 41.763 | 60.064903 |
10 rows × 4 columns
filter()는 그룹 전체에 대한 조건을 평가하여 조건을 만족하는 그룹의 모든 행을 반환합니다. 이는 특정 특성을 가진 그룹만 분석할 때 유용합니다.
| country | continent | ... | pop | gdpPercap | |
|---|---|---|---|---|---|
| 12 | Albania | Europe | ... | 1282697 | 1601.056136 |
| 13 | Albania | Europe | ... | 1476505 | 1942.284244 |
| 14 | Albania | Europe | ... | 1728137 | 2312.888958 |
| ... | ... | ... | ... | ... | ... |
| 1605 | United Kingdom | Europe | ... | 58808266 | 26074.531360 |
| 1606 | United Kingdom | Europe | ... | 59912431 | 29478.999190 |
| 1607 | United Kingdom | Europe | ... | 60776238 | 33203.261280 |
384 rows × 6 columns
예를 들어, 정렬과 groupby를 결합하여 각 그룹에서 상위 \(N\)개를 추출할 수 있습니다. 이는 각 그룹의 대표적인 데이터를 선택할 때 유용합니다.
| country | continent | ... | pop | gdpPercap | |
|---|---|---|---|---|---|
| 853 | Kuwait | Asia | ... | 212846 | 113523.13290 |
| 856 | Kuwait | Asia | ... | 841934 | 109347.86700 |
| 1151 | Norway | Europe | ... | 4627926 | 49357.19017 |
| ... | ... | ... | ... | ... | ... |
| 70 | Australia | Oceania | ... | 19546792 | 30687.75473 |
| 905 | Libya | Africa | ... | 2721783 | 21951.21176 |
| 545 | Gabon | Africa | ... | 706367 | 21745.57328 |
10 rows × 6 columns
2.5 시각화
데이터 시각화는 데이터 분석에서 중요한 단계입니다. 수치 데이터만으로는 파악하기 어려운 패턴, 추세, 이상치 등을 시각적으로 표현하면 훨씬 쉽게 이해할 수 있습니다. pandas는 Matplotlib을 기반으로 한 간단한 시각화 인터페이스를 제공하여, 복잡한 Matplotlib 코드 없이도 빠르게 차트를 생성할 수 있습니다.
pandas의 plot() 메서드는 Series나 DataFrame에 대해 다양한 차트 유형을 생성할 수 있습니다. 이 메서드는 내부적으로 Matplotlib을 사용하지만, 더 간단한 인터페이스를 제공하여 빠르게 시각화를 수행할 수 있습니다. plot() 메서드는 kind 매개변수로 차트 유형을 지정할 수 있으며, line, bar, hist, box, scatter 등 다양한 유형을 지원합니다. 한글을 표시하기 위해서는 적절한 폰트 설정이 필요합니다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import platform
if platform.system() == "Darwin":
plt.rcParams["font.family"] = "Apple SD Gothic Neo"
elif platform.system() == "Windows":
plt.rcParams["font.family"] = "Malgun Gothic"
else:
plt.rcParams["font.family"] = "NanumGothic"
plt.rcParams["axes.unicode_minus"] = False
yearly_life = gap.groupby("year")["lifeExp"].mean()
yearly_life.plot(kind="line", title="연도별 평균 기대수명")
plt.xlabel("연도")
plt.ylabel("기대수명 (세)")
plt.grid(True, alpha=0.3) # 격자 표시로 가독성 향상
plt.show()
시각화는 그 자체로 하나의 분야 혹은 학문을 구성하는 중요한 요소입니다. 시각화는 데이터의 특성과 분석 목적에 따라 적절한 차트 유형을 선택하는 것이 중요합니다. 각 차트 유형은 특정한 정보를 효과적으로 전달합니다. 우리는 시각화의 “표현” 측면에서 간단한 그래프를 중점으로 살펴볼 것입니다. 시각화에 관해서 더 궁금한 사항은 아래 교재를 참고하세요.
『월스트리트저널 인포그래픽 가이드』: 최소한의 형태로 ’무엇을 어떻게 그릴 것인가’를 체계적으로 정리한 실무형 참고서적입니다. 시각화 자료를 프레젠테이션, 보고서 등에 포함시켜서 차트 디자인 기준을 잡을 때 유용합니다[1].
『데이터 시각화 교과서』: 통계를 기반으로 시각화 원칙을 체계적으로 다룬 서적입니다. 연구, 교육 측면에서 참고하기 좋습니다[2].
『사례 분석으로 배우는 데이터 시각화』: 태블로(Tableau)를 중심으로 시나리오를 따라가며 차트 선택, 대시보드 구성까지 실습할 수 있는 서적입니다[3].
2.5.1 다양한 차트 유형과 각각의 용도
데이터의 특성과 분석 목적에 따라 적절한 차트 유형을 선택하는 것이 중요합니다. 각 차트 유형은 특정한 정보를 효과적으로 전달합니다.
막대 그래프 (Bar Chart)는 범주형 데이터 간의 비교에 적합합니다. 수평 막대 그래프(barh)는 긴 라벨이 있을 때 특히 유용합니다.
히스토그램 (Histogram)은 연속형 데이터의 분포를 확인하는 데 사용됩니다. bins 매개변수로 구간의 개수를 조절할 수 있으며, 데이터의 분포 형태를 파악하는 데 유용합니다.
박스플롯 (Box Plot)은 여러 그룹 간의 분포를 비교하는 데 적합합니다. 중앙값, 사분위수, 이상치 등을 한눈에 확인할 수 있어 그룹 간 차이를 파악하는 데 매우 유용합니다.
2.5.2 Seaborn과 함께 사용
Seaborn은 Matplotlib을 기반으로 한 고수준 시각화 라이브러리로, 더 세련되고 통계적으로 의미 있는 시각화를 제공합니다. 특히 그룹별 비교나 통계적 관계를 시각화하는 데 유용합니다.
산점도는 두 변수 간의 관계를 시각화하는 데 사용됩니다. 특히 상관관계나 패턴을 발견하는 데 유용하며, 이상치나 클러스터를 식별하는 데도 도움이 됩니다.