info) 정규세션 1주차 Pandas 실습 (World_2012 데이터 사용)


1. 데이터 확인

1-1. 데이터 불러오기

import pandas as pd
import numpy as np

df = pd.read_excel('./World_2012.xlsx')
df

1-2. 데이터 형태 확인

df.head(3) #상위 3개 확인
#df.tail() #하위 5개 확인

1-3. 데이터 크기 확인

df.shape #(행,열) 크기 확인

1-4. 데이터의 결측치 확인

df.isna().sum()

1-5.  ‘Continent’ 컬럼의 고유값 확인

df['Continent'].unique()

2. 결측치 처리

2-1. GDP 결측치 처리

- 대륙별 GDP 평균을 구하여 결측치에 대체

#{Continent : GDP평균} dictionary 생성
gdp_avg = dict(df.groupby('Continent')['GDP'].mean())

#해당 continent와 GDP가 null인 값에 대해 대륙별 GDP평균으로 대체
for (continent, gdp) in gdp_avg.items():
    df.loc[(df['Continent'] == continent) & (df['GDP'].isnull()), 'GDP'] = gdp 
    
#결측치 재확인
df['GDP'].isna().sum()

2-2. 나머지 결측치도 동일한 방식으로 처리

- 함수를 정의하여 결측치가 있는 모든 컬럼에 적용

#대륙별 feature들의 평균값 확인
df.groupby('Continent').mean()

#대륙별 평균값으로 결측치를 대체하는 함수 정의
def fillwithAverage(x):
    continent_avg = dict(df.groupby('Continent')[x].mean())
    for i in continent_avg.keys():
        df.loc[(df['Continent'] == i) & (df[x].isnull()), x] = continent_avg[i]
        
column_list =list(df.columns) #feature 목록 리스트

#GDP를 비롯한 Country, Continent, Population은 제외
remove_list = ['Country', 'Continent', 'Population', 'GDP'] 
column_list = [col for col in column_list if col not in remove_list]
print(column_list)

#반복문을 통해 함수 적용
for col in column_list:
    fillwithAverage(col)
    
#결측치 재확인
df.isna().sum()

3. 파생변수 생성

3-1. 1인당 GDP 소득인 'PCI'라는 파생변수 생성

- GDP Population 활용

df['PCI'] = df['GDP'] / df['Population']
df.head(3)

3-2. 기대수명인 'Life Expectancy'라는 파생변수 생성

- Male Life Expectancy, Femal Life Expectancy feature의 평균 활용

df['Life Expectancy'] = (df['Male Life Expectancy'] + df['Femal Life Expectancy']) / 2
df.tail(2)

4. 유럽 데이터 생성

4-1. 'Continet' 컬럼에서 '유럽'에 해당하는 것만 필터링하여 europe_data에 저장

- loc 함수 사용

europe_data = df.loc[df['Continent'] == '유럽', :]
europe_data.head(3)

4-2. europe_data Population 10,000,000이상인 나라만 필터링하여 europe_data 갱신

europe_data = europe_data[europe_data.Population >= 10000000]
europe_data.head(3)

4-3. europe_data에서 PCI(1인당 소득) 기준 상위 3개 국가 확인

europe_data.sort_values('PCI', ascending = False)[:3]

4-4. europe_data에서 PCI(1인당 소득)가 가장 작은 국가 확인

europe_data.sort_values('PCI', ascending = True)[:1]

5. 선진국 데이터 생성

5-1. Population 10,000,000이상이고 PCI 30,000이상인 국가를 필터링하여 developed_data라는 변수에 저장

developed_data = df.loc[(df['Population'] >= 10000000) & (df['PCI'] >= 30000),:]
developed_data

5-2. developed_data에서 Business TR 0.3이하 이거나, Internet 0.8 이상인 국가를 필터링

- Column Country Business, TR Loan, IR만 표시

developed_data.loc[(developed_data['Business TR'] <= 0.3) | (developed_data['Internet'] >= 0.8),:][['Country', 'Business TR', 'Loan IR']]

 

info) 서브세션 1주차


