Machine Learning Technical

편하게 써보는 ML 2부 메뉴 추천 프로그램 (1/2) – 복잡한 데이터 다루기

2부를 시작하며…

1부에서는 로또 당첨 데이터를 이용해서 ML을 적용해보며 ML의 전체적인 진행 절차를 살펴 보았었습니다. 2부에서는 메뉴 추천 프로그램을 만들어보며 데이터 수집이나 전처리 과정에 대해서 살펴보려고 합니다. 다시 한 번 강조해도 부족함이 없을 정도로 ML에서는 이 데이터를 처리하는 부분이 가장 중요합니다. ML을 동작하도록 하는 코드 자체는 어느정도만 노력한다면 금방 따라갈 수 있지만, 데이터에 대한 부분은 다양한 통계학적 지식과 더불어 많은 시간을 들여서 수행해야 합니다. 이 글을 쓰고 있는 필자도 개발자적 지식은 어느정도 있을지 모르나 통계학, 데이터 분석에 관한 지식은 계속 공부하고 있는 중입니다. 따라서 어디까지나 ‘쉽게 써 볼 수 있는’ 가이드글로 읽어주시며, 혹시 어색한 내용이 있을 경우 의견 주시길 바랍니다.

캐글(Kaggle) 과 데이터 소개

이번 글에서는 캐글에서 데이터를 가져오려고 합니다. 캐글(Kaggle)은 2010년도 만들어진 데이터 분석, 예측 플랫폼입니다. 기업 이나 단체 또는 개인이 데이터에 대한 과제를 등록하고, 이를 해결하기 위한 ML모델을 분석하고 개발하고 경쟁하는 일종의 ML 경시대회 사이트입니다. kaggle에서는 꼭 대회에 참가하는게 아니더라도 ML을 수행해볼만한 다양한 데이터 집합을 얻을 수 있습니다. 특히 다음 라이센스들의 경우 사실상 상업적인 용도를 포함하여 자유로운 사용이 가능합니다.

  1. Creative Commons : 데이터의 복사, 수정, 배포 및 상업적 용도로(even for commercial purposes) 사용가능 . 가장 자유로운 라이센스
  2. GPL (General Public License) : 소스를 공개할 경우 상업적인 용도로 사용 가능
  3. Database Contents License : 상업적인 용도로 사용 가능

http://www.kaggle.com로 들어가 좌측의 data 탭을 누르거나. www.kaggle.com/datasets 로 들어갈 경우 데이터 집합들을 확인할 수 있으며, 우측 중앙의 filter 항목을 통해서 이 라이센스들로 이루어진 데이터집합만을 볼 수 있습니다.

www.kaggle.com/Datasets
원하는 라이센스나 파일 타입을 지정하고 Done을 누르면 검색이 됩니다.

other 라이센스의 경우에도 조건부로 상업적인 용도로 사용할 수 있는 경우도 있으니 사용하고 싶은 데이터가 있다면 포기하지 말고 한 번 확인해봐야 합니다.

메뉴 추천 프로그램 사용 데이터

우리가 오늘 사용할 데이터는 Creative Commons 라이센스(CCO) 를 갖고 있는 food choice 라는 데이터 집합입니다.

가운데 License 항목을 클릭하면 어떤 라이센스인지 자세히 볼 수 있다.

이 데이터는 Mercyhurst University 의 학생들에게서 음식의 선호도와 관련된 설문조사를 한 데이터입니다. 위 사진의 우측 위 Download 버튼을 누르면 워드 형식으로 작성된 설문지와 엑셀파일 형식으로 저장된 답변을 볼 수 있습니다. 설문지의 질문을 보면 단순히 ‘어떤 종류의 음식을 좋아하세요’ 와 같이 직접적으로 음식의 취향을 물어보는 질문만이 아니라, 답변자의 심리적인 요소를 물어보는 특이한 질문도 많이 존재합니다. 예를 들어 다음과 같은 질문이 있습니다.

