혼자 공부하는 데이터 분석 with 파이썬 03-2 잘못된 데이터 수정하기

손지호·2024년 1월 22일
0

데이터프레임 정보 요약 확인하기

import gdown

gdown.download('http s://bit.ly/3GisL6J', 'ns_book4.csv', quiet=False)

# 판다스 데이터프레임으로 불러 온 후,  head() 메서드로 처음 다섯 개 행 출력해 데이터 확인.
import pandas as pd

ns_book4 = pd.read_csv('ns_book4.csv', low_memory=False)
ns_book4.head()
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319	NaN	NaN	NaN	NaN	1	0.0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969	NaN	NaN	NaN	NaN	1	0.0	2021-03-19
2	3	나도 한 문장 잘 쓰면 바랄 게 없겠네	김선영 지음	블랙피쉬	2021	9788968332982	NaN	NaN	NaN	NaN	1	0.0	2021-03-19
3	4	예루살렘 해변	이도 게펜 지음, 임재희 옮김	문학세계사	2021	9788970759906	NaN	NaN	NaN	NaN	1	0.0	2021-03-19
4	5	김성곤의 중국한시기행 : 장강·황하 편	김성곤 지음	김영사	2021	9788934990833	NaN	NaN	NaN	NaN	1	0.0	2021-03-19

# info() 메서드 활용해 NaN 값 몇 개인지 확인
ns_book4.info()
>>> <class 'pandas.core.frame.DataFrame'>
RangeIndex: 384591 entries, 0 to 384590 # '384591' : 전체 행 개수
Data columns (total 13 columns):
 #   Column   Non-Null Count   Dtype   # 'Column' : 열 이름
---  ------   --------------   -----  
 0   번호       384591 non-null  int64  
 1   도서명      384188 non-null  object 
 2   저자       384393 non-null  object 
 3   출판사      379950 non-null  object  # '379950 non-null' : 누락된 값이 없는 행 가수
 4   발행년도     384577 non-null  object 
 5   ISBN     384591 non-null  object 
 6   세트 ISBN  56576 non-null   object 
 7   부가기호     310386 non-null  object 
 8   권        63378 non-null   object 
 9   주제분류번호   364727 non-null  object 
 10  도서권수     384591 non-null  int64  
 11  대출건수     384591 non-null  float64
 12  등록일자     384591 non-null  object 
dtypes: float64(1), int64(2), object(10) → 사용하는 데이터 타입
memory usage: 38.1+ MB → 메모리 사용량

'도서명' 열은 NaN이 아닌 행 개수가 384,188개이므로 전체 행 개수 중 누락된 값이 403개임을 알 수 있다.
float64는 실수형, int64는 정수형, object는 문자열 또는 혼합형 데이터 타입.


누락된 값 처리하기

각 열의 누락된 값이 정확하게 몇 개가 있는지 확인하고, 누락된 값 표시하는 방법 알아보기!


누락된 값 개수 확인하기: insa() 메서드

info() 메서드로 출력한 값으로 누락된 값이 있는 행 개수를 헤아릴 수 있지만 조금 번거로움. 대신 isna() 메서드를 사용하면 각 행이 비어 있는지를 나타내는 불리언 배열을 반환한다. 그리고 sum() 메서드를 이어서 호출하면 불리언 배열의 True 개수로 비어 있는 행 개수를 얻을 수 있다.

ns_book4.isna().sum()
>>> 번호              0
도서명           403
저자            198
출판사          4641
발행연도           14
ISBN            0
세트 ISBN    328015
부가기호        74205
권          321213
주제분류번호      19864
도서권수            0
대출건수            0
등록일자            0
dtype: int64 → 열 데이터 타입은 int64로 64 비트 정수를 저장할 수 있다.

+ 누락되지 않은 값을 확인하는 메서드도 있나요?
isna() 메서드와 반대로 누락되지 않은 값을 확인할 때는 notna() 메서드를 사용함.


누락된 값으로 표시하기: None과 np.nan

판다스 데이터프레임에서는 정수를 저장하는 열에 파이썬의 None을 입력하면 누락된 값으로 인식한다.
ns_book4 데이터프레임의 '도서권수' 열에 있는 첫 번째 행 값을 None으로 바꾼 후, '도서권수' 열에 isna() 메서드를 적용해 누락된 값을 세어보자.

ns_book4.loc[0, '도서권수'] = None
ns_book4['도서권수'].isna().sum()
>>> 1

# head() 메서드로 처음 두 개의 행을 출력해보자!
ns_book4.head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319	NaN	NaN	NaN	NaN	NaN	0.0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969	NaN	NaN	NaN	NaN	1.0	0.0	2021-03-19