[문제1]

  • 입력 파라메타 4개를 받아서 result 값을 리턴하는 함수 calculateSecond()를 작성합니다.
  • 입력 파라메타는 모두 정수이며, day는 날짜 수, hour는 시간 수, minute는 분 수, second는 초 수입니다.
  • 리턴 값인 result는 입력 파라메타를 모두 초로 환산하여 합산한 결과인 정수 값 입니다.
  • 예를 들어, calculateSecond(1,1,1,1)을 실행하면, result는 하루(24시간) + 한시간 + 1분 + 1초를 초(second)로 환산한 값으로 90061 입니다.

[풀이]

#calculateSecond 함수 정의
def calculateSecond(day: int, hour: int, minute: int, second: int): # 각 변수에 타입 설정
    result = second + (minute * 60) + (hour * 60 ** 2) + (day * 24 * 60 ** 2) # 계산식을 통해 초 변환
    return int(result)
    
#calculateSecond(1,1,1,1) 결과 출력
calculateSecond(1,1,1,1)

[문제2]

  • 입력 파라메타 1개를 받아서 result 값을 리턴하는 함수 reverseString()를 작성합니다.
  • 입력 파라메타는 문자열이며, 길이의 제한은 없습니다.
  • 리턴 값인 result는 입력 파라메타의 문자열에 속한 글자들의 순서를 뒤집은 문자열 입니다.
  • 예를 들어, reverseString("Hello World!")을 실행하면, result는 !dlroW olleH를 저장한 문자열이 됩니다.

[풀이]

Sol1) 문자열 슬라이싱을 활용하여 역순 출력

문자열[시작:끝:규칙]에서 세 번째 숫자를 잘 설정해주면 해당 문자열의 역순을 출력할 수 있다. 세 번째 숫자의 default는 1로 문자열의 인덱스를 순서대로 하여 슬라이싱을 하게 된다. 이를 2로 바꿔주면 한 칸씩 건너뛰며 [0], [2], [4]...인덱스를 슬라이싱하게 된다. 만약, -1를 넣게 되면 뒤에서부터 [-1], [-2]. [-3] 순으로 슬라이싱하게 된다.

# reverseString1() 함수 정의
def reverseString1(string):
    result = string[::-1] #
    return result   
    
reverseString1("Hello World!")

Sol2) for문을 통해 역순 출력

original 문자열의 앞에서부터 하나씩 가져오고 이를 빈 문자열의 앞에 계속 넣어준다. 아래처럼 역순을 반환한다.

'' + 'H'
'e' + 'H'
'l' + 'eH'
'l' + 'leH' 

# reverseString2() 함수 정의
def reverseString2(string):
    reversed_word = ''
    for a in string:
        reversed_word = a + reversed_word
    return reversed_word
    
reverseString2("Hello World!")

 

3) 리스트의 reverse() 함수를 사용해 역순 출력

문자열을 리스트로 변환하여 리스트를 reverse하고 이를 빈 문자열과 join하여 reversed_word로 만들어 준다.

# reverseString3() 함수 정의
def reverseString3(string):
    reversed_word = ''
    string_list = list(string)
    string_list.reverse()
    return reversed_word.join(string_list)
    
reverseString3("Hello World!")

[문제3]

  • 입력 파라메타 3개를 받아서 result 값을 리턴하는 함수 calcTwoCharactersFromString()를 작성합니다.
  • 입력 파라메타 중 첫번째(iString)는 문자열이며, 두번째(iCh1)와 세번째(iCh2)는 각각 글자 하나 입니다.
  • 리턴 값인 result는 iString 문자열 안에 iCh1 글자와 iCh2 글자가 몇 개나 포함되었는지에 대한 정수 입니다.
  • 예를 들어, calcTwoCharactersFromString("You only live once", 'o', 'Y')을 실행하면,result는 문자열내 o의 갯수인 3과 Y의 갯수인 1을 더한 정수 4가 됩니다.

[풀이]

문자열의 count()함수를 통해 해당 문자열이 몇 개 있는지 카운트 한 후 이를 합하여 반환한다.

# calcTwoCharactersFromString함수 정의
def calcTwoCharactersFromString(iString, iCh1, iCh2):
    iCh1_count = iString.count(iCh1)
    iCh2_count = iString.count(iCh2)
    return iCh1_count + iCh2_count 
    
calcTwoCharactersFromString("You only live once", 'o', 'Y')

'BACS > 서브세션' 카테고리의 다른 글

