자료 준비 및 손질

자료 분석 및 모델링 작업 시간 중 80% 이상이 자료 준비에 소모된다. 자료 준비란 읽어오기, 정제, 변환, 정렬등을 말한다. 파일 또는 데이터베이스 자료는 특정 작업을 하는데 적당한 형태가 아닌 경우가 많다. 많은 개발자들은 하나의 형태로부터 다른 형태의 자료로 변경하기 위해 파이썬, 자바, C 등 프로그래밍 언어를 사용하기도 한다. 판다스는 파이썬 내장 기능들을 포함하면서 고수준, 유연하고, 빠르게 적당한 형태로 변경을 할 수 있는 기능들을 제공한다.

소실 자료 다루기

자료 분석 과정에서 소실값은 흔하게 일어난다. 판다스는 소실값에 대해서 어려움을 당하지 않게 다루도록 하려고 하고 있다. 예를 들면 판다스는 기술 통계량을 구할 때 기본적으로 소실값을 제외하고 계산을 한다.

판다스는 소실값을 다루는데 있어서 완벽하지는 않다. 숫자 자료에서는 소실값을 나타내기 위해서 NaN(Not a Number)를 사용한다.

In [196]:
import pandas as pd
import numpy as np
In [197]:
 = pd.Series(['아보카도', '아리랑', np.nan, '아리아'])

Out[197]:
0    아보카도
1     아리랑
2     NaN
3     아리아
dtype: object
In [198]:
.isnull()
Out[198]:
0    False
1    False
2     True
3    False
dtype: bool

판다스에서도 R 프로그래밍 언어에서와 마찬가지로 소실값을 NA(Not Available)로 부른다. NA는 자료가 없는 상태 또는 자료값이 있더라도 적절치 않은 상태를 의미한다.

자료를 다듬을 때, 소실값으로 인해 자료가 왜곡되거나 소실값들이 어떤 것들인지를 알기위해서 소실값들을 분석하는 것은 중요한 문제이다. 파이썬 내장 객체인 None도 소실값으로 간주한다.

In [199]:
[0] = None
.isnull()
Out[199]:
0     True
1    False
2     True
3    False
dtype: bool

다음은 NA를 다루기위한 메소드들이다.

Argument Description
dropna Filter axis labels based on whether values for each label have missing data, with varying thresholds for how much missing data to tolerate.
fillna Fill in missing data with some value or using an interpolation method such as ‘ffill’ or ‘bfill’.
isnull Return boolean values indicating which values are missing/NA.
notnull Negation of isnull.

소실값 걸러내기

pandas.isnull 메소드와 논리 연산을 통한 소실값 제거하는 방법이 있지만 dropna 메소드도 도움이 된다. 시리즈에서는 소실값들이 들어간 자료를 제외한 자료들을 반환한다.

In [200]:
 = pd.Series([1.3, np.nan, 9, -20, np.nan])

Out[200]:
0     1.3
1     NaN
2     9.0
3   -20.0
4     NaN
dtype: float64
In [201]:
.dropna()
Out[201]:
0     1.3
2     9.0
3   -20.0
dtype: float64

이것은 다음과 같이도 얻을 수 있다.

In [202]:
[.notnull()]
Out[202]:
0     1.3
2     9.0
3   -20.0
dtype: float64

데이터프레임의 경우는 약간 더 복잡하다. NA를 하나라도 포함한 행 또는 열을 모두 제거할 수도 있고 모든 자료가 NA로만 이루어진 행을 제거할 수도 있다. 기본적으로 dropna는 NA를 하나라도 포함한 행이 있으면 모두 제거한다.

In [203]:
 = pd.DataFrame([[1., 3.5, 2.2], [1., np.nan, np.nan], [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.0]])

Out[203]:
0 1 2
0 1.0 3.5 2.2
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
In [204]:
.dropna()
Out[204]:
0 1 2
0 1.0 3.5 2.2

how='all' 인자를 건네줌으로 모든 값이 NA로 이루어진 행만 제거할 수 있다.

In [205]:
.dropna(how='all')
Out[205]:
0 1 2
0 1.0 3.5 2.2
1 1.0 NaN NaN
3 NaN 6.5 3.0

열에 대해서 같은 작업을 하려면 axis=1을 인자로 넘기면 된다.

In [206]:
[3] = np.nan

Out[206]:
0 1 2 3
0 1.0 3.5 2.2 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
In [207]:
.dropna(axis=1, how='all')
Out[207]:
0 1 2
0 1.0 3.5 2.2
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0

thresh= 인자를 사용해서 원하는 갯수의 NA들을 포함하는 행들만 삭제할 수 있다.

In [208]:
 = pd.DataFrame(np.random.randn(7, 3))
.iloc[:4, 1] = np.nan
.iloc[:2, 2] = np.nan

Out[208]:
0 1 2
0 0.895511 NaN NaN
1 -0.484012 NaN NaN
2 -0.066663 NaN -0.791498
3 0.023971 NaN 1.020213
4 1.346790 0.257321 0.608375
5 1.199808 -0.483835 -0.159827
6 -0.168269 0.771990 0.245259

thresh=2를 이용해서 NaN이 2이상인 행들을 모두 제거한다.

In [209]:
.dropna(thresh=2)
Out[209]:
0 1 2
2 -0.066663 NaN -0.791498
3 0.023971 NaN 1.020213
4 1.346790 0.257321 0.608375
5 1.199808 -0.483835 -0.159827
6 -0.168269 0.771990 0.245259

소실값 채우기

