<jupyter_start><jupyter_text>- Github: [yoonkt200](https://github.com/yoonkt200/python-data-analysis)  5.1 중고나라 휴대폰 거래가격 예측하기- 데이터 분석을 프로그램, 혹은 서비스의 형태에 응용한다.사용하는 데이터는 https://www.data.go.kr/ 에서 제공하는 '네이버 중고나라'데이터, 그리고 통신 3사(Sk Telecom, LG U+, kt olleh) 사이트에서 제공하는 휴대폰 공시가격 데이터를 기반으로 만들어진 것이다. Step1 탐색적 분석: 중고나라 데이터 분석하기예제에서 사용할 중고나라 데이터셋은 다음과 같은 피처로 구성되어 있다.- create_date : 판매(혹은 구매) 게시글이 올라온 시점- price: 게시글 작성자가 제안한 휴대폰의 거래가격- text: 게시글의 제목과 본문을 합친 텍스트 데이터- phone_model: 휴대폰의 기종- factory_price :휴대폰 공시가격- maker: 휴대폰 제조사- price_index: 판매 게시글이 올라온 시점에서의 휴대폰 물가 지수 데이터아래의 코드를 통해 데이터를 살펴본 결과, 약 5,000여 개의 데이터가 결측값 없이 구성되어 있다는 것을 알 수 있다.- 중고나라 데이터셋 살펴보기<jupyter_code># -*- coding: urf-8 -*-
%matplotlib inline

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

df = pd.read_csv("./data/used_mobile_phone.csv")
print(df.info())
df.head()<jupyter_output><empty_output><jupyter_text>다음으로 개별 피처들을 탐색한다.- 개별 피처 탐색하기: date 피처 탐색<jupyter_code># create_date로부터 '월'을 의미하는 month 정보를 피처로 추출한다.
df['month'] = df['create_date'].apply(lambda x: x[:7])
# 월별 거래 횟수를 계산하여 출력한다.
df['month'].value_counts()<jupyter_output><empty_output><jupyter_text>date 피처에서 월별 게시글의 개수를 살펴본 결과, 이 데이터는 2016년 10월, 그리고 2017년 3월에 집중적으로 수집된 데이터라는 것을 알 수 있다. 데이터의 개수를 일자별 시계열 그래프로 나타내 보자.- 개별 피처 탐색하기: date 피처 탐색<jupyter_code># 일별 거래 횟수를 계산하여 그래프로 출력하자.
df_day = pd.to_datetime(df['create_date'].apply(lambda x: x[:10])).value_counts()

df_day.plot()
plt.show()<jupyter_output><empty_output><jupyter_text>그리고 아래와 같이 price 피처의 탐색을 통해 중고나라에서 거래되고 있는 중고 휴대폰의 가격이 어느 정도인지 살펴볼 수 있다. 그래프로 분포르 살펴보면, 대부분의 중고 휴대폰이 60만 원 이하의 가격대를 형성하고 있으며, 몇몇은 그 이상에도 거래되고 있는 것을 알 수 있다.- 개별 피처 탐색하기: price 피처 탐색<jupyter_code># 가격의 분포를 그래프로 탐색한다.
df['price'].hist(bins="auto")<jupyter_output><empty_output><jupyter_text>이번에는 휴대폰의 동일 기종 내에서의 가격 분포를 살펴보자. 이를 위해 phone_model을 groupby하여 그룹별 평균값과 표준편차를 구한 뒤, 모든 price의 z-score를 계산하였다. 이 값은 '동일 기종'내에서의 상대적 가격을 나타내는 값이다.- 개별 피처 탐색하기: price 피처 탐색<jupyter_code># 휴대폰 기종(phone_model)별 가격의 평균과 표준 편차를 계산한다.
df_price_model_mean = df.groupby('phone_model')['price'].transform(lambda x: np.mean(x))
df_price_model_std = df.groupby('phone_model')['price'].transform(lambda x: np.std(x))
# 이를 바탕으로 모든 데이터의 z-score를 계산한다. 이는 해당 데이터의 가격이 기종별 평균에 비해 어느 정도로 높거나 낮은지를 알 수 있게 하는 점수이다.
df_price_model_z_score = (df['price'] - df_price_model_mean) / df_price_model_std
df_price_model_z_score.hist(bins="auto")<jupyter_output><empty_output><jupyter_text>이를 그래프로 출력한 결과, 동일 기종 내에서 price 피처의 분포는 정규분포에 가깝다는 것을 알 수 있다. 이번에는 factory_price 피처의 분포, 그리고 factory_price와 price간의 관계를 탐색해보자.- 개별 피처 탐색하기: factory_price 피처 탐색<jupyter_code># factory_price 피처의 분포를 탐색한다.
df['factory_price'].hist(bins="auto")
# factoty_price와 price 피처를 산점도 그래프로 출력하여 상관 관계를 살펴보자.
df.plot.scatter(x='factory_price', y='price')<jupyter_output><empty_output><jupyter_text>위 그래프의 출력 결과, 두 피처는 양의 상관 관계를 가지고 있다. 이를 통해 factory_price는 중고 휴대폰 판매 가격에 꽤 중요한 영향을 미칠 것이라고 추정할 수 있다. 다음으로 phone_model 피처를 탐색한 결과, 휴대폰 기종의 개수를 총 64개로 나타났다. 상위 5개의 기종을 제외하면 대부분 10개~90개 사이의 데이터를 가지고 있다는 것을 알 수 있다. - 개별 피처 탐색하기: phone_model 피처 탐색<jupyter_code># 기종별 총 거래 데이터 개수를 집계한다.
model_counts = df['phone_model'].value_counts()
print(model_counts.describe())
# 기종별 총 거래 데이터 개수를 상자 그림으로 살펴보자.
plt.boxplot(model_counts)<jupyter_output><empty_output><jupyter_text>- maker, price_index 피처에 대한 특징 살펴보기- value_counts를 이용하여 피처의 분포를 탐색하기<jupyter_code># maker 피처 탐색하기

maker_counts = df['maker'].value_counts()
print(maker_counts)
plt.boxplot(maker_counts)
# price_index 피처 탐색
df['price_index'].value_counts()<jupyter_output><empty_output><jupyter_text>지금까지 탐색한 피처를 바탕으로 가격 예측 모델을 학습해보자. 그 과정에서 도출되는 피처 중요도 분석을 통해 어떤 피처가 예측 모델에 중요한 피처인지를 알 수 있다. 이러한 분석은 랜덤포레스트 라는 모델로 수행 할 수 있다. - 랜덤 포레스트 포델의사결정 나무 분석 방법을 응용한 것으로 의사결정 나무를 여러 개 모아 하나의 숲을 구성하는 방법이다. 하나의 모델이 정답을 푸는 것이 아닌, 여러개의 모델이 정답을 함께 푸는 것이기 때문에 더 정확한 학습이 가능하다. 또한 이 방법은 모델이 생성되는 과정에서의 피처 중요도를 계산하기 때문에 탐색적 데이터 분석에 자주 사용된다. 랜덤 포레스트 모델은 회귀와 분류, 두 가지에 모두 적용이 가능하다. 랜덤 포레스트는 sklearn라이브러리의 RandomForestRegressor 클래스로 사용 가능하다. 이 클래스의 fit함수를 실행하여 모델을 학습한 뒤, feature_importtances_를 출력하면 피처 중요도를 출력할 수 있다. 아래의 실행 결과는 모델의 예측 평가 점수인 R2 score, RMSE를 나타낸 것이다. Train score와 Test score의 차이가 심한 것을 미루어 볼 때, 이 예측 모델은 과적합이 일어났다고 할 수 있다.- Random forest regressor를 이용한 가격 예측<jupyter_code>from sklearn.model_selection import train_test_split
from sklearn.feature_extraction import DictVectorizer
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import r2_score
from sklearn.metrics import mean_squared_error

# 데이터를 학습/테스트용 데이터로 분리한다.
df = df[['price', 'phone_model', 'factory_price', 'maker', 'price_index', 'month']]
df = pd.get_dummies(df, columns = ['phone_model', 'maker', 'month'])

X = df.loc[:, df.columns != 'price']
y = df['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state = 0)
# 랜덤 포레스트 모델을 학습한다.
forest = RandomForestRegressor(n_estimators = 1000, criterion = 'mse')
forest.fit(X_train, y_train)
y_train_pred = forest.predict(X_train)
y_test_pred = forest.predict(X_test)
# 학습한 모델을 평가한다.

print('MSE train: %.3f, test: %.3f' % (
mean_squared_error(y_train, y_train_pred),
mean_squared_error(y_test, y_test_pred)))

print('R^2 train: %.3f, test: %.3f' %(
r2_score(y_train, y_train_pred),
r2_score(y_test, y_test_pred)))<jupyter_output>MSE train: 10631769356.838, test: 13893835508.093
R^2 train: 0.781, test: 0.682<jupyter_text>아래의 코드와 실행 결과는 포레스트 모델의 피처 중요도를 출력할 것이다. 이를 통해 factory_price, maker_apple ... 순으로 모델에 중요한 피처라는 것을 알 수 있다.- 피처 중요도 분석하기<jupyter_code># 학습한 모델의 피처 중요도를 그래프로 살펴본다.
importances = forest.feature_importances_
indices = np.argsort(importances)[::-1]
plt.bar(range(X.shape[1]), importances[indices])
# 학습한 모델의 피처 중요도를 출력하자.
feat_labels = X.columns.tolist()
feature = list(zip(feat_labels, forest.feature_importances_))
sorted(feature, key=lambda tup: tup[1], reverse=True)[:10]<jupyter_output><empty_output><jupyter_text>그렇다면 '시간'은 모델에 어떤 영향을 미칠까? 이를 알아보기 위해 month 피처의 중요도만 살펴보자.- 피처 중요도 분석하기<jupyter_code># month 피처 중, 영향력이 높은 순으로 정렬하여 출력한다.
for sorted_feature in sorted(feature, key=lambda tup: tup[1], reverse=True):
    if "month" in sorted_feature[0]:
        print(sorted_feature)<jupyter_output>('month_2017-03', 0.014758013072924299)
('month_2016-05', 0.011122052301575943)
('month_2016-09', 0.00847222055624826)
('month_2016-04', 0.007528916754662687)
('month_2016-10', 0.0062100395453149855)
('month_2016-06', 0.004456270137109047)
('month_2016-08', 0.003597900320564794)
('month_2016-07', 0.002729281592498599)
('month_2016-03', 0.0024479147943340667)
('month_2016-02', 0.0010468618066257456)<jupyter_text>위 코드의 실행 결과를 살펴보면 '최근'에 가까운 'month'일수록 피처의 중요도가 높은 것을 알 수 있다. Step2 피처 엔지니어링: 예측 모델 개선하기지금까지 수행한 탐색적 데이터 분석의 내용은 아래와 같다. 이 내용을 토대로 가격 예측 모델의 성능을 개선하기 위한 피처 엔지니어링을 적용해 보자.- date: 월 단위로 살펴본 결과, 2016년 10월과 2017년 3월의 데이터가 가장 많다. 최근에 가까운 월 일수록 예측 모델에 중요한 피처이다.- price: 전체 휴대폰의 거래가와 달리 기종별 가격의 분포는 정규 분포의 형태를 띠고있다.- factory_price: price 피처와의 양의 상관 관계가 관찰된다. 또한 예측 모델의 피처 중요도 분석 결과, 가장 중요한 피처로 나타난다.- phone_model: 소수의 인기 기종이 많은 데이터를 가지고 있다.- maker: Apple 브랜드가 가장 많으며, 가격 예측에서도 Apple 브랜드 여부는 가장 중요한 피처 중 하나이다.- price_index: 월별 변동이 크지 않으며, 총 4개의 값만을 가지고 있다. 가격 예측에서 그다지 중요한 피처는 아니다.가장 먼저 create_date 피처를 분석에 활용하기 위해 '시간 점수' 라는 것을 만들어 보자. 생성 과정은 다음과 같다.- create_date를 unixtime으로 변환한다. unixtime은 시간을 정수로 표현한 것이며, 값이 클수록 '최근'에 가까워 진다.- min-max 스케일링을 적용하여 unixtime을 상대적인 점수로 계산한다.- 점수가 1에 가까울수록 최근에 작성한 게시물을 의미하며, 0에 가까울수록 오래된 게시물을 의미한다.- 기존 피처 가공하기 : 'creat_date'<jupyter_code>#데이터를 다시 불러온다.
df = pd.read_csv("./data/used_mobile_phone.csv")
from datetime import datetime
import time
# create_date 피처를 수치적으로 계산하기 위해, unixtime으로 변환하는 함수를 정의합니다.
def date_to_unixtime(date_str):
    timestamp = time.mktime(datetime.strptime(date_str, '%Y-%m-%d').timetuple())
    return timestamp
# create_date 피처를 '현재와 얼마나 가까운 데이터인지' 판단하기 위한 점수를 생성한다. 먼저 unixtime으로 데이터를 변환한다.

df['create_unixtime'] = df['create_date'].apply(lambda x: date_to_unixtime(x[:10]))
# 변환된 unixtime에 min-max 스케일링을 적용한다.

df['create_time_score'] = (df['create_unixtime'] - df['create_unixtime'].min()) / \
                          (df['create_unixtime'].max() - df['create_unixtime'].min())

df[['create_date', 'create_unixtime', 'create_time_score']].head()<jupyter_output><empty_output><jupyter_text>다음으로 phone_model에 대한 피처 엔지니어링을 수행한다. phone_model 피처는 '제품명 + 용량'으로 구성되어 있기 때문에 나누어 주기만 하면 2개의 분리된 피처를 얻을 수 있다. phone_model 피처를 분리하는 것은 어떤 효과가 있을까?- 기존 피처의 가공 : phone_model<jupyter_code># phone_model 피처에서 저장 용량 (phone_model_storage) 피처를 추출한다.

df['phone_model_storage'] = df['phone_model'].apply(lambda x: x.split(" ")[-1])
# phone_model 피처에서 기종 세부명(phone_model_detail)피처를 추출한다.

df['phone_model_detail'] = df['phone_model'].apply(lambda x: ' '.join(x.split(" ")[:-1]))
df[['phone_model_storage', 'phone_model_detail']].head()<jupyter_output><empty_output><jupyter_text>아래 코드에서는 phone_model 피처를 분리함으로써 얻을 수 있는 효과를 알아보기 위해 피처 분리 이전과 이후의 분포를 상자 그림으로 비교해보자.- 기존 피처의 가공 : phone_model<jupyter_code># phone_model 피처의 기종별 거래 데이터 개수를 집계한다.
model_counts = df['phone_model'].value_counts()
# phone_model_detail 피처의 기종별 거래 데이터 개수를 집계한다.
model_detail_counts = df['phone_model_detail'].value_counts()
data = [model_counts, model_detail_counts]
# 두 피처 간의 기종별 거래 데이터 개수를 비교한다.

mpl_fig = plt.figure()
ax = mpl_fig.add_subplot(111)
ax.boxplot(data)<jupyter_output><empty_output><jupyter_text>이를 살펴본 결과, 쏠림 현상이 심해진 하나의 기종을 제외하면 전체적인 분포가 안정적으로 변환것을 알 수 있다. 게다가 분석에 덤으로 사용할 수 있는 'phone_model_storage'라는 새로운 피처까지 생겼다. 이번에는 텍스트 정보에 대한 피처 엔지니어링을 수행한다. 텍스트 정보에는 물품의 상태를 분류할 수 있는 감성분류 가 필요하다. 그렇다면 감성분류의 정답, 즉'상태가 좋다' 혹은 '상태가 나쁘다'는 어떻게 판단할 수 있을까? 이를 위해 '기종별 가격의 분포는 정규분포다'라는 정보를 활용해보자. - price 피처를 같은 기종 내에서의 z-core로 변화한다.- z-score 기준, 상위 5%에 속하는 가격의 게시글은 '상태가 좋음', 하위 5%는 '상태가 나쁨', 그리고 그 외는 '보통'으로 판단한다. 상위 n%의 판단은 quantile() 함수를 사용한다.- 정의한 세 가지 상태를 감성 분류의 정답 데이터로 사용한다.- 감성 분류로 물품의 상태 분류하기<jupyter_code># 거래 가격(price)의 z-score를 계산한다.
# 이는 해당데이터의 가격이 기종의 평균에 비해 어느 정도로 높거나 낮은지를 알 수 있게 하는 점수이다.

df['price_by_group'] = df.groupby('phone_model_detail')['price'].transform(lambda x:(x - x.mean()) / x.std())
# 거래 가격의 z-scoore(price_by_group)의 분포를 그래프로 출력한다.

ax = df['price_by_group'].hist(bins="auto")

# lower_bound, upper_bound 그래프에 추가한다.

ax.axvline(x=lower_bound, color='r', linestyle='dashed', linewidth=2)
ax.axvline(x=upper_bound, color='r', linestyle='dashed', linewidth=2)
# z-score(price_by_group) 기준으로 하위 5%, 상위 5%에 해당하는 점수를 lower_bound, upper_bound라고 지정한다.
lower_bound = df['price_by_group'].quantile(0.05)
upper_bound = df['price_by_group'].quantile(0.95)
# lower_bound를 upper_bound 출력한다
print(lower_bound)
print(upper_bound)<jupyter_output>-1.3966616903783375
1.666982156397844<jupyter_text>감성 분류의 정답 데이터를 'price_level'이라는 피처로 생성한 결과는 다음과 같다. 이제 우리는 price_level,그리고 text피처를 이용하여 감성 분류를 수행 할 수 있다.- 감성 분류로 물품의 상태 분류하기<jupyter_code># lower_bound보다 낮으며  0, upper_bound보다 높으면 2, 그 중간이면 1로 가격의 상태를 분류하는 함수를 정의한다.
def get_price_level(price, lower, upper):
    if price <= lower:
        return "0"
    elif price >= upper:
        return "2"
    else:
        return "1"
# lower_bound보다 낮으면 0, upper_bound보다 높으면 2, 그 중간이면 1로 가격의 상태를 분류 하자.

df['price_lower'] = df.groupby('phone_model_detail')['price'].transform(lambda x: x.quantile(0.05))
df['price_upper'] = df.groupby('phone_model_detail')['price'].transform(lambda x: x.quantile(0.95))
df['price_level'] = df.apply(lambda row: get_price_level(row['price'], row['price_lower'], row['price_upper']), axis=1)

df[['price', 'price_lower', 'price_upper', 'price_level', 'text']].head()<jupyter_output><empty_output><jupyter_text>다음으로 텍스트 전처리 과정을 진행할 차례이다.- 텍스트 전처리하기 (실습폴더 불러오기)<jupyter_code>import pickle
import re

# 중고나라 불용어 사전을 불러온다.
with open("./data/used_mobile_phone_stopwords.pkl", 'rb') as f:
    stopwords = pickle.load(f)
    
# 불용어 사전에 등록된 단어 10개를 출력한다.
print(stopwords[:10])<jupyter_output>['거래', '입니', '판매', '아이폰', '갤럭시', '골드', '팝', '만원', '폰', '시']
{'fraction_non_alphanumeric': 0.11800033835222466, 'fraction_numerical': 0.03704956860091355, 'mean_word_length': 4.929287863590773, 'pattern_counts': {'":': 0, '<': 66, '<?xml version=': 0, '>': 66, 'https://': 2, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '226585', 'license': 'MIT', 'n_tokens_mistral': 7844, 'n_tokens_neox': 9452, 'n_words': 1470, 'path': 'Chapter 5.1.ipynb', 'repo_name': 'goldang01/python_data_analysis_study', 'size': '11822'}