[서브세션] 서브세션 3주차  (0) 2022.11.06
[BACS] 서브세션 2주차  (0) 2022.10.01

info) BACS 1주차는 Titanic 데이터를 사용하여 Pandas 실습을 진행하였습니다.

실습을 위한 Titanic csv 파일은 Kaggle의 'Titanic - Machine Learning from Disaster' 페이지에서 다운로드 가능하며 원본 파일은 train.csv이나 다른 파일과의 혼동을 방지하기 위해 Titanic으로 파일명을 변경하여 진행하였습니다. 


4. 파생변수 만들기

'Age' 컬럼을 변형하여 연령대를 의미하는 'AgeBand' 컬럼을 새로운 파생변수로 추가하고자 합니다. 이를 위해서 2가지 방법을 사용할 수 있는데 각각의 방법을 살펴보겠습니다.  'AgeBand' 컬럼의 value는 '20대 미만', '20대', '30대', '40대', '50대', '60대 이상' 총 6개의 값으로 이루어지도록 구성하겠습니다. 연령대를 더 세분화하고 싶다면 조건을 더 추가해 주시면 됩니다. 

1) loc 메소드 사용

먼저 'AgeBand'라는 컬럼을 빈 값들로 채워 지정해줍니다. 그리고 loc[행에 대한 부분, 열에 대한 부분]을 사용해 행부분에 'Age'값에 대한 조건을 지정해주고 열부분에는 'AgeBand'를 넣어 조건에 맞는 해당되는 부분을 빈값에서 연령대로 바꿔주면 됩니다.

df['AgeBand'] = '' #빈값으로 구성된 새로운 AgeBand 컬럼을 생성 

#loc메소드에 각 조건을 지정하여 빈값을 해당 연령대로 변경
df.loc[(df['Age'] < 20), 'AgeBand'] = '20대 미만'
df.loc[(df['Age'] >= 20) & (df['Age'] < 30), 'AgeBand'] = '20대'
df.loc[(df['Age'] >= 30) & (df['Age'] < 40), 'AgeBand'] = '30대'
df.loc[(df['Age'] >= 40) & (df['Age'] < 50), 'AgeBand'] = '40대'
df.loc[(df['Age'] >= 50) & (df['Age'] < 60), 'AgeBand'] = '50대'
df.loc[df['Age'] >= 60, 'AgeBand'] = '60대 이상'

2) age_band() 함수를 정의하고 apply lambda 적용

다른 방법으로는 if문을 통해 각 연령대를 반환하는 함수를 정의하고 이를 컬럼에 apply lambda를 통해 적용하는 것입니다. 아래 코드를 통해 직접 살펴보겠습니다.  

#age_band() 함수 정의
def age_band(x):
    if x < 20:
        return '20대 미만'
    elif x < 30:
        return '20대'
    elif x < 40:
        return '30대'
    elif x < 50:
        return '40대'
    elif x < 60:
        return '50대'
    else:
        return '60대 이상'
#apply lambda를 통해 함수를 적용하여 'AgeBand'컬럼 생성
df['AgeBand'] = df['Age'].apply(lambda x: age_band(x))

5. 그룹별로 집계하기

groupby를 통해 생존률에 대해 여러 조건으로 집계를 해보도록 하겠습니다. Sex별, Pclass별, Embarked별, AgeBand별 등 각 그룹별 생존율을 살펴보고 이를 통해 타이타닉 침몰 사건에서 생존을 한 사람들의 특징들을 확인해보시기 바랍니다. 번쩍이는 인사이트는 아니지만 이러한 방식으로 가설들을 세울 수 있다는 점을 살펴 볼 수 있습니다. 

추가) 생존률은 mean() 평균을 통해 구할 수 있습니다. 'Survived'컬럼이 생존 시 1, 사망 시 0으로 되어 있기 때문에 이들 값들의 평균은 '생존인원 / 전체 인원'과 동일하며 이는 생존율이라고 할 수 있습니다. 

#Sex별 생존율
df.groupby(['Sex'], as_index=False)['Survived'].mean()

 

#Pclass별 생존율
df.groupby(['Pclass'], as_index=False)['Survived'].mean()

#Embarked별 생존율
df.groupby(['Embarked'], as_index=False)['Survived'].mean()

