데이콘의 영화 관객수 예측 모델 : https://dacon.io/competitions/open/235536/overview/description
[문화] 영화 관객수 예측 모델 개발 - DACON
좋아요는 1분 내에 한 번만 클릭 할 수 있습니다.
dacon.io
- 분석목적 : 영화 관련 변수를 이용하여 영화 관객 수 예측하기
- 데이터 : movies_train.csv / movies_test.csv
- 변수
- title : 영화의 제목
- distributor : 배급사
- genre : 장르
- release_time : 개봉일
- time : 상영시간(분)
- screening_rat : 상영등급
- director : 감독이름
- dir_prev_bfnum : 해당 감독이 이 영화를 만들기 전 제작에 참여한 영화에서의 평균 관객수(단 관객수가 알려지지 않은 영화 제외)
- dir_prev_num : 해당 감독이 이 영화를 만들기 전 제작에 참여한 영화의 개수(단 관객수가 알려지지 않은 영화 제외)
- num_staff : 스텝수
- num_actor : 주연배우수
- box_off_num : 관객수
+ 코드 작성시 참고 하였습니다 : https://dacon.io/competitions/open/235536/codeshare/2721?page=1&dtype=recent
+ 틀린 곳이 있다면 알려주세요 :)
필요한 모듈 불러오기
import pandas as pd
from pandas import DataFrame
import seaborn as sns
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
1. 데이터 불러오기
# 데이터 불러오기
train = pd.read_csv('C://WORK/DACON/movies_train.csv')
test = pd.read_csv('C://WORK/DACON/movies_test.csv')
submission = pd.read_csv('C://WORK/DACON/submission.csv')
2. 탐색적 자료분석 ( EDA )
print(train.shape) # (600, 12)
print(test.shape) # (243, 11)
print(submission.shape) # (243, 2)




2.1 결측치 확인
train 데이터셋의 결측치 확인
# Number of missing in each column
missing_train = pd.DataFrame(train.isnull().sum()).rename(columns = {0 :'total'})
# Create a percentage missing
missing_train['percent'] = missing_train['total'] / len(train)


- 'dir_prev_bfnum' (해당 감독이 이 영화를 만들기 전 제작에 참여한 영화에서의 평균 관객수변수) 에 결측값 330 개 존재한다.
- 평균 관객 수가 알려지지 않은 영화는 제외 되었으므로
- 결측값이 존재하는 경우는 이 감독이 제작한 전 영화의 관객 수가 0 명이였다고 생각하였다.
- 따라서 'dir_prev_bfnum' 변수 결측값을 0 으로 대체할 필요가 있다고 판단하였다.
- 그 외, 나머지 변수들은 결측값이 존재하지 않는다.
test 데이터셋의 결측치 확인
# NUmber of missing in each column
missing_test = pd.DataFrame(test.isnull().sum()).rename(columns = {0 :'total'})
# Create a percentage missing
missing_test['percent'] = missing_test['total'] / len(train)

- test 데이터셋에도 'dir_prev_bfnum' 변수 외에는 결측값이 존재하지 않는다.
2.2 결측값 대치
train , test 데이터셋 모두 'dir_prev_bfnum' 변수의 결측값을 0 로 대치한다.
train = train.fillna(0)
test = test.fillna(0)


- 'dir_prev_bfnum' 변수의 결측치가 제거되었음을 확인할 수 있다.
2.3 배급사 변수 전처리
unique() 함수를 통해 'distributor' 변수 값을 보면 같은 배급사가 다르게 표현된 경우가 있음을 알 수 있다.
- 리틀빅픽처스 / 리틀빅쳐스 / (주)리틀빅픽처스
- (주)마운틴픽쳐스 / 마운틴픽처스 / 더픽쳐스/(주)마운틴픽쳐스


train / test 데이터셋 배급사명 앞의 '(주)' 를 replace 함수를 사용하여 제거
train['distributor'] = train.distributor.str.replace("[(주)]","")
test['distributor'] = train.distributor.str.replace("[(주)]","")