NaN이 출력된다. 그런데 두 번째 행의 '도서권수' 값이 1이었는데 1.0으로 바뀌었다. 이는 판다스가 NaN을 특별한 실수 값으로 저장하기 때문이다. 그래서 원래 데이터 타입이 int64였던 '도서권수' 열이 NaN을 표시하기 위해 float64로 자동으로 바뀐다.
데이터 타입을 지정할 때는 astype() 메서드를 사용한다. 매개변수를 [ 열이름:데이터 타입 ] 형식으로 딕셔너리로 전달한다.

ns_book4.loc[0, '도서권수'] = 1
ns_book4 = ns_book4.astype({'도서권수':'int32', '대출건수': 'int32'})
# {'도서권수':'int32', '대출건수': 'int32'} : 매개변수를 딕셔너리 형식으로 전달.
ns_book4.head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319	NaN	NaN	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969	NaN	NaN	NaN	NaN	1	0	2021-03-19

'대출건수'가 모두 정수형으로 바뀜!

# 문자열을 저장할 수 있는 열에 None을 입력해보자.
# 데이터 타입이 object인 '부가기호' 열의 첫 번째 행에 None을 입력.
ns_book4.loc[0, '부가기호'] = None
ns_book4.head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319	NaN	None	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969	NaN	NaN	NaN	NaN	1	0	2021-03-19

NaN이 아닌 문자열 그대로 None으로 표시됨. 판다스는 NaN이란 값 따로 가지고 있지 않음. 대신 넘파이 패키지에 있는 np.na을 사용. 따라서 첫 번째 행의 '부가기호' 열의 값을 None에서 NaN으로 표시하려면 np.nan을 사용한다.

import numpy as np

ns_book4.loc[0, '부가기호'] = np.nan
ns_book4.head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319	NaN	NaN	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969	NaN	NaN	NaN	NaN	1	0	2021-03-19

이제 누락된 값을 다른 값으로 바꿔보자!


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

loc 메서드를 사용하면 누락된 값을 원하는 값으로 바꿀 수 있다. 그러려면 누락된 값을 가리키는 불리언 배열을 만드러야 하는데, 누락된 값을 확인하는 isna() 메서드로 간단하게 만들 수 있다.
isna() 메서드로 '세트 ISBN' 열의 NaN을 가리키는 set_isbn_na_rows 불리언 배열을 만들고 loc 메서드에 전달한다.

set_isbn_na_rows = ns_book4['세트 ISBN'].isna() → 누락된 값을 찾아 불리언 배열로 반환
ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''  → 누락된 값을 빈 문자열로 바꿈.
ns_book4['세트 ISBN'].isna().sum() → 누락된 값이 몇 개인지 셈.
>>> 0

'세트 ISBN' 열의 NaN을 모두 빈 문자열로 바꾸었기 때문에 누락된 행의 개수는 0!!
fillna() 메서드를 사용하면 loc 메서드로 NaN을 다른 값으로 바꾸는 것보다 원하는 값을 전달하면 된다!!

# ns_book4에 있는 모든 NaN을 '없음' 문자열로 바꾸어보자.
# fillna() 메서드는 기본적으로 새로운 데이터프레임을 반환하므로 fillna() 메서드에 이어서 isna() 메서드를 연결하면 Nan의 개수를 샐 수 있다.

ns_book4.fillna('없음').isna().sum()
>>> 번호         0
도서명        0
저자         0
출판사        0
발행연도       0
ISBN       0
세트 ISBN    0
부가기호       0
권          0
주제분류번호     0
도서권수       0
대출건수       0
등록일자       0
dtype: int64

NaN이 모두 '없음' 문자열로 바뀌어 NaN 개수 세면 0이 출력됨!

# 특정 열만 선택해서 NaN을 바꿀 수도 있음!
ns_book4['부가기호'].fillna('없음').isna().sum()
>>> 0

하지만 특정 열을 선택한 후, fillna() 메서드를 적용하면 앞의 실행 결과처럼 열 이름 없이 개수만 있는 판다스 시리즈 객체로 반환한다.
'부가기호' 열의 NaN을 바꾸면서 전체 데이터프레임을 반환하려면 다음처럼 열 이름과 바꾸려는 값으로 이루어진 딕셔너리를 전달하면 된다.

ns_book4.fillna({'부가기호':'없음'}).isna().sum()
>>> 번호              0
도서명           403
저자            198
출판사          4641
발행연도           14
ISBN            0
세트 ISBN         0
부가기호            0
권          321213
주제분류번호      19864
도서권수            0
대출건수            0
등록일자            0
dtype: int64

누락된 값 바꾸기(2): replace() 메서드

replace() 메서드 : NaN은 물론 어떤 값도 바꿀 수 있는 편리한 메서드.

첫째, 바꾸려는 값이 하나일 때

replace() 메서드의 첫 번째 매개변수에는 원래 값을, 두 번째 매개변수에는 새로운 값을 전달한다.

  • replace(원래 값, 새로운 값)