#'AgeBand'별 생존율
df.groupby(['AgeBand'], as_index=False)['Survived'].mean()

[분석결과]

1) Sex별 생존율에서 남성에 비해 여성의 생존율이 월등히 높았습니다. 탈출 과정에서 여성, 노인 등과 같이 보호 대상들을 우선으로 하였기 때문이지 않을까 짐작됩니다. AgeBand별 생존율에서는 60대 이상을 제외하고는 거의 비슷한 확률 분포를 보였습니다. 노인분들의 경우 아무리 탈출 우선 순위에 두었다 하더라도 신체적인 제약으로 인해 생존율이 현저히 떨어진 것이 아닐까 생각됩니다. 

2) Pclass별 생존율에서는 1등급에서 낮은 등급으로 갈수록 생존율이 급격히 떨어지는 것을 확인할 수 있었습니다. 1등급 탑승객들을 우선으로 하여 탈출을 진행했을 수도 있고 1등급 객실이 2,3등급에 비해 위치 상 탈출 경로에 더 가까웠을 수도 있습니다. 물이 먼저 차오르는 낮은 층에 낮은 등급의 객실이 많이 분포되어 있었을 것 같다는 생각이 듭니다. 이러한 분석 결과는 타이타닉호의 선실 구조 자료를 통해 검증해볼 필요가 있을 것입니다. 

3) 가장 흥미로웠던 것은 Embarked별 생존율이었습니다. 개인적으로 탑승 지역은 생존율과 큰 관계가 없을 것으로 생각하였습니다. 따라서, Embarked별 생존율은 비슷하게 분포되었을 것이라고 예상하였습니다. 하지만 다른 지역과 달리 C지역에서 탑승한 승객의 생존확률이 50%이상이라는 수치를 보였습니다.

이것이 유의미한 수치인지에 대해 추가적인 가설검증의 과정이 필요하지만 대충 짐작해보면 C지역의 탑승객 중 부유층이 많지 않았을까 생각됩니다. 부유층일수록 1등급칸에 많이 탑승했을 것이고 고로 탈출 확률이 높았을 것입니다. 이를 위해 한번 Embarked별 평균 Fare(혹시 몰라 중앙값도 확인)와 Pclass 분포를 확인해보겠습니다. 즉, 탑승지역에 따라 부유층의 비율이 다른지 살펴보는 것입니다. 

#Embarked별 Fare의 평균
df.groupby(['Embarked'], as_index=False)['Fare'].mean()

##Embarked별 Fare의 중앙값
df.groupby(['Embarked'], as_index=False)['Fare'].median()

(좌) 평균 운임료 (우) 운임료 중앙값

운임료에 대한 평균값, 중앙값 둘 다 확인해본 결과 C지역 탑승객들이 다른 지역보다 더 비싼 객실을 이용한 것으로 확인되고 있습니다. 확실히 C지역의 탑승객들이 다른 지역에 비해 더 부유한 것을 알 수 있습니다. 

pclass_df = pd.DataFrame(df.groupby(['Embarked','Pclass'])['Pclass'].count())
embarked_df = pd.DataFrame(df.groupby(['Embarked'])['Pclass'].count())
ratio_df = ((pclass_df / embarked_df) * 100).round(2) 
ratio_df.columns = ['Pclass 비율']
ratio_df

한편, Embarked 지역별 Pclass 분포를 확인했을 때 절대적인 1등급 탑승객의 수는 S지역이 가장 많았지만 비율로 환산했을 때는 C지역이 제일 높았습니다. C지역 탑승객 중 무려 50%가 1등급칸을 이용한 것을 볼 수 있습니다. 지금까지의 분석을 종합해 보면 확실히 C지역의 탑승자들에 부유층이 많음을 확인할 수 있고 때문에 생존율이 높았다고 결론 지을 수 있을 듯 합니다. 물론, 탈출 과정에서 부유층이 더 우선순위에 있었는지 혹은 1등급 객실이 탈출 경로에 더 가까웠는지는 다른 자료를 통해 확인은 해보아야 할 것입니다. 하지만, 지금까지의 분석을 통해서 적어도 1등급칸을 이용한 승객이 살아남을 확률이 꽤 높았음을 충분히 유추할 수 있습니다. 