음료수라는 단어를 들었을 때 생각나는 음료는 어떤 건가요?
1. 오렌지 주스
2. 콜라

이런 질문들은 food_choice 데이터의 가장 큰 장점 중 하나입니다. ML을 하기 위해 데이터를 모을 때 우리는 직관적으로 의도하는 결과와 관련되어 있어 보이는 특성만을 데이터 모으는 경우가 많습니다. food_choice 데이터의 이런 질문 들을 통해서 우리는 이 질문과 같이 음식의 선호도와 관련이 없어 보이는 데이터가 실제로 ML을 돌려보면 어느정도의 영향을 끼치게 되는지 살펴 볼 수 있을 것입니다.

데이터 전처리 (Data Preprocessing)

데이터 전처리 작업의 필요성

food_chioce 데이터의 경우 흥미로운 질문과 다양한 답변을 얻을 수는 있지만 한가지 문제가 있습니다. 1편의 로또 데이터와 다르게 데이터가 정돈되어있지 않다는 점입니다.

nan은 무응답을 말합니다. 강제성있는 설문조사가 데이터가 아니기 때문에 특정 질문에 대답하지 않는 경우가 있습니다.

심지어 주관식 질문들도 존재합니다. 이런 설문조사 데이터가 아니더라도 데이터가 정리되어있지 않은 경우는 많이 존재합니다. 예를 들어 센서를 통해 자동으로 가져오는 데이터라면 센서가 고장날 때는 데이터를 가져올 수 없을 것입니다. 따라서 ML을 하기 위해서는 이런 무응답이나 주관식 데이터와 같이 예상치 못한 데이터를 어떻게 처리할지에 대해서 많은 고민을 해야합니다. 어떤식으로 처리하느냐에 따라서 ML의 결과값을 많이 달라집니다. 아예 해당 데이터 자체를 제거하는 방법도 있고, 칼럼에 따라 nan은 0으로 하는 방법을 사용할 수 도 있습니다.

데이터 전처리 방법

데이터 전처리에 대한 부분만 공부하려 해도 책 한 권은 봐야 합니다. 하지만 우리 글의 목표는 ML을 ‘써보는’ 것이기 때문에 여기서는 직관적으로만 살펴보고 딱 필요한 만큼만 간단하게 해보려고 합니다.

데이터 전처리 1. 특정 칼럼 지우기

이제부터 데이터 정리를 한번 시작해 보도록 합시다. 방정리를 할 때 가장 먼저 해야하는 일은 버릴껄 버리는 작업입니다. 현재 우리가 가져온 food_coded 파일은 총 61개의 속성을 갖는 126개의 데이터로 이루어져 있니다. 우리는 이번에도 분류 알고리즘을 사용해보려고 합니다. 분류 알고리즘에서 쓰기 힘든 속성을 먼저 제외 해보도록 하겠습니다 . 가장 대표적으로 제외할 수 있는게 바로 서술형 속성들입니다. 분류 알고리즘의 경우 수치화된 데이터가 필요하기 때문에, 서술형으로 이루어진 데이터는 수치화하거나 제외하는 작업이 필요합니다. 서술형 데이터 내의 단어들을 파싱을 하여 수치화된 데이터로 변환할 수 있습니다. 하지만 이번 글에서는 우선은 그냥 해당 칼럼을 제외하도록 하겠습니다.

데이터 전처리 2. 특정 조건의 데이터 행 지우기

다음으로 정리해야 할 부분은 불성실한 응답자의 자료들 입니다. 불성실한 응답자란, nan(무응답)으로 대답한게 많은 경우를 말합니다. ML에서는 이 nan값을 처리할 때 상당히 많은 고민이 들어가게 된다. 어떤방식으로 처리하느냐에 따라서 결과값이 완전히 달라질 수도 있기 때문입니다. 예를 들어, (1) 불만, (2) 보통 (3)좋음 의 3가지 대답이 가능한 질문지가 있다고 가정하자. 이때 만약 무응답지가 있다면 우리는 다음과 같이 해당 응답을 처리할 수 있습니다.

  1. 무응답지는 (2) 보통으로 놓는다.
  2. 무응답지는 0이나 4와 같은 전혀 다른 값으로 놓는다.
  3. 무응답지가 있는 데이터를 지운다.
  4. ….

