고급 pandas
고급 pandas
지금까지는 다양한 종류의 데이터를 다듬는 과정과 NumPy, pandas 그리고 다른 라이브러리의 기능을 소개했다. 시간이 갈수록 pandas에는 고급 사용자를 위한 깊이 있는 기능들이 추가되고 있다. 이 장에서는 고급 pandas 사용자가 되기 위한 몇 가지 고급 기능을 소개하겠다.
Categorical 데이터
이 절에서는 pandas의 Categorical형을 활용하여 pandas 메모리 사용량을 줄이고 성능을 개선할 수 있는 방법을 소개한다. 통계와 머신러닝에서 범주형 데이터를 활용하기 위한 도구들도 함께 소개하겠다.
개발 배경과 동기
하나의 칼럼 내에 특정 값이 반복되어 존재하는 경우는 흔하다. 우리는 이미 배열 내에서 유일한 값을 추출하거나 특정 값이 얼마나 많이 존재하는지 확인할 수 있는 unique()
와 value_counts()
같은 메서드를 공부했다.
import numpy as np
import pandas as pd
values = pd.Series(['apple', 'orange', 'apple',
'apple'] * 2)
values
0 apple 1 orange 2 apple 3 apple 4 apple 5 orange 6 apple 7 apple dtype: object
np.unique(values)
array(['apple', 'orange'], dtype=object)
pd.value_counts(values)
apple 6 orange 2 dtype: int64
데이터웨어하우스, 분석 컴퓨팅 외 여러 다양한 데이터 시스템은 중복되는 데이터를 얼마나 효율적으로 저장하고 계산할 수 있는가를 중점으로 개발되었다. 데이터웨어하우스의 경우 구별되는 값을 담고 있는 차원 테이블과 그 테이블을 참조하는 정수키를 사용하는 것이 일반적이다.
values = pd.Series([0, 1, 0, 0] * 2)
dim = pd.Series(['apple', 'orange'])
display(values)
display(dim)
0 0 1 1 2 0 3 0 4 0 5 1 6 0 7 0 dtype: int64
0 apple 1 orange dtype: object
take()
메서드를 사용하면 Series 내에 저장된 원래 문자열을 구할 수 있다.
dim.take(values)
0 apple 1 orange 0 apple 0 apple 0 apple 1 orange 0 apple 0 apple dtype: object
여기서 정수로 표현된 값은 범주형 또는 사전형 표기법이라고 한다. 별개의 값을 담고 있는 배열은 범주, 사전 또는 단계 데이터라고 부른다. 편의상 이런 종류의 데이터를 categorical 또는 범주형 데이터라고 부르겠다. 범주형 데이터를 가리키는 정숫값은 범주 코드 또는 그냥 단순히 코드라고 한다.
범주형 표기법을 사용하면 분석 작업에 있어서 엄청난 성능 향상을 얻을 수 있다. 범주 코드를 변경하지 않은 채로 범주형 데이터를 변형하는 것도 가능하다. 비교적 적은 연산으로 수행할 수 있는 변형의 예는 다음과 같다.
-
범주형 데이터의 이름 변경하기
-
기존 범주형 데이터의 순서를 바꾸지 않고 새로운 범주 추가하기
pandas의 Categorical
pandas에는 정수 기반의 범주형 데이터를 표현(또는 인코딩)할 수 있는 Categorical형이라고 하는 특수한 데이터형이 존재한다. 앞서 살펴본 Series를 다시 보자.
fruits = ['apple', 'orange', 'apple', 'apple'] * 2
N = len(fruits)
df = pd.DataFrame({'fruit': fruits,
'basket_id': np.arange(N),
'count': np.random.randint(3, 15, size=N),
'weight': np.random.uniform(0, 4, size=N)},
columns=['basket_id', 'fruit', 'count', 'weight'])
df
basket_id | fruit | count | weight | |
---|---|---|---|---|
0 | 0 | apple | 5 | 2.979297 |
1 | 1 | orange | 5 | 1.468452 |
2 | 2 | apple | 9 | 0.088356 |
3 | 3 | apple | 9 | 0.543027 |
4 | 4 | apple | 7 | 2.750640 |
5 | 5 | orange | 14 | 2.381390 |
6 | 6 | apple | 14 | 3.295749 |
7 | 7 | apple | 6 | 3.716580 |
아래 예제에서 df[‘fruit’]는 파이썬 문자열 객체의 배열로, 아래 방법으로 쉽게 범주형 데이터로 변경할 수 있다.
fruit_cat = df['fruit'].astype('category')
fruit_cat
0 apple 1 orange 2 apple 3 apple 4 apple 5 orange 6 apple 7 apple Name: fruit, dtype: category Categories (2, object): ['apple', 'orange']
fruit_cat의 값은 NumPy 배열이 아니라 pandas.Categorical의 인스턴스다.
c = fruit_cat.values
type(c)
pandas.core.arrays.categorical.Categorical
Categorical 객체는 categories와 codes 속성을 가진다.
c.categories
Index(['apple', 'orange'], dtype='object')
c.codes
array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)
변경 완료된 값을 대입함으로써 DataFrame의 칼럼을 범주형으로 변경할 수 있다.
df['fruit'] = df['fruit'].astype('category')
df.fruit
0 apple 1 orange 2 apple 3 apple 4 apple 5 orange 6 apple 7 apple Name: fruit, dtype: category Categories (2, object): ['apple', 'orange']
파이썬 열거형에서 pandas.Categorical형을 직접 생성하는 것도 가능하다.
my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
my_categories
['foo', 'bar', 'baz', 'foo', 'bar'] Categories (3, object): ['bar', 'baz', 'foo']
기존에 정의된 범주와 범주 코드가 있다면 from_codes()
함수를 이용해서 범주형 데이터를 생성하는 것도 가능하다.
categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1]
my_cats_2 = pd.Categorical.from_codes(codes, categories)
my_cats_2
['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo', 'bar', 'baz']
범주형으로 변경하는 경우 명시적으로 지정하지 않는 한 특정 순서를 보장하지 않는다. 따라서 categories 배열은 입력 데이터의 순서에 따라 다른 순서로 나타날 수 있다. from_codes()
를 사용하거나 다른 범주형 데이터 생성자를 이용하는 경우 순서를 지정할 수 있다.
ordered_cat = pd.Categorical.from_codes(codes, categories,
ordered=True)
ordered_cat
['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo' < 'bar' < 'baz']
여기서 [foo < bar < baz]는 foo, bar, baz 순서를 가진다는 의미다. 순서가 없는 범주형 인스턴스는 as_ordered()
메서드를 이용해 순서를 가지도고 만들 수 있다.
my_cats_2.as_ordered()
['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo' < 'bar' < 'baz']
여기서는 문자열만 예로 들었지만 범주형 데이터는 꼭 문자열일 필요는 없다. 범주형 배열은 변경이 불가능한 값이라면 어떤 자료형이라도 포함할 수 있다.
Categorical 연산
pandas에서 Categorical은 문자열 배열처럼 인코딩되지 않은 자료형을 사용하는 방식과 거의 유사하게 사용할 수 있다. groupby()
같은 일부 pandas 함수는 범주형 데이터에 사용할 때 더 나은 성능을 보여준다. ordered 플래그를 활용하는 함수들도 마찬가지다.
임의의 숫자 데이터를 pandas.qcut()
함수로 구분해보자. 그렇게 하면 pandas.Categorical 객체를 반환한다. 앞서 pandas.cut()
함수를 살펴봤지만 어떻게 범주형 데이터를 다루는지는 제대로 설명하지 않았다.
np.random.seed(12345)
draws = np.random.randn(1000)
draws[:5]
array([-0.20470766, 0.47894334, -0.51943872, -0.5557303 , 1.96578057])
이 데이터를 사분위로 나누고 통계를 내보자.
bins = pd.qcut(draws, 4)
bins
[(-0.684, -0.0101], (-0.0101, 0.63], (-0.684, -0.0101], (-0.684, -0.0101], (0.63, 3.928], ..., (-0.0101, 0.63], (-0.684, -0.0101], (-2.9499999999999997, -0.684], (-0.0101, 0.63], (0.63, 3.928]] Length: 1000 Categories (4, interval[float64, right]): [(-2.9499999999999997, -0.684] < (-0.684, -0.0101] < (-0.0101, 0.63] < (0.63, 3.928]]
bins.value_counts()
(-2.9499999999999997, -0.684] 250 (-0.684, -0.0101] 250 (-0.0101, 0.63] 250 (0.63, 3.928] 250 dtype: int64
사분위 이름을 실제 데이터로 지정하는 것은 별로 유용하지 않아 보인다. qcut()
함수의 labels 인자로 직접 이름을 지정하자.
bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
bins
['Q2', 'Q3', 'Q2', 'Q2', 'Q4', ..., 'Q3', 'Q2', 'Q1', 'Q3', 'Q4'] Length: 1000 Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
bins.codes[:10]
array([1, 2, 1, 1, 3, 3, 2, 2, 3, 3], dtype=int8)
bins에 이름을 붙이고 나면 데이터의 시작값과 끝값에 대한 정보를 포함하지 않으므로 groupby()
를 이용해서 요약 통계를 내보자.
bins = pd.Series(bins, name='quartile')
results = (pd.Series(draws)
.groupby(bins)
.agg(['count', 'min', 'max'])
.reset_index())
results
quartile | count | min | max | |
---|---|---|---|---|
0 | Q1 | 250 | -2.949343 | -0.685484 |
1 | Q2 | 250 | -0.683066 | -0.010115 |
2 | Q3 | 250 | -0.010032 | 0.628894 |
3 | Q4 | 250 | 0.634238 | 3.927528 |
결과에서 quartile 칼럼은 bins의 순서를 포함한 원래 범주 정보를 유지하고 있다.
results.quartile
0 Q1 1 Q2 2 Q3 3 Q4 Name: quartile, dtype: category Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
categorical을 이용한 성능 개선
특정 제이터셋에 대해 다양한 분석을 하는 경우 범주형categorical으로 변환하는 것만으로도 전체 성능을 개선할 수 있다. 범주형으로 변환한 DataFrame의 칼럼은 메모리도 훨씬 적게 사용한다. 소수의 독립적인 카테고리로 분류되는 천만 개의 값을 포함하는 Series를 살펴보자.
N = 10000000
draws = pd.Series(np.random.randn(N))
labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))
labels를 categorical로 변환하자.
categories = labels.astype('category')
categories가 labels에 비해 훨씬 더 적은 메모리를 사용하는 것을 확인할 수 있다.
display(labels.memory_usage())
display(categories.memory_usage())
80000128
10000332
범주형으로 변환하는 과정이 그냥 이루어지는 것은 아니지만 이는 한 번만 변환하면 되는 일회성 비용이다.
%time _ = labels.astype('category')
CPU times: total: 594 ms Wall time: 571 ms
범주형에 대한 그룹 연산은 문자열 배열을 사용하는 대신 정수 기반의 코드 배열을 사용하는 알고리즘으로 동작하므로 훨씬 빠르게 동작한다.
Categorical 메서드
범주형 데이터를 담고 있는 Series는 특화된 문자열 메서드인 Series.str과 유사한 몇 가지 특수 메서드를 제공한다. 이를 통해 categories와 codes에 쉽게 접근할 수 있다. 다음 Series를 살펴보자.
s = pd.Series(['a', 'b', 'c', 'd'] * 2)
cat_s = s.astype('category')
cat_s
0 a 1 b 2 c 3 d 4 a 5 b 6 c 7 d dtype: category Categories (4, object): ['a', 'b', 'c', 'd']
특별한 속성인 cat을 통해 categorical 메서드에 접근할 수 있다.
cat_s.cat.codes
0 0 1 1 2 2 3 3 4 0 5 1 6 2 7 3 dtype: int8
cat_s.cat.categories
Index(['a', 'b', 'c', 'd'], dtype='object')
이 데이터의 실제 카테고리가 데이터에서 관측되는 4종류를 넘는 것을 이미 알고 있다고 가정하자. 이 경우 set_categories()
메서드를 이용해서 변경하는 것이 가능하다.
actual_categories = ['a', 'b', 'c', 'd', 'e']
cat_s2 = cat_s.cat.set_categories(actual_categories)
cat_s2
0 a 1 b 2 c 3 d 4 a 5 b 6 c 7 d dtype: category Categories (5, object): ['a', 'b', 'c', 'd', 'e']
데이터는 변함이 없지만 위에서 변경한 대로 새로운 카테고리가 추가되었다. 예를 들어 value_counts()
를 호출해보면 변경된 카테고리를 반영하고 있다.
cat_s.value_counts()
a 2 b 2 c 2 d 2 dtype: int64
cat_s2.value_counts()
a 2 b 2 c 2 d 2 e 0 dtype: int64
큰 데이터셋을 다룰 경우 categorical을 이용하면 메모리를 아끼고 성능도 개선할 수 있다. 분석 과정에서 큰 DataFrame이나 Series를 한 번 걸러내고 나면 실제로 데이터에는 존재하지 않는 카테고리가 남아 있을 수 있다. 이 경우 remove_unused_categories()
메서드를 이용해서 관측되지 않는 카테고리를 제거할 수 있다.
cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
cat_s3
0 a 1 b 4 a 5 b dtype: category Categories (4, object): ['a', 'b', 'c', 'd']
cat_s3.cat.remove_unused_categories()
0 a 1 b 4 a 5 b dtype: category Categories (2, object): ['a', 'b']
[표 12-1]에 categorical 메서드의 종류를 나열해두었다.
메서드 | 설명 |
---|---|
add_categories | 기존 카테고리 끝에 새로운 카테고리를 추가한다. |
as_ordered | 카테고리가 순서를 가지도록 한다. |
as_unordered | 카테고리가 순서를 가지지 않도록 한다. |
remove_categories | 카테고리를 제거한다. 해당 카테고리에 속한 값들은 null로 설정한다. |
remove_unused_categories | 데이터에서 관측되지 않은 카테고리를 삭제한다. |
rename_categories | 카테고리 이름을 지정한 이름으로 변경한다. 카테고리 수는 변하지 않는다. |
reorder_categories | rename_categories와 유사하지만 새로운 카테고리가 순서를 가지도록 한다. |
set_categories | 카테고리를 지정한 새로운 카테고리로 변경한다. 카테고리 추가나 삭제가 가능하다. |
모델링을 위한 더미값 생성하기
통계나 머신러닝 도구를 사용하다 보면 범주형 데이터를 더미값으로 변환(원핫one-hot 인코딩 이라고도 함)해야 하는 경우가 생긴다. 이를 위해 각각의 구별되는 카테고리를 칼럼으로 가지는 DataFrame을 생성하는데, 각 칼럼에는 해당 카테고리 여부에 따라 0과 1의 값을 가지게 된다.
앞서 살펴본 예제를 다시 살펴보자.
cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
cat_s
0 a 1 b 2 c 3 d 4 a 5 b 6 c 7 d dtype: category Categories (4, object): ['a', 'b', 'c', 'd']
7장에서 다루었듯이 pandas.get_dummies()
함수는 이런 1차원 범주형 데이터를 더미값을 가지는 DataFrame으로 변환한다.
pd.get_dummies(cat_s)
a | b | c | d | |
---|---|---|---|---|
0 | 1 | 0 | 0 | 0 |
1 | 0 | 1 | 0 | 0 |
2 | 0 | 0 | 1 | 0 |
3 | 0 | 0 | 0 | 1 |
4 | 1 | 0 | 0 | 0 |
5 | 0 | 1 | 0 | 0 |
6 | 0 | 0 | 1 | 0 |
7 | 0 | 0 | 0 | 1 |
고급 GroupBy 사용
10장에서 Series와 DataFrame에 대해 groupby()
메서드를 사용하는 방법을 이미 살펴봤지만 유용한 몇 가지 기법을 더 소개하겠다.
그룹 변환과 GroupBy 객체 풀어내기
10장에서는 그룹 연산에 apply()
메서드를 이용해서 DataFrame을 변환하는 방법을 살펴봤다. transform()
이라는 내장 메서드를 이용하면 apply()
메서드와 유사하게 동작하면서도 사용할 수 있는 함수의 종류에 대해 좀 더 많은 제한을 포함할 수 있다.
-
그룹 형태로 브로드캐스트할 수 있는 스칼라값을 생성해야 한다.
-
입력 그룹과 같은 형태의 객체를 반환해야 한다.
-
입력을 변경하지 않아야 한다.
설명을 위해 간단한 예제를 살펴보자.
df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,
'value': np.arange(12.)})
df
key | value | |
---|---|---|
0 | a | 0.0 |
1 | b | 1.0 |
2 | c | 2.0 |
3 | a | 3.0 |
4 | b | 4.0 |
5 | c | 5.0 |
6 | a | 6.0 |
7 | b | 7.0 |
8 | c | 8.0 |
9 | a | 9.0 |
10 | b | 10.0 |
11 | c | 11.0 |
key에 따른 그룹의 평균을 구해보자.
g = df.groupby('key').value
g.mean()
key a 4.5 b 5.5 c 6.5 Name: value, dtype: float64
df[‘value’]와 같은 형태의 Series를 원한 것이 아니라 ‘key’에 따른 그룹의 평균값으로 값을 변경하기 원했다고 가정한다면 transform()
에 람다 함수 lambda x: x.mean()을 넘기면 된다.
g.transform(lambda x: x.mean())
0 4.5 1 5.5 2 6.5 3 4.5 4 5.5 5 6.5 6 4.5 7 5.5 8 6.5 9 4.5 10 5.5 11 6.5 Name: value, dtype: float64
내장 요약함수에 대해서는 agg()
메서드에서처럼 문자열 그룹 연산 이름을 넘기면 된다.
g.transform('mean')
0 4.5 1 5.5 2 6.5 3 4.5 4 5.5 5 6.5 6 4.5 7 5.5 8 6.5 9 4.5 10 5.5 11 6.5 Name: value, dtype: float64
apply()
와 마찬가지로 transform()
은 Series를 반환하는 함수만 사용할 수 있지만 결과는 입력과 똑같은 크기여야 한다. 예를 들어 람다 함수를 이용해서 각 그룹에 모두 2를 곱할 수 있다.
g.transform(lambda x: x * 2)
0 0.0 1 2.0 2 4.0 3 6.0 4 8.0 5 10.0 6 12.0 7 14.0 8 16.0 9 18.0 10 20.0 11 22.0 Name: value, dtype: float64
좀 더 복잡한 예제로, 각 그룹에 대해 내림차순으로 순위를 계산할 수도 있다.
g.transform(lambda x: x.rank(ascending=False))
0 4.0 1 4.0 2 4.0 3 3.0 4 3.0 5 3.0 6 2.0 7 2.0 8 2.0 9 1.0 10 1.0 11 1.0 Name: value, dtype: float64
간단한 요약을 통해 그룹 변환을 수행하는 함수를 살펴보자.
def normalize(x):
return (x - x.mean()) / x.std()
이 경우에는 transform()
이나 apply()
를 이용해서 같은 결과를 얻을 수 있다.
g.transform(normalize)
0 -1.161895 1 -1.161895 2 -1.161895 3 -0.387298 4 -0.387298 5 -0.387298 6 0.387298 7 0.387298 8 0.387298 9 1.161895 10 1.161895 11 1.161895 Name: value, dtype: float64
g.apply(normalize)
0 -1.161895 1 -1.161895 2 -1.161895 3 -0.387298 4 -0.387298 5 -0.387298 6 0.387298 7 0.387298 8 0.387298 9 1.161895 10 1.161895 11 1.161895 Name: value, dtype: float64
mean이나 sum 같은 내장 요약함수는 일반적인 apply()
함수보다 더 빠르게 동작한다. 또한 이 함수들은 transform()
과 함께 사용하면 뒤로 되돌릴 수 있는데 이를 통해 그룹 연산을 풀어낼 수 있다.
g.transform('mean')
0 4.5 1 5.5 2 6.5 3 4.5 4 5.5 5 6.5 6 4.5 7 5.5 8 6.5 9 4.5 10 5.5 11 6.5 Name: value, dtype: float64
normalized = (df['value'] - g.transform('mean')) / g.transform('std')
normalized
0 -1.161895 1 -1.161895 2 -1.161895 3 -0.387298 4 -0.387298 5 -0.387298 6 0.387298 7 0.387298 8 0.387298 9 1.161895 10 1.161895 11 1.161895 Name: value, dtype: float64
org = normalized * g.transform('std') + g.transform('mean')
org
0 0.0 1 1.0 2 2.0 3 3.0 4 4.0 5 5.0 6 6.0 7 7.0 8 8.0 9 9.0 10 10.0 11 11.0 Name: value, dtype: float64
그룹 연산을 풀어내면 수차례의 그룹 연산을 수행하게 되지만 전체 백터 연산의 장점이 더 크다.
시계열 그룹 리샘플링
시계열 데이터에서 resample()
메서드는 의미적으로 시간 간격에 기반한 그룹 연산이다. 다음 에제를 살펴보자.
N = 15
times = pd.date_range('2017-05-20 00:00', freq='1min', periods=N)
df = pd.DataFrame({'time': times,
'value': np.arange(N)})
df
time | value | |
---|---|---|
0 | 2017-05-20 00:00:00 | 0 |
1 | 2017-05-20 00:01:00 | 1 |
2 | 2017-05-20 00:02:00 | 2 |
3 | 2017-05-20 00:03:00 | 3 |
4 | 2017-05-20 00:04:00 | 4 |
5 | 2017-05-20 00:05:00 | 5 |
6 | 2017-05-20 00:06:00 | 6 |
7 | 2017-05-20 00:07:00 | 7 |
8 | 2017-05-20 00:08:00 | 8 |
9 | 2017-05-20 00:09:00 | 9 |
10 | 2017-05-20 00:10:00 | 10 |
11 | 2017-05-20 00:11:00 | 11 |
12 | 2017-05-20 00:12:00 | 12 |
13 | 2017-05-20 00:13:00 | 13 |
14 | 2017-05-20 00:14:00 | 14 |
여기서 time으로 색인한 후 리샘플링해보자.
df.set_index('time').resample('5min').count()
value | |
---|---|
time | |
2017-05-20 00:00:00 | 5 |
2017-05-20 00:05:00 | 5 |
2017-05-20 00:10:00 | 5 |
key 칼럼으로 구분되는 여러 시계열 데이터를 담고 있는 DataFrame을 생각해보자.
df2 = pd.DataFrame({'time': times.repeat(3),
'key': np.tile(['a', 'b', 'c'], N),
'value': np.arange(N * 3.)})
df2[:7]
time | key | value | |
---|---|---|---|
0 | 2017-05-20 00:00:00 | a | 0.0 |
1 | 2017-05-20 00:00:00 | b | 1.0 |
2 | 2017-05-20 00:00:00 | c | 2.0 |
3 | 2017-05-20 00:01:00 | a | 3.0 |
4 | 2017-05-20 00:01:00 | b | 4.0 |
5 | 2017-05-20 00:01:00 | c | 5.0 |
6 | 2017-05-20 00:02:00 | a | 6.0 |
‘key’의 각 값에 대해 같은 리샘플링을 수행하기 위해서는 pandas.Grouper 객체를 이용한다.
time_key = pd.Grouper(freq='5min')
그리고 time을 색인으로 한 다음 ‘key’와 time_key로 그룹지어 합을 구해보자.
resampled = (df2.set_index('time')
.groupby(['key', time_key])
.sum())
resampled
value | ||
---|---|---|
key | time | |
a | 2017-05-20 00:00:00 | 30.0 |
2017-05-20 00:05:00 | 105.0 | |
2017-05-20 00:10:00 | 180.0 | |
b | 2017-05-20 00:00:00 | 35.0 |
2017-05-20 00:05:00 | 110.0 | |
2017-05-20 00:10:00 | 185.0 | |
c | 2017-05-20 00:00:00 | 40.0 |
2017-05-20 00:05:00 | 115.0 | |
2017-05-20 00:10:00 | 190.0 |
resampled.reset_index()
key | time | value | |
---|---|---|---|
0 | a | 2017-05-20 00:00:00 | 30.0 |
1 | a | 2017-05-20 00:05:00 | 105.0 |
2 | a | 2017-05-20 00:10:00 | 180.0 |
3 | b | 2017-05-20 00:00:00 | 35.0 |
4 | b | 2017-05-20 00:05:00 | 110.0 |
5 | b | 2017-05-20 00:10:00 | 185.0 |
6 | c | 2017-05-20 00:00:00 | 40.0 |
7 | c | 2017-05-20 00:05:00 | 115.0 |
8 | c | 2017-05-20 00:10:00 | 190.0 |
Grouper를 사용할 때 주의해야 할 점은 시간값이 Series 혹은 DataFrame의 색인이어야 한다는 점이다.
메서드 연결 기법
데이터셋을 여러 차례 변형해야 하는 경우 분석에는 전혀 필요 없는 임시 변수를 계속 생성하는 상황이 발생한다. 다음 예제를 살펴보자.
df = load_data()
df2 = df[df['col2'] < 0]
df2['col1_demeaned'] = df2['col1'] - df2['col1'].mean()
result = df2.groupby('key').col1_demeaned.std()
여기서 실제 데이터를 사용하지는 않지만 새로운 메서드 몇 가지를 만나게 되는에 그중 하나는 df[k] = v처럼 칼럼에 값을 대입하는 함수형 DataFrame.assing()
메서드다. 객체를 변형하는 대신 값 대입이 완료된 새로운 DataFrame을 반환한다. 아래 두 코드는 동일하다.
# 실용적이지 않은 방법
df2 = df.copy()
df2['k'] = v
# 실용적인 방법
df2 = df.assign(k=v)
값을 직접 대입하는 것이 assign()
을 사용하는 것보다 빠르게 수행되지만 assing()
을 이용하면 메서드를 연결해서 사용할 수 있다.
result = (df2.assing(col1_demeaned=df2.col1 - df2.col2.mean())
.groupby('key')
.col1_demeaned.std())
여기서는 줄바꿈을 편리하게 하기 위해 위 코드를 괄호로 둘러쌌다.
메서드를 연결해서 사용할 때 주의해야 할 점은 임시 객체를 참조해야 할 경우가 있을 수도 있다는 것이다. 앞선 예제에서 load_data의 반환값을 임시 변수인 df에 담기 전까지는 그 결과를 참조할 수 없었다. 이런 경우 assing()
이나 호출이 가능한 객체 또는 함수를 인자로 받는 pandas의 다른 함수를 이용해서 해결할 수 있다.
호출이 가능한 객체callable의 예시를 보기 위해 위 예제의 일부 코드를 다시 살펴보자.
df = load_data()
df2 = df[df['col2'] < 0]
위 코드는 다음과 같이 고쳐 쓸 수 있다.
df = (load_data()
[lambda x: x['col2'] < 0])
여기서 load_data의 결과를 변수에 저장하지 않았다. 그래서 []에 함수를 전달해서 메서드 연결이 이어지도록 했다.
계속해서 전체 코드를 하나의 메서드 연결 표현으로 작성할 수도 있다.
result = (load_data()
[lambda x: x.col2 < 0]
.assing(col1_demeaned=lambda x: x.col1 - x.col1.mean())
.groupby('key')
.col1_demeaned.std())
어떤 스타일을 선호하는지는 개인의 취향이지만 코드를 적당히 끊어서 사용하는 것은 가독성 향상에 도움이 된다.
pipe 메서드
pandas의 내장 함수와 방금 살펴본 메서드 연결을 통해 다양한 일을 할 수 있다. 하지만 직접 작성한 함수나 다른 서드파티 라이브러리의 함수를 사용해야 하는 경우도 생긴다. 이때 pipe()
메서드를 사용할 수 있다.
다음과 같은 일련의 함수 호출을 생각해보자.
a = f(df, arg1=v1)
b = g(a, v2, arg3=v3)
c = h(b, arg4=v4)
Series나 DataFrame 객체를 인자로 취하고 반환하는 함수를 사용하는 경우 위 코드를 pipe()
를 이용해서 아래처럼 고쳐 쓸 수 있다.
result = (df.pipe(f, arg1=v1)
.pipe(g, v2, arg3=v3)
.pipe(h, arg4=v4))
f(df)와 df.pipe(f)는 동일하다. 하지만 pipe()
는 메서드 연결을 좀 더 쉽게 쓸 수 있도록 해준다.
pipe()
를 이용한 유용한 패턴 중 하나는 일련의 연산을 재사용 가능한 함수로 일반화하는 것이다. 칼럼에서 그룹 평균을 빼는 과정을 생각해보자.
g = df.groupby(['key1', 'key2'])
df['col1'] = df['col1'] - g.transform('mean')
한 칼럼이 아니라 여러 칼럼에 대해 그룹 평균을 뺄 수 있고 그룹의 키를 쉽게 변경할 수 있기 바란다면, 또 이 작업을 메서드 연결로도 수행할 수 있기 바란다면 아래 구현 에제를 살펴보자.
def group_demean(df, by, cols):
result = df.copy()
g = df.groupby(by)
for c in cols:
result[c] = df[c] - g[c].transform('mean')
return result
이제 group_demean 함수를 사용해서 아래처럼 작성할 수 있다.
result = (df[df.col1 < 0]
.pipe(group_demean, ['key1', 'key2'], ['col1']))
마치며
pandas는 다른 많은 오픈소스 프로젝트와 마찬가지로 여전히 새로운 기능과 개선점을 수용하며 변하고 있다. 이 책은 추후 몇 년간 큰 변화가 없을 만한 안정적인 기능 위주로 소개하고 있다.
pandas의 고급 사용자가 되고 싶다면 pandas 공식 문서와 개발팀에서 작성한 새로운 기능에 대한 내용이 담긴 릴리스 노트를 읽어보기 권장한다. 또한 직접 pandas의 버그를 수정하고 새로운 기능을 구현하고 문서를 개선하는 pandas 개발에도 직접 참여하기 고대하겠다.
댓글남기기