소실값을 포함하는 행, 열을 제거하는 대신 소실값을 적당한 값으로 채워넣는 것이 필요할 때가 있다. fillna 메소드를 이용하면 소실값을 채울 수 있다. 먼저 스칼라값을 대입하면 모든 소실값이 스칼라 값으로 대체된다.

In [210]:
.fillna(0)
Out[210]:
0 1 2
0 0.895511 0.000000 0.000000
1 -0.484012 0.000000 0.000000
2 -0.066663 0.000000 -0.791498
3 0.023971 0.000000 1.020213
4 1.346790 0.257321 0.608375
5 1.199808 -0.483835 -0.159827
6 -0.168269 0.771990 0.245259

열별로 다른 값으로 소실값을 대체하고 싶으면 사전형을 대입하면 된다.

In [211]:
.fillna({1: 0, 2: 0.5})
Out[211]:
0 1 2
0 0.895511 0.000000 0.500000
1 -0.484012 0.000000 0.500000
2 -0.066663 0.000000 -0.791498
3 0.023971 0.000000 1.020213
4 1.346790 0.257321 0.608375
5 1.199808 -0.483835 -0.159827
6 -0.168269 0.771990 0.245259

fillna는 기본적으로 새로운 객체를 반환한다. inplace=True를 이용하면 자신의 값을 변경한다.

In [212]:
.fillna({1: 0, 2: 0.5}, inplace=True)

Out[212]:
0 1 2
0 0.895511 0.000000 0.500000
1 -0.484012 0.000000 0.500000
2 -0.066663 0.000000 -0.791498
3 0.023971 0.000000 1.020213
4 1.346790 0.257321 0.608375
5 1.199808 -0.483835 -0.159827
6 -0.168269 0.771990 0.245259

재인덱싱에서 사용되었던 보간법들을 여기서도 사용할 수 있다.

In [213]:
 = pd.DataFrame(np.random.randn(6, 3))
.iloc[2:, 1] = np.nan
.iloc[4:, 2] = np.nan

Out[213]:
0 1 2
0 0.828091 -1.068962 -1.113197
1 -0.033009 -0.339193 -0.334388
2 1.601875 NaN -1.205323
3 0.373438 NaN 0.095143
4 -0.035278 NaN NaN
5 -0.941367 NaN NaN
In [214]:
.fillna(method='ffill')
Out[214]:
0 1 2
0 0.828091 -1.068962 -1.113197
1 -0.033009 -0.339193 -0.334388
2 1.601875 -0.339193 -1.205323
3 0.373438 -0.339193 0.095143
4 -0.035278 -0.339193 0.095143
5 -0.941367 -0.339193 0.095143
In [215]:
.fillna(method='ffill', limit=2)
Out[215]:
0 1 2
0 0.828091 -1.068962 -1.113197
1 -0.033009 -0.339193 -0.334388
2 1.601875 -0.339193 -1.205323
3 0.373438 -0.339193 0.095143
4 -0.035278 NaN 0.095143
5 -0.941367 NaN 0.095143

다른 여러 가지 값들로 소실값들을 대체할 수 있다. 다음과 같이 시리즈의 평균 또는 중앙값으로 소실값을 대체할 수 있다.

In [216]:
 = pd.Series([np.nan, -1.5, np.nan, 3, 2.54])

Out[216]:
0     NaN
1   -1.50
2     NaN
3    3.00
4    2.54
dtype: float64
In [217]:
.fillna(.mean())
Out[217]:
0    1.346667
1   -1.500000
2    1.346667
3    3.000000
4    2.540000
dtype: float64
In [218]:
.fillna(.median())
Out[218]:
0    2.54
1   -1.50
2    2.54
3    3.00
4    2.54
dtype: float64

다음 표는 fillna 메소드 인자들이다.

Argument Description
value Scalar value or dict-like object to use to fill missing values
method Interpolation; by default ‘ffill’ if function called with no other arguments
axis Axis to fill on; default axis=0
inplace Modify the calling object without producing a copy
limit For forward and backward filling, maximum number of consecutive periods to fill

자료 변환

중복 제거

중복된 행을 갖는 자료는 종종 발생한다. 다음은 마지막 두 행의 자료가 중복된다.

In [219]:
 = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                 'k2': [1, 1, 2, 3, 3, 4, 4]})

Out[219]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4

duplicated 메소드는 앞 행에 중복된 행이 있으면 True를 그렇지 않으면 False를 반환한다.

In [220]:
.duplicated()
Out[220]:
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

직접하기

  • 중복된 행들만 찾아라.

drop_duplicates 메소드는 duplicated 메소드의 반환값이 False인 행들을 반환한다.

In [221]:
.drop_duplicates()
Out[221]:
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4

위 두 메소드들은 기본적으로 모든 열에 대해서 중복된 행을 찾는다. 지정된 열에 대해서만 중복을 확인하고 싶으면 열 이름에 대한 리스트를 인자로 넘기면 된다.

In [222]:
['v1'] = range(7)

Out[222]:
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
5 two 4 5
6 two 4 6
In [223]:
.drop_duplicates(['k1'])
Out[223]:
k1 k2 v1
0 one 1 0
1 two 1 1

두 메소드들은 기본적으로 중복된 행 중에서 첫번째로 나오는 행만 유지한다. 마지막 행을 유지하고 싶으면 keep='last'를 인자로 넘긴다.

In [224]:
.drop_duplicates(['k1', 'k2'], keep='last')
Out[224]:
k1 k2 v1
0 one 1 0
1 two 1 1
2 one 2 2
3 two 3 3
4 one 3 4
6 two 4 6