ns_book4.replace(np.nan, '없음').isna().sum()
>>> 번호         0
도서명        0
저자         0
출판사        0
발행연도       0
ISBN       0
세트 ISBN    0
부가기호       0
권          0
주제분류번호     0
도서권수       0
대출건수       0
등록일자       0
dtype: int64
둘째, 바꾸려는 값이 여러 개일 때

리스트 형식으로 전달.

  • replace([원래 값1, 원래 값1], [새로운 값1, 새로운 값2])
ns_book4.replace([np.nan, '2021'], ['없음', '21']).head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	21	9788937444319		없음	없음	없음	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	21	9791190123969		없음	없음	없음	1	0	2021-03-19

또한 리스트 대신 ([원래 값1: 새로운 값1, 원래 값2 : 새로운 값2])처럼 딕셔너리 형식으로도 전달 가능.

ns_book4.replace({np.nan: '없음', '2021': '21'}).head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	21	9788937444319		없음	없음	없음	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	21	9791190123969		없음	없음	없음	1	0	2021-03-19
셋째, 열 마다 다른 값으로 바꿀 때

딕셔너리 형식으로 전달하여 열마다 다른 값을 바꿀 수 있음.

  • replace({열 이름: 원래 값}, 새로운 값)
ns_book4.replace({'부가기호': np.nan}, '없음').head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319		없음	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969		없음	NaN	NaN	1	0	2021-03-19

→ '부가기호' 열의 NaN만 '없음'으로 바뀐다.
또는 열 이름과 변경 전후의 값을 ({열 이름: {원래 값1: 새로운 값1}})과 같이 중첩된 딕셔너리로 전달할 수 있음.
'부가기호' 열의 NaN을 '없음'으로 바꾸고 '발행연도' 열의 '2021'을 '21'로 바꾸면 다음과 같다.

ns_book4.replace({'부가기호': {np.nan: '없음'}, '발행연도': {'2021': '21'}}).head(2)
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	21	9788937444319		없음	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	21	9791190123969		없음	NaN	NaN	1	0	2021-03-19

정규 표현식

정규 표현식(regular expression) : 줄여서 정규식은 문자열 패턴을 찾아서 대체하기 위한 규칙의 모음. replace() 메서드로 인덱스 100번과 101번 행의 네 자리 연도('2021')를 두 자리('21')로 바꾸어 보면 다음과 같다. 이때 연도가 다르면 문제가 된다. 연도가 '2021'일 경우 '21'로 바뀌지만 '2018'은 적용되지 않는다.

ns_book4.replace({'발행연도': {'2021': '21'}})[100:102]
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
100	101	No라고 말할 줄 아는 남편과 아내 - 개정판	헨리 클라우드, 존 타운센드 (지은이), 김진웅 (옮긴이)	좋은씨앗	2018	9788958743019		NaN	NaN	234.9	1	1	2021-03-15
101	102	D2C 레볼루션 - 스타트업부터 글로벌 기업까지, 마켓 체인저의 필수 전략	로런스 인그래시아 (지은이), 안기순 (옮긴이)	부키	21	9788960518483		NaN	NaN	325.1	1	0	2021-03-15

+ replace() 메서드 뒤에 대괄호[] 써도 되나요?
가능!! 판다스 데이터프레임의 다른 메서드와 마찬가지로 replace() 메서드는 기본적으로 새로운 데이터프레임을 반환함. 그래서 여러 메서드를 연이어 쓸 수 있다. 마찬가지로 대괄호를 사용한 인덱싱도 연이어 쓸 수 있다. 앞 코드는 다음과 같이 풀어서 쓴 것과 동일하다.
ns_temp = ns_book4.replace({'발행연도' : {'2021' : '21'}})
ns_temp[100:102]

물론 replace() 메서드에 '2018'을 '18'로 바꾸도록 추가할 수 있다. 하지만 도서 발행 연도는 범위가 넓기 때문에 이런 식으로 일일이 모두 기재하여 바꾸는 것은 번거로움! 이럴 때 정규 표현식 사용하면 훨씬 간편하게 이런 작업 가능!


숫자 찾기: \d

정규 표현식에서 숫자를 나타내는 기호는 \d이다. 네 자리 연도에 해당하는 표현은 \d\d\d\d이다. 표현식을 그룹으로 묶을 때는 괄호를 쓴다. 뒤에 두 자리만 하나의 그룹으로 묶을 때는 \d\d(\d\d) 처럼 쓴다. 문자열로 원본 문자열을 바꾸는 과정을 그림으로 살펴보면 오른쪽과 같다.
앞의 그림에서 네 개의 숫자로 이루어진 것은 '2021' 하나 뿐. 이 문자열이 \d\d(\d\d) 패턴과 일치. 'A21'은 영문자가 있고 세 글자로 이루어져 있기 때문에 해당 X. '21.9'는 중간에 숫자가 아닌 문자가 들어 있기에 해당 X.
따라서 패턴에 맞는 문자열을 찾은 후 첫 번째 그룹에 해당하는 뒷자리 연도 두 개를 추출함. 패턴 안에 있는 그룹을 나타낼 때는 \1, \2처럼 사용. 그룹의 번호는 패턴 안에 등장하는 순서대로 매겨진다.
그럼 '발행연도' 열의 값을 정규 표현식으로 두 자리 연도로 바꾸어보자. 앞에서 사용한 replace() 메서드와 아주 비슷하지만 정규 표현식을 사용한다는 의미로 regex 매개변수 옵션을 True로 지정한다.