info) BACS 1주차는 Titanic 데이터를 사용하여 Pandas 실습을 진행하였습니다. 

실습을 위한 Titanic csv 파일은 Kaggle의 'Titanic - Machine Learning from Disaster' 페이지에서 다운로드 가능하며 원본 파일은 train.csv이나 다른 파일과의 혼동을 방지하기 위해 Titanic으로 파일명을 변경하여 진행하였습니다. 

 

1. 데이터 불러오기

Titanic 파일은 콤마(,)로 구분된 csv파일입니다. Titanic 파일을 read_csv()를 통해 데이터프레임 형식으로 가져오도록 하겠습니다. 이때 경로 설정에 유의해주시기 바랍니다. 1) 모든 경로를 설정해주시거나 또는 2) 사전에 디렉토리 경로를 설정해주어 파일명만을 통해 가져오는 방법이 있습니다. 편하신 방법을 이용하시면 됩니다. 

import pandas as pd

df = pd.read_csv('./Titanic.csv') #다운로드한 csv파일을 데이터프레임 형식으로 불러오기
df

 

2. 데이터 개요 확인하기

데이터프레임은 행과 열로 구성된 2차원의 자료구조입니다. 따라서, 행과 열의 크기를 통해 데이터의 전체적인 사이즈를 확인할 수 있습니다.

train.shape

또한, 행의 수가 많은 경우 상위 n개와 하위 n개만을 가져와 약식으로 데이터의 모습을 확인할 수 있습니다.  head()와 tail()의 파라미터로 원하는 개수의 숫자를 넣어주면 되고 생략시 default값은 5입니다.

df.head() #df의 상위 5개 행을 확인

df.tail(10) #df의 하위 10개 행 확인

지금까지 Titanic의 개괄적인 부분을 살펴보았으니 각 feature들의 value 빈도를 확인해보겠습니다. 그래프를 통해 값의 분포를 확인하는게 좋으나 이번에는 pandas만을 사용하는 시간이니 value_counts를 통해 결과를 가져와 보겠습니다. 수치형 데이터는 분포가 너무 퍼져 있어 한눈에 보이지가 않고 출력물이 너무 길어지게 되어 간단히 범주형 데이터로 구성된 컬럼 몇 개만 확인해보겠습니다. 

df.columns #df의 column에 어떤 항목들이 있는지 확인
category_list = ['Survived', 'Pclass', 'Sex','Embarked'] #category 데이터로 이루어진 컬럼 중 일부만 가져오기

#반복문을 활용해 출력
for col in category_list:
    print("[{}]".format(col)) #해당 컬럼을 제목으로 입력
    print(df[col].value_counts()) #value_counts()를 사용해 값의 빈도 가져오기
    print("-" * 40, end='\n') #컬럼 간 구분

 

3. 결측치 확인 및 처리

isnull() 또는 isna()를 통해 결측치에 해당하는 부분을 boolean 값(True/False)로 반환할 수 있습니다. 파이썬에서는 True를 1로 인식하고 False를 0으로 인식하기 때문에 sum()을 통해 '합계'를 구하면 결국 '개수'와 동일한 결과를 얻을 수 있게 됩니다. 

df.isna().sum() #isna() 대신 isnull()도 가능

df.notnull().sum() #notnull()을 사용하면 null값이 아닌 것의 개수를 반환

결측치를 확인한 결과 'Age' 컬럼에 177개, 'Cabin' 컬럼에 687개, 'Embarked' 컬럼에 2개가 있습니다. 그러면 각 컬럼에서 결측치를 처리해보도록 하겠습니다. 각 case별로 하나씩 해보겠습니다. 

[Case1] 'Age'의 결측치 처리

결측치가 발견되는 경우 해당 결측치를 다른 값으로 '대체'를 하거나 결측치가 존재하는 컬럼이나 행을 '삭제'할 수 있습니다. 데이터의 성격에 따라 처리 방식은 달라질 수 있기 때문에 반드시 충분한 검토 후에 결측치 처리를 진행해야 합니다. 우선 'Age' 컬럼의 경우 결측치는 177개입니다. 꽤 많은 결측치가 있으나 우리가 이후 Titanic 데이터에 대한 가설을 설정할 때 연령 데이터는 중요한 요인이 될 수 있기 때문에 제거보다는 값을 대체하는 것이 더 좋은 선택이라 할 수 있습니다. 