함수 또는 대응을 이용한 자료 변환

시리즈, 배열 또는 데이터프레임의 열의 값에 기초한 자료 변환들을 하기를 원하는 경우가 있다. 다음과 같은 가상 자료를 생각해보자.

In [225]:
 = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 'corned beef', 'Bacon', 'pastrami', 'honey ham', 'nova lox'],
                  'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})

Out[225]:
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 corned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0

각 재료들이 만들어진 동물들에 대한 열을 추가하고 싶다고 하자. 각 고기에 대응되는 동물들에 대한 사전을 생각하자.

In [226]:
meat_to_animal = {
  'bacon': 'pig',
  'pulled pork': 'pig',
  'pastrami': 'cow',
  'corned beef': 'cow',
  'honey ham': 'pig',
  'nova lox': 'salmon'
}

시리즈의 map 메소드는 인자를 사전형을 받아 시리즈의 각 값이 인자로 넘겨진 사전형의 키에 대응되는 값을 반환한다. 하지만 이름이 대/소문자로 표기된 것이 있기 때문에 시리즈의 str.lower 메소드를 이용해 모두 소문자로 변경해서 처리하자.

기본적으로 map은 호출자의 값들을 인자로 넘겨서 각 값에 해당하는 함수를 처리한다.

호출자.map(함수)

함수 대신에 사전형이나 시리즈가 대입되면 인자로 넘겨진 호출자의 값이 인덱스로 간주해서 인덱스에 대응되는 값을 반환한다.

In [227]:
lowered_case = ['food'].str.lower()
lowered_case
Out[227]:
0          bacon
1    pulled pork
2          bacon
3       pastrami
4    corned beef
5          bacon
6       pastrami
7      honey ham
8       nova lox
Name: food, dtype: object
In [228]:
['animal'] = lowered_case.map(meat_to_animal)

Out[228]:
food ounces animal
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 Pastrami 6.0 cow
4 corned beef 7.5 cow
5 Bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon

map 메소드 인자로 함수를 대입해도 된다.

In [229]:
['food'].map(lambda x : meat_to_animal[x.lower()])
Out[229]:
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object

값 대체하기

fillna 메소드는 값을 대체하는 특수한 메소드이다. map도 특정한 열에 대한 값들을 대체하는 방법이다. replace 메소드는 값을 대체하는데 유연하게 사용될 수 있다.

In [230]:
 = pd.Series([1., -999, 2, -999, -1000, -3])

Out[230]:
0       1.0
1    -999.0
2       2.0
3    -999.0
4   -1000.0
5      -3.0
dtype: float64

-999.0은 소실값으로 대체하기 위해서 replace 메소드를 사용한다.

In [231]:
.replace(-999, np.nan)
Out[231]:
0       1.0
1       NaN
2       2.0
3       NaN
4   -1000.0
5      -3.0
dtype: float64

여러 값을 한꺼번에 대체하고 싶으면 리스트를 사용한다.

In [232]:
.replace([-999, -1000], np.nan)
Out[232]:
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5   -3.0
dtype: float64

서로 다른 값으로 대체하기 위해서는 대응되는 리스트를 사용한다.

In [233]:
.replace([-999, -1000], [np.nan, 0])
Out[233]:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5   -3.0
dtype: float64

사전형 인자를 사용해도 된다.

In [234]:
.replace({-999: np.nan, -1000: 0})
Out[234]:
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5   -3.0
dtype: float64

축 인덱스 이름 변경하기

시리즈 값을 변경하듯이 축이름도 함수 또는 사상(mapping)에 의해 새로운 이름으로 변경할 수 있다. 다음 예를 보자.

In [235]:
 = pd.DataFrame(np.arange(12).reshape(3, 4), index=['Ohio', 'Colorado', 'New York'], columns=['one', 'two', 'three', 'four'])

Out[235]:
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11

시리즈와 같이 축 인덱스도 map 메소드를 가지고 있다.

In [236]:
 = lambda x: x[:4].upper()

.index.map()
Out[236]:
Index(['OHIO', 'COLO', 'NEW '], dtype='object')

데이터프레임의 index에 값을 할당해서 인덱스 이름을 변경할 수 있다.

In [237]:
.index = .index.map()

Out[237]:
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

rename 메소드를 이용하면 더 유연하게 이름을 변경할 수 있다.

In [238]:
.rename(index=str.title, columns=str.upper)
Out[238]:
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11

rename은 사전형 객체 인자를 받아서 지정된 이름들만 변경할 수 있다.

In [239]:
.rename(index={'OHIO': 'INDIANA'}, columns={'three': 'peekaboo'})
Out[239]:
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

inplace=True를 건네줌으로 새로운 데이터프레임을 만들지 않고 내부적으로 이름을 변경할 수 있다.

In [240]:
.rename(index={'OHIO': 'INDIANA'}, inplace=True)

Out[240]:
one two three four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

구간으로 나누기

연속인 자료들은 분석을 위해 구간으로 나눠 계산될 필요가 있다. 다음과 같이 사람들의 나이 자료를 연령대별로 나누자.

In [241]:
나이 = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

18 ~ 25, 26 ~ 35, 36 ~ 60 및 60세보다 많은 연령대별로 나눈다. 이렇게 하기위해서는 판다스의 cut 함수를 사용하면 된다.

In [242]:
구간 = [18, 25, 35, 60, 100]
연령대 = pd.cut(나이, 구간)
연령대
Out[242]:
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

