Chapter 03. 데이터 정제하기

강민석·2023년 1월 21일
0

데이터 분석

목록 보기
3/7

실습 코드 : https://colab.research.google.com/drive/1w0EtiFSRByNqe3urTkmTVoC2dx7Q9bi2?usp=sharing

데이터 정제

데이터에서 불필요한 부분을 삭제하거나 손상/부정확한 부분을 수정하는 등의 작업

불필요한 데이터 삭제하기

열 삭제하기

  • 데이터 프레임의 전체 열 나타내기
print(dataframe.columns)
  • drop() 메서드로 열 삭제하기
dataframe.drop(['주제분류번호','Unnamed: 13'], axis=1, inplace=True)
# inplace : 변경 결과를 기존 변수(dataframe)에 바로 적용하기.
  • dropna() NaN이 포함된 행/열 삭제하기
dataframeWithoutNaN = dataframe.dropna(axis=1, how='all')
# how : 'all'을 지정하면 모든 값이 NaN일 때만 삭제. how 변수를 지정하지 않으면 NaN이 포함된 행/열은 모두 삭제된다.

행 삭제하기

  • boolean 배열을 통해
selected_rows = dataframe['출판사'] == '한빛미디어'
# 행 별로 열의 값을 비교(출판사 값 == 한빛미디어)한 boolean 배열을 반환한다.

hanbit = dataframe[selected_rows]
highscore = dataframe[dataframe['대출건수'] > 1000]

중복 행 찾기

  • duplicated() 메서드를 통해
    duplicated()는 중복 데이터를 True로 표기한 boolean 배열 반환.
    sum을 통해 True 값의 합계를 구할 수 있다.
sum(dataframe.duplicated())
  • 특정 열을 기준으로 찾기
dup_rows = dataframe.duplicated(subset=['도서명','저자','ISBN'], keep=False)
# keep = 중복된 열 중 첫 번째 열도 True로 표시
  • groupby()로 중복데이터가 처리된 합계 결과 만들기
groupStandardDf = dataframe[['도서명','저자','ISBN','권','대출건수']]
loan_count = groupStandardDf.groupby(by=['도서명','저자','ISBN','권'], dropna=False).sum()

중복데이터 합계 결과를 원본에 업데이트하기

  • 중복데이터를 제외한 결과 만들기
dup_rows = dataframe.duplicated(subset=['도서명','저자','ISBN','권'])
unique_rows = ~dup_rows

nonDupDf = dataframe[unique_rows].copy()
# copy() : 명시적으로 복사하여 다른 메모리에 저장되도록 보장해줌.
  • 인덱스를 맞춰주기
nonDupDf.set_index(['도서명','저자','ISBN','권'], inplace=True)
  • 두 데이터 프레임 합치기
nonDupDf.update(loan_count)
  • 인덱스 되돌리기
nonDupDf.reset_index(inplace=True)
  • 열 순서를 원본과 같이 변경하기
nonDupDf = nonDupDf[dataframe.columns]

일괄 처리 커스텀 메소드 만들기

def data_cleaning(filename):
    origin = pd.read_csv(filename, low_memory=False)
    nonNull = origin.dropna(axis=1, how='all')

    count_df = nonNull[['도서명','저자','ISBN','권','대출건수']]
    loan_count = count_df.groupby(by=['도서명','저자','ISBN','권'], dropna=False).sum()

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

    duplicates.set_index(['도서명','저자','ISBN','권'], inplace=True)
    duplicates.update(loan_count)

    nonDup = duplicates.reset_index()
    nonDup = nonDup[nonNull.columns]
    
    return nonDup

잘못된 데이터 수정하기

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

  • info() 메서드를 활용하면 NaN이 아닌 행의 개수를 알 수 있다.
ns_book4 = pd.read_csv('ns_book4.csv', low_memory=False)
ns_book4.info()
  • isna() 메서드와 sum()을 함께 사용하면 더 쉽게 알 수 있다.(반대는 notna())
ns_book4.isna().sum()

값 처리하기

  • 데이터 형 바꾸기
ns_book4 = ns_book4.astype({'도서권수': 'int32', '대출건수': 'int32'})
  • NaN 값 넣기

    열의 데이터 형이 정수형인 경우 : None 값 지정
    이외 : np.nan

ns_book4.loc[0, '도서권수'] = None
ns_book4.loc[0, '부가기호'] = np.nan

NaN 값 처리하기

  • NaN 값을 일괄 처리하기 1
ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''
  • NaN 값을 일괄 처리하기 2
ns_book4.fillna('없음')
  • NaN 값 특정열만 처리하기
ns_book4['부가기호'].fillna('없음')
# ns_book4.fillna({'부가기호': '없음'})

값 변경하기

  • NaN을 '없음'으로 전체 변경하기
ns_book4.replace(np.nan, '없음')
  • 여러 값을 동시에 바꾸기
ns_book4.replace({np.nan: '없음', '2021': '21'}).head(2)
  • 특정 열의 값만 바꾸기
ns_book4.replace({'부가기호': {np.nan: '없음'}, '발행년도': {'2021': '21'}})

정규표현식

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

