Chapter03 "데이터 정제하기"
Chapter 03-2의 주제는 '잘못된 데이터 수정하기' 이다.
- 판다스에서 누락된 값은 NaN이라고 표시한다.
- NaN을 찾는 방법과 NaN을 수정하는 방법을 배울 예정
데이터 프레임 정보 요약 확인하기
먼저 03-1에서 처리한 남산도서관 데이터를 가져온다.
import gdown
gdown.download('https://bit.ly/3GisL6J','ns_book4.csv',quiet = False)
import pandas as pd
ns_book4 = pd.read_csv('ns_book4.csv',low_memory = False)
요약 정보는 아래와 같이 확인한다.
# ns_book4의 요약 정보
ns_book4.info()
누락된 값 처리하기
누락된 값 개수 확인하기: isna() 메서드
# isna로 NaN 개수확인
# 반대는 notna()
ns_book4.isna().sum()
누락된 값 으로 표시하기 : None과 np.nan
- 숫자를 담는 열에 None 저장
# 도서권수 누락 하나 만들어주기
ns_book4.loc[0, '도서권수'] = None
ns_book4['도서권수'].isna().sum()
ns_book4.head(2)
# 데이터 타입 지정 : astype()
ns_book4.loc[0,'도서권수'] = 1
ns_book4 = ns_book4.astype({'도서권수':'int32','대출건수':'int32'})
ns_book4.head(2)
- 문자열을 담는 열에 None 저장
# 문자열을 담는 열에 None 저장
ns_book4.loc[0,'부가기호'] = None
ns_book4.head(2)
# None -> NaN : np.nan
import numpy as np
ns_book4.loc[0,'부가기호'] = np.nan
ns_book4.head(2)
누락된 값 바꾸기(1): loc, fillna()메서드
# fillna() 없이 바꾸기
set_isbn_na_rows = ns_book4['세트 ISBN'].isna() # 누락된 값을 찾아 불리언 배열로 반환
ns_book4.loc[set_isbn_na_rows,'세트 ISBN'] = '' # 누락된 값을 빈 문자열로 바꿈
ns_book4['세트 ISBN'].isna().sum()
# fillna() 사용
ns_book4.fillna('없음').isna().sum()
# 특정 열만 선택
ns_book4['부가기호'].fillna('없음').isna().sum()
# 전체 데이터프레임 반환을 하려면 딕셔너리를 사용
ns_book4.fillna({'부가기호':'없음'}).isna().sum()
누락된 값 바꾸기(2): replace()메서드
- 바꾸려는 값이 하나일 때
- replace(원래 값, 새로운 값)
- 바꾸려는 값이 여러 개일 때: 리스트, 딕셔너리
- replace([ 원래 값1,원래 값2 ], [ 새로운 값1,새로운 값2 ] )
- 열 마다 다른 값으로 바꿀 때: 딕셔너리
- replace({열 이름:원래 값},새로운 값)
- 중첩된 딕셔너리 : replace({열 이름: {원래 값1,새로운 값1}})
## 1 바꾸려는 값이 하나일 때
# replace(원래 값, 새로운 값)
ns_book4.replace(np.nan, '없음').isna().sum()
## 2 바꾸려는 값이 여러 개일 때: 리스트, 딕셔너리
# replace([ 원래 값1,원래 값2 ], [ 새로운 값1,새로운 값2 ])
# 리스트
ns_book4.replace([np.nan,'2021'], ['없음','21']).head(2)
# 딕셔너리
ns_book4.replace({np.nan:'없음','2021':'21'}).head(2)
## 3 열 마다 다른 값으로 바꿀 때: 딕셔너리
# replace({열 이름:원래 값},새로운 값)
# 중첩된 딕셔너리 : replace({열 이름: {원래 값1,새로운 값1}})
ns_book4.replace({'부가기호':np.nan}, '없음').head(2)
# 중첩된 딕셔너리
ns_book4.replace({'부가기호':{np.nan: '없음'},
'발행년도':{'2021':'21'}}).head(2)
정규표현식
replace()메서드를 사용해도 2021을 21로 바꾸는게 가능해도 2018은 바꿀 수 없다.
이를 위해 정규표현식 사용
정규표현식은 문자열 패턴을 찾아서 대체하기 위한 규칙의 모음
숫자 찾기: \d
# 숫자는 \d. 네 자리 숫자는 \d\d\d\d가 됨. 그룹으로 묶으면 ()를 사용. 그룹은 \1\2
# 정규표현식을 사용한다는 의미로 regex 매개변수를 True로 저장
ns_book4.replace({'발행년도':{r'\d\d(\d\d)':r'\1'}}, regex = True)[100:102]
# r은 파이썬에서 정규 표현식을 다른 문자열과 구분하기 위해 접두사처럼 붙임
# \d\d\d\d처럼 일일이 쓰지 않고 중괄호로 묶기 가능
ns_book4.replace({'발행년도':{r'\d{2}}(\d{2})':r'\1'}}, regex = True)[100:102]
문자 찾기: 마침표( . )
- 모든 문자 : .
- 공백 : \s
- 만약 "로런스 인그래시아 (지은이), 안기순(옮긴이)"에서 지은이와 옮긴이를 삭제한다면
- (.*)\s\(지은이\)(.*)\s\(옮긴이\)
- ()는 정규표현식에서 그룹을 의미하지만 그룹으로 쓰이지 않는 괄호는 \를 이용해 탈출
- *은 왼쪽의 표현식이 한 번 이상 나타난다는 의미이다.
ns_book4.replace({'저자':{r'(.*)\s\(지은이\)(.*)\s\(옮긴이\)':r'\1\2'},
'발행년도':{r'\d{2}}(\d{2})':r'\1'}}, regex = True)[100:102]
잘못된 값 바꾸기
ns_book4.astype({'발행년도':'int32'})
# "1988." 를 변환하지 못함
# 1988.을 얼마나 가지고 있는지 확인
ns_book4['발행년도'].str.contains('1988').sum()
# 407
# contains 메서드는 기본적으로 정규 표현식을 인식.
# \d가 숫자이면 \D는 숫자가 아닌 다른 모든 문자를 뜻함
# '발행년도' 열에서 숫자가 아닌 문자를 포함하는 모든 행 찾기
# '발행년도' 열에서 숫자가 아닌 문자를 포함하는 모든 행 찾기
invalid_number = ns_book4['발행년도'].str.contains('\D', na = True)
print(invalid_number.sum())
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)
print(unkown_year.sum())
ns_book5[unkown_year].head()
# 임의로 -1로 바꾸고 정수형 변환을 해주겠다
ns_book5.loc[unkown_year, '발행년도'] = -1
ns_book5 = ns_book5.astype({'발행년도':'int32'})
# gt : 전달된 값보다 더 큰 값 찾기
ns_book5['발행년도'].gt(4000).sum()
# 단군기원인 경우에는 2333을 빼줌
dangun_yy_rows = ns_book5['발행년도'].gt(4000) # 4000보다 큰 값
ns_book5.loc[dangun_yy_rows,'발행년도'] = ns_book5.loc[dangun_yy_rows,'발행년도'] - 2333
dangun_year = ns_book5['발행년도'].gt(4000)
# 그래도 연도가 이상하게 높으면 -1
ns_book5.loc[dangun_year, '발행년도'] = -1
old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900) #0보다 크고 1900보다 작은 값
ns_book5.loc[old_books, '발행년도'] = -1
ns_book5['발행년도'].eq(-1).sum()
네 자리 숫자에서도 연도가 너무 작거나, 너무 큰 경우를 처리해주는 코드이다.
누락된 정보 채우기
뷰티플수프를 사용해 값을 채우도록 하겠다
import requests
from bs4 import BeautifulSoup
# isbn으로 책 제목을 반환해주는 함수
def get_book_title(isbn):
# Yes24 도서 검색 페이지 URL
url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
# URL에 ISBN을 넣어 HTML을 가져옴
r = requests.get(url.format(isbn))
soup = BeautifulSoup(r.text,'html.parser')
# 클래스 이름이 'gd_name'인 <a>태그의 텍스트를 가져오기
title = soup.find('a',attrs = {'class':'gd_name'}).get_text()
return title
# 저자, 출판사, 발행 연도를 추출하여 반환하는 함수
import re
def get_book_info(row):
title = row['도서명']
author = row['저자']
pub = row['출판사']
year = row['발행년도']
# Yes24 도서 검색 페이지 URL
url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'
# URL에 ISBN을 넣어 HTML을 가져옴
r = requests.get(url.format(row['ISBN']))
soup = BeautifulSoup(r.text,'html.parser')
try:
if pd.isna(title):
# 클래스 이름이 'gd_name'인 <a>태그의 텍스트를 가져옴
title = soup.find('a',attrs = {'class':'gd_name'}).get_text()
except AttributeError:
pass
try:
if pd.isna(author):
# 클래스 이름이 'info_auth'인 <span>태그 아래 <a>태그의 텍스트를 가져옴
authors = soup.find('span', attrs = {'class':'info_auth'} ).find_all('a')
author_list = [auth.get_text() for auth in authors] # 모든 텍스트를 리스트에 저장
author = ','.join(author_list) # 하나의 문자열로 합침
except AttributeError:
pass
try:
if pd.isna(pub):
# 클래스 이름이 'info_pub'인 <span> 태그 아래 <a> 태그의 텍스트를 가져옴
pub = soup.find('span',attrs = {'class':'info_pub'}).find('a').get_text()
except AttributeError:
pass
try:
if year == -1:
# 클래스 이름이 'info_date'인 <span> 태그 아래 텍스트를 가져옴
year_str = soup.find('span',attrs = {'class':'info_date'}).get_text()
# 정규 표현식으로 찾은 값 중에 첫 번재 것만 사용
year = re.findall(r'\d{4}', year_str)[0]
except AttributeError:
pass
return title, author, pub, year
# 함수 적용
updated_sample = ns_book5[na_rows].head(2).apply(get_book_info, axis = 1, result_type = 'expand')
updated_sample
교재에서는 함수를 적용한 데이터 프레임을 미리 만들어 놓았다.
# 함수 적용
ns_book5_update = ns_book5[na_rows].apply(get_book_info, axis = 1, result_type = 'expand')
# ns_book5를 ns_book5_update로 업데이트 한 후 누락된 행 확인
ns_book5.update(ns_book5_update)
na_rows = ns_book5['도서명'].isna() | ns_book5['저자'].isna()
| ns_book5['출판사'].isna() | ns_book5['발행년도'].eq(-1)
print(na_rows.sum())
# dropna()로 행 삭제
ns_book5 = ns_book5.astype({'발행년도':'int32'})
ns_book6 = ns_book5.dropna(subset = ['도서명','저자','출판사'])
ns_book6 = ns_book6[ns_book6['발행년도'] != -1]
ns_book6.head()
데이터를 이해하고 올바르게 정제하기
- 누락된 값을 바꾸고 정규 표현식을 이용해 수정함
- 누락된 값 채우는 방법을 알아봄
일괄 처리 함수
def data_fixing(ns_book4):
"""
잘못된 값을 수정하거나 NaN을 채우는 함수
:param ns_book4: data_cleaning 함수에서 전처리된 데이터프레임
"""
# 도서권수와 대출건수를 int32로 바꾼다
ns_book4 = ns_book4.astype({'도서권수':'int32','대출건수':'int32'})
# NaN인 세트 ISBN을 빈 문자열로 바꾼다
set_isbn_na_rows = ns_book4['세트 ISBN'].isna()
ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''
# 발행년도 열에서 연도 네 자리를 추출하여 대체한다. 나머지 발행년도는 -1로 대체
ns_book5 = ns_book4.replace({'발행년도':'.*(\d{4}).*'},r'\1',regex = True)
unknown_year = ns_book5['발행년도'].str.contains('\D', na = True)
ns_book5.loc[unknown_year, '발행년도'] = '-1'
# 발행년도를 int32로 바꾼다
ns_book5 = ns_book5.astype({'발행년도':'int32'})
# 4000년 이상인 경우 2333년을 빼줌
dangun_yy_rows = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_yy_rows, '발행년도'] = ns_book5.loc[dangun_yy_rows, '발행년도'] - 2333
# 여전히 4000년 이상인 경우 -1로 바꿈
dangun_year = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_year, '발행년도'] = -1
# 0~1900년 사이의 발행년도는 -1
old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
ns_book5.loc[old_books, '발행년도'] = -1
# 도서명, 저자, 출판사가 NaN이거나 발행년도가 -1인 행 찾기
na_rows = ns_book5['도서명'].isna() | ns_book5['저자'].isna()
| ns_book5['출판사'].isna() | ns_book5['발행년도'].eq(-1)
# Yes24 도서 상세 페이지에서 누락된 정보 채우기
updated_sample = ns_book5[na_rows].apply(get_book_info, axis = 1, result_type = 'expand')
# 도서명, 저자, 출판사가 NaN이거나 발행년도가 -1인 행 삭제
ns_book6 = ns_book5.dropna(subset = ['도서명', '저자', '출판사'])
ns_book6 = ns_book6[ns_book6['발행년도'] != -1]
return ns_book6
기본미션
df1 데이터 프레임 | col1 | col2 | col3 |
0 | 1 | a | NaN |
1 | 2 | NaN | NaN |
2 | 3 | c | 100.0 |
선택미션
3주차 후기
3주차는 데이터 정제란 주제로 많은 걸 배웠다. 정말 양이 많다!
사실 결과 캡처하는게 귀찮아서 블로그도 주피터처럼 실행결과가 자동으로 나오게 됐음 좋겟다 ~ 라는 마음에 후기를 남겼는데.. 그냥 실행결과 공유라고 나와버려서 혼공족장님뿐만 아니라 다른 사람들도 그냥 읽는데 이해가 안되는 문장을 작성한 것 같다. 하지만 이번주는 귀찮음을 참고 캡처했다.
'Data Analysis > 혼공학습단9기' 카테고리의 다른 글
혼자 공부하는 데이터 분석 with 파이썬: 4주차(Chapter 04-2) (0) | 2023.02.19 |
---|---|
혼자 공부하는 데이터 분석 with 파이썬: 4주차(Chapter 04-1) (0) | 2023.02.19 |
혼자 공부하는 데이터 분석 with 파이썬: 3주차(Chapter 03-1) (0) | 2023.01.25 |
혼자 공부하는 데이터 분석 with 파이썬: 2주차(Chapter 02-2) (0) | 2023.01.15 |
혼자 공부하는 데이터 분석 with 파이썬 : 2주차(Chapter 02-1) (0) | 2023.01.11 |