pd.cut에 의해서 반환된 값은 판다스 범주형(categorical) 객체이며 구간을 나타내는 문자열처럼 다룰 수 있다. 반환값은 codes 속성을 통해서 나이에 대응되는 범주형 정수값을 알 수 있으며 categories 속성을 통해 범주형 문자열 리스트를 알 수 있다.

In [243]:
연령대.categories
Out[243]:
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
              closed='right',
              dtype='interval[int64]')
In [244]:
연령대.codes
Out[244]:
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)

value_counts 메소드를 이용해서 연령대별 갯수를 셀 수 있다.

In [245]:
연령대.value_counts()
Out[245]:
(18, 25]     5
(25, 35]     3
(35, 60]     3
(60, 100]    1
dtype: int64

구간의 오른쪽 끝 값이 포함되지 않게 하려면 right=False를 이용하면 된다.

In [246]:
pd.cut(나이, 구간, right=False)
Out[246]:
[[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)]
Length: 12
Categories (4, interval[int64]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]

선택 인자 labels=를 이용해 구간 이름을 지정할 수 있다.

In [247]:
구간이름 = ['청년', '청장년', '중년', '노년']
pd.cut(나이, 구간, labels=구간이름)
Out[247]:
[청년, 청년, 청년, 청장년, 청년, ..., 청장년, 노년, 중년, 중년, 청장년]
Length: 12
Categories (4, object): [청년 < 청장년 < 중년 < 노년]

구간 대신에 정수를 입력하면 같은 크기의 구간으로 나눈다.

In [248]:
rng = np.random.RandomState(0)
data = rng.randn(20)
In [249]:
pd.cut(data, 4, precision=2)
Out[249]:
[(1.44, 2.24], (-0.17, 0.63], (0.63, 1.44], (1.44, 2.24], (1.44, 2.24], ..., (-0.17, 0.63], (1.44, 2.24], (-0.98, -0.17], (-0.17, 0.63], (-0.98, -0.17]]
Length: 20
Categories (4, interval[float64]): [(-0.98, -0.17] < (-0.17, 0.63] < (0.63, 1.44] < (1.44, 2.24]]

선택인자 precision=은 소수점 자릿수를 제한할 수 있다.

백분위수를 이용해서도 구간을 나눌 수 있다. pd.qcut을 이용하면 cut에서와 비슷하게 사용할 수 있다.

In [250]:
data = rng.randn(1000)

cats = pd.qcut(data, 4)
cats
Out[250]:
[(-3.0469999999999997, -0.708], (0.582, 2.759], (0.582, 2.759], (-3.0469999999999997, -0.708], (0.582, 2.759], ..., (-3.0469999999999997, -0.708], (-0.0601, 0.582], (-3.0469999999999997, -0.708], (-0.0601, 0.582], (-0.0601, 0.582]]
Length: 1000
Categories (4, interval[float64]): [(-3.0469999999999997, -0.708] < (-0.708, -0.0601] < (-0.0601, 0.582] < (0.582, 2.759]]

qcut은 백분위수를 이용해서 나누기 때문에 대략 같은 갯수의 구간들로 나누는 것을 볼 수 있다.

In [251]:
pd.value_counts(cats)
Out[251]:
(0.582, 2.759]                   250
(-0.0601, 0.582]                 250
(-0.708, -0.0601]                250
(-3.0469999999999997, -0.708]    250
dtype: int64

다음과 같이 직접 백분위수를 입력해서 구할 수 있다.

In [252]:
pd.qcut(data, [0, 0.1, 0.3, 0.9, 1.])
Out[252]:
[(-3.0469999999999997, -1.307], (-0.559, 1.212], (-0.559, 1.212], (-1.307, -0.559], (1.212, 2.759], ..., (-1.307, -0.559], (-0.559, 1.212], (-3.0469999999999997, -1.307], (-0.559, 1.212], (-0.559, 1.212]]
Length: 1000
Categories (4, interval[float64]): [(-3.0469999999999997, -1.307] < (-1.307, -0.559] < (-0.559, 1.212] < (1.212, 2.759]]

이상치(outlier) 찾아 걸러내기

주어진 자료에서 이상치를 찾아 걸러내는일은 중요한 부분중의 하나이다. 다음과 같은 1000 x 4 크기의 정규분포 자료를 생각하자.

In [253]:
data = pd.DataFrame(rng.randn(1000, 4))
data.describe()
Out[253]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.036400 0.011587 -0.003675 -0.002479
std 0.992339 0.968516 0.994501 0.988656
min -3.740101 -3.007437 -3.116857 -3.392300
25% -0.760730 -0.656073 -0.671151 -0.646979
50% 0.015750 -0.012295 0.003705 -0.063378
75% 0.643699 0.656041 0.684573 0.646954
max 2.929096 2.979976 3.801660 3.427539

절대값이 3보다 큰 값들을 골라내야 한다고 가정해보자. 절대값이 3보다 큰 값을 포함하는 행들을 찾아내기 위해서 any 메소드를 사용할 수 있다.

In [254]:
data[(np.abs(data) > 3).any(1)]
Out[254]:
0 1 2 3
8 -0.156024 1.049093 3.170975 0.189500
249 0.708860 0.422819 -3.116857 0.644452
515 -0.387313 -0.347585 3.306574 -1.510200
524 -1.091033 -0.126856 3.801660 2.315171
606 0.236225 -0.752582 0.045113 3.427539
610 -0.087328 -0.553965 -3.006499 -0.047166
664 -0.953179 -0.479297 -1.345508 -3.392300
683 -3.740101 0.973577 1.175155 -1.124703
862 0.903088 -3.007437 -2.330467 -0.567803

