데이터사이언스/추천시스템

맥주 추천시스템 구현 - 2. 데이터 전처리

ghtis1798 2021. 2. 3. 05:37

데이터 전처리

🍺리뷰 데이터 전처리

1. 여러 개의 파일 병합하기

우선 이전 포스팅에서 수집한 맥주별 csv 파일들을 하나로 합치겠습니다.

import pandas as pd

# 합친 데이터를 저장할 데이터프레임
data = pd.DataFrame(data=[], columns=['맥주정보', '검색이름', '맥주이름'])

# 수집한 파일의 개수
files_cnt = 77

for i in range(files_cnt):

    # 해당 경로에서 beer_n_1.csv 형식의 파일들만 수집한 뒤 병합합니다.
    try : 
        tmp = pd.read_csv(r'C:\Users\Ghyeon\beer_n_'+str(i)+'.csv', index_col=0)
        data = pd.concat([data,tmp])
    # 오류 발생 시 넘어갑니다.
    except :
        print(i, '번째에서 오류가 발생했습니다. 다음 파일로 넘어갑니다.')

# 합친 데이터를 저장합니다.
data.to_csv('전처리전데이터.csv', encoding='utf-8')

그리고 데이터를 불러와서 잘 합쳐졌는지 확인해 보겠습니다.

data = pd.read_csv('전처리전데이터.csv', encoding='utf-8', index_col=0)

data = data[['맥주이름', '맥주정보']]

data.head()

data.info()

잘 병합되었습니다. 👏👏

이제부터는 본격적인 전처리 작업을 시작하겠습니다.

import pandas as pd

data = pd.read_csv('크롤링_원본데이터.csv', encoding='utf-8', index_col=0)

# 맥주정보 Raw 데이터값
data['맥주정보'].iloc[0]

우리가 처리할 데이터인 '맥주정보' 컬럼값을 보겠습니다.

수집한 리뷰 예시 사진입니다.

보시다시피 하나의 셀에 아이디, 평점, 날짜 등의 모든 정보가 \n을 기준으로 저장되어 있습니다.

2. 데이터 전처리

1. 문자열 분리하기('\n')

우선 \n을 기준으로 분리한 뒤, list 형식으로 저장하겠습니다.

사용할 함수는 .str.split('\n')입니다.

# tmp 변수에 카피
tmp = data.copy()

# \n 개행문자 기준으로 분리
tmp['맥주정보'] = tmp['맥주정보'].str.split('\n')
tmp['맥주정보']

# 분리된 스트링 값 확인 -> List로 변환됨
tmp['맥주정보'].iloc[0]

2. 좋아요 수 삭제하기

꼭 필요한 데이터는 아이디, 평점|날짜, Aroma, Appearance, Flavor, Mouthfeel, Overall 입니다.

리스트 인덱스로는 앞에서 2개, 뒤에서 10개는 꼭 필요합니다.

그런데 문제는 패턴이 항상 같지 않다는 것입니다. 🤦‍♂️

아이디, 평점|날짜 순이 아니라 아이디, 국적, 평점|날짜인 경우도 있고

아이디, 국적, Trash값, 평점|날짜인 경우도 있습니다.

# 특이점 확인 : 0,1,2,3번째 리스트 요소까지 잘라야함.
for i in range(75252, 75255):
    print(tmp['맥주정보'].iloc[i], '\n')

따라서 0,1,2,3번째 요소까지 가져온 뒤 처리해야합니다.

또한 뒤에서 10개의 값에도 예외처리가 필요하진 않은지 확인해야 합니다.

# 새로운 데이터 프레임 ttmp에 copy()후 전처리 하겠습니다.
ttmp = tmp.copy()
# 맥주정보 리스트 출력 : 좋아요 수가 기록된 유저 정보
ttmp['맥주정보'].iloc[10]

맨 마지막 값 1은 좋아요 수가 표현된 경우입니다.

좋아요 수가 없는 경우 뒤에서부터 10개의 값을 가져와야 합니다.

하지만 있는 경우라면, 제거한 뒤 10개의 값을 가져와야겠죠. 🤔

lambda 식을 사용해 좋아요 수가 포함된 경우 맨 뒤 값을 삭제하도록 하겠습니다.

# 전체 데이터프레임에서 좋아요가 1개인 것 찾아서 맨 뒤에 것 삭제
ttmp['맥주정보'] = ttmp['맥주정보'].apply(lambda x : x if x[-2]=='Overall' else x[:-1] )

그 뒤 전체 리스트에서 앞에서 0,1,2,3번째 요소와 맨 뒤에서 10개의 요소만 남기도록 하겠습니다.

# 맥주정보에서 0,1,2,3번째 리스트 요소와 뒤에서부터 10개의 리스트요소(평점값들)추출
ttmp['맥주정보'] = ttmp['맥주정보'].apply(lambda x : x[:4]+x[:-11:-1])

# 좋아요 수가 정상적으로 삭제됨
ttmp['맥주정보'].iloc[10]


맨 마지막에 있던 좋아요 숫자값이 삭제되었습니다. 👍👍

3. 실제 컬럼으로 데이터 분리

이제 추출한 값을 각각의 Column으로 분리하겠습니다.

# 맨 첫번째 리스트 요소에 ID 저장
# 그 뒤로는 뒤에서부터 각 평가값 저장
ttmp['ID'] = ttmp['맥주정보'].apply(lambda x: x[0])
ttmp['Aroma'] = ttmp['맥주정보'].apply(lambda x: x[-2])
ttmp['Appearance'] = ttmp['맥주정보'].apply(lambda x: x[-4])
ttmp['Flavor'] = ttmp['맥주정보'].apply(lambda x: x[-6])
ttmp['Mouthfeel'] = ttmp['맥주정보'].apply(lambda x: x[-8])
ttmp['Overall'] = ttmp['맥주정보'].apply(lambda x: x[-10])