데이터가 충분하다면 사실은 3번이 가장 좋은 방법입니다. 어떤식으로 처리하든 데이터를 바꾸는 순간, 그 데이터는 일종의 Noise 가 되며 전체적인 ml 학습의 품질을 저하시키기 때문입니다. 따라서 데이터가 한 1만개, 2만개 있다면 과감하게 노이즈 데이터들은 지우는게 좋은경우가 많습니다. 하지만 상황에 따라서 특정패턴 데이터 종류가 어쩔 수 없이 적은 경우와 같이 다소의 노이즈를 감소하고서도 데이터를 치환해서 쓸 때도 있습니다. 우리가 지금 사용하는 데이터의 경우가 사실 그런경우인데, 데이터의 전체적인 수 자체가 100여개로 매우 적습니다. 따라서 nan을 다른 값으로 치환해서 쓰는 방법을 사용하려고 합니다. 하지만 그래도 너무 심한 노이즈는 자제하기 위해서, nan으로 4번이상 대답한 데이터는 지우도록 하겠습니다.

데이터 전처리 3. 특정 조건의 데이터를 임의로 치환하기

데이터를 치환하는 방법은 데이터마다도 사람마다도 다 다른 방법으로 수행할 수 있습니다. 필자의 경우 범주형 데이터의 경우 ‘0’으로, 숫자 데이터의 경우 ‘평균값’으로 치환하는 방법으로 한 번 수행 해보려고 합니다. 여러분도 여러분만의 규칙으로 시도해 보는걸 추천합니다.

PYTHON을 활용한 데이터 전처리 작업

이제 실제로 데이터를 지우는 작업을 수행해 봅시다. 이전 글과 동일하게 환경은 Google Colab에서 python을 사용려고 합니다. 물론 데이터 정리는 엑셀에서 하는게 훨씬 편하게 느껴질 수 있을 것입니다. 하지만, 엑셀의 경우 프로그램이 무겁기 때문에 사양에 따라 20만개만 넘어가도 느려지기도 합니다. 또한 머신러닝 코드상의 에러를 방지하기 위해 특정 조건의 데이터를 프로그램 구동시 필터링 하는 작업도 필요합니다. 따라서 데이터 수가 얼마 안되지만 직접 python 으로 데이터 정리작업을 수행해 보도록 하겠습니다.

데이터 로드

from google.colab import drive

drive.mount('/content/gdrive')

import pandas as pd

import csv
 
f = open('/content/gdrive/My Drive/food_coded.csv', 'r', encoding='utf-8')


rdr = csv.reader(f)

dataset = pd.DataFrame(rdr)

1부랑 동일한 방식으로 구글드라이브에 있는 파일을 읽어와서 불러온 뒤 Dataframe 형식으로 변환하여 출력해보면 다음과 같이 나옵니다.

칼럼에 해당하는 맨 위의 데이터 값이 칼럼으로 들어가지 않고 그대로 데이터 형식으로 들어갔음을 확인할 수 있습니다. 칼럼으로 넣기위해 우선 칼럼행을 추출해봅시다.

칼럼 추출

dataColunms = list(dataset.iloc[0,0:61])

iloc 함수는 DataFrame 형식에서 지원하는 함수로서 열 또는 행의 순서로 데이터를 조작할 수 있습니다. iloc으로 데이터를 가져오면 데이터 타입이 object 형식이 됩니다. DataFrame에 칼럼형태로 넣기 위해서는 list 타입이 필요하므로 list()를 통해 형변환을 하였습니다.

칼럼 설정

# DataFrame 변수에 위와 같이 list 형식의 데이터로 칼럼 설정
baseData= dataset
baseData.columns = dataColunms