절대값이 3보다 큰 값들을 부호에 맞춰서 일정값으로 지정하려면 다음과 같이 할 수 있다.

In [255]:
data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()
Out[255]:
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.035660 0.011594 -0.004831 -0.002515
std 0.989846 0.968493 0.989858 0.985992
min -3.000000 -3.000000 -3.000000 -3.000000
25% -0.760730 -0.656073 -0.671151 -0.646979
50% 0.015750 -0.012295 0.003705 -0.063378
75% 0.643699 0.656041 0.684573 0.646954
max 2.929096 2.979976 3.000000 3.000000

위 표에서 보는 바와같이 최대값, 최소값이 각각 -3, 3을 넘지 않는 것을 알 수 있다.

무작위 추출및 순열

시리즈 또는 데이터프레임의 행을 무작위로 순서있게 나열하는 것을 numpy.random.permutation을 이용할 수 있다. 축 크기에 해당되는 permutation 함수를 호출하면 무작위 순열이 반환된다.

In [256]:
df = pd.DataFrame(np.arange(5 * 4). reshape(5, 4))
sampler = rng.permutation(5)
sampler
Out[256]:
array([1, 0, 3, 2, 4])

iloc 또는 take 함수를 이용해서 순열에 대응되도록 배열의 행들을 배치할 수 있다.

In [257]:
df
Out[257]:
0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19
In [258]:
df.iloc[sampler]
Out[258]:
0 1 2 3
1 4 5 6 7
0 0 1 2 3
3 12 13 14 15
2 8 9 10 11
4 16 17 18 19
In [259]:
df.take(sampler)
Out[259]:
0 1 2 3
1 4 5 6 7
0 0 1 2 3
3 12 13 14 15
2 8 9 10 11
4 16 17 18 19

시리즈 또는 데이터프레임으로부터 중복없이 무작위 추출을 하기위해서는 sample 메소드를 사용할 수 있다.

In [260]:
df.sample(3)
Out[260]:
0 1 2 3
2 8 9 10 11
0 0 1 2 3
1 4 5 6 7

중복을 허락해서 추출하려면 replace=True 인자를 사용하면 된다.

In [261]:
s1 = pd.Series([7, 3, -2, 1, 4])
s1.sample(n=10, replace=True)
Out[261]:
4    4
1    3
4    4
0    7
3    1
2   -2
2   -2
2   -2
2   -2
4    4
dtype: int64

Computing Indicator/Dummy Variables

기계학습이나 통계적 모델링에서 사용되는 변환 중에는 범주형 변수를 지표 또는 더미(dummy) 변수로 나타낼 필요가 있다. 이것을 one-hot encoding이라고도 한다. 데이터프레임의 한 열의 값들이 k 개의 서로 다른 값을 갖는다면 각 값을 열이름으로 갖고 행은 그 값이 포함되면 1, 안되면 0으로 표현된 배열을 만들려고 한다. 이러한 기능을 get_dummies 함수를 이용해서 얻을 수 있다.

In [262]:
df = pd.DataFrame({'keys': ['b', 'b', 'a', 'c', 'a', 'b'],
                  'data1': range(6)})
df
Out[262]:
data1 keys
0 0 b
1 1 b
2 2 a
3 3 c
4 4 a
5 5 b
In [263]:
pd.get_dummies(df['keys'])
Out[263]:
a b c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0

