본문 바로가기
혼자 공부하는 데이터 분석

3장 데이터 정제하기

by chaechaekim 2023. 5. 22.

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

  • 데이터 정제: 데이터에서 손상되거나 부정확한 부분을 수정하고, 불필요한 데이터를 삭제하거나 불완전한 값을 교체하는 등의 작업
    • 데이터를 분석 목적에 맞데 변환하는 데이터 탱글링 또는 데이터 먼징의 일부로 수행될 수 있다.

 

열 삭제하기

gdown 패키지 사용하여 데이터 다운

import gdown
gdown.download('https://bit.ly/3RhoNho', 'ns_202104.csv', quiet=False)
import pandas as pd
ns_df = pd.read_csv('ns_202104.csv', low_memory=False)
ns_df.head()

*판다스에서 NaN: 누락된 값, 비어 있는 값 의미

 

불필요한 열 삭제: loc 메서드에 슬라이싱 사용

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

 

  • loc 메서드와 불리언 배열
print(ns_df.columns)

*columns 속성은 판다스의 Index 클래스 객체: 객체의 원소는 숫자 인덱스로 참조가능

*원소별 비교: 어떤 값과 비교할 때 자동으로 배열에 있는 모든 원소와 하나씩 비교

 

원소별 비교를 활용하여 ns_df.columns에서 'Unnamed: 13'열이 아닌 것을 표시하는 배열 만들기

ns_df.columns != 'Unnamed: 13'

*!=는 비교연산자

*반환된 결과=넘파이 배열

 

selected_columns 변수에 저장하고 판다스 데이터프레임의 loc 메서드에 전달하면 True인 열의 행만 선택

selected_columns = ns_df.columns != 'Unnamed: 13'
ns_book = ns_df.loc[:, selected_columns] //True인 열의 모든 행 선택
ns_book.head()

 

데이터프레임 중간에 있는 '부가 기호'열 제외

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

 

  • drop 메서드

: 첫 번째 매개변수에 삭제하려는 열 이름 전달하고 axis 매개변수를 1로 지정

 

ns_df 데이터프레임에서 불필요한 'Unnamed: 13' 열 삭제

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

 

중간에 있는 열 간단하게 제외/ 첫 번째 매개변수에 제외할 열 이름을 리스트 형식으로 여러 개 지정 가능

부가기호 열과 Unnamed: 13 열 제외

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

 

 

inplace 매개변수를 True로 지정하면 현재 선택된 데이터프레임 바로 수정 가능

주제분휴번호 열 삭제

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

 

dropna() 메서드

: NaN이 하나 이상 포함된 행이나 열 삭제

axis 매개변수를 1로 지정하여 ns_df 데이터프레임에서 NaN이 포함된 열을 삭제

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

 

모든 값이 NaN인 열을 삭제하려면 dropna() 메서드에 how 매개변수를 'all'로 지정

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

 

행 삭제하기

drop() 메서드

 

처음 2개 행 삭제

ns_book2 = ns_book.drop([0,1])
ns_book2.head()

 

  • []연산자와 슬라이싱

: []연산자에 슬라이싱이나 불리언 배열을 전달하면 행을 선택

인덱스가 0,1인 행을 제외한 모든 행 선택하기 위해 []연산자에 슬라이싱 사용

ns_book2 = ns_book[2:]
ns_book2.head()

*마지막 인덱스 포함X

 

[0:2]로 작성하면 처음 두 개 행만 선택

ns_book2 = ns_book[0:2]
ns_book2.head()

 

  • []연산자와 불리언 배열

: 원하는 행을 True, 제외할 행은 False로 표시

출판사가 한빛미디어인 행만 선택

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

 

대출건수가 1,000 이하인 행 모두 삭제: ns_book['대출건수']>1000 과 같이 조건을 넣는다.

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

 

중복된 행 찾기

duplicated() 메서드 사용

:중복된 행 중에서 처음 행을 제외한 나머지 행은 True, 그 외 중복되지 않은 나머지 모든 행은 False로 표시된 불리언 배열 반환

파이썬의 sum() 함수 함꼐 사용 → True를 1로 인식

 

불필요한 열을 정리한 ns_book 데이터프레임에 중복된 행이 있는지 확인

sum(ns_book.duplicated())

 

도서명, 저자, ISBN을 기준으로 중복된 행이 있는지 찾기

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