# 0번 행 제거 
baseData.drop([0],inplace=True) 

# inplace = True 는 함수를 호출하면서 호출한 오브젝트의 값을 직접 수정한다는 의미이다.
# inplace = False일 경우 다음과 같이 결과값을 반환해서 받아야 한다.  temp = baseData.drop([0])

이제 아래와 같이 데이터가 칼럼에 잘 들어간 형태로 변수에 저장된 것을 확인할 수 있을 것입니다.

사용하지 않을 속성들 삭제

이제 칼럼부터 지워보자. 우리가 지워야할 칼럼은 서술형 칼럼들입니다.

delColunm = ['comfort_food', 'comfort_food_reasons', 'diet_current', 'eating_changes', 'father_profession', 'fav_cuisine', 'food_childhood', 'healthy_meal', 'ideal_diet', 'meals_dinner_friend', 'mother_profession', 'type_sports']

지울 칼럼명을 list 자료형 형태로 저장해두면, 이후 제거할 칼럼을 변경할 일이 있을 때 편하게 바꿀 수 있습니다.

baseData = baseData.drop(columns = delColunm)

이번엔 행을 지워보도록 합시다. 앞서 이야기 하였듯이 미응답 답안지 (nan) 가 많은 (4개 이상) 데이터를 지우려고 합니다. 데이터 순회는 for문으로 하려고 합니다. DataFrame 형식은 배열처럼 baseData[0][1] 과 같은 형식으로 순회가 안되고, 다음과 같은 방법으로 순회를 수행할 수 있습니다.

DataFrame 형식의 for문 순회

# 칼럼명이 GPA인 모든 행 순회
for i in baseData.index:  # DataFrame의 행은 index 이라 한다
  val = baseData.loc[i,'GPA']
  print(val)

# 첫 번째 행의 모든 칼럼 순회
for i in baseData.columns:  # DataFrame의 열은 column 이라 한다
  val = baseData.loc[1,i]
  print(val)

# 전체 순회 
for i in baseData.index:
  for j in baseData.columns:
    val = baseData.loc[i,j]  #  loc[인덱스, 칼럼] 으로 각각의 데이터에 접근 가능하다. 
    print(val)

위 코드는 꼭 한 번씩 수행하길 바랍니다. 왜냐하면 이상한 데이터가 발견되기 때문입니다. 우선 comfort_food_reasons_coded 칼럼이 두 개 들어 있습니다. 원본 데이터를 볼 때에도 데이터까지 동일한게 확인이 됩니다. 따라서 위 칼럼 중 중복된 칼럼을 제거해야 합니다. 위에서 썼던 방식대로 drop 함수를 사용하여 comfort_food_reasons_coded를 제거할 경우 아예 데이터가 지워져 버리므로 조금 돌아서 지워야 합니다. (drop_duplicates 라는 함수가 있기는 한데 이 함수는 동일한 레코드에 대한 중복처리를 해줍니다.)

중복 속성 제거

# comfort_food_reasons_coded 칼럼데이터 추출
temp = baseData.pop('comfort_food_reasons_coded')

# 추출한 데이터 중 두 번째 열의 데이터만 다시 삽입 (첫 번째 데이터의 경우 일부 데이터가 누락되어있음)
baseData['comfort_food_reasons_coded'] = temp.iloc[:,1]

중복 칼럼은 지웠으므로 이제는 레코드별로 nan 또는 nan이 아닌 이상 데이터에 대한 처리를 해주어야 합니다. nan이 아닌 이상 데이터란, 예를 들어 GPA 칼럼 데이터를 살펴보면 다음과 같은 경우를 말합니다.

personal 과 같은 별개의 답안이 있거나 숫자에 한글이 섞여 있는 데이터들이 보인다. 이런 데이터가 있을 경우 머신러닝 함수가 돌아갈 때 에러가 난다. 따라서 이런 이상 데이터를 제거하는 작업을 해야합니다. 앞서 이야기 하였듯이 분류데이터는 0으로 수치데이터는 평균값으로 치환하려고 합니다. 그전에 앞서 범주형 데이터와 숫자데이터에 대해서 잠시만 설명하고 넘어가겠습니다.