ns_book4.replace({'발행연도': {r'\d\d(\d\d)': r'\1'}}, regex=True)[100:102]
# '{r'\d\d(\d\d)': r'\1'}' : 패턴 안에 있는 첫 번째 그룹으로 연도를 바꾼다.
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
100	101	No라고 말할 줄 아는 남편과 아내 - 개정판	헨리 클라우드, 존 타운센드 (지은이), 김진웅 (옮긴이)	좋은씨앗	18	9788958743019		NaN	NaN	234.9	1	1	2021-03-15
101	102	D2C 레볼루션 - 스타트업부터 글로벌 기업까지, 마켓 체인저의 필수 전략	로런스 인그래시아 (지은이), 안기순 (옮긴이)	부키	21	9788960518483		NaN	NaN	325.1	1	0	2021-03-15

이때 정규 표현식 앞에 붙인 r 문자는 파이썬에서 정규 표현식을 다른 문자열과 구분하기 위해 접두사처럼 붙인다.
정규 표현식이 반복될 때는 일일이 쓰는 대신 중괄호를 사용해 개수를 지정할 수 있다. 예를 들어 \d{2}는 \d\d와 동일하게 연속된 숫자 두 개를 의미한다. 앞에서 사용한 숫자 네 개를 찾는 정규 표현식을 이런 식으로 표현하면 \d{2}(\d{2})와 같이 쓸 수 있다.

ns_book4.replace({'발행연도': {r'\d{2}(\d{2})': r'\1'}}, regex=True)[100:102]
# '\d{2}(\d{2})' : \d\d(\d\d)와 같은 의미
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
100	101	No라고 말할 줄 아는 남편과 아내 - 개정판	헨리 클라우드, 존 타운센드 (지은이), 김진웅 (옮긴이)	좋은씨앗	18	9788958743019		NaN	NaN	234.9	1	1	2021-03-15
101	102	D2C 레볼루션 - 스타트업부터 글로벌 기업까지, 마켓 체인저의 필수 전략	로런스 인그래시아 (지은이), 안기순 (옮긴이)	부키	21	9788960518483		NaN	NaN	325.1	1	0	2021-03-15

문자 찾기: 마침표(.)