*일부 열을 기준으로 중복된 행을 찾으려면 subset 매개변수에 기준열 나열

 

도서명, 저자, ISBN을 기준으로 어떤 데이터가 중복되었는지 확인

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

*keep 매개변수를 False로 지정하여 중복된 모든 행을 True로 표시

 

그룹별로 모으기

: groupby() 메서드의 by 매개변수에는 행을 합칠 때 기준이 되는 열 지정

 

ns_book 데이터프레임에서 그룹으로 묶을 기준 열과 '대출건수' 열만 선택하여 사용

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

*groupby() 메서드는 기본적으로 by 매개변수에 지정된 열에 NaN이 포함되어 있으면 해당 행 삭제

dropna 매개변수를 False로 지정: 연산할 때 NaN이 있는 행도 포함하겠다는 의미

 

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

 

원본 데이터 업데이트하기

  1. duplicated() 메서드로 중복된 행을 True로 표시한 불리언 배열 만들기
  2. 1번에서 구한 불리언 배열을 반전시켜서 중복되지 않은 고유한 행을 True로 표시
  3. 2번에서 구한 불리언 배열을 사용해 원본 배열에서 고유한 행만 선택

중복된 행을 True로 표시한 불리언 배열을 반전시킬 때는 판다스의 ~연산자 사용

→ 원본 배열에서 고유한 배열을 선택하여 copy() 메서드로 ns_book3 데이터프레임 만들기

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

*copy() 메서드는 데이터프레임의 복사본을 만든다.

 

ns_book3에 중복된 행이 없는지 duplicated() 메서드로 확인

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

 

  • 원본 데이터프레임 인덱스 설정하기

set_index() 메서드: 지정한 열을 인덱스로 설정할 때

inplace 매개변수를 True로 지정해 새로운 데이터프레임을 반환하지 않고 ns_book3 데이터프레임 수정

 

인덱스가 변경된 ns_book3 데이터프레임의 처음 5개 행 출력

ns_book3.set_index(['도서명','저자','ISBN','권'], inplace=True)
ns_book3.head()

 

  • 업데이트하기: update() 메서드

다른 데이터프레임을 사용해 원본 데이터프레임의 값을 업데이트

ns_book3.update(loan_count)
ns_book3.head()

 

reset_index() 메서드로 인덱스 열 해제

ns_book4 = ns_book3.reset_index()
ns_book4.head()

 

원본 데이터프레임 ns_book에서 대출건수가 100회 이상인 책의 개수 세기

: 비교 연산자를 사용하고 sum() 함수로 True 원소 개수 센다.

sum(ns_book['대출건수']>100)

 

새로 만든 ns_book4 데이터프레임에서 대출건수가 100회 이상인 책 개수 세기

sum(ns_book4['대출건수']>100)

 

열 순서 바꾸는 가장 간단한 방법: []연산자에 원하는 열 이름을 순서대로 전달

ns_book4 = ns_book4[ns_book.columns]
ns_book4.head()

 

불필요한 데이터를 제거하여 만든 ns_book4 데이터프레임 저장

ns_book4.to_csv('ns_book4.csv', index=False)

 

일괄 처리 함수 만들기

일괄 처리 함수

불필요한 행과 열을 제거하기 위해 작성했던 코드를 새로운 데이터에 적용하기 쉽도록 일괄 처리하는 data_cleaning() 함수

def data_cleaning(filename):
    """
    남산 도서관 장서 CSV 데이터 전처리 함수
    
    :param filename: CSV 파일이름
    """
    # 파일을 데이터프레임으로 읽습니다.
    ns_df = pd.read_csv(filename, low_memory=False)
    # NaN인 열을 삭제합니다.
    ns_book = ns_df.dropna(axis=1, how='all')

    # 대출건수를 합치기 위해 필요한 행만 추출하여 count_df 데이터프레임을 만듭니다.
    count_df = ns_book[['도서명','저자','ISBN','권','대출건수']]
    # 도서명, 저자, ISBN, 권을 기준으로 대출건수를 groupby합니다.
    loan_count = count_df.groupby(by=['도서명','저자','ISBN','권'], dropna=False).sum()
    # 원본 데이터프레임에서 중복된 행을 제외하고 고유한 행만 추출하여 복사합니다.
    dup_rows = ns_book.duplicated(subset=['도서명','저자','ISBN','권'])
    unique_rows = ~dup_rows
    ns_book3 = ns_book[unique_rows].copy()
    # 도서명, 저자, ISBN, 권을 인덱스로 설정합니다.
    ns_book3.set_index(['도서명','저자','ISBN','권'], inplace=True)
    # load_count에 저장된 누적 대출건수를 업데이트합니다.
    ns_book3.update(loan_count)
    
    # 인덱스를 재설정합니다.
    ns_book4 = ns_book3.reset_index()
    # 원본 데이터프레임의 열 순서로 변경합니다.
    ns_book4 = ns_book4[ns_book.columns]
    
    return ns_book4

 

