자기계발/Python

[혼공단 9기] 3주차 : Chapter 03 데이터 정제하기

호등 2023. 1. 22. 13:21
반응형


혼공단 9기 3주차 학습 내용 정리~
이거 하면서 더 크게 느낀건데 시간은 정말 빨리 가고 일주일은 매우 짧다.

이번 챕터에서는 데이터 정제하는 방법을 공부했다.
책은 쉽게쉽게 알려주었지만 저번주에 비해 갑자기 난이도가 크게 상승하여 시간도 꽤 많이 걸렸다.

내가 분석하고 싶은 데이터를 직접 전처리하려면 책을 다 공부한 뒤에 직접 해보아야 될 것 같다..!
책만 보고 따라하는건 아무 의미가 없으니 책 한 번 훑고 다시 공부해봐야지


Chapter 03 데이터 정제하기

03-1 불필요한 데이터 삭제

열 삭제하기

위의 데이터 프레임에서 불필요한 열을 삭제해보겠다.
불필요한 열을 삭제하는 방법은 정말 많다.

1) loc 메서드 슬라이싱

ns_book = ns_df.loc[:, '번호':'등록일자']
ns_book.head()

사용 방법이 편하지만 중간에 있는 열을 제외하기 힘들다는 단점이 있다.

2) 불리언 배열 사용

selected_columns = ns_df.columns != 'Unnamed: 13'
ns_book = ns_df.loc[:, selected_columns]
ns_book.head()

selected_columns = ns_df.columns != '부가기호'
ns_book = ns_df.loc[:, selected_columns]
ns_book.head()

Index 클래스 포함 판다스 배열 성격의 객체는 어떤 값과 비교할 때 자동으로 배열의 모든 원소와 하나씩 비교해 준다.
( ➡ 원소별 비교라고 부름 )
!= 비교 연산자를 사용하면 'Unnamed: 13' 열이 아닌 것들은 True가 되거 넘파이 배열로 결과가 반환된다.
위 코드 실행 결과로 각각 'Unnamed: 13', '부가기호' 열이 삭제된 것을 확인할 수 있었다.

3) drop() 메서드

ns_book = ns_df.drop('Unnamed: 13', axis=1)
ns_book.head()

ns_book = ns_df.drop(['부가기호', 'Unnamed: 13'], axis=1)
ns_book.head()

ns_book.drop('주제분류번호', axis=1, inplace=True)
ns_book.head()

drop() 메서드로 원하는 행이나 열을 삭제할 수 있다.
axis 매개변수 기본값은 0(행 삭제), 1을 넣으면 열이 삭제된다.
여러 행을 지정하여 삭제할 수도 있고, inplace 매개변수를 사용해서 별도의 변수에 저장 없이 바로 수정도 가능하다.
( 바로 수정하는 것처럼 보이지만 판다스 내부에서 새로운 수정된 객체를 만들어서 연결해주는 것이다. )

4) dropna() 메서드

ns_book = ns_df.dropna(axis=1)
ns_book.head()

ns_book = ns_df.dropna(axis=1, how='all')
ns_book.head()

비어있는 값(NaN)이 있는 행이나 열을 삭제하는 메서드이다.
모든 값이 NaN인 열을 삭제하려면 how 매개변수를 'all'로 지정해준다.
drop() 메서드와 마찬가지로 axis, inplace 매개변수를 사용할 수 있다.

행 삭제하기

1) [] 연산자와 불리언 배열

selected_rows = ns_df['출판사'] == '한빛미디어'
ns_book2 = ns_book[selected_rows]

ns_book2.head()

# loc 메서드에 불리언 배열 사용
ns_book2 = ns_book.loc[selected_rows]
ns_book3 = ns_book.loc[selected_rows, :] #전체 열을 선택한다는 것을 명시적으로 보여주는 코드(위랑 같음)
ns_book2.head()

불리언 배열은 행을 선택할 때 가장 자주 이용하는 방법이다.
loc 메서드에 불리언 배열을 넣어서 사용할 수도 있다.