범주형 데이터 란 숫자가 아닌 대상을 정수 또는 실수로 나타낸 특성 데이터를 말합니다. 예를 들어, 무지개의 색깔을 1 -빨강 2 – 주황 이런식으로 숫자화 해놓은 데이터를 범주형 데이터라고 합니다. 숫자데이터 는 말 그대로 정제하기전 데이터 자체가 숫자이면서 의미를 갖는 데이터를 말한다 인간의 키나 몸무게와 같이 숫자화 되어있는 것들을 숫자 데이터라고 합니다. 이제 다음 코드를 통해 수치 데이터는 이상데이터에 대해 0 으로, 숫자 데이터는 평균값으로 치환하도록 하겠습니다.

먼저 범주형 데이터를 치환해 보자. 정석적인 방법은 다음과 같이 모든 칼럼에 대해서 도메인을 파악하여 도메인에 해당하지 않는 값들을 정의하여 치환해 주어야 합니다. 현재 우리가 쓰는 데이터의 경우 질문지가 있기 때문에 쉽게 파악이 되지만, 도메인에 대한 정보가 없을 경우 다음 코드를 통해 파악할 수 있습니다.

칼럼의 도메인 확인

# 도메인을 알고 싶은 칼럼을 임시변수에 확인
substiTemp = baseData['calories_day']

# drop_duplicates 함수로 중복을 제거하여 현재 데이터가 갖고있는 데이터의 종류를 확인
dataTemp = substiTemp.drop_duplicates()
 

print(dataTemp)


질문지 또는 코드를 통해 도메인을 파악했다면 for문을 활용하여 데이터를 치환합시다. 아래 코드에 대해서 칼럼명, if 조건문만 변경하여 모든 칼럼에 적용하면 됩니다.

분류데이터의 불량 값 치환

tempDomain = ['1','2','3','4','5']

for i in range(len(baseData['calories_day'])):
  if(baseData['calories_day'][i+1] not in tempDomain  ):
   baseData['calories_day'][i+1] ='0'

print(baseData['calories_day'])

필자의 경우에는 아래 코드로 일괄적으로 변경을 하였습니다. 이건 데이터의 갯수가 얼마 안되고 도메인이 비슷하기 때문에 가능한 방법입니다.

tempDomain = ['1','2','3','4','5','6','7','8','9','10','11','12']

categoricalColumn = ['Gender', 'breakfast', 'calories_day', 'coffee', 'cook', 
                     'cuisine' , 'diet_current_coded', 'drink', 'eating_changes_coded', 'eating_changes_coded1',
                     'eating_out','employment', 'ethnic_food', 'exercise', 'father_education' , 
                     'fav_cuisine_coded', 'fav_food' , 'fries','fruit_day','grade_level', 
                     'greek_food' , 'healthy_feeling' , 'ideal_diet_coded' , 'income' , 'indian_food' , 
                     'italian_food', 'life_rewarding' ,'marital_status', 'mother_education','nutritional_check', 
                     'on_off_campus', 'parents_cook' , 'pay_meal_out' , 'persian_food', 'self_perception_weight', 
                     'soup', 'sports', 'thai_food', 'veggies_day', 'vitamins', 
                     'comfort_food_reasons_coded' ]


for i in baseData.index:
  for j in categoricalColumn:
    if(baseData.loc[i,j] not in tempDomain  ):
      baseData.loc[i,j] = '0'