equals() 메서드: 다른 데이터프레임 비교

원본 데이터인 ns_202104.csv 파일을 전달하여 새로운 데이터프레임인 new_ns_book4를 만든 후, ns_book4 데이터프레임과 동일한지 비교

new_ns_book4 = data_cleaning('ns_202104.csv')
ns_book4.equals(new_ns_book4)

 

정리

함수/메서드 기능
DataFrame.drop() 데이터프레임의 행이나 열을 삭제
DataFrame.dropna() 누락된 값이 포함된 행이나 열 삭제
DataFrame.duplicated() 중복된 행을 찾아 불리언 값으로 표시한 배열 반환
DataFrame.groupby() 데이터프레임의 행을 그룹으로 모은다.
DataFrame.sum() 행 또는 열을 기준으로 합계 계산
DataFrame.set_index() 지정한 열을 인덱스로 설정
DataFrame.reset_index() 데이터프레임의 인덱스를 재설정
DataFrame.update() 다른 데이터프레임을 사용해 원본 데이터프레임의 값을 업데이트
다른 데이터프레임에 있는 NaN은 업데이트에서 제외
DataFrame.equals() 다른 데이터프레임과 동일한 원소를 가졌는지 비교
두 데이터프레임이 동일하면 True, 그렇지 않으면 False 반환

 


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

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

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

 

ns_book4.csv 파일을 판다스 데이터프레임으로 불러 온 후, head() 메서드로 처음 5개 행을 출력해 데이터 확인

import pandas as pd
ns_book4 = pd.read_csv('ns_book4.csv', low_memory=False)
ns_book4.head()

 

info() 메서드: 데이터프레임 정보를 요약해서 출력

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

ns_book4.info()

 

누락된 값 처리하기

  • 누락된 값 개수 확인하기: isna() 메서드
ns_book4.isna().sum()

*isna() 메서드: 각 행이 비어 있는지를 나타내는 불리언 배열 반환

*notna() 메서드: 누락되지 않은 값 확인

 

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

판다스 데이터프레임에서는 정수를 저장하는 열에 파이썬의 None을 입력하면 누락된 값으로 인식

 

ns_book4 데이터프레임의 '도서권수' 열에 있는 첫 번째 행 값을 None으로 바꾼 후, '도서권수' 열에 isna() 메서드 적용

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

 

head() 메서드로 처음 2개 행을 출력하여 첫 번째 행의 '도서권수' 값이 어떻게 바뀌었는지 확인 → NaN 출력

ns_book4.head(2)

*두 번째 행의 도서권수 값이 1 → 1.0으로 바뀐 이유: 판다스가 NaN을 특별한 실수값으로 저장하기 때문

 

'도서권수' 열의 첫 번째 행을 원래대로 1로 바꾸기

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

*astype() 메서드: 데이터 타입 지정

 

데이터 타입이 object인 '부가기호' 열의 첫 번째 향애 None 입력

ns_book4.loc[0, '부가기호'] = None
ns_book4.head(2)

 

판다스는 NaN이라는 값을 따로 가지고 있지 않아서 넘파이 패키지에 있는 np.nan을 사용

import numpy as np
ns_book4.loc[0, '부가기호'] = np.nan
ns_book4.head(2)

 

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

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() //누락된 값이 몇 개인지 센다.

*loc 메서드: 누락된 값을 원하는 값으로 바꿀 수 있다.

 

ns_book4에 있는 모든 NaN을 '없음' 문자열로 바꾸기

ns_book4.fillna('없음').isna().sum()

*fillna() 메서드: 원하는 값을 전달하면 NaN을 대체할 수 있다.

 

특정 열만 선택해서 NaN 바꾸기

ns_book4['부가기호'].fillna('없음').isna().sum()