2) 조건 넣기

ns_book2 = ns_book[ns_book['대출건수'] > 1000]
ns_book2.head()

조건을 [] 연산자에 바로 넣어 원하는 행을 선택할 수 있다.

중복된 행 찾기 : duplicated() 메서드

sum(ns_book.duplicated())
sum(ns_book.duplicated(subset=['도서명','저자','ISBN']))

dup_rows = ns_book.duplicated(subset=['도서명','저자','ISBN'], keep = False)
ns_book3 = ns_book[dup_rows]
ns_book3.head()

중복된 행 중 처음 행을 제외한 나머지 행은 True, 그 외의 행은 False로 표시한 불리언 배열을 반환하는 메서드
subset 매개변수에 기준이 될 일부 열을 넣어 결과를 확인할 수도 있다.
keep 매개변수는 중복된 행을 모두 True로 표시한다.
(keep = False 를 지정하면 중복된 행 중 처음 행도 True로 표시한다는 말)

그룹별로 합치기

count_df = ns_book[['도서명', '저자', 'ISBN', '권', '대출건수']]

#group_df = count_df.groupby(by=['도서명', '저자', 'ISBN', '권'], dropna=False)
#loan_count = group_df.sum()

#위 코드와 결과가 같지만 일반적으로 두 메서드를 연이어 호출하는 것을 선호함
loan_count = count_df.groupby(by=['도서명','저자','ISBN','권'], dropna=False).sum()
loan_count.head()

groupby() 메서드를 사용하여 행을 합치고, 기준이 되는 열을 지정(by 매개변수 사용)할 수 있다.
groupby() 메서드로 데이터를 합칠 때 기본적으로 열에 NaN 값이 있는 행은 삭제된다. 이 때 행이 삭제가 되지 않게 하기 위해 dropna 매개변수 값을 False로 지정해주었다.

위 코드 실행 결과 기준 열('도서명', '저자', 'ISBN', '권')은 인덱스가 되어 굵은 글씨로 표시된다.

원본 데이터 업데이트

dup_rows = ns_book.duplicated(subset=['도서명','저자','ISBN','권'])

unique_rows = ~dup_rows
ns_book3 = ns_book[unique_rows].copy()

duplicated() 메서드로 중복된 행을 True로 표시한 불리언 배열을 만든 뒤, '~' 연산자를 사용해 중복 안 된 행을 True로 표시해주었다. 판다스에선 copy()메서드를 사용하지 않으면 데이터프레임이 별도의 메모리 공간에 저장되는지 보장하지 않는다고 한다. 복사하지 않고 열을 업데이트 하면 데이터가 바뀔 수 있으므로 업데이트 전 복사하는 것을 권장한다.


03-2 잘못된 데이터 수정하기

누락된 값이 있는 행 확인

#1. 데이터프레임의 요약 정보 출력
ns_book4.info()
#2. isna() 메서드 사용
ns_book4.isna().sum()

info() 메서드로 각 열마다 NaN 값이 있는 행 개수를 대략적으로 파악할 수 있다.
isna() 메서드는 각 행이 비어 있는지를 나타내는 불리언 배열을 반환하는데 sum() 메서드와 함께 사용하여 NaN 값이 있는 행 개수를 바로 파악할 수 있다.

데이터 타입 수정

ns_book4 = ns_book4.astype({'도서권수': 'int32', '대출건수': 'int32'})
ns_book4.head(2)

판다스는 NaN를 특별한 실수 값으로 저장한다. 그렇기 때문에 원래 데이터 타입이 int64였던 열에 NaN이 들어가면 데이터 타입이 float64로 바뀌게 된다. 이럴 때 astype() 메서드로 데이터 타입을 수정해준다.
astype() 메서드에 바꾸고 싶은 내용을 {열 이름:데이터 타입} 형식의 딕셔너리로 넣어주면 된다.

누락된 값 바꾸기(1) - loc(), fillna() 메서드 사용