‘keys``열은``’a’,’b’,’c’ 이루어져 있고 첫번째 행은’b’이므로b열만 1이고 나머지는 0이되는 것을 있다.get_dummies함수에prefix=` 인자를 이용해서 열이름 앞에 덧붙일 수 있다.

In [264]:
pd.get_dummies(df['keys'], prefix='key')
Out[264]:
key_a key_b key_c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0

데이터프레임의 열이 여러 개의 범주형 값을 포함하면 복잡해지기 시작한다. 다음과 같이 영화 자료를 살펴보자.

In [265]:
movies = pd.read_table('http://compmath.korea.ac.kr/appmath/data/movies.dat', encoding='utf-8', sep='::', header=None, names=['id', 'title', 'genre'], engine='python')
pd.set_option('display.max_rows', 10)
movies.head()
Out[265]:
id title genre
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy

genre 열을 보면 | 구분자를 이용해서 여러 개의 장르가 속해있는 것을 볼 수 있다. 우선 모든 장르를 구해보자.

In [266]:
all_genres = []
for x in movies.genre:
    all_genres.extend(x.split('|'))

genres = pd.unique(all_genres)
genres
Out[266]:
array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
       'Romance', 'Drama', 'Action', 'Crime', 'Thriller', 'Horror',
       'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
       'Western'], dtype=object)

지표 데이터프레임을 만들어 보자.

In [267]:
dummies = pd.DataFrame(np.zeros((len(movies), len(genres))), columns=genres)

각 영화에 대응되는 행을 돌면서 장르에 맞으면 1을 설정하자. 이것을 위해서 dummies.columns.get_indexer 메소드를 이용한다. 이 메소드는 컬럼 이름을 인자로 받으면 거기에 해당하는 인덱스 번호를 반환한다.

In [268]:
for i, gen in enumerate(movies.genre):
    indices = dummies.columns.get_indexer(gen.split('|'))
    dummies.iloc[i, indices] = 1
In [269]:
dummies.iloc[0]
Out[269]:
Animation     1.0
Children's    1.0
Comedy        1.0
Adventure     0.0
Fantasy       0.0
             ...
War           0.0
Musical       0.0
Mystery       0.0
Film-Noir     0.0
Western       0.0
Name: 0, Length: 18, dtype: float64

cutget_dummies를 결합해서 사용하면 편리하다.

In [270]:
values = rng.rand(10)
values
Out[270]:
array([0.34732627, 0.50963835, 0.06663242, 0.09009125, 0.18575988,
       0.26186792, 0.51859327, 0.53516714, 0.76301263, 0.45040548])
In [271]:
bins = [ 0., 0.2, 0.4, 0.6, 0.8, 1.]

pd.get_dummies(pd.cut(values, bins))
Out[271]:
(0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0]
0 0 1 0 0 0
1 0 0 1 0 0
2 1 0 0 0 0
3 1 0 0 0 0
4 1 0 0 0 0
5 0 1 0 0 0
6 0 0 1 0 0
7 0 0 1 0 0
8 0 0 0 1 0
9 0 0 1 0 0

문자열 처리

문자열 객체 메소드

대부분의 문자열 관련 처리는 내장 문자열 객체로 충분하다. 다음과 같이 쉼표로 구분된 문자열은 split 메소드를 이용해서 간단히 리스트로 변환된다.

In [272]:
val = 'a,b,   guido'
val.split(',')
Out[272]:
['a', 'b', '   guido']

strip과 결합해서 공백 문자들을 없앨 수 있다.

In [273]:
pieces = [x.strip() for x in val.split(',')]
pieces
Out[273]:
['a', 'b', 'guido']

join 메소드를 이용해서 문자열 리스트를 하나로 합칠 수 있다.

In [274]:
"::".join(pieces)
Out[274]:
'a::b::guido'

부분문자열을 포함하고 있는지를 in 메소드를 이용해서 알 수 있고, findindex를 이용해서 몇 번째에 위치한지를 알 수 있다.

In [275]:
'guido' in val
Out[275]:
True
In [276]:
val.index(',')
Out[276]:
1
In [277]:
val.find(':')
Out[277]:
-1

findindex의 차이는 find는 부분문자열이 없으면 오류를 발생시키고 index-1을 반환한다는 것이다.

replace는 문자열을 다른 문자열로 바꿀 수 있다.

In [278]:
val.replace(',', '')
Out[278]:
'ab   guido'

다음은 자주 쓰이는 문자열 메소드들이다.

Argument Description
count Return the number of non-overlapping occurrences of substring in the string.
endswith Returns True if string ends with suffix.
startswith Returns True if string starts with prefix.
join Use string as delimiter for concatenating a sequence of other strings.
index Return position of first character in substring if found in the string; raises ValueError if not found.
find Return position of first character of first occurrence of substring in the string; like index, but returns –1 if not found.
rfind Return position of first character of last occurrence of substring in the string; returns –1 if not found.
replace Replace occurrences of string with another string.
strip, rstrip, lstrip Trim whitespace, including newlines; equivalent to x.strip() (and rstrip, lstrip, respectively) for each element.
split Break string into list of substrings using passed delimiter.
lower Convert alphabet characters to lowercase.
upper Convert alphabet characters to uppercase.
casefold Convert characters to lowercase, and convert any region-specific variable character combinations to a common comparable form.
ljust, rjust Left justify or right justify, respectively; pad opposite side of string with spaces (or some other fill character) to return a string with a minimum width.

정규 표현식

정규표현식은 텍스트에서 주어진 유형에 맞는 문자열을 찾는 강력한 방법들을 가진다. [정규표현식](https://docs.python.org/3/library/re.html#regular-expression-syntax}이란 정규 표현식 문법에 의해 작성된 문자열이다. 파이썬 모듈 re를 이용해 정규표현식을 다룰 수 있다. 여기서는 몇 가지 예를 들 것이다.

re 모듈은 유형과 일치하는 것 찾기, 유형과 일치하는 것을 다른 문자열로 대체, 유형에 따라 문자열을 분리하는 기능들을 수행한다. 예를 들어 주어진 텍스트에서 다양한 공백 문자(탭, 스페이스, 엔터)들에 대해서 분리하고자 한다고 하자. 정규표현식은 \s+가 모든 공백 문자들을 대표하는 문자열이다.

In [279]:
import re

text = "foo    bar\t baz  \tqux"
In [280]:
re.split('\s+', text)
Out[280]:
['foo', 'bar', 'baz', 'qux']

다음과 같이 정규표현식을 먼저 컴파일한 후 그것을 이용해서 원하는 작업을 반복해서 사용할 수도 있다.

In [281]:
regex = re.compile('\s+')

regex.split(text)
Out[281]:
['foo', 'bar', 'baz', 'qux']

컴파일된 정규표현식을 이용해서 유형에 맞는 문자열들 모두 찾아낼 수도 있다.

In [282]:
regex.findall(text)
Out[282]:
['    ', '\t ', '  \t']

정규표현식에서 문자열에서와 같이 역슬래시 \는 탈출문자 역할을 하기 때문에 역슬래시를 일반문자같이 사용하려면 r'c:\temp'와 같이 r을 붙여 사용한다.

정규표현식의 주요 메소드로는 match, search, findall 이 있다. matchsearch는 모두 정규표현식과 일치하는 것을 찾아 `match object <https://docs.python.org/3/library/re.html#match-objects>`__를 반환하지만 차이점은 match는 텍스트의 첫문자로부터 유형을 찾고 search는 그렇지 않고 모든 부분에서 찾는다.