*열 이름 없이 개수만 있는 판다스 시리즈 객체로 반환

 

NaN을 바꾸면서 전체 데이터프레임을 반환하려면 열 이름과 바꾸려는 값으로 이루어진 딕셔너리 전달

ns_book4.fillna({'부가기호':'없음'}).isna().sum()

 

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

1. 바꾸려는 값이 하나일 때

replace( 원래 값, 새로운 값 )

ns_book4.replace(np.nan, '없음').isna().sum()

 

2. 바꾸려는 값이 여러 개일 때

리스트 형식

replace([ 원래 값1, 원래 값2], [ 새로운 값1, 새로운 값2])

ns_book4.replace([np.nan, '2021'], ['없음', '21']).head(2)

 

딕셔너리 형식

({ 원래 값1: 새로운 값1, 원래 값2: 새로운 값2})

ns_book4.replace({np.nan: '없음', '2021': '21'}).head(2)

 

3. 열 마다 다른 값으로 바꿀 때

replace({ 열 이름: 원래 값}, 새로운 값 )

ns_book4.replace({'부가기호': np.nan}, '없음').head(2)

 

중첩된 딕셔너리

({열 이름: {원래 값1: 새로운 값1}})

ns_book4.replace({'부가기호': {np.nan: '없음'}, '발행년도': {'2021': '21'}}).head(2)

 

정규 표현식

: 문자열 패턴을 찾아서 대체하기 위한 규칙의 모음

 

replace() 메서드로 인덱스 100번과 101번 행의 네 자리 연도(2021)를 두자리(21)로 바꾼다.

ns_book4.replace({'발행년도': {'2021': '21'}})[100:102]

*연도가 자르면 적용X

 

  • 숫자 찾기:\d

정규 표현식에서 숫자를 나타내는 기호: \d

표현식을 그룹으로 묶을 때는 괄호 사용

ex) 뒤에 두 자리만 하나의 그룹으로 묶을 때는 \d\d(\d\d)

→ 패턴에 맞는 문자열을 찾은 후 첫 번째 그룹에 해당하는 뒷자리 연도 두 개 추출

패턴 안에 있는 그룹을 나타낼 때는 \1, \2처럼 사용 (그룹의 번호는 패턴 안에 등장하는 순서대로)

 

발행년도 열의 값을 정규 표현식으로 두 자리 연도로 바꾸기

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

*regex 매개변수: 정규 표현식을 사용한다는 의미

*정규 표현식 앞에 붙인 r문자: 파이썬에서 정규 표현식을 다른 문자열과 구분하기 위해 접두사처럼 붙인다.

 

중괄호를 사용하여 개수 지정

숫자 4개를 찾는 정규 표현식

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

 

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

마침표(.): 어떤 문자에도 대응

(*)문자: 0개 이상 반복된다고 표시

역슬래시(\): 일반 문자라고 인식

\s: 공백문자

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

 

잘못된 값 바꾸기

 

astype() 메서드로 '발행년도' 열을 int32로 바꾸면 오류 발생

# 아래 코드는 오류 발생
# ns_book4.astype({'발행년도': 'int32'})

*'1988.' 이라는 연도를 변환할 수 없다

 

'발행년도' 열에 '1988'이 포함된 행의 개수 세기

ns_book4['발행년도'].str.contains('1988').sum()

*contains() 메서드: 시리즈나 인덱스에 문자열 패턴을 포함하고 있는지 검사

*정규 표현식에서 숫자가 아닌 다른 모든 문자에 대응하는 표현: \D

 

'발행년도' 열에서 숫자가 아닌 문자를 포함하는 모든 행 찾기

invalid_number = ns_book4['발행년도'].str.contains('\D', na=True)
print(invalid_number.sum()) //숫자 이외의 문자가 들어간 행의 개수를 출력
ns_book4[invalid_number].head()

*contains() 메서드의 na 매개변수를 True로 지정하여 연도가 누락된 행을 True로 표시

→ 발행년도 열에 누락된 값이 있다면 contains() 메서드는 기본적으로 np.nan으로 채워서 invalid_number배열을 인덱싱에 사용할 수 없다.

 

연도 앞과 뒤에 있는 문자 제외

ns_book5 = ns_book4.replace({'발행년도':r'.*(\d{4}).*'}, r'\1', regex=True)
ns_book5[invalid_number].head()