# 리스트의 1,2,3번째 요소만(평점날짜 or 이상한 값) 뽑아오기
ttmp['맥주정보'] = ttmp['맥주정보'].apply(lambda x:x[1:4])
ttmp['길이'] = ttmp['맥주정보'].apply(lambda x:len(x))

# 결과 확인
ttmp.head(3)

사진이 잘렸지만, 아이디와 5가지 평가 요소가 모두 잘 분리되었습니다. 😊

4. 평점|날짜 데이터 골라내기

다음 목표는 맥주정보로부터 평점과 날짜를 추출하는 것입니다.

문제는 평점, 날짜가 있는 리스트의 인덱스가 그때 그때 다르다는 것입니다.

# 특이점 확인 : 0번째, 1번째, 2번째 요소 중 뽑아낼 데이터의 위치가 불분명
for i in range(75252, 75255):
    print(ttmp['맥주정보'].iloc[i], '\n\n')

하지만 뽑아낼 데이터의 규칙성은 알 수 있습니다.

1.8May 23, 2001처럼 숫자.숫자 + 문자열로 이루어져 있죠.

이걸 정규표현식으로 표현해 보겠습니다.

# 4.0+알파벳으로 처리된 텍스트를 뽑아내기 위해 정규표현식 사용

import re

# ex) 4.0December 28, 2020 추출
reg = re.compile('[0-9]+.+[0-9]+[A-Za-z0-9]*')

그리고 정규표현식의 .match()함수를 이용해 해당 문자열 형식과 일치되는 요소만 추출하겠습니다.

# 정규표현식에 해당하는 문자열과 매칭되는 경우 해당 리스트 요소를 맥주정보에 저장
# reg.match()는 re.compile()의 정규표현식과 일치하는 문자열을 반환, 아니면 False를 반환
# 중첩 삼항 표현식 사용 : https://ooyoung.tistory.com/116

ttmp['맥주정보'] = ttmp['맥주정보'].apply(lambda x: x[0] if reg.match(x[0]) else 
                                  (x[1] if reg.match(x[1]) else x[2]))

# 결과 확인
ttmp.head()


정확하게 잘 추출되었습니다. 😊😊

5. 평점과 날짜 데이터 분리

이제 할 일은 4.0December 31, 2020 값에서 평점 4.0과 날짜 December 31, 2020을 분리하는 것입니다.

String이므로 0,1,2번째까지는 평점으로, 그 이후는 날짜로 추출하겠습니다.

# 평점은 0번째부터 3번째, 날짜는 그 이후 문자열로 처리
ttmp['평점'] = ttmp['맥주정보'].apply(lambda x : x[:3])
ttmp['날짜'] = ttmp['맥주정보'].apply(lambda x : x[3:])

ttmp.head(3)

마지막에 평점과 날짜가 잘 추출되었습니다.

전체 컬럼들에서 필요한 컬럼들만 추출하겠습니다.

# 컬럼명 변경
ttmp.columns = ['맥주', '맥주정보', '길이', '아이디', 'Aroma', 'Appearance', 'Flavor',
       'Mouthfeel', 'Overall', '평점', '날짜']

# 필요한 컬럼만 추출
ttmp = ttmp[['아이디', '맥주', '날짜', '평점', 'Aroma', 'Appearance', 'Flavor',
       'Mouthfeel', 'Overall']]

최종적으로 데이터 값을 확인하고 저장하겠습니다.

ttmp.평점.unique()

ttmp.Aroma.unique()

Aroma, Appearance, Flavor, Mouthfeel, Overall 값에는 -가 있을 수도 있습니다.

값이 -인 경우는 삭제하겠습니다.

# 세부 리뷰 값이 '-'가 아닌 데이터만 저장
ttmp = ttmp[ttmp['Aroma']!='-']
ttmp = ttmp[ttmp['Appearance']!='-']
ttmp = ttmp[ttmp['Flavor']!='-']
ttmp = ttmp[ttmp['Mouthfeel']!='-']
ttmp = ttmp[ttmp['Overall']!='-']
ttmp[ttmp['Aroma']=='-']

ttmp.info()

그런데 현재 평점, Aroma, Appearance, Flavor, Mouthfeel, Overall 값이 실수값이 아닙니다.

이것만 변경해주고 최종 데이터를 저장하겠습니다.

컬럼의 오브젝트 타입들을 실수로 변환할 때는 pd.to_numeric()함수를 이용합니다.

# 수치형 데이터는 실수로 변환 : pd.to_numeric() 함수 사용
ttmp['평점'] = pd.to_numeric(ttmp['평점'])
ttmp['Aroma'] = pd.to_numeric(ttmp['Aroma'])
ttmp['Appearance'] = pd.to_numeric(ttmp['Appearance'])
ttmp['Flavor'] = pd.to_numeric(ttmp['Flavor'])
ttmp['Mouthfeel'] = pd.to_numeric(ttmp['Mouthfeel'])
ttmp['Overall'] = pd.to_numeric(ttmp['Overall'])
# 중복된 행들을 제거합니다.
ttmp.drop_duplicates(keep='first', inplace=True)

# 최종 데이터 확인
ttmp.info()

# 최종 데이터 값 분포 확인
ttmp.describe()

그리고 저장해주겠습니다.

ttmp.to_csv('전처리후데이터.csv', encoding='utf-8')

분석하다보니 역시 데이터 수집과 전처리가 전체 프로세스의 50%이상은 차지하는 것 같습니다.

다음 포스팅에서는 분석한 데이터를 바탕으로 EDA를 해보려고 합니다. 🙂