()로 그룹을 지정할 수 있다.
파이썬에서는 r 문자를 붙여 정규표현식임을 나타낼 수 있다.
표현식이 반복될 때는 중괄호와 숫자로 표현할 수 있다. ex) \d\d(\d\d) == \d{2}(\d{2})

  • 연도를 두자리로 바꾸기 -> 패턴 안의 첫번째 그룹(뒷 두자리)으로 연도 변경
ns_book4.replace({'발행년도': {r'\d\d(\d\d)': r'\1'}}, regex=True)
  • 불필요한 문자 제거하기 (로런스 인그래시아 (지은이), 안기순 (옮긴이) -> 로런스 인그래시아, 안기순)
ns_book4.replace({'저자': {r'(.*)\s\(지은이\)(.*)\s\(옮긴이\)': r'\1\2'}, '발행년도': {r'\d\d(\d\d)': r'\1'}}, regex=True)

잘못된 값 바꾸기

데이터프레임에서 1988년도에 발간된 책들으 찾을 수 없는 오류 발생. 원인 파악 후 조치하기

  • contains() 메서드로 '1988'이 포함된 행 개수 찾기
ns_book4['발행년도'].str.contains('1988').sum()
  • 숫자 이외의 문자가 들어간 행 찾기
invalid_number = ns_book4['발행년도'].str.contains('\D', na=True) # na를 true로 하여 NaN 행이 NaN이 아닌 True로 저장되도록.
print(invalid_number.sum())
ns_book4[invalid_number].head()
  • 정규표현식으로 연도 이외의 문자 삭제하기
ns_book5 = ns_book4.replace({'발행년도':'.*(\d{4}).*'}, r'\1', regex=True)

정규표현식으로 처리 안된 특수케이스 처리하기

  • 처리 안된 데이터 확인
unknown_year = ns_book5['발행년도'].str.contains('\D', na=True)
print(unknown_year.sum())
ns_book5[unknown_year].head()
  • 확인 불가 데이터를 -1로 통일 후 데이터타입 지정
ns_book5.loc[unknown_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, '발행년도'] -2333
  • 처리 안된 잘못된 년도를 -1로 변경
ns_book5.loc[ns_book5['발행년도'].gt(4000), '발행년도'] = -1
  • 년도가 0~1900년인 도서 찾기
old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
ns_book5[old_books]
ns_book5.loc[old_books, '발행년도'] = -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)

누락된 데이터를 웹 스크래핑으로 획득하는 함수

  • 저자는 두 명 이상일 수 있기 때문에 find_all()메서드로 모두 추출후 for문으로 리스트에 저장. 그 후 join()메서드로 하나의 문자열로 변경.
  • 발행 연도는 re 모듈의 findall()함수로 원하는 정규식에 매칭되는 모든 문자열을 리스트로 받기. 그 후 정규식으로 추출
import re

def get_book_info(row):
    title = row['도서명']
    author = row['저자']
    pub = row['출판사']
    year = row['발행년도']
    url = 'http://www.yes24.com/Product/Search?domain=BOOK&query={}'

    r = requests.get(url.format(row['ISBN']))
    soup = BeautifulSoup(r.text, 'html.parser')
    try:
        if pd.isna(title):
            title = soup.find('a', attrs={'class':'gd_name'}) \
                    .get_text()
    except AttributeError:
        pass

    try:
        if pd.isna(author):

            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):
            pub = soup.find('span', attrs={'class':'info_pub'}) \
                      .find('a') \
                      .get_text()
    except AttributeError:
        pass
    
    try:
        if year == -1:
            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

데이터 삭제하기

beautifulsoup를 활용하여도 채우지 못한 데이터는 비정상 데이터로 간주하여 삭제해보자.

  • dropna() 메서드로 도서명, 저자, 출판사 열에서 NaN이 있는 행을 삭제하고, 발행년도 값이 -1인 행을 삭제하기
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):
    ns_book4 = ns_book4.astype({'도서권수':'int32', '대출건수': 'int32'})

    set_isbn_na_rows = ns_book4['세트 ISBN'].isna()
    ns_book4.loc[set_isbn_na_rows, '세트 ISBN'] = ''
    
    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'
    
    ns_book5 = ns_book5.astype({'발행년도': 'int32'})
    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)
    ns_book5.loc[dangun_year, '발행년도'] = -1
    old_books = ns_book5['발행년도'].gt(0) & ns_book5['발행년도'].lt(1900)
    ns_book5.loc[old_books, '발행년도'] = -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')

    ns_book6 = ns_book5.dropna(subset=['도서명','저자','출판사'])
    ns_book6 = ns_book6[ns_book6['발행년도'] != -1]
    
    return ns_book6

소감

데이터 분석 전 결과에 영향을 줄 수 있는 잘못된 데이터들을 처리하는 방법에 대해 알아보았다.
어느정도 정제되어 있는 데이터였기에 간단하게 처리 할 수 있었지만 러프하게 수집된 데이터들을 분석하기 위해서는 꽤 고단한 데이터 처리 작업이 필요할 것 같다.
데이터 분석 작업에서 가장 중요한 부분중 하나로 데이터 정제를 꼽는 이유를 체감할 수 있었다.

profile
새내기 개발자입니다.

0개의 댓글