#1.l'세트 ISBN' 열 누락된 값 빈 문자열로 바꾸기
set_isbn_na_rows = ns_book4['세트 ISBN'].isna()

ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''
ns_book4['세트 ISBN'].isna().sum()

#2. fillna() 메서드로 NaN 값 대체하기
ns_book4.fillna('없음').isna().sum()

#3. 아래 차이 고민해보기
ns_book4['부가기호'].fillna('없음').isna().sum()
ns_book4.fillna({'부가기호': '없음'}).isna().sum()

loc(), fillna() 메서드를 사용하여 NaN 값을 다른 값으로 대체해보았다.

3번 코드에서 특정 열 선택 후 fillna() 메서드를 적용하면 열 이름 없이 개수만 있는 판다스 시리즈 객체로 반환한다. 특정 열의 NaN 값을 대체하면서 전체 데이터프레임을 반환하려면 fillna() 메서드 안에 딕셔너리로 값을 전달해줘야 한다.

누락된 값 바꾸기(2) - replace() 메서드 사용

import numpy as np

#1. 바꾸려는 값이 하나일 때
ns_book4.replace(np.nan, '없음').isna().sum()

#2. 바꾸려는 값이 여러 개일 때
ns_book4.replace([np.nan, '2021'], ['없음', '21']).head(2)
ns_book4.replace({np.nan: '없음', '2021': '21'}).head(2)

#3. 열 마다 다른 값으로 바꿀 때
ns_book4.replace({'부가기호': np.nan}, '없음').head(2)
ns_book4.replace({'부가기호': {np.nan:'없음'}, '발행년도': {'2021': '21'}}).head(2)

바꾸려는 값이 하나일 때 : replace(원래 값, 새로운 값)
바꾸려는 값이 여러 개일 때 :
replace([원래 값1, 원래 값2], [새로운 값1, 새로운 값2])
replace({원래 값1: 새로운 값1, 원래 값2: 새로운 값2})
열 마다 다른 값으로 바꿀 때 :
replace({열 이름: 원래 값}, 새로운 값)
replace({열 이름: {원래 값1: 새로운 값1} })

정규 표현식

표현이 생소해서 이번 챕터에서 제일 이해하기 힘들었다.😂

1) 숫자 찾기: \d

ns_book4.replace({'발행년도': {r'\d\d(\d\d)': r'\1'}}, regex=True)[100:102]
ns_book4.replace({'발행년도': {r'\d{2}(\d{2})': r'\1'}}, regex=True)[100:102]

#테스트
#ns_book4.replace({'발행년도': {s'\d{2}(\d{2})': s'\1'}}, regex=True)[100:102]

정규 표현식(정규식) : 문자열 패턴을 찾아서 대체하기 위한 규칙의 모음
발행연도를 네 자리에서 뒤에 두 자리로 수정하고 싶을 때, 모든 연도를 딕셔너리로 지정하려면 번거롭기에 정규 표현식을 사용하여 수정을 한다.

정규 표현식에서 숫자를 나타내는 기호는 \d, 연도는 네 자리 숫자이므로 \d\d\d\d로 표현할 수 있다.
괄호를 사용해 그룹으로 묶을 수 있고 \d\d(\d\d) 묶은 그룹은 순서대로 1번, 2번 번호가 매겨진다.

{\d\d(\d\d) : \1} ➡ 딕셔너리 형태로 {찾는 패턴 : 수정할 패턴} 지정할 수 있고
{r'\d\d(\d\d)' : r'\1'} ➡ r 문자를 접두사처럼 붙여 정규 표현식을 다른 문자열과 구분한다. (r을 s로 바꿔봤더니 에러뜸)
{r'\d\d(\d\d)' : r'\1'}, regex = True ➡정규 표현식을 사용한다는 의미로 regex 매개변수 옵션을 지정하면 끝

2) 문자 찾기: 마침표(.)