예를 들어 다음과 같이 match를 이용해서 공백문자를 찾으면 아무것도 찾지 못한다. 왜냐면 text의 첫문자가 공백문자가 아니기 때문이다.

In [283]:
re.match('\s+', text)

하지만 search는 찾는 것을 알 수 있다. search는 주어진 유형과 일치하는 첫번째 문자열만 반환한다.

In [284]:
re.search('\s+', text)
Out[284]:
<_sre.SRE_Match object; span=(3, 7), match='    '>

findall은 유형에 맞는 모든 문자열을 리스트형으로 반환한다. 이메일을 포함한 텍스트에서 이메일 주소를 찾는 예를 살펴보자.

In [285]:
text = """Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com
"""
In [286]:
pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
In [287]:
regex = re.compile(pattern, flags=re.IGNORECASE)
In [288]:
regex.findall(text)
Out[288]:
['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']

search는 반환값으로 match 객체를 반환해서 다음과 같이 메소드 start, end를 이용할 수 있다.

In [289]:
m = regex.search(text)
m
Out[289]:
<_sre.SRE_Match object; span=(5, 20), match='dave@google.com'>
In [290]:
text[m.start():m.end()]
Out[290]:
'dave@google.com'

sub 메소드는 텍스트에서 유형과 일치하는 문자열을 대체 문자열로 바꾼 텍스트를 반환한다.

In [291]:
regex.sub('이메일필요', text)
Out[291]:
'Dave 이메일필요\nSteve 이메일필요\nRob 이메일필요\nRyan 이메일필요\n'

찾은 이메일 문자열 중에서 사용자 이름, 하위 도메인 이름, 최상위 도메인 이름으로 구분해서 사용하고 싶을 때 소괄호를 이용할 수 있다.

In [292]:
pattern_g = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'

소괄호 하나 하나를 그룹이라고 부른다. 여기서는 3개의 그룹으로 나눈 것이다.

In [293]:
regex_g = re.compile(pattern_g, flags=re.IGNORECASE)

search 메소드가 반환하는 match 객체는 groups() 메소드를 이용해서 튜플 형태의 값을 반환받는다.

In [297]:
m_g = regex_g.search(text)

m_g.groups()
Out[297]:
('dave', 'google', 'com')

group 메소드를 이용해서 각각의 성분을 접근할 수 있다. group()group(0)와 같은 것으로 유형에 일치하는 문자열을 반환한다.

In [298]:
m_g.group()
Out[298]:
'dave@google.com'

group(숫자)숫자에 해당되는 문자열을 반환한다.

In [299]:
m_g.group(1)
Out[299]:
'dave'
In [300]:
m_g.group(2)
Out[300]:
'google'
In [301]:
m_g.group(3)
Out[301]:
'com'

findall은 그룹을 성분으로 하는 튜플 리스트를 반환한다.

In [294]:
regex_g.findall(text)
Out[294]:
[('dave', 'google', 'com'),
 ('steve', 'gmail', 'com'),
 ('rob', 'gmail', 'com'),
 ('ryan', 'yahoo', 'com')]

다음은 정규표현식 메소드들이다.

Argument Description
findall Return all non-overlapping matching patterns in a string as a list
finditer Like findall, but returns an iterator
match Match pattern at start of string and optionally segment pattern components into groups; if the pattern matches, returns a match object, and otherwise None
search Scan string for match to pattern; returning a match object if so; unlike match, the match can be anywhere in the string as opposed to only at the beginning
split Break string into pieces at each occurrence of pattern
sub, subn Replace all (sub) or first n occurrences (subn) of pattern in string with replacement expression; use symbols 1, 2, … to refer to match group elements in the replacement string

직접하기

  • http://sejong.korea.ac.kr 웹페이지를 requests.get 메소드를 이용해서 읽어 온 후, 그 문서 중에서 인스타가 포함된 줄을 모두 출력하는 코드를 작성하시오.(참고: Response.text를 이용해서 텍스트 문서로 저장한다.)
  • 인스타몬스타로 바꾸시오.

판다스 벡터 문자열 함수

판다스 시리즈(Series)와 인덱스(Index) 객체는 배열의 각 성분에 일괄적으로 적용할 수 있는 함수들을 갖추고 있다. 이 함수들은 소실값 NA들을 자동적으로 제외할 수 있는 기능을 갖추고 있으며 str 속성을 통해 접근하면 된다. str 속성은 파이썬 문자열 객체 str과 동일한 이름을 가지며 기능도 거의 같다. 단 판다스 str은 배열 각 성분에 적용된다는 점이 다른다.

In [303]:
data = pd.Series({'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com', 'Rob': 'rob@gmail.com', 'Wes': np.nan})

data
Out[303]:
Dave     dave@google.com
Rob        rob@gmail.com
Steve    steve@gmail.com
Wes                  NaN
dtype: object

pd.Series.map 함수를 이용해 각 성분에 적용할 수 있는 함수를 사용할 수 있겠지만 np.nan을 만나면 오류가 발생해서 제대로 처리를 하지 못한다. 다음은 np.nanfloat 형이기 때문에 문자열 함수인 count를 적용할 수 없어 오류를 발생한다.

In [304]:
data.map(lambda x: x.count('g'))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-304-532f4f311e25> in <module>()
----> 1 data.map(lambda x: x.count('g'))

~\Anaconda3\lib\site-packages\pandas\core\series.py in map(self, arg, na_action)
   2352         else:
   2353             # arg is a function