특수문자 제거 - 숫자,영문,한글만 포함하여 가져오기
import re
train['distributor'] = [re.sub(r'[^0-9a-zA-Z가-힣]','',x) for x in train.distributor]
test['distributor'] = [re.sub(r'[^0-9a-zA-Z가-힣]','',x) for x in test.distributor]
이제 동일한 배급사지만 다르게 표현된 것을 처리하자.
'get_dis' 함수를 생성하여 동일한 배급사의 다양한 표현들을 한가지로 정리하였다.
def get_dis(x) :
if 'CJ' in x or 'CGV' in x :
return 'CJ'
elif '쇼박스' in x :
return '쇼박스'
elif 'SK' in x :
return 'SK'
elif '리틀빅픽' in x :
return '리틀빅픽처스'
elif '스폰지' in x :
return '스폰지'
elif '싸이더스' in x :
return '싸이더스'
elif '에이원' in x :
return '에이원'
elif '마인스' in x :
return '마인스'
elif '마운틴픽' in x :
return '마운틴픽처스'
elif '디씨드' in x :
return '디씨드'
elif '드림팩트' in x :
return '드림팩트'
elif '메가박스' in x :
return '메가박스'
elif '마운틴' in x :
return '마운틴'
else :
return x
train['distributor'] = train.distributor.apply(get_dis)
test['distributor'] = test.distributor.apply(get_dis)

2.4 변수별 막대그래프와 산점도
상관계수
corr_mat = train.corr().round(2)
corr_mat

sns.heatmap(corr_mat, vmin = -0.5 , vmax = 0.8 , center = 0,
cmap = plt.cm.RdYlGn_r, annot = True)

관객수
plt.hist(train['box_off_num'])

- 'box_off_num' 변수는 왜도가 심하게 나타난다. ( 이후 log 변환 예정 )
상영시간
plt.hist(train['time'])

- 상영시간은 80 ~ 120 분이 가장 많다.
- 상영시간과 관객수의 산점도
plt.scatter(data = train,x='time',y='box_off_num')

- 상영시간과 관객 수는 0.44 의 상관계수를 가지며 다른 변수들보다 상관계수가 높은 편
스탭 수
plt.hist(train['num_staff'])

- 스탭수와 관객수의 산점도
plt.scatter(data = train,x='num_staff',y='box_off_num')

- 스태프 수가 많을 수록 관객 수도 많아지는 경향이 있다.
- 0.54 로 상대적으로 높은 양의 상관 관계를 가진다.
주연 배우 수
plt.hist(train['num_actor'])

상영시간과 스탭수의 산점도
plt.scatter(data = train,x='time',y='num_staff') # 상관계수 : 0.62

- 상영시간과 스탭수와의 상관관계가 높으므로 둘 중 하나의 변수만 사용하기로 하였다.
- 관객수와 상관관계가 좀 더 높은 스탭수의 변수를 모델 적합에 사용하겠다.
상영 등급

# 한글 깨짐 해결
import matplotlib
import platform
print(platform.system()) # 플랫폼 확인
# Window
if platform.system() == 'Windows':
matplotlib.rc('font', family='Malgun Gothic')
elif platform.system() == 'Darwin': # Mac
matplotlib.rc('font', family='AppleGothic')
else: #linux
matplotlib.rc('font', family='NanumGothic')
# 그래프에 마이너스 표시가 되도록 변경
matplotlib.rcParams['axes.unicode_minus'] = False
plt.hist(train['screening_rat'])

- 청소년 관람불가 , 15세 관람가는 전체 관람가, 12세 관람가에 비해 2배 가량 높다,
df = train[['screening_rat','box_off_num']]
df

rat_agg = df.groupby('screening_rat').sum('bpx_off_num')
rat_agg

- 상영 등급별 관객수 막대 그래프
screening_rat_order = ['전체 관람가', '12세 관람가', '15세 관람가', '청소년 관람불가']
rat_agg = rat_agg .loc[screening_rat_order]
plt.bar(rat_agg.index,rat_agg.box_off_num)
plt.title('상영등급별 관객수', fontsize = 15)
plt.xlabel('상영등급')
plt.xticks(fontsize=10)
plt.ylabel('관객수')
plt.show()

- 15세 관람의 상영등급의 관객수가 가장 높다.
감독이름

- 감독 이름별 관객수 막대 그래프
plt.bar(train['director'],train['box_off_num'])
plt.title('감독별 관객 수')
plt.show()

- 특정 감독의 영화의 인기가 많음을 확인할 수 있다.
배급사

- 배급사별 관객수 막대 그래프
plt.bar(train['distributor'],train['box_off_num'])
plt.title('배급사별 관객 수')
plt.show()

- 특정 배급사의 영화의 인기가 많음을 학인할 수 있다.
장르
plt.hist(train['genre'])
plt.title('영화 장르')
plt.xticks(rotation=45)
plt.show()

- 드라마, 다큐멘터리, 멜로 / 로맨스의 값이 많다.
- 장르별 관객수를 오름차순으로 정리

- 느와르, 액션 장르의 영화가 인기가 많다.
감독의 이전 영화 평균 관객수

- 관객수 변수와의 관계

감독의 이전 영화 평균 관객수와 관객수의 상관계수 : 0.29
plt.figure(figsize=[5,5])
sns.scatterplot(data=train, x ='dir_prev_bfnum', y ='box_off_num')

감독의 과거 참여 영화 수

- 관객수 변수와의 관계

감독의 과거 참여 영화수와 관객수의 상관계수 : 0.26
plt.figure(figsize =[5,5])
sns.scatterplot(data=train , x ='dir_prev_num', y= 'box_off_num')

3. Feature Engineering
3.1 새로운 변수 생성 - 월 변수
Q. 영화가 개봉한 날짜(월) 이 관객수에 영향을 미칠까 ?

- 다음과 같이 개봉일 변수의 데이터타입이 object 임을 확인하였다.
날짜 변수를 datetime64형식 으로 데이터 타입 변경하기
train['release_time']=pd.to_datetime(train['release_time'],format='%Y-%m-%d')
test['release_time']=pd.to_datetime(test['release_time'],format='%Y-%m-%d')

월 (month) 만 추출
release_month_train = []
for i in train['release_time'] :
month = i.month
release_month_train.append(month)
release_month_test = []
for i in test['release_time'] :
month = i.month
release_month_test.append(month)

train['month']= release_month_train
test['month']= release_month_test
- 다음과 같이 영화 개봉일 변수에서 월(month) 만 추출하였다.
- 이렇게 생성한 변수를 train / test 데이터셋에 추가하였다.
- 월변수와 관객수와의 관계

plt.bar(train['month'], train['box_off_num'])
plt.xlabel('month')
plt.ylabel('box_off_num')
plt.show()

sns.boxplot(x='month',y='box_off_num',data=train)
plt.show()

- 주로 12월 ~ 1월 , 7 ~ 9 월에 관객수가 많다.
- 연말, 연초, 여름 ( 방학시즌 )
- month 변수는 관객수와 관련이 있어 보인다.
4. Feature selection
이제 모델에 적합시킬 변수를 선정하자.
독립변수
- genre : 장르
- month : 개봉월
- time :상영시간(분)
- screening_rat : 상영등급
- num_staff : 스탭수
종속변수
- box_off_num : 관객수
+ director 변수에 대해

- 감독이름도 추가하고 싶었으나, train set 과 test set 의 범주 수가 다르다.
( 유명한 감독의 영화이면 영화 내용에 상관없이 관람하는 사람도 있기 때문에 )
- 따라서 독립변수에서 'director' 변수 는 선택하지 않았다.
4.1 상영등급, 장르 변수 더미화
범주형 변수를 라벨 인코딩 하는 과정에서 범주형 변수에 크기라는 특징이 생겨 예측 성능이 떨어질 수 있다.
따라서 영화 관객수 평균값 크기를 반영하여 범주형 변수 라벨 인코딩을 해주었다.
상영등급별 영화 관객수 평균값 기준으로 상영등급 랭크 인코딩
tr_nm_rank = train.groupby('screening_rat').box_off_num.mean().reset_index(name = 'num_rank').sort_values(by = 'num_rank')
tr_nm_rank

tr_nm_rank['num_rank'] = [i + 1 for i in range(tr_nm_rank.shape[0])]
tr_nm_rank

# train, test 셋에 반영
train = pd.merge(train, tr_nm_rank, how = 'left')
test = pd.merge(test, tr_nm_rank, how = 'left')
장르별 영화 관객수 평균값 기준으로 장르 랭크 인코딩
genre_rank = train.groupby('genre').box_off_num.mean().reset_index(name ='genre_rank').sort_values(by= 'genre_rank')
genre_rank

genre_rank['genre_rank'] = [i + 1 for i in range(genre_rank.shape[0])]
genre_rank

train = pd.merge(train, genre_rank, how = 'left')
test = pd.merge(test, genre_rank, how = 'left')
4.2 관객 수 로그변환

plt.subplot(1,2,1)
plt.hist(train['box_off_num'])
plt.title('관객수 로그변환 전')
plt.subplot(1,2,2)
plt.hist(y)
plt.title('관객수 로그변환 후')

5. Modeling
5 가지 모델을 사용하여 적합시키겠다.
- GradintBoostingRegressor
- NGBRegressor
- LGBRefressor
- XGBRegresor
- CatBoostRegressor
from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
from catboost import CatBoostRegressor
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import KFold
from ngboost import NGBRegressor
X = train[['genre_rank','month','time','num_rank','num_staff']]
X_test = test[['genre_rank','month','time','num_rank','num_staff']]
5.1 10Fold로 교차검증
train 데이터를 validation 데이터와 train 데이터로 나눈다. (10 회)

kf = KFold(n_splits = 10, shuffle = True, random_state = 42)
5.2 GradientBoostingRegressor
모델 적합 과정
- K-Fold 를 통해 train 데이터를 train / validaition 데이터셋으로 나눈다.
- 나눈 데이터셋에 대해 GradientBoostingRegressor 모델에 적합시킨다.
- 만약 예측값이 음수이면 0 의 값을 주고, 그 외에는 예측값을 입력한다. ( 관객수는 음수가 될 수 없음 )
- validation 데이터로 예측한 값과 validation의 y 값을 이용하여 RMSE 를 구한다.
rmse_list = []
gb_pred = np.zeros((test.shape[0]))
for tr_idx, val_idx in kf.split(X, y) :
tr_x, tr_y = X.iloc[tr_idx], y.iloc[tr_idx] # 하나의 Fold 에 대해 train 데이터 10개로 쪼개기
val_x, val_y = X.iloc[val_idx], y.iloc[val_idx] # 하나의 Fold 에 대해 validation 데이터 10개로 쪼개기
gbm.fit(tr_x, tr_y) # GradientBoostingRegressor 적합시키기
pred = np.expm1([0 if x < 0 else x for x in gbm.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in gbm.predict(X_test)])
rmse = np.sqrt(mean_squared_error(val_y, pred))
rmse_list.append(rmse)
gb_pred += (sub_pred / 10)
- a += b = a= a+b 를 의미한다.
- (sub_pred1 / 10) + ... (sub_pred10 / 10) = (sub_pred1 + ...+ subpred10 ) / 10
gbm_rmse = np.mean(rmse_list)
gbm_rmse # 7.8690842366385665
5.3 NGBRegressor
ngb = NGBRegressor(random_state = 518)
rmse_list = []
ngb_pred = np.zeros((test.shape[0]))
for tr_idx, val_idx in kf.split(X,y) :
tr_x,tr_y = X.iloc[tr_idx], y.iloc[tr_idx]
val_x,val_y = X.iloc[val_idx], y.iloc[val_idx]
ngb.fit(tr_x,tr_y)
pred = np.expm1([0 if x < 0 else x for x in ngb.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in ngb.predict(X_test)])
rmse = np.sqrt(mean_squared_error(val_y,pred))
rmse_list.append(rmse)
ngb_pred += (sub_pred / 10)
ngb_rmse = np.mean(rmse_list)
ngb_rmse # 7.916303120582562
5.4 LGBMRegressor
lgbm = LGBMRegressor(random_state = 518)
rmse_list = []
lgb_pred = np.zeros((test.shape[0]))
for tr_idx, val_idx in kf.split(X,y) :
tr_x, tr_y = X.iloc[tr_idx],y.iloc[tr_idx]
val_x, val_y = X.iloc[val_idx],y.iloc[val_idx]
lgbm.fit(tr_x,tr_y)
pred = np.expm1([0 if x < 0 else x for x in lgbm.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in lgbm.predict(X_test)])
rmse = np.sqrt(mean_squared_error(val_y,pred))
rmse_list.append(rmse)
lgb_pred += (sub_pred / 10)
lgbm_rmse = np.mean(rmse_list)
lgbm_rmse # 7.8937840358118
5.5 XGBRegressor
xgb = XGBRegressor(random_state = 518)
rmse_list = []
xgb_pred = np.zeros((test.shape[0]))
for tr_idx, val_idx in kf.split(X,y) :
tr_x , tr_y = X.iloc[tr_idx],y.iloc[tr_idx]
val_x, val_y = X.iloc[val_idx],y.iloc[val_idx]
xgb.fit(tr_x,tr_y)
pred = np.expm1([0 if x < 0 else x for x in xgb.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in xgb.predict(X_test)])
rmse - np.sqrt(mean_squared_error(val_y,pred))
rmse_list.append(rmse)
xgb_pred += (sub_pred / 10)
xgb_rmse = np.mean(rmse_list)
xgb_rmse # 8.303954925493036
5.6 CatBoostRegressor
cat = CatBoostRegressor(random_state = 518, silent = True)
rmse_list = []
cat_pred = np.zeros((test.shape[0]))
for tr_idx,val_idx in kf.split(X,y) :
tr_x,tr_y = X.iloc[tr_idx], y.iloc[tr_idx]
val_x,val_y = X.iloc[val_idx], y.iloc[val_idx]
cat.fit(tr_x,tr_y)
pred = np.expm1([0 if x < 0 else x for x in cat.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in cat.predict(X_test)])
rmse = np.sqrt(mean_squared_error(val_y,pred))
rmse_list.append(rmse)
cat_pred += (sub_pred / 10)
cat_rmse = np.mean(rmse_list)
cat_rmse # 7.879773050833765
5.7 RandomForestRegressor
rf = RandomForestRegressor(random_state = 518)
rmse_list = []
rf_pred = np.zeros((test.shape[0]))
for tr_idx,val_idx in kf.split(X,y) :
tr_x,tr_y = X.iloc[tr_idx],y.iloc[tr_idx]
val_x,val_y = X.iloc[val_idx],y.iloc[val_idx]
rf.fit(tr_x,tr_y)
pred = np.expm1([0 if x < 0 else x for x in rf.predict(val_x)])
sub_pred = np.expm1([0 if x < 0 else x for x in rf.predict(X_test)])
rmse = np.sqrt(mean_squared_error(val_y,pred))
rmse_list.append(rmse)
rf_pred += (sub_pred / 10)
rf_rmse =np.mean(rmse_list)
rf_rmse # 7.831001696712167
- 각 모델의 RMSE
print('GradientBoostingRegressor RMSE : ',gbm_rmse)
print('NGBRegressor RMSE : ',ngb_rmse)
print('LBGMRegressor RMSE : ',lgbm_rmse)
print('XGBRegressor RMSE : ',xgb_rmse)
print('CatRegressor RMSE : ',cat_rmse)
print('RandomForestRegressor RMSE : ',rf_rmse)
GradientBoostingRegressor RMSE : 7.8690842366385665
NGBRegressor RMSE : 7.916303120582562
LBGMRegressor RMSE : 7.8937840358118
XGBRegressor RMSE : 8.303954925493036
CatRegressor RMSE : 7.879773050833765
RandomForestRegressor RMSE : 7.831001696712167
5.8 최종 예측결과 블렌딩
각 모델에서 나온 예측결과를 평균한다.
submission['box_off_num'] = (xgb_pred + cat_pred + lgb_pred + rf_pred + gb_pred + ngb_pred) / 6 # 점수 : 1987079.1271094256
블렌딩을 하지 않고 모델 중 RMSE 가 가장 적었던 RandomForestRegressor 만을 이용했을 때
submission['box_off_num'] = rf_pred # 랜덤 포레스트를 최종 모델로 선택한 경우 , 점수 : 1987078.3409955238
submission.to_csv("0324_2.csv", index = False)
- 각 모델을 블렌딩한 결과와 크게 차이가 나진 않았다.