'대체'의 경우에도 2가지 방식을 사용할 수 있습니다. 

1) 평균 연령으로 대체

mean()함수를 통해 'Age'의 평균을 구하고 이를 fillna()값에 넣어 대체할 수 있습니다. 

df.fillna(df['Age'].mean(), inplace = True) #'Age'의 평균값으로 결측치 대체

2) 성별 별 연령의 평균으로 대체

groupby()함수를 사용해 성별 별 평균 연령을 확인해보겠습니다. 

df.groupby('Sex')['Age'].mean()

각 평균값을 반올림하여 각각 28, 31로 하고 NaN값에 대체해보겠습니다. loc()함수에 2개의 조건을 주어 male이면서 나이가 null인 경우와 female이면서 나이가 null인 경우를 확인하고 각각의 평균값을 넣어주겠습니다. 

df.loc[(df['Sex'] == 'male') & (df['Age'].isnull()), 'Age'] = 31
df.loc[(df['Sex'] == 'female') & (df['Age'].isnull()), 'Age'] = 28

[Case2] 'Cabin'의 결측치 처리

'Cabin' 컬럼의 경우 결측치가 687개로 전체 데이터 개수인 891개에서 너무 많은 비중을 차지하고 있습니다. 따라서 값을 대체해도 무의미한 결과가 나올뿐만 아니라 'Cabin' 컬럼의 특성 상 그 값들이 특정 값으로 대체되기 어려운 데이터입니다. 상식적으로 cabin(선실)이라는 것은 대부분의 사람들이 각자가 예약한 방을 사용하기 때문에 가족이 아닌 이상 모두 다른 cabin을 이용해 하나의 값으로 대체하기가 어렵습니다. 따라서, 'Cabin' 컬럼의 경우에는 아예 컬럼을 삭제하는 선택을 하도록 하겠습니다. drop()에서는 index를 통해 행을 columns를 통해 열을 삭제할 수 있습니다.

df.drop(columns = 'Cabin', inplace = True)

한편, column을 drop해주는 방법도 있지만 결측치가 있는 데이터 row를 제거해주는 방법이 있습니다. 하지만 'Cabin'의 경우 결측치가 너무 많기 때문에 추천하지는 않습니다. 왜냐하면 너무 많은 데이터 row값이 삭제되어 데이터 샘플 수가 부족해지고 데이터에 왜곡이 생기는 문제가 생기기 때문입니다. 

df.dropna(subset=['Cabin'], inplace = True) #df에서 'Cabin'컬럼이 null인 행만 삭제

[Case3] 'Embarked'의 결측치 처리

마지막으로 'Embarked' 컬럼의 경우 결측치가 2개로 적은 편입니다. 'Embarked' 데이터의 분포를 보면 다음과 같이 'S'가 644개로 가장 많습니다. 따라서, 결측치 값을 'S'로 채워줄 수 있습니다. 

df['Embarked'].value_counts() #'Embarked'컬럼의 값 분포를 확인
df['Embarked'].fillna('S', inplace =True) #결측치를 'S'로 대체

 

Warning) 아래 코드들은 버전이 바뀌어 정상적으로 작동되지 않을 수도 있습니다. 따로 버전을 명시하지 않았기 때문에 에러가 발생되면 조금씩 수정해 나가면서 사용하시길 바랍니다. 또한, 형편없는 저의 실력으로 코드들이 다소 비효율적일 수 있음을 미리 말씀드립니다. 우연히 이 글을 보게 되신 분들은 참고해주시기 바랍니다. 

file-merge.ipynb
0.07MB


STEP 3. 데이터 수집하기