-> 2354             new_values = map_f(values, arg)
   2355
   2356         return self._constructor(new_values,

pandas/_libs/src/inference.pyx in pandas._libs.lib.map_infer()

<ipython-input-304-532f4f311e25> in <lambda>(x)
----> 1 data.map(lambda x: x.count('g'))

AttributeError: 'float' object has no attribute 'count'

다음과 같이 np.nan 행을 빼고 하면 문자 g의 갯수에 대한 시리즈를 반환하는 것을 알 수 있다.

In [313]:
data[:-1].map(lambda x: x.count('g'))
Out[313]:
Dave     2
Rob      1
Steve    1
dtype: int64

판다스 str 속성을 이용하면 문제없이 다루는 것을 볼 수 있다. str 속성은 각 성분을 일괄적으로 처리할 수 있는 함수들을 제공한다. 다음은 gmail이란 문자열을 포함하고 있는지를 판단하는 contains 메소드를 이용한 것이다.

In [314]:
data.str.contains('gmail')
Out[314]:
Dave     False
Rob       True
Steve     True
Wes        NaN
dtype: object

정규표현식을 이용해서도 사용할 수 있다. findall 메소드는 유형에 맞는 그룹들을 튜플들로 하는 리스트 시리지를 반환한다.

In [315]:
pattern = '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
data.str.findall(pattern, flags=re.IGNORECASE)
Out[315]:
Dave     [(dave, google, com)]
Rob        [(rob, gmail, com)]
Steve    [(steve, gmail, com)]
Wes                        NaN
dtype: object

반면에 extractall은 그룹들을 행으로 갖고 열은 그룹에 해당하는 성분으로 이루어진 데이터프레임을 반환한다.

In [316]:
data.str.extractall(pattern, flags=re.IGNORECASE)
Out[316]:
0 1 2
match
Dave 0 dave google com
Rob 0 rob gmail com
Steve 0 steve gmail com

위에서 match는 일치되는 것이 여러 개 있을 때 하나씩 행이 늘어난다. 다음과 같이 첫번째 성분에 dooly@gmail.com을 추가해보자.

In [317]:
ser = data.copy()
ser[0] = ser[0] + ', dooly@gmail.com'
ser
Out[317]:
Dave     dave@google.com, dooly@gmail.com
Rob                         rob@gmail.com
Steve                     steve@gmail.com
Wes                                   NaN
dtype: object

아래와 같이 match 하위 인덱스의 성분이 하나 늘어난 것을 알 수 있다.

In [318]:
ser.str.extractall(pattern, flags=re.IGNORECASE)
Out[318]:
0 1 2
match
Dave 0 dave google com
1 dooly gmail com
Rob 0 rob gmail com
Steve 0 steve gmail com

각 성분의 원소들을 일괄적으로 접근할 수 있는 방법으로 str.get 메소드 또는 str[] 슬라이싱을 이용하면 된다.

In [320]:
ser.str.get(0)
Out[320]:
Dave       d
Rob        r
Steve      s
Wes      NaN
dtype: object
In [322]:
ser.str[:-1]
Out[322]:
Dave     dave@google.com, dooly@gmail.co
Rob                         rob@gmail.co
Steve                     steve@gmail.co
Wes                                  NaN
dtype: object

다음은 판다스 시리즈 str 속성에 있는 메소드들이다.

메소드 설명
cat() Concatenate strings
split() Split strings on delimiter
rsplit() Split strings on delimiter working from the end of the string
get() Index into each element (retrieve i-th element)
join() Join strings in each element of the Series with passed separator
get_dummies() Split strings on the delimiter returning DataFrame of dummy variables
contains() Return boolean array if each string contains pattern/regex
replace() Replace occurrences of pattern/regex with some other string or the return value of a callable given the occurrence
repeat() Duplicate values (s.str.repeat(3) equivalent to x * 3)
pad() Add whitespace to left, right, or both sides of strings
center() Equivalent to str.center
ljust() Equivalent to str.ljust
rjust() Equivalent to str.rjust
zfill() Equivalent to str.zfill
wrap() Split long strings into lines with length less than a given width
slice() Slice each string in the Series
slice_replace() Replace slice in each string with passed value
count() Count occurrences of pattern
startswith() Equivalent to str.startswith(pat) for each element
endswith() Equivalent to str.endswith(pat) for each element
findall() Compute list of all occurrences of pattern/regex for each string
match() Call re.match on each element, returning matched groups as list
extract() Call re.search on each element, returning DataFrame with one row for each element and one column for each regex capture group
extractall() Call re.findall on each element, returning DataFrame with one row for each match and one column for each regex capture group
len() Compute string lengths
strip() Equivalent to str.strip
rstrip() Equivalent to str.rstrip
lstrip() Equivalent to str.lstrip
partition() Equivalent to str.partition
rpartition() Equivalent to str.rpartition
lower() Equivalent to str.lower
upper() Equivalent to str.upper
find() Equivalent to str.find
rfind() Equivalent to str.rfind
index() Equivalent to str.index
rindex() Equivalent to str.rindex
capitalize() Equivalent to str.capitalize
swapcase() Equivalent to str.swapcase
normalize() Return Unicode normal form. Equivalent to unicodedata.normalize
translate() Equivalent to str.translate
isalnum() Equivalent to str.isalnum
isalpha() Equivalent to str.isalpha
isdigit() Equivalent to str.isdigit
isspace() Equivalent to str.isspace
islower() Equivalent to str.islower
isupper() Equivalent to str.isupper
istitle() Equivalent to str.istitle
isnumeric() Equivalent to str.isnumeric
isdecimal() Equivalent to str.isdecimal