ns_book4.replace({'저자': {r'(.*)\s\(지은이\)(.*)\s\(옮긴이\)': r'\1\2'}, 
				  '발행년도': {r'\d\d(\d\d)': r'\1'}}, regex=True)[100:102]

.* ➡ 문자를 나타내는 기호는 마침표(.), 문자가 몇 개 반복되는지 모르니 * 기호를 붙여준다.
.*(\s\(지은이\) ➡ 공백 문자는 \s로, 괄호 앞에는 일반 문자임을 인식시키기 위해 역슬래시(\)를 붙여 준다.
{ (.*)\s\(지은이\)(.*)\s\(옮긴이\) : r'\1\2' }, regex = Ture ➡ 그룹 지정해주고, regex 매개변수 옵션 지정

잘못된 값 바꾸기

#str속성 아래의 contains() 메서드
ns_book4['발행년도'].str.contains('1988').sum()

#숫자가 아닌 문자에 대응하는 표현식 : \D
invalid_number = ns_book4['발행년도'].str.contains('\D', na=True)
ns_book4[invalid_number].head()

#연도 앞 뒤의 문자 제외 - 정규표현식 사용
ns_book5 = ns_book4.replace({'발행년도':'.*(\d{4}).*'}, r'\1', regex=True)
ns_book5[invalid_number].head()

#이 외에 변환되지 않은 값
unkown_year = ns_book5['발행년도'].str.contains('\D', na=True)
ns_book5.loc[unkown_year, '발행년도'] = '-1'
ns_book5 = ns_book5.astype({'발행년도': 'int32'})

#발행년도가 4000년이 넘는 경우 확인
ns_book5['발행년도'].gt(4000).sum()
dangun_yy_rows = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_yy_rows, '발행년도'] = ns_book5.loc[dangun_yy_rows, '발행년도'] - 23333

#다시 정제된 데이터 확인 & 정제
dangun_year = ns_book5['발행년도'].gt(4000) #13건의 데이터 확인
ns_book5.loc[dangun_year, '발행년도'] = -1

#연도가 작은 값 확인 & 정제
old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
ns_book5[old_books]
ns_book5.loc[old_books, '발행년도'] = -1

str속성의 contains() 메서드 : 시리즈나 인덱스에서 문자열 패턴을 포함하고 있는지 검사
\D : \d의 반대 개념으로 숫자가 아닌 모든 문자에 대응하는 표현
gt() 메서드 : 전달된 값보다 큰 값을 찾는다
lt() 메서드 : 전달된 값보다 작은 값을 찾는다
eq() 메서드 : 전달된 값과 같은 값을 찾는다

누락된 정보 채우기(208p~216p)

데이터 분석에 쓰이는 열에는 누락된 정보가 있으면 안된다. 교재에선 requests와 BeautifulSoup 패키지를 이용하여 온라인 서점에 있는 정보를 가져와 채워 넣었다. 그랬음에도 데이터가 누락된 행들이 정말 많았는데, 데이터 정제하는 작업이 생각보다 어렵고 시간이 많이 드는 작업이라는 생각이 든다. 혼자서 이 코드들을 작성하면서 함수를 짤 수 있을까😭


혼공학습단(혼자 공부하는 데이터 분석 with 파이썬) 3주차_기본 미션

2. 1번 문제의 데이터프레임에서 'col1' 열의 합을 계산하는 명령으로 올밥르지 않은 것은 무엇인가요?
1) df['col1'].sum()
2) df[['col1']].sum()
3) df.loc[:, df.columns == 'col1'].sum()
4) df.loc[:, [False, False, True]].sum()

혼공학습단(혼자 공부하는 데이터 분석 with 파이썬) 3주차_선택 미션

5. 다음과 df 데이터프레임에서 df.replace(r'ba.*', 'new', regex=True)의 결과는 무엇인가요?

  A B
0 bat abc
1 foo bar
2 bait xyz

1)번, ba로 시작하는 'bat', 'bait', 'bar' 만 new로 바뀜

반응형