STEP3에서는 이전 단계에서 만든 스크래핑 함수를 사용해 데이터를 수집하도록 하겠습니다. 그리고 파일 형태로 출력한 데이터들을 일부 수정하고 파일들을 합쳐 하나의 데이터 파일로 만들어 보겠습니다. 이번 스텝에서는 인내를 가지고 오랜 시간을 기다리는 것이 관건입니다. 잘 기억은 나지 않지만 저의 경우 한 개 제품에 대해 스크래핑을 진행할 때 8분 정도 소요되었던 것으로 기억합니다. 그래서 8개 제품을 진행했을 때 대략 50분 정도 걸렸다고 할 수 있습니다. 수집하게 되는 샘플 데이터 수가 적은 편이라 일반적인 텍스트 분석 프로젝트에서 데이터를 수집하는 과정에서 소요되는 시간에 비해서는 세발이 피이지만 저한테는 쉽지 않았던 과정이었습니다. 런타임이 갱신되어 도중에 중단된 적도 몇번 있었습니다. 그래서 이후에도 다시 말씀드리겠지만 중간에 발생하는 오류로 데이터를 날리는 일이 없도록 하기 위해 제품별 url을 하나의 리스트로 묶어 반복 실행하지 않고 일부러 따로 따로 독립적으로 수집하였습니다.

(1) 제품별 리뷰 데이터 수집

8개 제품에 대한 네이버 쇼핑 url을 가져와 평점 리스트와 함께 shopping_review_scraping() 함수에 넣었습니다. 그리고 함수의 결과로 반환되는 데이터프레임을 csv 파일로 변환하여 저장하였습니다. 아래는 제품별로 스크래핑을 진행한 코드들입니다. url과 평점 리스트를 제외한 나머지 부분은 같기 때문에 2개의 코드만 가져왔고 이와 같은 방식을 8번 진행했다고 보시면 됩니다. 수집 결과 아래 사진과 같이 총 8개의 csv 파일이 만들어지게 됩니다.  

#풀무원 얇은피 꽉찬속 땡초만두
pulmuone_ddaengcho_url = 'https://search.shopping.naver.com/catalog/21554501906?NaPm=ct%3Dl77a62js%7Cci%3D5a5bdf33fddab8a692dc7dc7ad7b111851a8415a%7Ctr%3Dslsl%7Csn%3D95694%7Chk%3D3b59e90756e0c9de00247557f9d560f3908617a1'
pulmuone_ddaengcho_df = shopping_review_scraping(pulmuone_ddaengcho_url, [1,2,3,4,5])
pulmuone_ddaengcho_df.to_csv('pulmuone_ddaengcho_review.csv', index=False, encoding = 'utf-8-sig')​
#CJ제일제당 비비고 수제김치만두 400g×2
bibigo_url = 'https://search.shopping.naver.com/catalog/24088993894?query=%EB%B9%84%EB%B9%84%EA%B3%A0%20%EA%B9%80%EC%B9%98%EB%A7%8C%EB%91%90&NaPm=ct%3Dl75tunb4%7Cci%3D81e459f36f72ca8812ff811e28306c972e961483%7Ctr%3Dslsl%7Csn%3D95694%7Chk%3D923a154dbf1e412d62c5544e1e9435573a05215a'
bibigo_review_df = shopping_review_scraping(bibigo_url,[3,4,5])
bibigo_review_df.to_csv('bibigo_review.csv', index=False, encoding = 'utf-8-sig')


(2) 중복된 데이터 제거 후 파일 병합

지금까지 수집한 파일들은 output_file 폴더에 저장하였고 총 8개 파일이 있습니다. 파일들이 다 파편적으로 분리가 되어 있으면 텍스트 분석을 진행하는데 있어 번거로움이 발생합니다. 따라서, 파일들을 하나로 병합하도록 하겠습니다. 그리고 파일 병합을 진행하기 전에는 중복된 파일들이 있는지 확인하고 중복된 경우에는 제거하도록 하겠습니다.

사실 정확하게 스크래핑을 진행했다면 중복된 데이터가 없어야 하긴 합니다. 그러나 제가 만든 스크래핑 함수 중간에 페이지 이동이 원할하지 않은 부분이 있어서인지 csv 파일을 확인해본 결과 중복 데이터들이 존재하였습니다. 깔끔하게 수집이 되었으면 하는 아쉬움이 있었지만 여튼 중복을 제거하는 것은 간단한 일이니 함께 진행해보도록 하겠습니다. 

#output_file 폴더에 있는 리뷰 데이터 파일 리스트를 가져오기
path = './output_file'
file_list = os.listdir(path)
print ("file_list: {}".format(file_list))

#새로운 데이터 프레임 생성
df_all_review = pd.DataFrame()

