-
데이터 정제 및 준비Python 2023. 11. 21. 12:21
누락된 데이터 처리
float64 dtype을 가지는 데이터의 경우 판다스는 실숫값인 NaN으로 누락된 데이터 표시
이런 값을 감싯값(sentinel value)이라 부르며 누락된(혹은 null) 값임을 나타내기 위해 등장
In [24]: float_data = pd.Series([1.2, -3.5, np.nan, 0]) In [25]: float_data Out[25]: 0 1.2 1 -3.5 2 NaN 3 0.0 dtype: float64 # isna 메서드는 값이 null인 경우 True를 가지는 불리언 Series 반환 In [26]: float_data.isna() Out[26]: 0 False 1 False 2 True 3 False dtype: bool
파이썬의 내장 None 값 또한 NA 값으로 취급됨
In [27]: string_data = pd.Series(['aardvark', np.nan, None, 'avocado']) In [28]: string_data Out[28]: 0 aardvark 1 NaN 2 None 3 avocado dtype: object In [29]: string_data.isna() Out[29]: 0 False 1 True 2 True 3 False dtype: bool In [30]: float_data = pd.Series([1, 2, None], dtype='float64') In [31]: float_data Out[31]: 0 1.0 1 2.0 2 NaN dtype: float64 In [32]: float_data.isna() Out[32]: 0 False 1 False 2 True dtype: bool
누락된 데이터 골라내기
# Series에 dropna 메서드를 적용하면 null이 아닌 데이터와 인덱스 값만 들어 있는 Series 반환 In [34]: data = pd.Series([1, np.nan, 3.5, np.nan,7]) In [35]: data.dropna() Out[35]: 0 1.0 2 3.5 4 7.0 dtype: float64 # 위와 동일한 코드 In [36]: data[data.notna()] Out[36]: 0 1.0 2 3.5 4 7.0 dtype: float64
DataFrame 객체의 경우 모두 NA인 행이나 열을 제외하거나 NA 값을 하나라도 포함하고 있는 행이나 열을 제외할 수도 있음
dropna는 기본적으로 NA값이 하나라도 있는 행을 제외
In [37]: data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan], ...: [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]]) In [38]: data Out[38]: 0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 2 NaN NaN NaN 3 NaN 6.5 3.0 In [39]: data.dropna() Out[39]: 0 1 2 0 1.0 6.5 3.0 # how='all' 옵션을 넘기면 모든 값이 NA인 행만 제외됨 In [40]: data.dropna(how='all') Out[40]: 0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 3 NaN 6.5 3.0
이런 함수는 기본적으로 원본 객체 내용을 변경하지 않고, 새로운 객체 반환
# 열을 제외하는 방법도 동일하게 작동, axis='columns' In [42]: data[4] = np.nan In [43]: data Out[43]: 0 1 2 4 0 1.0 6.5 3.0 NaN 1 1.0 NaN NaN NaN 2 NaN NaN NaN NaN 3 NaN 6.5 3.0 NaN In [44]: data.dropna(axis='columns', how='all') Out[44]: 0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 2 NaN NaN NaN 3 NaN 6.5 3.0 In [45]: data.dropna(how='all') Out[45]: 0 1 2 4 0 1.0 6.5 3.0 NaN 1 1.0 NaN NaN NaN 3 NaN 6.5 3.0 NaN In [46]: data.dropna(axis='index', how='all').dropna(axis='columns', how='all') Out[46]: 0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 3 NaN 6.5 3.0
# 결측치가 특정 개수보다 적은 행만 살펴보고 싶은 경우 thresh 인수에 원하는 값 설정 In [47]: df = pd.DataFrame(np.random.standard_normal((7, 3))) In [48]: df.iloc[:4, 1] = np.nan In [49]: df.iloc[:2, 2] = np.nan In [50]: df Out[50]: 0 1 2 0 0.579400 NaN NaN 1 -2.226915 NaN NaN 2 -0.298148 NaN -0.598333 3 -0.892747 NaN -0.987098 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045 In [51]: df.dropna() Out[51]: 0 1 2 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045 In [52]: df.dropna(thresh=2) Out[52]: 0 1 2 2 -0.298148 NaN -0.598333 3 -0.892747 NaN -0.987098 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045
결측치 채우기
In [53]: df Out[53]: 0 1 2 0 0.579400 NaN NaN 1 -2.226915 NaN NaN 2 -0.298148 NaN -0.598333 3 -0.892747 NaN -0.987098 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045 In [54]: df.fillna(0) Out[54]: 0 1 2 0 0.579400 0.000000 0.000000 1 -2.226915 0.000000 0.000000 2 -0.298148 0.000000 -0.598333 3 -0.892747 0.000000 -0.987098 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045 # fillna에 딕셔너리 값을 넘기면 각 열마다 다른 값이 채워짐 In [55]: df.fillna({1:0.5, 2: 0}) Out[55]: 0 1 2 0 0.579400 0.500000 0.000000 1 -2.226915 0.500000 0.000000 2 -0.298148 0.500000 -0.598333 3 -0.892747 0.500000 -0.987098 4 -0.263859 1.235444 -1.338270 5 1.020750 0.841896 0.341438 6 -1.795927 0.716736 -2.130045 # 재색인에서 사용 가능한 보간(interpolation) 메서드는 fillna 메서드에서도 사용 가능 In [56]: df = pd.DataFrame(np.random.standard_normal((6, 3))) In [57]: df.iloc[2:, 1] = np.nan In [58]: df.iloc[4:, 2] = np.nan In [59]: df Out[59]: 0 1 2 0 -0.188093 -0.800852 -0.044144 1 0.851879 -0.168139 -1.194898 2 -0.547184 NaN 1.108437 3 1.445043 NaN -0.906778 4 -0.386706 NaN NaN 5 0.826388 NaN NaN In [60]: df.fillna(method='ffill') Out[60]: 0 1 2 0 -0.188093 -0.800852 -0.044144 1 0.851879 -0.168139 -1.194898 2 -0.547184 -0.168139 1.108437 3 1.445043 -0.168139 -0.906778 4 -0.386706 -0.168139 -0.906778 5 0.826388 -0.168139 -0.906778 In [61]: df.fillna(method='ffill', limit=2) Out[61]: 0 1 2 0 -0.188093 -0.800852 -0.044144 1 0.851879 -0.168139 -1.194898 2 -0.547184 -0.168139 1.108437 3 1.445043 -0.168139 -0.906778 4 -0.386706 NaN -0.906778 5 0.826388 NaN -0.906778 # fillna로 평균값이나 중간값을 넘겨 데이터를 채울 수도 있음 In [62]: data = pd.Series([1, np.nan, 3.5, np.nan,7]) In [63]: data.fillna(data.mean()) Out[63]: 0 1.000000 1 3.833333 2 3.500000 3 3.833333 4 7.000000 dtype: float64
데이터 변형
중복 제거
In [64]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'], ...: 'k2': [1, 1, 2, 3, 3, 4, 4]}) In [65]: data Out[65]: k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4 6 two 4 # DataFrame의 duplicated 메서드는 각 행이 중복인지 아닌지를 알려주는 불리언 Series 객체 반환 In [66]: data.duplicated() Out[66]: 0 False 1 False 2 False 3 False 4 False 5 False 6 True dtype: bool In [67]: data.drop_duplicates() Out[67]: k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4
중복을 찾아내기 위한 부분집합 따로 지정 가능
In [68]: data['v1'] = range(7) In [69]: data Out[69]: 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 # k1 열에 기반해 중복 걸러내기 In [70]: data.drop_duplicates(subset=['k1']) Out[70]: k1 k2 v1 0 one 1 0 1 two 1 1 # keep='last' 옵션을 넘기면 마지막으로 발견된 값 반환 In [71]: data.drop_duplicates(['k1', 'k2'], keep='last') Out[71]: 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 [72]: data.drop_duplicates(['k1', 'k2']) Out[72]: 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
함수나 매핑을 이용해서 데이터 변형
# DataFrame value 개수가 같아야 함! In [78]: data = 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]}) In [79]: data Out[79]: 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 [80]: meat_to_animal = { ...: 'bacon': 'pig', ...: 'pulled pork': 'pig', ...: 'pastrami': 'cow', ...: 'corned beef': 'cow', ...: 'honey ham': 'pig', ...: 'nova lox': 'salmon' ...: } # Series의 map 메서드는 변형을 위한 매핑 정보가 담긴 딕셔너리 같은 객체를 함수나 인수로 받음 In [81]: data['animal'] = data['food'].map(meat_to_animal) In [82]: data Out[82]: 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
함수 사용
In [83]: def get_animal(x): ...: return meat_to_animal[x] ...: In [84]: data['food'].map(get_animal) Out[84]: 0 pig 1 pig 2 pig 3 cow 4 cow 5 pig 6 cow 7 pig 8 salmon Name: food, dtype: object In [85]: meat_to_animal['bacon'] Out[85]: 'pig' In [86]: meat_to_animal Out[86]: {'bacon': 'pig', 'pulled pork': 'pig', 'pastrami': 'cow', 'corned beef': 'cow', 'honey ham': 'pig', 'nova lox': 'salmon'}
값 치환
In [87]: data = pd.Series([1., -999., 2., -999., -1000., 3.]) In [88]: data Out[88]: 0 1.0 1 -999.0 2 2.0 3 -999.0 4 -1000.0 5 3.0 dtype: float64 # -999는 누락된 데이터를 나타내는 감싯값, replace 메서드를 이용해 # 판다스에서 인식할 수 있는 NA 값으로 치환해 새로운 Series 생성 In [89]: data.replace(-999, np.nan) Out[89]: 0 1.0 1 NaN 2 2.0 3 NaN 4 -1000.0 5 3.0 dtype: float64 In [90]: data.replace([-999, -1000], np.nan) Out[90]: 0 1.0 1 NaN 2 2.0 3 NaN 4 NaN 5 3.0 dtype: float64
축 인덱스 이름 변경
In [97]: data = pd.DataFrame(np.arange(12).reshape((3, 4)), ...: index=['Ohio', 'Colorado', 'New York'], ...: columns=['one', 'two', 'three', 'four']) In [98]: def transform(x): ...: return x[:4].upper() ...: In [99]: data.index.map(transform) Out[99]: Index(['OHIO', 'COLO', 'NEW '], dtype='object') In [100]: data.index = data.index.map(transform) In [101]: data Out[101]: one two three four OHIO 0 1 2 3 COLO 4 5 6 7 NEW 8 9 10 11 # 원래 이름 변경하지 않고 새로운 객체를 생성하려면 rename 메서드 사용 In [102]: data.rename(index=str.title, columns=str.upper) Out[102]: ONE TWO THREE FOUR Ohio 0 1 2 3 Colo 4 5 6 7 New 8 9 10 11 # 딕셔너리 형식의 객체를 이용해 축 이름 중 일부만 변경 가능 In [103]: data.rename(index={'OHIO': 'INDIANA'}, ...: columns={'three': 'peekaboo'}) Out[103]: one two peekaboo four INDIANA 0 1 2 3 COLO 4 5 6 7 NEW 8 9 10 11
이산화
연속되는 데이터는 종종 개별로 분할하거나 분석을 위해 그룹으로 나누기도 함
In [104]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32] In [105]: bins = [18, 25, 35, 60, 100] # 범위 In [106]: ages_categories = pd.cut(ages, bins) In [107]: ages_categories Out[107]: [(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, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
여기서 반환된 판다스 객체는 Categorical(범주형)이라는 특수한 객체, 결과는 pandas.cut으로 계산된 그룹
각 그룹은 개별 그룹의 상한과 하한값을 담은 특수한 간격 값으로 구분
In [108]: ages_categories.codes Out[108]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8) In [109]: ages_categories.categories Out[109]: IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval[int64, right]') In [110]: ages_categories.categories[0] Out[110]: Interval(18, 25, closed='right') # closed='right' 여기까지 In [111]: pd.value_counts(ages_categories) # pandas.cut 결과에 대한 그룹 개수 Out[111]: (18, 25] 5 (25, 35] 3 (35, 60] 3 (60, 100] 1 Name: count, dtype: int64
( : 포함 x
[ : 포함 o
In [112]: ages Out[112]: [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32] In [113]: pd.cut(ages, bins, right=False) # right=False를 전달해 소괄호와 대괄호 위치 변경 Out[113]: [[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, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)] In [114]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior'] # labels 옵션으로 그룹의 이름을 리스트나 배열 형태로 직접 전달 In [115]: pd.cut(ages, bins, labels=group_names) Out[115]: ['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult'] Length: 12 Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']
pandas.cut에 명시적으로 그룹의 경곗값을 넘기지 않고 그룹의 개수를 넘기면 데이터의 최솟값과 최댓값을 기준으로 균등한 길이의 그룹을 자동으로 계산
In [116]: data = np.random.uniform(size=20) # precision=2 옵션으로 소수점 아래 두 자리까지로 제한 In [117]: pd.cut(data, 4, precision=2) Out[117]: [(0.72, 0.95], (0.72, 0.95], (0.72, 0.95], (0.014, 0.25], (0.72, 0.95], ..., (0.25, 0.48], (0.72, 0.95], (0.014, 0.25], (0.72, 0.95], (0.48, 0.72]] Length: 20 Categories (4, interval[float64, right]): [(0.014, 0.25] < (0.25, 0.48] < (0.48, 0.72] < (0.72, 0.95]]
표본 사분위수(quartile)를 기반으로 데이터를 나누는 가장 적합한 함수 pandas.qcut
pandas.cut 함수를 사용하면 데이터의 분산에 따라 각 그룹의 데이터 개수가 다르게 나뉘는 경우가 많음
pandas.qcut은 표준 사분위수를 사용하므로 적당히 비슷한 크기의 그룹으로 나눌 수 있음
In [118]: data = np.random.standard_normal(1000) In [119]: quartiles = pd.qcut(data, 4, precision=2) In [120]: quartiles Out[120]: [(-0.68, -0.0033], (0.73, 3.77], (-0.68, -0.0033], (-3.59, -0.68], (-0.68, -0.0033], ..., (-0.68, -0.0033], (-3.59, -0.68], (-3.59, -0.68], (-3.59, -0.68], (0.73, 3.77]] Length: 1000 Categories (4, interval[float64, right]): [(-3.59, -0.68] < (-0.68, -0.0033] < (-0.0033, 0.73] < (0.73, 3.77]] In [121]: pd.value_counts(quartiles) Out[121]: (-3.59, -0.68] 250 (-0.68, -0.0033] 250 (-0.0033, 0.73] 250 (0.73, 3.77] 250 Name: count, dtype: int64 # pandas.cut 함수처럼 사분위수(0부터 1까지의 값)를 직접 지정 가능 In [122]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]).value_counts() Out[122]: (-3.584, -1.3] 100 (-1.3, -0.00334] 400 (-0.00334, 1.348] 400 (1.348, 3.773] 100 Name: count, dtype: int64
이상치를 찾고 제외
이상치를 제외하거나 적당한 값으로 대체
In [8]: data = pd.DataFrame(np.random.standard_normal((1000, 4))) In [9]: data.describe() Out[9]: 0 1 2 3 count 1000.000000 1000.000000 1000.000000 1000.000000 mean 0.060391 0.012253 0.001039 -0.015959 std 0.951130 1.014482 0.955257 0.986806 min -2.764806 -2.633661 -3.151250 -3.146761 25% -0.563361 -0.727716 -0.646533 -0.653332 50% 0.014199 0.012906 0.005474 -0.058561 75% 0.719311 0.668322 0.650792 0.612658 max 2.815886 4.433775 3.065100 3.432014 In [10]: col = data[2] In [11]: col[col.abs() > 3] # 2번 column에서 절댓값이 3을 초과하는 값 Out[11]: 0 3.065100 187 -3.065525 662 -3.151250 Name: 2, dtype: float64 In [12]: data Out[12]: 0 1 2 3 0 -0.965558 0.722591 3.065100 -0.613409 1 -0.642368 0.691791 0.557011 -0.449008 2 -0.220209 1.087734 -1.036212 1.503724 3 -0.079624 -0.065160 -1.874135 0.076213 4 0.010716 -1.982512 1.553767 1.668419 .. ... ... ... ... 995 -0.650527 0.479224 -0.347631 2.361066 996 -0.396253 -1.436659 0.625960 0.417486 997 -0.225310 0.559574 0.710567 1.129161 998 -1.735939 -0.550929 1.098324 -1.191136 999 -0.058288 -0.064661 0.333518 1.986984 [1000 rows x 4 columns] In [13]: data[2] Out[13]: 0 3.065100 1 0.557011 2 -1.036212 3 -1.874135 4 1.553767 ... 995 -0.347631 996 0.625960 997 0.710567 998 1.098324 999 0.333518 Name: 2, Length: 1000, dtype: float64 In [14]: data[2].abs() Out[14]: 0 3.065100 1 0.557011 2 1.036212 3 1.874135 4 1.553767 ... 995 0.347631 996 0.625960 997 0.710567 998 1.098324 999 0.333518 Name: 2, Length: 1000, dtype: float64 In [15]: In [15]: data[2].abs() > 3 Out[15]: 0 True 1 False 2 False 3 False 4 False ... 995 False 996 False 997 False 998 False 999 False Name: 2, Length: 1000, dtype: bool # 절댓값 3을 초과하는 값이 들어 있는 모든 행 선택 # 불리언 DataFrame에서 any 메서드 사용 In [16]: data[(data.abs() > 3).any(axis='columns')] Out[16]: 0 1 2 3 0 -0.965558 0.722591 3.065100 -0.613409 172 -0.240364 4.433775 -0.525272 -1.842624 187 0.172205 -0.043776 -3.065525 -0.342836 509 1.248355 -1.150597 -1.270797 -3.146761 662 -1.315525 0.465956 -3.151250 0.451275 767 -0.387978 -0.864364 1.784505 3.075844 975 1.185064 -0.276083 0.207537 3.432014
비교 연산 결과에 any 메서드를 호출하려면 data.abs() > 3 구문을 괄호로 감싸야 함
이 기준대로 쉽게 값 선택 가능, 다음 코드로 -3이나 3을 초과하는 값을 -3 또는 3으로 지정 가능
In [17]: data[data.abs() > 3] = np.sign(data) * 3 In [18]: data.describe() Out[18]: 0 1 2 3 count 1000.000000 1000.000000 1000.000000 1000.000000 mean 0.060391 0.010819 0.001191 -0.016320 std 0.951130 1.009226 0.954354 0.984697 min -2.764806 -2.633661 -3.000000 -3.000000 25% -0.563361 -0.727716 -0.646533 -0.653332 50% 0.014199 0.012906 0.005474 -0.058561 75% 0.719311 0.668322 0.650792 0.612658 max 2.815886 3.000000 3.000000 3.000000 # np.sign(data)는 data가 양수인지 음수인지에 따라 1이나 -1이 담긴 배열 반환 In [19]: np.sign(data).head() Out[19]: 0 1 2 3 0 -1.0 1.0 1.0 -1.0 1 -1.0 1.0 1.0 -1.0 2 -1.0 1.0 -1.0 1.0 3 -1.0 -1.0 -1.0 1.0 4 1.0 -1.0 1.0 1.0 In [20]: np.sign(-3.4) Out[20]: -1.0 In [21]: np.sign(3.4) Out[21]: 1.0
뒤섞기와 임의 샘플링
numpy.random.permutation 함수를 이용하면 Series나 DataFrame의 행을 임의의 순서대로 재배치
순서를 바꾸고 싶은 만큼의 길이를 permutation 함수에 전달하면 순서가 바뀐 정수 배열 생성
In [23]: df = pd.DataFrame(np.arange(5 * 7).reshape((5, 7))) In [24]: df Out[24]: 0 1 2 3 4 5 6 0 0 1 2 3 4 5 6 1 7 8 9 10 11 12 13 2 14 15 16 17 18 19 20 3 21 22 23 24 25 26 27 4 28 29 30 31 32 33 34 In [25]: sampler = np.random.permutation(5) In [26]: sampler Out[26]: array([0, 2, 3, 4, 1]) # 이 배열은 iloc 기반 index나 take 함수에서 사용 가능 In [27]: df.take(sampler) Out[27]: 0 1 2 3 4 5 6 0 0 1 2 3 4 5 6 2 14 15 16 17 18 19 20 3 21 22 23 24 25 26 27 4 28 29 30 31 32 33 34 1 7 8 9 10 11 12 13 In [28]: df.iloc[sampler] Out[28]: 0 1 2 3 4 5 6 0 0 1 2 3 4 5 6 2 14 15 16 17 18 19 20 3 21 22 23 24 25 26 27 4 28 29 30 31 32 33 34 1 7 8 9 10 11 12 13 # take를 호출할 때 axis='columns'를 넘기면 열에 대해 작동 In [29]: columns_sampler = np.random.permutation(7) In [30]: columns_sampler Out[30]: array([2, 6, 3, 0, 5, 4, 1]) In [31]: df.take(columns_sampler, axis='columns') Out[31]: 2 6 3 0 5 4 1 0 2 6 3 0 5 4 1 1 9 13 10 7 12 11 8 2 16 20 17 14 19 18 15 3 23 27 24 21 26 25 22 4 30 34 31 28 33 32 29 # 치환 없이 일부만 임의로 선택하고 싶은 경우(깥은 행 두 번 나타날 수 x) # Series나 DataFrame의 sample 메서드 사용 In [32]: df.sample(n=3) Out[32]: 0 1 2 3 4 5 6 0 0 1 2 3 4 5 6 4 28 29 30 31 32 33 34 1 7 8 9 10 11 12 13 # 반복 선택을 허용하기 위해 치환을 통해 표본을 생성하려면 sample에 replace=True 옵션 전달 In [33]: choices = pd.Series([5, 7, -1, 6, 4]) In [34]: choices.sample(n=10, replace=True) Out[34]: 4 4 3 6 1 7 0 5 4 4 1 7 3 6 2 -1 3 6 1 7 dtype: int64
표시자, 더미 변수 계산
통계 모델이나 머신러닝 애플리케이션을 위한 데이터 변환은 분류 값을 더미나 표시자 행렬로 전환하는 것
ex) DataFrame의 한 열에 k가지 값이 있다면 k개의 열이 있는 DataFrame이나 행렬을 만들고 값으로 1과 0을 채워 넣을 것
In [39]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], ...: 'data1': range(6)}) In [40]: df Out[40]: key data1 0 b 0 1 b 1 2 a 2 3 c 3 4 a 4 5 b 5 In [41]: pd.get_dummies(df['key'], dtype=float) Out[41]: a b c 0 0.0 1.0 0.0 1 0.0 1.0 0.0 2 1.0 0.0 0.0 3 0.0 0.0 1.0 4 1.0 0.0 0.0 5 0.0 1.0 0.0 # 표시자 DataFrame 열에 접두어(predix) 추가 후 다른 데이터와 병합하고 싶은 경우 In [42]: pd.get_dummies(df['key'], prefix='key', dtype=float) Out[42]: key_a key_b key_c 0 0.0 1.0 0.0 1 0.0 1.0 0.0 2 1.0 0.0 0.0 3 0.0 0.0 1.0 4 1.0 0.0 0.0 5 0.0 1.0 0.0 In [43]: dummies = pd.get_dummies(df['key'], prefix='key', dtype=float) In [44]: dummies Out[44]: key_a key_b key_c 0 0.0 1.0 0.0 1 0.0 1.0 0.0 2 1.0 0.0 0.0 3 0.0 0.0 1.0 4 1.0 0.0 0.0 5 0.0 1.0 0.0 In [45]: df_with_dummy = df[['data1']].join(dummies) In [46]: df_with_dummy Out[46]: data1 key_a key_b key_c 0 0 0.0 1.0 0.0 1 1 0.0 1.0 0.0 2 2 1.0 0.0 0.0 3 3 0.0 0.0 1.0 4 4 1.0 0.0 0.0 5 5 0.0 1.0 0.0
DataFrame의 한 행이 여러 범주에 속하는 경우 다른 접근 방식을 사용해 더미 변수를 만들어야 함
In [7]: mnames = ['movie_id', 'title', 'genres'] In [8]: movies = pd.read_table('movies.dat', sep='::', ...: header=None, names=mnames, engine='python') In [9]: movies Out[9]: movie_id title genres 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 ... ... ... ... 3878 3948 Meet the Parents (2000) Comedy 3879 3949 Requiem for a Dream (2000) Drama 3880 3950 Tigerland (2000) Drama 3881 3951 Two Family House (2000) Drama 3882 3952 Contender, The (2000) Drama|Thriller [3883 rows x 3 columns] In [10]: movies[:10] Out[10]: movie_id title genres 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 5 6 Heat (1995) Action|Crime|Thriller 6 7 Sabrina (1995) Comedy|Romance 7 8 Tom and Huck (1995) Adventure|Children's 8 9 Sudden Death (1995) Action 9 10 GoldenEye (1995) Action|Adventure|Thriller # str.get_dummies 메서드는 구분 문자열을 이용해 여러 그룹에 속하는 구성원 처리 In [11]: dummies = movies['genres'].str.get_dummies('|') In [12]: dummies.iloc[:10, :6] Out[12]: Action Adventure Animation Children's Comedy Crime 0 0 0 1 1 1 0 1 0 1 0 1 0 0 2 0 0 0 0 1 0 3 0 0 0 0 1 0 4 0 0 0 0 1 0 5 1 0 0 0 0 1 6 0 0 0 0 1 0 7 0 1 0 1 0 0 8 1 0 0 0 0 0 9 1 1 0 0 0 0 # 이를 movies와 결합하고 add_prefix 메서드를 이용해 dummies의 열 이름에 'Genre_' 추가 In [13]: dummies Out[13]: Action Adventure Animation Children's Comedy Crime ... Mystery Romance Sci-Fi Thriller War Western 0 0 0 1 1 1 0 ... 0 0 0 0 0 0 1 0 1 0 1 0 0 ... 0 0 0 0 0 0 2 0 0 0 0 1 0 ... 0 1 0 0 0 0 3 0 0 0 0 1 0 ... 0 0 0 0 0 0 4 0 0 0 0 1 0 ... 0 0 0 0 0 0 ... ... ... ... ... ... ... ... ... ... ... ... ... ... 3878 0 0 0 0 1 0 ... 0 0 0 0 0 0 3879 0 0 0 0 0 0 ... 0 0 0 0 0 0 3880 0 0 0 0 0 0 ... 0 0 0 0 0 0 3881 0 0 0 0 0 0 ... 0 0 0 0 0 0 3882 0 0 0 0 0 0 ... 0 0 0 1 0 0 [3883 rows x 18 columns] In [14]: movies_windic = movies.join(dummies.add_prefix('Genre_')) In [15]: movies_windic Out[15]: movie_id title ... Genre_War Genre_Western 0 1 Toy Story (1995) ... 0 0 1 2 Jumanji (1995) ... 0 0 2 3 Grumpier Old Men (1995) ... 0 0 3 4 Waiting to Exhale (1995) ... 0 0 4 5 Father of the Bride Part II (1995) ... 0 0 ... ... ... ... ... ... 3878 3948 Meet the Parents (2000) ... 0 0 3879 3949 Requiem for a Dream (2000) ... 0 0 3880 3950 Tigerland (2000) ... 0 0 3881 3951 Two Family House (2000) ... 0 0 3882 3952 Contender, The (2000) ... 0 0 [3883 rows x 21 columns] In [16]: movies_windic.iloc[0] Out[16]: movie_id 1 title Toy Story (1995) genres Animation|Children's|Comedy Genre_Action 0 Genre_Adventure 0 Genre_Animation 1 Genre_Children's 1 Genre_Comedy 1 Genre_Crime 0 Genre_Documentary 0 Genre_Drama 0 Genre_Fantasy 0 Genre_Film-Noir 0 Genre_Horror 0 Genre_Musical 0 Genre_Mystery 0 Genre_Romance 0 Genre_Sci-Fi 0 Genre_Thriller 0 Genre_War 0 Genre_Western 0 Name: 0, dtype: object
pandas.get_dummies와 pandas.cut 같은 이산 함수를 조합해 통계 애플리케이션에서 사용
In [20]: np.random.seed(12345) # 난수가 반복 가능하도록 시드 값 고정 In [21]: values = np.random.uniform(size=10) In [22]: values Out[22]: array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503, 0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987]) In [23]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1] In [24]: pd.get_dummies(pd.cut(values, bins)) Out[24]: (0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0] 0 False False False False True 1 False True False False False 2 True False False False False 3 False True False False False 4 False False True False False 5 False False True False False 6 False False False False True 7 False False False True False 8 False False False True False 9 False False False True False
확장 데이터 유형
판다스는 넘파이 기능 기반으로 만들어짐
최근 판다스는 넘파이에서 기본적으로 지원하지 않는 자료형이더라도 사용할 수 있도록 하는 확장형 시스템 개발
> 이 새로운 자료형은 넘파이 배열에서 가져오는 데이터와 동일하게 취급
In [25]: s = pd.Series([1, 2, 3, None]) In [26]: s Out[26]: 0 1.0 1 2.0 2 3.0 3 NaN dtype: float64 In [27]: s.dtype Out[27]: dtype('float64')
호환성을 위해 Series는 float64 자료형 사용, 누락된 값에 대해 np.nan 사용하는 레거시 방식을 따름
pandas.Int64Dtype을 이용해 Series 생성 가능
In [28]: s = pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype()) In [29]: s Out[29]: 0 1 1 2 2 3 3 <NA> dtype: Int64 In [30]: s.isna() Out[30]: 0 False 1 False 2 False 3 True dtype: bool In [31]: s.dtype Out[31]: Int64Dtype()
<NA> 출력은 확장형 배열에 누락된 값이 있음을 나타냄
> pandas.NA라는 특수한 감싯값 사용
In [32]: s[3] Out[32]: <NA> In [33]: s[3] is pd.NA Out[33]: True In [34]: s[3] is np.nan Out[34]: False In [35]: s[3] is None Out[35]: False
pd.Int64Dtype() 대신 'Int64'로 줄여서 사용 가능, 대소문자 구분하지 않으면 넘파이 기반의 비확장형으로 처리됨
In [38]: s = pd.Series([1, 2, 3, None], dtype='Int64') In [39]: s Out[39]: 0 1 1 2 2 3 3 <NA> dtype: Int64 # 판다스에는 넘파이 객체 배열을 사용하지 않는 문자열 데이터를 위한 특수한 확장형 존재 # (별도로 설치해야 하는 pyarrow 라이브러리 필요) In [40]: s = pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype()) In [41]: s Out[41]: 0 one 1 two 2 <NA> 3 three dtype: string In [42]: s = pd.Series(['one', 'two', None, 'three']) In [43]: s Out[43]: 0 one 1 two 2 None 3 three dtype: object # Series의 astype 메서드에 확장형을 인수로 전달하면 데이터 정제 과정에서 손쉽게 변환 수행 In [44]: df = pd.DataFrame({'A': [1, 2, None, 4], ...: 'B': ['one', 'two', 'three', None], ...: 'C': [False, None, False, True]}) In [45]: df Out[45]: A B C 0 1.0 one False 1 2.0 two None 2 NaN three False 3 4.0 None True In [46]: df['A'] = df['A'].astype('Int64') In [47]: df['B'] = df['B'].astype('string') In [48]: df['C'] = df['C'].astype('boolean') In [49]: df Out[49]: A B C 0 1 one False 1 2 two <NA> 2 <NA> three False 3 4 <NA> True
기본적으로 숫자는 float, 다른 형태는 object 형식
문자열 다루기
복잡한 패턴 매칭이나 텍스트 조작에는 정규 표현식이 필요
판다스는 배열 데이터 전체에 쉽게 정규 표현식을 적용하고 누락된 데이터를 편리하게 처리하는 기능을 제공
파이썬 내장 문자열 객체 메서드
In [50]: val = 'a,b, guido' # 쉼표로 구분된 문자열 split 메서드로 분리 In [51]: val.split(',') Out[51]: ['a', 'b', ' guido'] # 공백 문자(개행 문자 포함)를 제거하는 strip 메서드와 조합해 사용하기도 함 In [52]: pieces = [x.strip() for x in val.split(',')] In [53]: pieces Out[53]: ['a', 'b', 'guido'] # 더하기 연산을 이용해 :: 문자열과 합칠 수도 있음 In [54]: first, second, third = pieces In [55]: first + '::' + second + '::' + third Out[55]: 'a::b::guido' # :: 문자열의 join 메서드에 리스트나 튜플을 전달하는 방법이 더 o In [56]: '=='.join(pieces) Out[56]: 'a==b==guido'
일치하는 부분 문자열(substring)의 위치를 찾는 메서드도 있음, index나 find 대신 파이썬의 in 예약어 사용
In [57]: 'guido' in val Out[57]: True In [58]: val.index(',') Out[58]: 1 In [59]: val.find(':') Out[59]: -1 In [60]: val.index(':') # index의 경우 찾지 못하면 예외 발생 --------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[60], line 1 ----> 1 val.index(':') ValueError: substring not found # count는 특정 부분 문자열이 몇 건 발견되었는지 반환 In [61]: val.count(',') Out[61]: 2 # replace는 찾아낸 패턴을 다른 문자열로 치환 # 이 메서드에 비어 있는 문자열을 넘겨 패턴을 삭제하는 방법으로도 사용됨 In [62]: val.replace(',', '::') Out[62]: 'a::b:: guido' In [63]: val.replace(',', '') Out[63]: 'ab guido' In [64]: val.replace(' ', '') Out[64]: 'a,b,guido'
정규 표현식
텍스트에서 문자열 패턴을 찾는 유연한 방법 제공
regex라 부르는 단일 표현식은 정규 표현식 언어로 구성된 문자열로 파이썬에 내장된 re 모듈이 문자열과 관련된 정규 표현식을 처리
re 모듈 함수는 패턴 매칭, 치환, 분리 세 가지로 나눌 수 있음
여러 가지 공백 문자(탭, 스페이스, 개행 문자)가 포함된 문자열을 나누고 싶다면 하나 이상의 공백 문자를 의미하는 \s+ 사용해 문자열 분리
In [65]: import re In [66]: text = 'foo bar\t baz \tqux' # 공백이 하나 이상이면 분리 In [67]: re.split(r'\s+', text) Out[67]: ['foo', 'bar', 'baz', 'qux'] # re.split(r'\s+', text)를 사용하면 정규 표현식이 컴파일되고 그 다음 split 메서드가 실행됨 # re.compile을 통해 직접 정규 표현식을 컴파일하고 얻은 정규 표현식 객체를 재사용할 수 있음 In [68]: regex = re.compile(r'\s+') In [69]: regex.split(text) Out[69]: ['foo', 'bar', 'baz', 'qux'] # 정규 표현식에 매칭되는 모든 패턴의 목록, findall 메서드 In [70]: regex.findall(text) Out[70]: [' ', '\t ', ' \t'] In [71]: re.findall(r'\s+', text) Out[71]: [' ', '\t ', ' \t'] In [72]: re.findall('\s+', text) Out[72]: [' ', '\t ', ' \t'] In [73]: re.findall('\\s+', text) Out[73]: [' ', '\t ', ' \t']
동일한 정규 표현식을 다른 문자열에도 적용해야 할 경우 re.compile로 정규 표현식 객체를 만들어 사용하는 방법 추천(CPU 사용량 절약)
match와 search는 findall 메서드와 밀접하게 관련되어 있음
· findall은 문자열에서 일치하는 모든 부분 문자열을 찾음
· search 메서드는 패턴과 일치하는 첫 번째 항목을 반환
· match 메서드는 문자열의 시작 부분에서 일치하는 것만 찾음
예제
In [99]: text = """Dave dave@google.com ...: Steve steve@gmail.com ...: Rob rob@gmail.com ...: Ryan ryan@yahoo.com""" In [100]: pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}" # re.IGNORECASE는 정규 표현식이 대소문자를 구분하지 않도록 함 In [101]: regex = re.compile(pattern, flags=re.IGNORECASE) # findall 메서드를 사용해 이메일 주소 리스트 생성 In [102]: regex.findall(text) Out[102]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com'] # search는 텍스트에서 첫 번째 이메일 주소만 찾음 # 이전 정규 표현식에 대한 match 객체는 문자열에서 패턴이 위치하는 시작점과 끝점만 알려줌 In [103]: m = regex.search(text) In [104]: m Out[104]: <re.Match object; span=(5, 20), match='dave@google.com'> In [105]: text[m.start():m.end()] Out[105]: 'dave@google.com' In [106]: print(regex.match(text)) None # sub 메서드는 찾은 패턴을 주어진 문자열로 치환 후 새로운 문자열 반환 In [107]: print(regex.sub("REDACTED", text)) Dave REDACTED Steve REDACTED Rob REDACTED Ryan REDACTED # 이메일 주소를 찾는 동시에 각 이메일 주소를 사용자 이름, 도메인 이름, 도메인 접미사 세 가지 # 컴포넌트로 나눠야 한다면 각 패턴을 괄호로 묶어줌 In [108]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})" In [109]: regex = re.compile(pattern, flags=re.IGNORECASE) # match 객체를 이용하면 groups 메서드로 각 패턴 컴포넌트의 튜플을 얻을 수 있음 In [110]: m = regex.match("wesm@bright.net") In [111]: m.groups() Out[111]: ('wesm', 'bright', 'net') # 패턴에 그룹이 존재하면 findall 메서드는 튜플 목록 반환 In [112]: regex.findall(text) Out[112]: [('dave', 'google', 'com'), ('steve', 'gmail', 'com'), ('rob', 'gmail', 'com'), ('ryan', 'yahoo', 'com')] # sub 역시 마찬가지로 \1, \2 같은 특수한 기호를 사용해 각 패턴 그룹에 접근 가능 # \1는 첫 번째로 찾은 그룹, \2는 두 번째로 찾은 그룹을 의미 In [113]: print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text)) Dave Username: dave, Domain: google, Suffix: com Steve Username: steve, Domain: gmail, Suffix: com Rob Username: rob, Domain: gmail, Suffix: com Ryan Username: ryan, Domain: yahoo, Suffix: com
판다스의 문자열 함수
뒤죽박죽 섞인 데이터를 분석을 위해 정리하려면 문자열을 다듬고 정규화하는 작업이 필요
문자열을 담은 열에 누락된 값이 있는 경우 복잡해짐
In [114]: data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com', ...: 'Rob': 'rob@gmail.com', 'Wes': np.nan} In [115]: data = pd.Series(data) In [116]: data Out[116]: Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Wes NaN dtype: object In [117]: data.isna() Out[117]: Dave False Steve False Rob False Wes True dtype: bool
문자열과 정규 표현식 메서드는 data.map을 사용해 각 값에 적용(lambda 혹은 다른 함수를 넘겨)할 수 있지만 NA 값을 만나면 실패
> 이런 문제에 대처하기 위해 Series에는 NA 값을 건너뛰도록 하는 문자열 처리 메서드가 있음, Series의 str 속성을 이용
# 각 이메일 주소가 gmail을 포함하고 있는지 str.contains로 검사 # 연산 결과로 object dtype을 가짐 In [118]: data.str.contains('gmail') Out[118]: Dave False Steve True Rob True Wes NaN dtype: object # 판다스는 문자열, 정수, 불리언 데이터를 특수하게 처리하는 확장형 제공 In [119]: data_as_string_ext = data.astype('string') In [120]: data_as_string_ext Out[120]: Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Wes <NA> dtype: string In [121]: data_as_string_ext.str.contains('gmail') Out[121]: Dave False Steve True Rob True Wes <NA> dtype: boolean
범주형 데이터
판다스의 메모리 사용량을 줄이고 성능을 개선하는 방법
# 하나의 열에서 특정 값이 반복되는 경우 In [122]: values = pd.Series(['apple', 'orange', 'apple', 'apple'] * 2) In [123]: values Out[123]: 0 apple 1 orange 2 apple 3 apple 4 apple 5 orange 6 apple 7 apple dtype: object # 배열 내 유일한 값 추출 unique In [124]: pd.unique(values) Out[124]: array(['apple', 'orange'], dtype=object) # 특정 값이 얼마나 많이 존재하는지 확인 value_counts In [125]: pd.value_counts(values) Out[125]: apple 6 orange 2 Name: count, dtype: int64 In [126]: values = pd.Series([0, 1, 0, 0] * 2) In [127]: dim = pd.Series(['apple', 'orange']) In [128]: values Out[128]: 0 0 1 1 2 0 3 0 4 0 5 1 6 0 7 0 dtype: int64 In [129]: dim Out[129]: 0 apple 1 orange dtype: object # take 메서드 사용해 Series에 저장된 원래 문자열 구하기 In [130]: dim.take(values) Out[130]: 0 apple 1 orange 0 apple 0 apple 0 apple 1 orange 0 apple 0 apple dtype: object
이러한 정수 표현을 범주형 또는 딕셔너리형 표기법
별개의 값을 담은 배열 - 범주형, 딕셔너리형 또는 단계별 데이터
이런 종류의 데이터 - Categorical 또는 범주형 데이터
범주형 데이터를 가리키는 정숫값 - 범주 코드 또는 코드
범주형 표현을 사용하면 분석 작업에서 엄청난 성능 향상, 범주 코드를 변경하지 않은 채로 범주형 데이터를 변형할 수 있음
비교적 적은 연산으로 수행할 수 있는 변형
· 범주형 데이터의 이름 변경
· 기존 범주형 데이터의 순서를 바꾸지 않고 새로운 범주 추가
판다스의 Categorical 확장형
주로 문자열 데이터에서 유사한 값이 다수 존재하는 경우 데이터를 효과적으로 압축해 적은 메모리에서도 빠른 성능을 내는 기법
In [131]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2 In [132]: N = len(fruits) In [133]: rng = np.random.default_rng(seed=12345) In [134]: df = pd.DataFrame({'fruit': fruits, ...: 'basket_id': np.arange(N), ...: 'count': rng.integers(3, 15, size=N), # 3부터 14까지 임의의 값 ...: 'weight': rng.uniform(0, 4, size=N)}, # 값이 중복되지 않도록 uniform ...: columns=['basket_id', 'fruit', 'count', 'weight']) In [135]: df Out[135]: basket_id fruit count weight 0 0 apple 11 1.564438 1 1 orange 5 1.331256 2 2 apple 12 2.393235 3 3 apple 6 0.746937 4 4 apple 5 2.691024 5 5 orange 12 3.767211 6 6 apple 10 0.992983 7 7 apple 11 3.795525 # df['fruit']은 파이썬 문자열 객체의 배열 > 범주형 데이터로 변경 In [136]: fruit_cat = df['fruit'].astype('category') In [137]: fruit_cat Out[137]: 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'] # .array 속성으로 접근할 수 있는 fruit_cat의 값은 pandas.Categorical 인스턴스 In [138]: c = fruit_cat.array In [139]: type(c) Out[139]: pandas.core.arrays.categorical.Categorical # Categorical 객체에는 categories와 codes 속성이 있음 In [140]: c.categories Out[140]: Index(['apple', 'orange'], dtype='object') In [141]: c.codes Out[141]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8) # codes 속성과 categories 속성 간의 매핑을 가져올 수 있음 In [142]: dict(enumerate(c.categories)) Out[142]: {0: 'apple', 1: 'orange'} # 변환 완료된 값을 대입해 DataFrame의 열을 범주형으로 변경 In [143]: df['fruit'] = df['fruit'].astype('category') In [144]: df['fruit'] Out[144]: 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 직접 생성 가능 In [145]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar']) In [146]: my_categories Out[146]: ['foo', 'bar', 'baz', 'foo', 'bar'] Categories (3, object): ['bar', 'baz', 'foo'] # 기존에 정의된 범주와 범주 코드가 있다면 from_codes로 범주형 데이터 생성 가능 In [147]: categories = ['foo', 'bar', 'baz'] In [148]: codes = [0, 1, 2, 0, 0, 1] In [149]: my_cats_2 = pd.Categorical.from_codes(codes, categories) In [150]: my_cats_2 Out[150]: ['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo', 'bar', 'baz']
범주형으로 변경하는 경우 명시적으로 지정하지 않는 한 특정 순서 보장 x
> 범주형 배열은 입력 데이터의 순서에 따라 순서가 다를 수 있음
> from_codes를 사용하거나 다른 범주형 데이터 생성자를 이용하면 순서 지정 가능
In [151]: ordered_cat = pd.Categorical.from_codes(codes, categories, ...: ordered=True) In [152]: ordered_cat Out[152]: ['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo' < 'bar' < 'baz']
코드에서 ['foo' < 'bar' < 'baz']는 foo, bar, baz의 순서를 갖는다는 의미
# 순서가 없는 범주형 인스턴스는 as_ordered 메서드를 사용해 정렬 가능 In [153]: my_cats_2.as_ordered() Out[153]: ['foo', 'bar', 'baz', 'foo', 'foo', 'bar'] Categories (3, object): ['foo' < 'bar' < 'baz']
범주형 데이터가 문자열일 필요는 없음, 범주형 배열은 변경이 불가능한 값이라면 어떤 자료형이라도 포함 가능
Categorical 연산
In [3]: rng = np.random.default_rng(seed=12345) In [4]: draws = rng.standard_normal(1000) In [5]: draws[:5] Out[5]: array([-1.42382504, 1.26372846, -0.87066174, -0.25917323, -0.07534331]) In [6]: # 사분위수, 통계 In [7]: bins = pd.qcut(draws, 4) In [8]: bins Out[8]: [(-3.121, -0.675], (0.687, 3.211], (-3.121, -0.675], (-0.675, 0.0134], (-0.675, 0.0134], ..., (0.0134, 0.687], (0.0134, 0.687], (-0.675, 0.0134], (0.0134, 0.687], (-0.675, 0.0134]] Length: 1000 Categories (4, interval[float64, right]): [(-3.121, -0.675] < (-0.675, 0.0134] < (0.0134, 0.687] < (0.687, 3.211]] # qcut 함수의 labels 인수를 통해 직접 이름 지정 In [9]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4']) In [10]: bins Out[10]: ['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2'] Length: 1000 Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4'] In [11]: bins.codes[:10] Out[11]: array([0, 3, 0, 1, 1, 0, 0, 2, 2, 0], dtype=int8) # 이름 붙인 bins는 데이터의 시작과 끝값에 포함된 정보를 포함하지 않으므로 groupby 이용해 # 요약 통계 추출 In [12]: bins = pd.Series(bins, name='quartile') In [13]: results = (pd.Series(draws) ...: .groupby(bins) ...: .agg(['count', 'min', 'max']) ...: .reset_index()) In [14]: results Out[14]: quartile count min max 0 Q1 250 -3.119609 -0.678494 1 Q2 250 -0.673305 0.008009 2 Q3 250 0.018753 0.686183 3 Q4 250 0.688282 3.211418 # 결과의 quartile 열은 bins 순서를 포함한 원래 범주 정보 유지 In [15]: results['quartile'] Out[15]: 0 Q1 1 Q2 2 Q3 3 Q4 Name: quartile, dtype: category Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
범주형을 활용한 성능 개선
범주형을 사용해 성능과 메모리 사용률 개선
In [17]: N = 10_000_000 In [18]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4)) # labels 범주형 변환 In [19]: categories = labels.astype('category') In [20]: labels.memory_usage(deep=True) Out[20]: 600000132 In [21]: categories.memory_usage(deep=True) Out[21]: 10000544 In [22]: %time _ = labels.astype('category') CPU times: total: 375 ms Wall time: 381 ms In [23]: In [23]: %timeit labels.value_counts() 263 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) In [24]: %timeit categories.value_counts() 36.4 ms ± 672 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Categorical 메서드
범주형 데이터를 담은 Series는 특화된 문자열 메서드인 Series.str과 유사한 몇 가지 특수한 메서드 제공
categories와 codes에 쉽게 접근 가능
In [25]: s = pd.Series(['a', 'b', 'c', 'd'] * 2) In [26]: cat_s = s.astype('category') In [27]: cat_s Out[27]: 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 메서드 접근 In [28]: cat_s.cat.codes Out[28]: 0 0 1 1 2 2 3 3 4 0 5 1 6 2 7 3 dtype: int8 In [29]: cat_s.cat.categories Out[29]: Index(['a', 'b', 'c', 'd'], dtype='object') # 데이터의 실제 범주가 데이터에서 관측된 네 종류를 넘는다 가정했을 때 # set_categories 메서드를 이용해 변경 가능 In [32]: actual_categories = ['a', 'b', 'c', 'd', 'e'] In [33]: cat_s2 = cat_s.cat.set_categories(actual_categories) In [34]: cat_s2 Out[34]: 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'] # 데이터에는 변함 x, 새로운 범주가 추가됨 In [35]: cat_s.value_counts() Out[35]: a 2 b 2 c 2 d 2 Name: count, dtype: int64 In [36]: cat_s2.value_counts() Out[36]: a 2 b 2 c 2 d 2 e 0 Name: count, dtype: int64
분석 과정에서 큰 DataFrame이나 Series를 한번 걸러내고 나면 실제로 데이터에는 존재하지 않는 범주가 남아 있을 수 있음
> remove_unused_categories 메서드로 관측되지 않는 범주 제거
In [37]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])] In [38]: cat_s3 Out[38]: 0 a 1 b 4 a 5 b dtype: category Categories (4, object): ['a', 'b', 'c', 'd'] In [39]: cat_s3.cat.remove_unused_categories() Out[39]: 0 a 1 b 4 a 5 b dtype: category Categories (2, object): ['a', 'b']
모델링을 위한 더미 변수 생성
In [41]: cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category') In [42]: pd.get_dummies(cat_s, dtype='int') Out[42]: 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
'Python' 카테고리의 다른 글
그래프와 시각화 (1) 2023.11.28 데이터 준비: 조인, 병합, 변형 (0) 2023.11.27 데이터 로딩과 저장, 파일 형식 (0) 2023.11.20 판다스 (0) 2023.11.14 넘파이 기본: 배열과 벡터 연산 (0) 2023.11.07