*연도를 나타내는 숫자 4개는 \d{4}이고 그룹으로 묶어 \1로 참조

*숫자 앞뒤에 어떤 문자가 나오더라도 모두 매칭하기 위해 .* 사용

 

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

unkown_year = ns_book5['발행년도'].str.contains('\D', na=True)
print(unkown_year.sum())
ns_book5[unkown_year].head()

*변환되지 않은 값은 NaN이거나 네 자리 숫자가 아닌 값

 

임의로 -1 값으로 바꾼 다음 astype() 메서드로 '발행년도' 열의 데이터 타입을 정수형인 int32로 변환

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

 

연도가 4000년이 넘는 경우 확인

ns_book5['발행년도'].gt(4000).sum()

*gt() 메서드: 전달된 값보다 큰 값 찾는다.

 

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

dangun_yy_rows = ns_book5['발행년도'].gt(4000)
ns_book5.loc[dangun_yy_rows, '발행년도'] = ns_book5.loc[dangun_yy_rows, '발행년도'] - 2333

dangun_year = ns_book5['발행년도'].gt(4000)
print(dangun_year.sum())
ns_book5[dangun_year].head(2)

 

연도가 이상하게 높은 도서도 모두 -1로 표시

ns_book5.loc[dangun_year, '발행년도'] = -1

 

연도가 작은 값 확인: 0보다 크고 1900년도 이전의 도서

old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
ns_book5[old_books]

 

잘못된 값 -1로 설정하고 전체 행 개수 확인

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

 

누락된 정보 채우기

 

'도서명', '저자', '출판사' 열에 누락된 값이 있거나 '발행년도' 열이 -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)

 

뷰티플수프를 사용하여 이런 값 채우기

requests 패키지와 bs4 패키지 임포트

import requests
from bs4 import BeautifulSoup

 

Yes24에서 ISBN으로 검색한 결과 페이지에서 크롬 브라우저 개발자 도구로 도서명 태그 정보 확인

→ <a> 태그에서 도서명을 가져오는 함수 작성

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')   # HTML 파싱
    # 클래스 이름이 'gd_name'인 a 태그의 텍스트를 가져옵니다.
    title = soup.find('a', attrs={'class':'gd_name'}) \
            .get_text()
    return title

 

작성한 get_book_title() 함수를 '골목의 시간을 그리다' 책의 ISBN으로 테스트

get_book_title(9791191266054)

 

같은 방식으로 저자, 출판사, 발행 연도를 추출하여 반환하는 함수 만들기

:저자는 2명 이상일 수 있기 때문에 뷰티플수프의 find_all() 메서드를 사용해 저자를 담은 <a> 태그를 모두 추출

→ <a> 태그의 텍스트가 여러 개 추출되면 하나로 합쳐준다.

리스트 안에 for 문을 사용하는 리스트 내포로 <a> 태그에 속한 모든 텍스트를 파이썬 리스트에 저장 후, 추출한 결과를 join() 메서드를 사용해 하나의 문자열로 합쳐준다.

 

발행연도는 '2020년 12월'처럼 쓰여 있으므로 정규식을 이용하여 연도만 추출

파이썬에서 정규 표현식을 지원하는 re 모듈의 findall() 함수를 사용하면 원하는 정규식에 매칭되는 모든 문자열을 찾아 리스트로 반환

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')   # 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

*누락된 값에만 뷰티플수프로 추출한 값 저장

*만약 뷰티플수프로 추출할 수 없는 경우(Yes24에 도서 정보가 없거나, HTML 요소가 누락된 경우)에는 오류 발생

→ 함수 실행이 종료되지 않고 이어서 다음 요소를 추출하도록 try ~ except 문으로 예외 처리

 

누락된 값이 있었던 처음 2개의 행에 방금 작성한 get_book_info() 함수 적용

: result_type 매개변수를 'expand'로 지정하여 반환된 값을 각기 다른 열로 만든다.

updated_sample = ns_book5[na_rows].head(2).apply(get_book_info,
    axis=1, result_type ='expand')
updated_sample

*함수가 여러 개의 값을 반환하는 경우 apply() 메서드는 기본적으로 반환된 값을 하나의 튜플로 만든다.

 

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() 메서드에 '도서명', '저자', '출판사' 열을 리스트로 지정한 후 누락된 값이 있는 행 삭제하고 '발행년도' 열 값이 -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()

 

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

 

정리

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

댓글