![](http s://velog.velcdn.com/images/sjh1112/post/96515778-d91e-46ac-9623-2c49df7d5fc1/image.png)
어떤 문자에도 대응하는 정규 표현식 문자는 마침표(.) 이다. '로런스 인그래시아'와 같은 이름에는 어떤 문자가 올지 모르기 때문에 마침표를 쓰는 게 좋다. 또한 이름은 몇 개의 글자로 이루어질지 알 수 없기 때문에 앞서 연도처럼 반복 개수를 지정하기 어렵다. 이럴 때는 * 문자 를 사용하여 0개 이상 반복된다고 표시할 수 있다.
저자 이름은 삭제하지 않고 남겨 놓아야 하므로 나중에 첫 번째 그룹으로 참조하기 위해 괄호로 묶는다. 그 다음 '(지은이)' 문자열을 찾는다. 그런데 찾으려는 패턴에 괄호가 있다. 정규 표현식에서 괄호는 그룹을 나타내는 데 사용하므로 문자라고 인식하게 하려면 역슬래시()를 앞에 붙여야 한다. 그리고 띄어쓰기가 있으므로 공백 문자를 나타내는 정규 표현식 \s를 앞에 붙인다.
이어서 쉼표와 역자 이름을 포함한 ', 안기순'은 그대로 남겨 놓아야 한다. 역자 이름도 몇 글자가 될지 모르기 때문에 저자 이름과 마찬가지로 .*표현을 적용한다.
마지막으로 '(옮긴이)'에서 괄호는 역슬래시를 앞에 붙여 그룹을 만들기 위한 괄호가 아니라는 것을 표시한다.
이 정규 표현식을 replace() 메서드에 적용해보자!

ns_book4.replace({'저자': {r'(.*)\s\(지은이\)(.*)\s\(옮긴이\)': r'\1\2'},
                  '발행연도': {r'\d{2}(\d{2})': r'\1'}}, regex=True)[100:102]
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
100	101	No라고 말할 줄 아는 남편과 아내 - 개정판	헨리 클라우드, 존 타운센드, 김진웅	좋은씨앗	18	9788958743019		NaN	NaN	234.9	1	1	2021-03-15
101	102	D2C 레볼루션 - 스타트업부터 글로벌 기업까지, 마켓 체인저의 필수 전략	로런스 인그래시아, 안기순	부키	21	9788960518483		NaN	NaN	325.1	1	0	2021-03-15

잘못된 값 바꾸기

astype() 메서드로 '발행연도' 열을 int32로 바꾸고 싶었는데, 오류가 난다!
'1988.'이라는 연도를 변환할 수 없어 오류가 발생했다. 아무래도 숫자가 아닌 다른 문자가 들어있는 연도가 있나 보다. 정규 표현식으로 다른 문자가 있는 연도를 찾아보자!
판다스 시리즈 객체는 str 속성 아래 다양한 문자열 처리 함수를 제공한다. 그 중 contains() 메서드는 시리즈나 인덱스에서 문자열 패턴을 포함하고 있는지 검사한다. '발행연도' 열에 '1988'이 포함된 행의 개수를 세어보자.

ns_book4['발행연도'].str.contains('1988').sum()
>>> 407 

+ ns_book4['발행연도']=='1988' 처럼은 쓰면 안되나요?
ns_book4['발행연도']=='1988'처럼 쓰면 '발행연도'가 정확히 '1988'인 것만 찾을 수 있다. '1988.'와 같은 것은 제외된다. contains() 메서드는 주어진 문자열이 포함된 모든 행을 찾는다.

contain() 메서드는 기본적으로 정규 표현식을 인식한다. 숫자가 아닌 모든 문자에 대응하는 표현은 \D이다. 정규 표현식은 이렇게 대소문자를 반대의 용도로 사용한다.
contains() 메서드의 na 매개변수를 True로 지정하여 연도가 누락된 행을 True로 표시. 만약 '발행연도' 열에 누락된 값이 있다면 contains() 메서드는 기본적으로 np.nan으로 채워져 invalid_number 배열을 인덱싱에 사용할 수 없기 때문이다.

invalid_number = ns_book4['발행연도'].str.contains('\D', na=True)
print(invalid_number.sum()) → 숫자 이외의 문자가 들어간 행의 개수를 출력.
ns_book4[invalid_number].head()
>>> 1777
번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
19138	19565	단국강토	홍태수 저	매일경제신문사	1988.	9788974420031		NaN	NaN	511.1	1	1	2019-12-19
19227	19736	삼성의 역사	송부웅 撰	삼양	단기4334[2001]	9788985464369		0	NaN	911.02	1	1	2019-12-06
26097	26812	배고픈 애벌레	에릭 칼 글·그림 ;이희재 옮김	더큰컴퍼니	[2019]	9788959514083		NaN	NaN	843	1	0	2019-08-12
29817	30586	(The) Sopranos sessions	Matt Zoller Seitz,
eLaura Lip...	Harry N Abrams Inc	2019.	9781419734946		NaN	NaN	326.76	1	0	2019-06-13
29940	30709	다음엔 너야	에른스트 얀들 글;노르만 융에 그림;박상순 옮김	비룡소	2018.	9788949110646	9788949110004	7	NaN	853	1	9	2019-06-04

정규 표현식을 사용하여 연도 앞과 뒤에 있는 문자를 제외해보자. 연도를 나타내는 숫자 네 개는 \d{4}이고 이 숫자는 그룹으로 묶어 \1로 참조한다. 그리고 숫자 앞뒤에 어떤 문자가 나오더라도 모두 매칭하기 위해 .*를 사용한다.

ns_book5 = ns_book4.replace({'발행연도':r'.*(\d{4}).*'}, r'\1', regex=True)
# '\d{4}' : 연도 | 'r'\1'' : 첫 번째 그룹
ns_book5[invalid_number].head()
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
19138	19565	단국강토	홍태수 저	매일경제신문사	1988	9788974420031		NaN	NaN	511.1	1	1	2019-12-19
19227	19736	삼성의 역사	송부웅 撰	삼양	2001	9788985464369		0	NaN	911.02	1	1	2019-12-06
26097	26812	배고픈 애벌레	에릭 칼 글·그림 ;이희재 옮김	더큰컴퍼니	2019	9788959514083		NaN	NaN	843	1	0	2019-08-12
29817	30586	(The) Sopranos sessions	Matt Zoller Seitz,
eLaura Lip...	Harry N Abrams Inc	2019	9781419734946		NaN	NaN	326.76	1	0	2019-06-13
29940	30709	다음엔 너야	에른스트 얀들 글;노르만 융에 그림;박상순 옮김	비룡소	2018	9788949110646	9788949110004	7	NaN	853	1	9	2019-06-04

정규 표현식에 맞게 숫자 값이 바뀐 것을 확인할 수 있다!

다시 숫자 이외의 문자가 들어간 행의 개수와 데이터를 확인한다.

unkown_year = ns_book5['발행연도'].str.contains('\D', na=True)
print(unkown_year.sum())
ns_book5[unkown_year].head()
>>> 67
번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
30838	31616	본격 한중일 세계사 5	굽시니스트 지음	위즈덤하우스	NaN	9791190065092		NaN	NaN	NaN	0	0	2019-05-28
39130	40141	정책금융의 현황과 발전과제	정책금융연구회	한국산업은행	NaN	9788992784108		NaN	NaN	327.1	1	0	2019-01-22
39256	40268	서울지역 유적 발굴조사 총서 3	서울역사박물관	서울역사박물관	NaN	9791186324714	9791186324431	NaN	NaN	NaN	0	0	2019-01-22
76836	81202	흰머리 큰줄기	한호진 지음	秀文出版社	[발행년불명]	9788973010769		0	NaN	699.1	1	1	2016-11-10
150543	160436	(속) 경제학사	박장환 지음	NaN	[20--]	9788994339207		1	NaN	320.9	1	2	2012-11-19

1,777개에서 67개로 줄었다. 이제 변환되지 않은 값은 NaN이거나 네 자리 숫자가 아닌 값이다. 이런 값은 어떻게 변환할지 알 수 없기 때문에 -1 값으로 바꾼 다음 astype() 메서드로 '발행연도' 열의 데이터 타입을 정수형인 int32로 변환한다.

ns_book5.loc[unkown_year, '발행연도'] = '-1'
ns_book5 = ns_book5.astype({'발행연도': 'int32'})

연도가 4000년이 넘는 경우 확인을 위해 gt() 메서드로 전달된 값보다 큰 값을 찾는다. 연도가 4000년이 넘는 행의 전체 개수를 확인하기 위해 sum() 함수를 함께 사용한다.

ns_book5['발행연도'].gt(4000).sum() → 4000보다 큰 값의 개수를 센다.
>>> 131

+ gt() 메서드대신 > 기호를 쓸 수 없나요?
가능하다! 부등호 기호를 사용하면 다음처럼 쓸 수 있다.

(ns_book5['발행연도'] > 4000).sum()

gt() = > : 지정된 값보다 큰 값을 검사한다.
ge() = >= : 지정된 값보다 크거나 같은 값을 검사한다.
lt() = < : 지정된 값보다 작은 값을 검사한다.
le() = <= : 지정된 값보다 작거나 같은 값을 검사한다.
eq() = == : 지정된 값과 같은 값을 검사한다.
ne() = != : 지정된 값과 같지 않은 값을 검사한다

4000년이 넘는 연도에서 2333을 빼서 서기로 바꾼 다음 연도가 4,000년이 넘는 도서가 있는지 확인해보자.

dangun_yy_rows = ns_book5['발행연도'].gt(4000) → 4000보다 큰 값 찾기.
ns_book5.loc[dangun_yy_rows, '발행연도'] = ns_book5.loc[dangun_yy_rows, '발행연도'] - 2333 → 찾은 값에서 2333을 빼서 서기로 바꿈.

dangun_year = ns_book5['발행연도'].gt(4000) → 다시 4000보다 큰 값 찾기.
print(dangun_year.sum())
ns_book5[dangun_year].head(2)
>>> 13
번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
222858	234268	Modern art	[computer file]	GROLIER	7611	9780717233243		NaN	NaN	609.205	1	1	2009-05-07
270269	282852	현대 영어학=Linguistic theory in modern english	이재영	열림기획	7634	9788986705072		1	NaN	740.1	1	6	2007-04-14

# 연도가 이상하게 높은 도서가 13권이나 됨. 모두 -1로 표시
ns_book5.loc[dangun_year, '발행연도'] = -1

마지막으로 연도가 작은 값도 확인. 이번에는 0보다 크고 1900년도 이전의 도서를 찾아보자.

old_books = ns_book5['발행연도'].gt(0) & ns_book5['발행연도'].lt(1900)
# 'gt(0)' : 0보다 큰 값 찾기 | 'lt(1900)' : 1900보다 작은 값 찾기
ns_book5[old_books]
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
61149	64093	고흐 씨, 시 읽어 줄까요 :내 마음을 알아주는 시와 그림의 만남	이운진 지음	사계절	176	9788958284277		0	NaN	811.7	1	6	2017-10-27
70566	74347	정신 병리학의 문제들	지은이: 지그문트 프로이트 ;옮긴이: 황보석	열린책들	151	9788932905181	9788932905082	9	10	185.5	1	4	2017-04-26
79550	84164	(최근 7년간) 중요 민법판례 :[2009년 1월~2016년 6월 중요 판례]	이광섭 편저	법학사	163	9788962898651		9	NaN	365	1	14	2016-09-27
147950	157759	(한·중·일) 밥상 문화 :대표음식으로 본 3국 문화비교	김경은 지음	이가서	132	9788958643012		0	NaN	381.75	1	30	2013-02-19
194576	205407	책으로 뒤덮인 집의 비밀	N.E. 보드 지음 ;피터 퍼거슨 그림 ;김지현 옮김	개암나무	1015	9788992844413		7	NaN	843	1	15	2010-08-18
287252	300283	(밝혀질)우리歷史	吳在城 著	黎民族史硏究會	1607	9788986892130		0	NaN	911	1	5	2006-07-06

여전히 잘못된 값 존재! 이 도서도 연도를 -1로 설정하고 전체 행 개수 확인.

ns_book5.loc[old_books, '발행연도'] = -1
ns_book5['발행연도'].eq(-1).sum()
>>> 86

총 86권이 연도가 잘못 저장되었거나 알 수 없음. 이제 '발행연도' 열을 비롯해 다른 열의 잘못되거나 누락된 값 채워보자!


누락된 정보 채우기

'도서명', '저자', '출판사', '발행연도' 열이 분석에 중요하기 때문에 이 4개의 열에는 누락된 값이 있으면 안된다! 그래서 이 4개의 열에 누락된 값이 있거나 '발행연도' 열이 -1인 행의 개수를 확인해보자.

na_rows = ns_book5['도서명'].isna() | ns_book5['저자'].isna() \
          | ns_book5['출판사'].isna() | ns_book5['발행연도'].eq(-1)
print(na_rows.sum())
ns_book5[na_rows].head(2)
>>> 5268
번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
78	79	아산 정주영 레거시	김화진	NaN	2021	9788952129529		NaN	NaN	325	1	1	2021-03-15
265	278	골목의 시간을 그리다	정명섭.김효찬 지음	NaN	2021	9791191266054		NaN	NaN	NaN	1	0	2021-03-12

누락되거나 알 수 없는 행이 5,268개. 뷰티플수프를 사용해 이런 값을 채워보자!

import requests
from bs4 import BeautifulSoup

def get_book_title(isbn):
    # Yes24 도서 검색 페이지 URL
    url = 'https://www.yes24.com/Product/Search?domain=BOOK&query={}'
    # URL에 ISBN을 넣어 HTML 가져옵니다.
    r = requests.get(url.format(isbn))
    soup = BeautifulSoup(r.text, 'html.parser')   # HTML 파싱
    # 클래스 이름이 'gd_name'인 a 태그의 텍스트를 가져옵니다.
    title = soup.find('a', attrs={'class':'gd_name'}) \
            .get_text()
    return title

get_book_title(9791191266054)
>>> '골목의 시간을 그리다'

도서명과 달리 저자는 두 명 이상일 수도 있기 때문에 뷰티플수프의 find_all() 메서드를 사용해 저자를 담은 < a > 태그를 모두 추출한다. 이 태그 안 텍스트가 여러 개 추출되면 하나로 합쳐주는 것이 좋다. 리스트 안에 for 문을 사용하는 리스트 내포로 < a > 태그에 속한 모든 텍스트를 파이썬 리스트에 저장해준다. 그 다음 추출한 결과를 join() 메서드를 사용해 하나의 문자열로 합쳐준다.
발행연도는 정규식을 사용하여 연도만 추출해야 한다. 파이썬에서 정규 표현식을 지원하는 re 모듈findall() 함수를 사용하면 원하는 정규식에 매칭되는 모든 문자열을 찾아 리스트로 반환해준다. 이 함수의 첫 번째 매개변수는 원하는 정규식이고 두 번째 매개변수는 검색 대상 문자열이다.

import re

def get_book_info(row):
    title = row['도서명']
    author = row['저자']
    pub = row['출판사']
    year = row['발행연도']
    # Yes24 도서 검색 페이지 URL
    url = 'https://www.yes24.com/Product/Search?domain=BOOK&query={}'
    # URL에 ISBN을 넣어 HTML 가져옵니다.
    r = requests.get(url.format(row['ISBN']))
    soup = BeautifulSoup(r.text, 'html.parser')   # HTML 파싱
    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_auth'인 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

이 함수는 누락된 값에만 뷰티플수프로 추출한 값을 저장한다. 만약 뷰티플수프로 추출할 수 없는 경우에는 오류가 난다. 따라서 오류 때문에 함수 실행이 종료되지 않고 이어서 다음 요소를 추출하도록 try ~ except 문으로 예외 처리를 해주었다.

그럼 누락된 값이 있었던 처음 두 개의 행에 방금 작성한 get_book_info() 함수를 적용해보자. 함수가 여러 개의 값을 반환하는 경우 apply() 메서드는 기본적으로 반환된 것을 하나의 튜플로 만든다. 따라서 result_type 매개변수를 'expand'로 지정하여 반환된 값을 각기 다른 열로 만들자.

updated_sample = ns_book5[na_rows].head(2).apply(get_book_info,
    axis=1, result_type ='expand')
updated_sample
>>> 0	1	2	3
78	아산 정주영 레거시	김화진	서울대학교출판문화원	2021
265	골목의 시간을 그리다	정명섭.김효찬 지음	초록비책공방	2021

이 함수를 5,268개 행에 모두 적용할 수 잇지만 시간이 오래 걸린다. 여기에서는 미리 만들어 놓은 ns_book5_update.csv 파일을 코랩에 다운로드 하여 다음과 같이 실행한다.

gdown.download('https://bit.ly/3UJZiHw', 'ns_book5_update.csv', quiet=False)

ns_book5_update = pd.read_csv('ns_book5_update.csv', index_col=0)
ns_book5_update.head()
>>> 	도서명	저자	출판사	발행연도
78	아산 정주영 레거시	김화진	서울대학교출판문화원	2021
265	골목의 시간을 그리다	정명섭.김효찬 지음	초록비책공방	2021
354	한국인의 맛	정명섭 지음	추수밭	2021
539	한성부, 달 밝은 밤에	김이삭 지음	고즈넉이엔티	2021
607	100일 완성 마그마 러시아어 중고 급 단어장	러포자 구제 연구소 외 지음	문예림	2021

# 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())
>>> 4615

누락된 값이 있는 행은 4,615개로 뷰티플수프로 데이터를 채우기 전보다 653개 줄었다.
이제 누락된 값을 가진 행을 삭제하여 분석 대상에서 제외하자.
dropna() 메서드에 '도서명', '저자', '출판사' 열을 리스트로 지정한 후 누락된 값이 있는 행을 삭제하고 그 다음 '발행연도' 열 값이 -1이 아닌 행만 선택하여 ns_book6 데이터프레임을 생성한다.

ns_book5 = ns_book5.astype({'발행연도': 'int32'})
ns_book6 = ns_book5.dropna(subset=['도서명','저자','출판사'])
ns_book6 = ns_book6[ns_book6['발행연도'] != -1]
ns_book6.head()
>>> 번호	도서명	저자	출판사	발행연도	ISBN	세트 ISBN	부가기호	권	주제분류번호	도서권수	대출건수	등록일자
0	1	인공지능과 흙	김동훈 지음	민음사	2021	9788937444319		NaN	NaN	NaN	1	0	2021-03-19
1	2	가짜 행복 권하는 사회	김태형 지음	갈매나무	2021	9791190123969		NaN	NaN	NaN	1	0	2021-03-19
2	3	나도 한 문장 잘 쓰면 바랄 게 없겠네	김선영 지음	블랙피쉬	2021	9788968332982		NaN	NaN	NaN	1	0	2021-03-19
3	4	예루살렘 해변	이도 게펜 지음, 임재희 옮김	문학세계사	2021	9788970759906		NaN	NaN	NaN	1	0	2021-03-19
4	5	김성곤의 중국한시기행 : 장강·황하 편	김성곤 지음	김영사	2021	9788934990833		NaN	NaN	NaN	1	0	2021-03-19

# ns_book6 데이터프레임 저장
ns_book6.to_csv('ns_book6.csv', index=False)

일괄 처리 함수

지금까지 수행한 작업을 하나의 함수로 정리하자.

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)
    unkown_year = ns_book5['발행연도'].str.contains('\D', na=True)
    ns_book5.loc[unkown_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)
    # 교보문고 도서 상세 페이지에서 누락된 정보를 채웁니다.
    updated_sample = ns_book5[na_rows].apply(get_book_info,
        axis=1, result_type ='expand')
    updated_sample.columns = ['도서명','저자','출판사','발행연도']
    ns_book5.update(updated_sample)

    # 도서명, 저자, 출판사가 NaN이거나 발행연도가 -1인 행을 삭제합니다.
    ns_book6 = ns_book5.dropna(subset=['도서명','저자','출판사'])
    ns_book6 = ns_book6[ns_book6['발행연도'] != -1]

    return ns_book6

정리

  • NaN은 판다스에서 누락된 값을 표시하는 기호. isna() 메서드를 사용하여 NaN의 여부를 확인하거나 notna() 메서드를 사용해 NaN이 아닌 값인지 체크할 수 있다.
  • 정규 표현식은 문자열에서 패턴을 찾고 대체하기 위한 규칙의 모음. 정규 표현식을 사용하면 복잡한 패턴을 가진 문자열을 쉽게 검색할 수 있다.

핵심 함수와 메서드 정리

DataFrame.info() : 데이터프레임의 요약 정보를 출력한다.
DataFrame.isna() : 누락된 값을 감지하는 메서드로 셀의 값이 None이나 NaN일 경우 True를 반환한다.
DataFrame.astype() : 데이터 타입을 지정한다.
DataFrame.fillna() : 데이터프레임에서 누락된 원소의 값을 채운다.
DataFrame.replace() : 데이터프레임의 값을 다른 값으로 바꾼다.
Series.str.contains() : 시리즈나 인덱스에서 문자열 패턴을 포함하고 있는지 검사한다.
DataFrame.gt() : 데이터프레임의 원소보다 큰 값을 검사한다.

profile
초보 중의 초보. 열심히 하고자 하는 햄스터!

0개의 댓글