#각 파일을 불러와 읽어낸 후 df_all_review에 결합
for file_name in file_list:
    df_new = pd.read_csv(path + '/' + file_name)
    df_all_review = pd.concat([df_all_review, df_new], ignore_index= True)

8개 파일에 있는 내용들을 새로운 데이터프레임 한 곳에 모은 후에는 중복된 리뷰 데이터들을 제거하겠습니다. 중복이 제거된 데이터프레임은 다시 새로운 파일로 저장하겠습니다. 

#중복된 리뷰 제거
df_all_review.drop_duplicates(subset=None, keep='first', inplace=True, ignore_index=True)
df_all_review = df_all_review.reset_index(drop=True)
df_all_review = df_all_review.rename(columns={'Reveiw':'Review'})

#병합한 데이터프레임을 새로운 파일로 저장
df_all_review.to_csv('./output_file/all_review.csv', index=False)

중복 제거까지 진행한 결과 최종 수집된 데이터는 19445개로 당초 예상했던 2만개에는 조금 못 미치는 정도입니다. 중복된 내용 때문에 계획한 샘플 수를 충족시키지는 못했지만 결과에는 큰 영향을 주지 않을 것 같아 그냥 진행하도록 하겠습니다. 


(3) 쇼핑몰별 리뷰 데이터 수 확인

조금은 쓸데없는 과정이기는 하지만 쇼핑몰별로 수집된 리뷰 데이터의 수를 확인해보도록 하겠습니다. 앞서 프로젝트 계획 단계에서 편향되지 않은 분석 결과를 도출하기 위해 가능한 다양한 쇼핑몰에서의 리뷰를 가져오겠다고 말씀드린 적이 있습니다. 사실 리뷰의 출처가 되는 쇼핑몰이 다양할수록 리뷰 또한 다양하고 편향되지 않는다고 말할 수는 없습니다. 쇼핑몰과 리뷰 내용의 편향성은 상관관계가 높지 않을 것입니다. 하지만 한쪽 쇼핑몰의 리뷰만 가져올 경우에는 분명 해당 쇼핑몰만의 문제들이 분석 결과에 다소간 영향을 끼칠 수도 있는 있을 것입니다. 때문에 제 생각에는 쇼핑몰의 다양성을 유지하는 것이 그리 나쁜 선택은 아닌 것 같습니다. 

#all_review 파일을 불러와 'Mall'에 대해 value_counts()를 적용
df_all_review.to_csv('./output_file/all_review.csv', index=False)
df_mall = pd.DataFrame(df_all_review['Mall'].value_counts())
print(df_mall)

#쇼핑몰별 리뷰 수에 대해 수평 막대 그래프 그리기
df = df_mall.sort_values(by=['Mall'], ascending=True)
df.plot.barh(y = 'Mall',
             figsize=(8, 8),
             xlabel="Mall Name",
             title="Reveiw Counts from Each Mall",
             width=0.8, 
             color=['#2a9d8f', '#e9c46a', '#f4a261', '#e76f51'], 
             legend=False)
plt.tight_layout()
plt.show()

아래 결과를 보면 이건 쫌... 이라는 생각이 드실 것 같습니다. 생각보다 쇼핑몰간 편차가 커 보입니다. SSG닷컴이 거의 전체 샘플 데이터의 절반 이상을 차지하고 있고 11번가까지 정도만 천단위 샘플이 수집되었고 나머지는 거의 미미한 수준입니다. 한편, 쿠팡이나 마켓컬리와 같이 최근 많은 매출을 기록하고 있는 쇼핑 플랫폼의 데이터가 없는 것이 아쉬움이 남습니다. 쿠팡 같은 경우는 네이버 쇼핑과 연동이 되지 않아 따로 수집해야 되는 것으로 보입니다. 여튼 분포 결과가 샘플의 균형을 맞추기에는 다소 부족한 부분이 있지만 실제로 결과물을 가져와 보아야 정확히 알 수 있을 듯 싶습니다. 

다음 단계에서는 이번 프로젝트의 첫번째 핵심이라고 할 수 있는 텍스트 전처리를 진행하도록 하겠습니다. 전처리 수준에 따라 결과물의 내용이 달라질 수 있기 때문에 많은 심혈을 기울여야 될 것 입니다. 다음 스텝에서 뵙도록 하겠습니다. 

+ Recent posts