이제 나머지 숫자 데이터에 대한 치환을 해봅시다. 숫자데이터의 경우에는 숫자인지 아닌지에 대한 판별을 해줘야 합니다. 다양한 방법이 있겠지만, 필자가 찾은 방법 중 정수, 소수에 대한 처리가 모두 가능한 코드는 다음과 같은 함수를 선언하는 방법입니다. 파이썬에서는 float(), str()와 같은 함수로 형변환이 가능하며, 타입이 안맞을 경우 에러가 나옵니다. 이를 이용하여 예외처리를 통해서 숫자인지 아닌지 판별하는 함수입니다. 단, 문자열만 있을 경우 에러가 안나는 경우도 있기 때문에 문자열만 있는 데이터인지 한 번 더 확인합니다.

숫자가 아닌 데이터 판별 함수

def is_number_tryexcept(s):
    """ Returns True is string is a number. """
    try:
        float(s)
        if(s.isalpha()):
          return False
        return True
    except ValueError:
        return False

숫자데이터는 평균값이 필요하므로 위 함수를 활용하여 평균값을 저장해둡시다. 또한, 이후에 사용할 일이 있으므로 최솟값과 최댓값도 같이 저장해두도록 하겠습니다.

데이터의 최소, 최대, 평균, 합계 값 저장

dataAttr = pd.DataFrame(columns=numberColumn , index = ['min','max','sum','avg'])

tempMin = 99999;
tempMax = -99999
tempSum = 0;
tempAvg = 0;
tempValue = [];


for j in  numberColumn:
  for i in baseData.index:
    if( is_number_tryexcept(baseData.loc[i,j])):
      tempValue.append(float(baseData.loc[i,j]))
      #print(float(baseData.loc[i,j]))
    #print(tempValue) 
    tempNp = np.array(tempValue)
    tempAvg = np.mean(tempNp)
    tempSum = np.sum(tempNp)
    tempMin = np.min(tempNp)
    tempMax = np.max(tempNp)
    tempSeries = Series([tempMin,tempMax,tempSum,tempAvg], index = ['min','max','sum','avg'] )
    dataAttr[j] = tempSeries
  tempValue = [];

이제 다시 순회하며 평균값으로 대치하는 작업만 하면 됩니다 .

저장된 값으로 데이터 치환

for j in  numberColumn:
  for i in baseData.index:
    if( not (is_number_tryexcept(baseData.loc[i,j]))):
     baseData.loc[i,j] = dataAttr.loc['avg',j]
   

2부를 마치며

이상으로 복잡한 데이터 다루기 글을 마치려고 합니다. 다음에는 다양한 Machine Learning 알고리즘을 살펴보고, 위 데이터를 적용해보도록 하겠습니다. 데이터 속성도 여러가지 방향으로 바꿔보면서 비교분석하여 예측율이 높은 방식이 어떤 방식인지 찾아볼 것입니다. 특히, 앞서 언급했었던 ‘특이한 질문’에 해당하는 속성들이 어떤 영향을 끼치는지 자세히 살펴보도록 하겠습니다.

참고 문헌

kaggle이란 ? : https://ko.wikipedia.org/wiki/캐글
kaggle 라이센스 : https://chan-lab.tistory.com/22
python 행과 열 추출 : https://hogni.tistory.com/7
Data Preprocessing : https://sacko.tistory.com/60
DataFrame에서 특정 행과 열 추출 : https://m.blog.naver.com/PostView.nhn blogId=rising_n_falling&logNo=221622971970&proxyReferer=https:%2F%2Fwww.google.com%2F
pandas.Dataframe – 2편 column명,값 수정방법 : https://ldgeao99.tistory.com/7
반복문으로 2차원 리스트 출력 : https://dojang.io/mod/page/view.php?id=2292
머신러닝 용어집 : https://developers.google.com/machine-learning/glossary?hl=ko#feature
문자열 숫자 판별: https://www.it-swarm.dev/ko/python/%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%B4-%EC%88%AB%EC%9E%90-float%EC%9D%B8%EC%A7%80-%EC%96%B4%EB%96%BB%EA%B2%8C-%ED%99%95%EC%9D%B8%ED%95%A9%EB%8B%88%EA%B9%8C/958036534/

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.

%d 블로거가 이것을 좋아합니다: