본문 바로가기

파이썬/데이콘

영화 관객 수 예측 모델 개발

데이콘의 영화 관객수 예측 모델 : 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)

 

 

전 영화의 평균 관객 수가 0 인 경우가 없음

 

  • '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 가지 모델을 사용하여 적합시키겠다.

  1. GradintBoostingRegressor
  2. NGBRegressor
  3. LGBRefressor
  4. XGBRegresor
  5. 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)
  • 각 모델을 블렌딩한 결과와 크게 차이가 나진 않았다.

 

 

 

 

영화 관객 수 예측모델.ipynb
0.49MB