美文网首页
kaggle电影票房预测(特征工程与建模调参)

kaggle电影票房预测(特征工程与建模调参)

作者: 科学技术爱好者 | 来源:发表于2019-04-17 14:20 被阅读0次

    前言:

    电影票房预测项目中,我们需要根据电影的预算,类型,语言,发行时间,电影时长,受欢迎程度,演员信息,工作人员信息等信息来预测电影票房。

    与之前的Kaggle共享单车租赁预测的不同之处在于,共享单车项目中,我们的数据都是数值型变量,但是在这个票房预测项目中,我们的数据绝大多数都是些json,str格式,所以我们需要花费大量的时间来处理数据,所以这篇文章我们只涉及数据处理与建模调参,中间的数据分析与可视化可以戳这里:Kaggle电影票房预测(数据分析与可视化)

    本次项目说明:

    1.0 数据概览

    #导入相关库
    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    import seaborn as sns
    import json
    from collections import Counter
    from wordcloud import WordCloud
    from dateutil.parser import parse
    import warnings
    from xgboost import XGBRegressor
    from sklearn.model_selection import GridSearchCV, cross_val_score
    
    #设置jupyter可以多行输出
    from IPython.core.interactiveshell import InteractiveShell 
    InteractiveShell.ast_node_interactivity = 'all' #默认为'last'
    
    #jupyter绘图魔术方法
    %matplotlib notebook
    
    #忽略警告
    warnings.filterwarnings("ignore")
    
    #读取数据
    train_data = pd.read_csv('train.csv')
    test_data = pd.read_csv('test.csv')
    
    #查看对象尺度
    train_data.shape
    test_data.shape
    

    output:

    (3000, 23)
    (4398, 22)
    

    发现测试集的数据竟然比训练集还要多...只能希望给这些数据够模型训练了,测试集少的一列是revenue列,也就是票房列,需要我们建模预测。

    #训练集总览
    train_data.info()
    

    output:

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 3000 entries, 0 to 2999
    Data columns (total 23 columns):
    id                       3000 non-null int64
    belongs_to_collection    604 non-null object
    budget                   3000 non-null int64
    genres                   2993 non-null object
    homepage                 946 non-null object
    imdb_id                  3000 non-null object
    original_language        3000 non-null object
    original_title           3000 non-null object
    overview                 2992 non-null object
    popularity               3000 non-null float64
    poster_path              2999 non-null object
    production_companies     2844 non-null object
    production_countries     2945 non-null object
    release_date             3000 non-null object
    runtime                  2998 non-null float64
    spoken_languages         2980 non-null object
    status                   3000 non-null object
    tagline                  2403 non-null object
    title                    3000 non-null object
    Keywords                 2724 non-null object
    cast                     2987 non-null object
    crew                     2984 non-null object
    revenue                  3000 non-null int64
    dtypes: float64(2), int64(3), object(18)
    memory usage: 539.1+ KB
    

    字段解释:

    • id:电影的唯一id
    • belongs_to_collection:这个电影系列的系列名称(不是系列电影就是空值)/TMDB上的ID/海报和背景图链接,json格式
    • budget:电影的预算,0代表未知
    • genres:电影的类型以及类型对应的TMDB上的ID,使用json格式封装信息
    • homepage:电影官方主页
    • imdb_id:电影在TMDB的ID
    • orginal_language:电影的原始语言
    • orginal_title:电影的原始名称
    • overview:简短的描述
    • popularity:电影的流行程度,使用浮点数代表
    • poster_path:电影海报链接
    • production_copanies:电影的出品公司,使用json格式
    • production_countries: 电影出品公司所在国家,使用json格式
    • release_date:发行时间
    • runtime:电影时长
    • spoken_languages:电影语言,json格式
    • status:电影的状态,是否已经发布
    • tagline:电影的宣传标语
    • title:电影英文名
    • keywords:电影的关键词以及相应关键词在TMDB上的ID
    • cast:演员的姓名/id/性别,使用json格式
    • crew:职员(导演/编辑/摄影...)的姓名/id/性别,使用json格式
    • revenue:电影总收入
    #训练集和测试集的空值一览
    train_data.isna().sum()
    test_data.isna().sum()
    

    output:

    id                          0
    belongs_to_collection    2396
    budget                      0
    genres                      7
    homepage                 2054
    imdb_id                     0
    original_language           0
    original_title              0
    overview                    8
    popularity                  0
    poster_path                 1
    production_companies      156
    production_countries       55
    release_date                0
    runtime                     2
    spoken_languages           20
    status                      0
    tagline                   597
    title                       0
    Keywords                  276
    cast                       13
    crew                       16
    revenue                     0
    dtype: int64
    id                          0
    belongs_to_collection    3521
    budget                      0
    genres                     16
    homepage                 2978
    imdb_id                     0
    original_language           0
    original_title              0
    overview                   14
    popularity                  0
    poster_path                 1
    production_companies      258
    production_countries      102
    release_date                1
    runtime                     4
    spoken_languages           42
    status                      2
    tagline                   863
    title                       3
    Keywords                  393
    cast                       13
    crew                       22
    dtype: int64
    

    可以看到训练集和测试集中,belongs_to_collection/homepage/tagline这些列缺失值较多,不过也正常,很多电影都只有一部,没有官网和宣传标语也是非常正常的,对于缺失值我们后面载处理就行了。

    2.0 数据处理

    train_data[:3]
    
    image.png

    由于数据有很多列,超出了显示边界,所以就只展示了一部分数据。可以看到数据集中很多都是json格式的数据,所以,处理数据的第一步,我们要将json格式的数据处理掉!

    使用eval()函数,就可以将字符串类型的list转换为list类型了,这样后面处理各列就方便多了。

    dict_columns = ['belongs_to_collection', 'genres', 'production_companies',
                    'production_countries', 'spoken_languages', 'Keywords', 'cast', 'crew']
    
    def text_to_dict(df):
        for column in dict_columns:
            df[column] = df[column].apply(lambda x: {} if pd.isna(x) else eval(x) )
        return df
            
    train_data = text_to_dict(train_data)
    test_data = text_to_dict(test_data)
    
    train_data[:3]
    
    image.png

    2.1 belongs_to_collection列处理

    一般来说一个电影就是属于一个系列而不会是多个系列,为了防止意外情况,我们来验证下belongs_to_collection所有的list是否都是只有一个dict( list元素中一个dict就是一个系列)。

    train_data['belongs_to_collection'].map(lambda x:len(x) if x!='{}' else 0).value_counts()
    

    output:

    0    2396
    1     604
    Name: belongs_to_collection, dtype: int64
    

    OK,那么现在我们知道了,belongs_to_collection列中有2396部电影都不属于电影系列,剩下的604部电影都是只属于一个电影系列的。其实电影属于哪一个系列对票房的影响不大,而这个电影是否是属于一个电影系列的,这是对票房有影响的,所以这里我们新增1列就是电视是否属于系列电影。

    #训练集处理
    train_data['has_collection'] = train_data['belongs_to_collection'].map(lambda x:1 if x!={} else 0)
    
    #测试集处理
    test_data['has_collection'] = test_data['belongs_to_collection'].map(lambda x:1 if x!={} else 0)
    

    2.2 genres列处理

    一个电影可能属于多种类型(喜剧/动作/悬疑...这种),所以首先我们来看下,电影类型数量的分布。

    (train_data['genres'].map(lambda x:len(x) if x!={} else 0).value_counts().sort_index()).plot(kind='bar')
    
    image.png

    大部分电影还是只属于2-3种类型,不属于任何电影类型或者属于7种电影类型都是极少数。

    对于genres列,如果一个电影它属于多种类型,比如既是动作片也是悬疑片,那么他可能会受到更多人欢迎,票房也就好,所以这里我们要增加一列,就是这个电影是属于几种类型的。

    #训练集处理
    train_data['genres_num'] = train_data['genres'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['genres_num'] = test_data['genres'].map(lambda x:len(x) if x!={} else 0)
    

    不同类型的电影可能受欢迎的程度也会不同,比如喜剧片比动作片要更受欢迎,所以类型的不同可能也会导致票房的不同,这里我就是要将所有的电影类型类型进行分类。

    首先需要新增一列,将所有的电影类型从dict中分离出来。

    #训练集处理
    train_data['genres_all'] = train_data['genres'].map(lambda x:','.join([i['name'] for i in x]))
    
    #测试集处理
    test_data['genres_all'] = test_data['genres'].map(lambda x:','.join([i['name'] for i in x]))
    
    train_data['genres_all'][:5]
    

    output:

    0                         Comedy
    1    Comedy,Drama,Family,Romance
    2                          Drama
    3                 Thriller,Drama
    4                Action,Thriller
    Name: genres_all, dtype: object
    

    然后将所有的电影类型找出来,顺便可以看下哪些电影类型最多,哪些电影类型最少。

    list_genres = list(train_data['genres'].map(lambda x:[i['name'] for i in x]))
    
    list_genres_count = []
    for i in list_genres:
        for j in i:
            list_genres_count.append(j)
    
    #%matplotlib notebook
    fig,ax=plt.subplots(figsize=(12,6))
    w = WordCloud( \
        width = 1000, height = 700,\
        background_color = "white",
        collocations=False
        ).generate(','.join(list_genres_count))
    plt.imshow(w)
    plt.xticks([])
    plt.yticks([])
    
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_visible(False)
    ax.spines['left'].set_visible(False)
    
    image.png

    可以看到剧情和喜剧类的电影最多,惊悚和动作电影也不少,这些电影类型其实从侧面也反映了观众的喜好。

    然后我们就可以给电影进行分类操作了,找出所有的电影类型并创建相应的列,如果genres_all列中包括这个类型,那就是1,否则就是0。

    genres_top = Counter(list_genres_count).most_common()
    
    list_genres_count = Counter(list_genres_count)
    list_genres_count = list(list_genres_count)
    
    #训练集处理
    for i in list(list_genres_count):
        train_data['genres_'+i] = train_data['genres_all'].map(lambda x:1 if i in x else 0)
    
    #测试集处理
    for i in list(list_genres_count):
        test_data['genres_'+i] = test_data['genres_all'].map(lambda x:1 if i in x else 0)
    

    2.3 homepage列处理

    有官方主页说明宣传力度大,那么关注也会越多,票房越好,所以新增一列,判断电影是否有主页。

    训练集处理
    train_data['has_homepage'] = train_data['homepage'].map(lambda x:0 if pd.isna(x) else 1)
    
    #测试集处理
    test_data['has_homepage'] = test_data['homepage'].map(lambda x:0 if pd.isna(x) else 1)
    

    2.4 original_language

    先看下有多少种原始语言,以及他们的分布如何。

    train_data['original_language'][:5]
    
    0    en
    1    en
    2    en
    3    hi
    4    ko
    Name: original_language, dtype: object
    
    len(train_data['original_language'].value_counts())
    

    output:

    36
    
    sns.countplot(train_data['original_language'])
    
    image.png

    可以看到,一共36种语言,但是绝大多电影都是英语,所以这里只取频次前15的语言新建列进行分类。

    original_language_list = [i[0] for i in Counter(train_data['original_language']).most_common(15)]
    
    #训练集处理
    for i in original_language_list:
        train_data['orginal_language_' + i] = train_data['original_language'].map(lambda x:1 if x==i else 0 )
    
        #测试集处理
    for i in original_language_list:
        test_data['orginal_language_' + i] = test_data['original_language'].map(lambda x:1 if x==i else 0 )
    

    2.5 production_companies列处理

    首先看下,电影的出品公司数量分布,出品公司越多,说明电影投入啊,宣发力度越大,所以公司数量对票房也是很重要的一点。

    #训练集处理
    train_data['pr_companies_num'] = train_data['production_companies'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['pr_companies_num'] = test_data['production_companies'].map(lambda x:len(x) if x!={} else 0)
    
    sns.countplot(train_data['pr_companies_num'])
    
    image.png

    可以看到,绝大多是电影是由1-3家公司出品的。

    然后将电影公司从dict中抽离出来,形成一个单独的列。

    #测试集处理
    train_data['companies_all'] = train_data['production_companies'].map(lambda x:','.join([i['name'] for i in x]))
    
    #测试集处理
    test_data['companies_all'] = test_data['production_companies'].map(lambda x:','.join([i['name'] for i in x]))
    
    list_companies = list(train_data['production_companies'].map(lambda x:[i['name'] for i in x]))
    
    list_companies_count = []
    for i in list_companies:
        for j in i:
            list_companies_count.append(j)
    
    companies_top = Counter(list_companies_count).most_common()
    
    len(Counter(list_companies_count))
    

    output:

    3695
    

    电影公司由大公司,也有小公司,所以不同公司出品对票房的影像也是不同,但是由于电影公司一共有3695家,太多了,所以我们只取频次前30家公司新建列。

    production_companies_count_list = [i[0] for i in Counter(list_companies_count).most_common(30)]
    
    #训练集处理
    for i in production_companies_count_list:
        train_data['companies_' + i] = train_data['companies_all'].map(lambda x:1 if i in x else 0 )
    
    #测试集处理
    for i in production_companies_count_list:
        test_data['companies_' + i] = test_data['companies_all'].map(lambda x:1 if i in x else 0 )
    

    2.6 production_countries列处理

    出品国家这列其实和出品公司列的处理基本一致,先找出出品国家的数量,然后根据频次进行分类处理。

    train_data['production_countries'][:3]
    

    output:

    0    [{'iso_3166_1': 'US', 'name': 'United States o...
    1    [{'iso_3166_1': 'US', 'name': 'United States o...
    2    [{'iso_3166_1': 'US', 'name': 'United States o...
    Name: production_countries, dtype: object
    

    先新增一列,电影的出品方国家数量有多少,出品国家越多,意味着在更多的国家上线,票房也就越好。

    #训练集处理
    train_data['pr_countries_num'] = train_data['production_countries'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['pr_countries_num'] = test_data['production_countries'].map(lambda x:len(x) if x!={} else 0)
    

    然后我们需要将出品方国家从json格式种分离出来,找出所有的出品方国家有哪些,再进行分类。

    #测试集处理,新增一列,将出品方国家从json中分离出来
    train_data['countries_all'] = train_data['production_countries'].map(lambda x:','.join([i['iso_3166_1'] for i in x]))
    
    #测试集处理,新增一列,将出品方国家从json中分离出来
    test_data['countries_all'] = test_data['production_countries'].map(lambda x:','.join([i['iso_3166_1'] for i in x]))
    
    list_countries = list(train_data['production_countries'].map(lambda x:[i['iso_3166_1'] for i in x]))
    
    list_countries_count = []
    for i in list_countries:
        for j in i:
            list_countries_count.append(j)
    
    fig,ax=plt.subplots(figsize=(12,6))
    w = WordCloud( \
        width = 1000, height = 700,\
        background_color = "white",
        collocations=False
        ).generate(','.join(list_countries_count))
    plt.imshow(w)
    plt.xticks([])
    plt.yticks([])
    
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_visible(False)
    ax.spines['left'].set_visible(False)
    
    image.png
    pd.DataFrame(Counter(list_countries_count).most_common(30)).set_index([0]).iloc[:,0].plot(kind='bar')
    
    image.png
    len(Counter(list_countries_count))
    

    output:

    74
    
    countries_top = Counter(list_countries_count).most_common()
    

    可以看到,出品方国家虽然有74个,但是绝大多是电影是由美国出品的,远超其他国家。

    所以这里我们只取频次前25的国家新建列。

    production_countries_count_list = [i[0] for i in Counter(list_countries_count).most_common(25)]
    
    #训练集处理
    for i in production_countries_count_list:
        train_data['pr_countries_' + i] = train_data['countries_all'].map(lambda x:1 if i in x else 0 )
    
    #测试集处理
    for i in production_countries_count_list:
        test_data['pr_countries_' + i] = test_data['countries_all'].map(lambda x:1 if i in x else 0 )
    

    2.7 release_date列处理

    对于发行时间这列,我们需要将时间拆分为年月日,季度以及星期几,一共新增5列。

    def func_one(x):
        if x<=3:
            return 1
        elif x<=6:
            return 2
        elif x<=9:
            return 3
        else:
            return 4
    
    train_data['release_year'] = train_data['release_date'].map(lambda x:parse(x).year)
    train_data['release_month'] = train_data['release_date'].map(lambda x:parse(x).month)
    train_data['release_day'] = train_data['release_date'].map(lambda x:parse(x).day)
    train_data['release_weekday']  = train_data['release_date'].map(lambda x:parse(x).isoweekday())
    train_data['release_quarter']  = train_data['release_month'].map(func_one)
    
    test_data.loc[test_data['release_date'].isnull(),'release_date']='1/1/2000'
    
    test_data['release_year'] = test_data['release_date'].map(lambda x:parse(x).year)
    test_data['release_month'] = test_data['release_date'].map(lambda x:parse(x).month)
    test_data['release_day'] = test_data['release_date'].map(lambda x:parse(x).day)
    test_data['release_weekday']  = test_data['release_date'].map(lambda x:parse(x).isoweekday())
    test_data['release_quarter']  = test_data['release_month'].map(func_one)
    

    这里要注意下,如果一个时间比如说是8/6/61,这个61他是1961年,而不是2061年,但是有时候parse(x)却会转化错误,导致生成的年份是2061,所以这里我们需要再将大于2019的年份减去100,这才是正确的时间。

    def fun_year(x):
        if x>2019:
            x = x-100
        return x
    
    train_data['release_year'] = train_data['release_year'].map(fun_year)
    
    test_data['release_year'] = test_data['release_year'].map(fun_year)
    

    2.8 runtime列处理

    runtime列存在一些空值,这里统一使用众数进行填充。

    #训练集处理
    train_data['runtime'] = train_data['runtime'].fillna(train_data['runtime'].mode()[0])
    
    #测试集处理
    test_data['runtime'] = test_data['runtime'].fillna(test_data['runtime'].mode()[0])
    

    2.9 spoken_languages列处理

    spoken_languages列的处理和上面的json列处理也是差不太多,我们新增一列语言数量的列,然后找出频次前25的语言新建列进行分类。

    train_data['spoken_languages'][:2]
    

    output:

    0    [{'iso_639_1': 'en', 'name': 'English'}]
    1    [{'iso_639_1': 'en', 'name': 'English'}]
    Name: spoken_languages, dtype: object
    
    #训练集处理
    train_data['spoken_languages_num'] = train_data['spoken_languages'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['spoken_languages_num'] = test_data['spoken_languages'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    train_data['spoken_languages_all'] = train_data['spoken_languages'].map(lambda x:','.join([i['iso_639_1'] for i in x]))
    
    #测试集处理
    test_data['spoken_languages_all'] = test_data['spoken_languages'].map(lambda x:','.join([i['iso_639_1'] for i in x]))
    
    list_spoken_languages = list(train_data['spoken_languages'].map(lambda x:[i['iso_639_1'] for i in x]))
    
    list_spoken_languages_count = []
    for i in list_spoken_languages:
        for j in i:
            list_spoken_languages_count.append(j)
    
    languages_top = Counter(list_spoken_languages_count).most_common()
    
    spoken_languages_count_list = [i[0] for i in Counter(list_spoken_languages_count).most_common(25)]
    
    #训练集处理
    for i in spoken_languages_count_list:
        train_data['spoken_languages_' + i] = train_data['spoken_languages_all'].map(lambda x:1 if i in x else 0 )
    
    #测试集处理
    for i in spoken_languages_count_list:
        test_data['spoken_languages_' + i] = test_data['spoken_languages_all'].map(lambda x:1 if i in x else 0 )
    

    3.0 status列处理

    train_data['status'].value_counts()
    
    Released    2996
    Rumored        4
    Name: status, dtype: int64
    

    可以看到绝大多是电影都是已发行状态,所以这列我们将电影的状态分为Released和其他其他两种情况,那么1就是Released,0就是其他。

    train_data['has_Released'] = train_data['status'].map(lambda x:1 if x=='Released' else 0)
    
    test_data['has_Released'] = test_data['status'].map(lambda x:1 if x=='Released' else 0)
    

    3.1 tagline列处理

    和homepage列一样,我们新增一列,将数据分为有tagline和没有tagline列。

    train_data['has_tagline'] = train_data['tagline'].map(lambda x:0 if pd.isna(x) else 1)
    
    test_data['has_tagline'] = test_data['tagline'].map(lambda x:0 if pd.isna(x) else 1)
    

    3.2 Keywords列处理

    关键词越多,说明电影内容越丰富,同时也会吸引更多人注意,潜在票房也会越高,所以这里首先新增1列,判断电影关键词数量。

    #训练集处理
    train_data['Keywords_num'] = train_data['Keywords'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['Keywords_num'] = test_data['Keywords'].map(lambda x:len(x) if x!={} else 0)
    

    然后我们找出所有的关键词,根据频次进行排序,取频次前N的关键词进行分类处理。

    #训练集处理
    train_data['Keywords_all'] = train_data['Keywords'].map(lambda x:','.join([i['name'] for i in x]))
    
    #测试集处理
    test_data['Keywords_all'] = test_data['Keywords'].map(lambda x:','.join([i['name'] for i in x]))
    
    list_Keywords = list(train_data['Keywords'].map(lambda x:[i['name'] for i in x]))
    
    list_Keywords_count = []
    for i in list_Keywords:
        for j in i:
            list_Keywords_count.append(j)
    
    keywords_top = Counter( list_Keywords_count).most_common()
    
    fig,ax=plt.subplots(figsize=(12,6))
    w = WordCloud( \
        width = 1000, height = 700,
        background_color = "white",
        collocations=True
        ).generate(','.join(list_Keywords_count))
    plt.imshow(w)
    plt.xticks([])
    plt.yticks([])
    
    ax.spines['top'].set_visible(False)
    ax.spines['right'].set_visible(False)
    ax.spines['bottom'].set_visible(False)
    ax.spines['left'].set_visible(False)
    
    image.png

    可以看到,独立电影,女权,暴力,谋杀,警匪,性,爱情这些是比较常见的关键词。

    由于关键词数量较多,这里我们取频次前30的关键词进行分类处理。

    list_Keywords_count = [i[0] for i in Counter(list_Keywords_count).most_common(30)]
    
    #训练集处理
    for i in list(list_Keywords_count):
        train_data['Keywords_'+i] = train_data['Keywords_all'].map(lambda x:1 if i in x else 0)
    
    #测试集处理
    for i in list(list_Keywords_count):
        test_data['Keywords_'+i] = test_data['Keywords_all'].map(lambda x:1 if i in x else 0)
    

    3.3 cast列处理

    演员数量越多,说明电影投入越大,所以一样要新增1列判断电影的演员数量。

    #训练集处理
    train_data['cast_num'] = train_data['cast'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['cast_num'] = test_data['cast'].map(lambda x:len(x) if x!={} else 0)
    

    将演员的姓名从json格式中分离出来。

    #训练集处理
    train_data['cast_all'] = train_data['cast'].map(lambda x:','.join([i['name'] for i in x]))
    
    #测试集处理
    test_data['cast_all'] = test_data['cast'].map(lambda x:','.join([i['name'] for i in x]))
    
    list_cast = list(train_data['cast'].map(lambda x:[i['name'] for i in x]))
    
    list_cast_count = []
    for i in list_cast:
        for j in i:
            list_cast_count.append(j)
    
    cast_top = Counter(  list_cast_count).most_common()
    

    演员也分大牌演员和跑龙套的,不同演员对电影的票房影像不一样,所以这里取频次前30的演员,对进行分类处理,判断电影是否包含这些演员。

    list_cast_count = [i[0] for i in Counter(list_cast_count).most_common(30)]
    
    for i in list(list_cast_count):
        train_data['cast_'+i] = train_data['cast_all'].map(lambda x:1 if i in x else 0)
    
    for i in list(list_cast_count):
        test_data['cast_'+i] = test_data['cast_all'].map(lambda x:1 if i in x else 0)
    

    另外就是演员中性别的数量,比例可能也会对电影票房造成影响,有些就是大部分演员都是男性,比如敢死队这种,有些就是大部分演员都是女性,比如国产宫斗剧,电影的性别的组成不同,潜在观众也会不同,所以可能对最后的票房造成影响。

    train_data['gender_0_Cast'] = train_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 0]))
    train_data['gender_1_Cast'] = train_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 1]))
    train_data['gender_2_Cast'] = train_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 2]))
    
    test_data['gender_0_Cast'] = test_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 0]))
    test_data['gender_1_Cast'] = test_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 1]))
    test_data['gender_2_Cast'] = test_data['cast'].apply(lambda x: sum([1 for i in x if i['gender'] == 2]))
    

    3.2 crew列处理

    crew列的处理和cast列的处理基本一致,但是由于电影的部门数量不多,而且都是一样的(就是导演,编剧,摄影这种电影工作岗位),所以将这些岗位都进行了分类处理,来判断这个电影是否有某个岗位。

    #训练集处理
    train_data['crew_num'] = train_data['crew'].map(lambda x:len(x) if x!={} else 0)
    
    #测试集处理
    test_data['crew_num'] = test_data['crew'].map(lambda x:len(x) if x!={} else 0)
    
    #训练集处理
    train_data['crew_all'] = train_data['crew'].map(lambda x:','.join([i['name'] for i in x]))
    train_data['crew_department_all'] = train_data['crew'].map(lambda x:','.join([i['department'] for i in x]))
    
    #测试集处理
    test_data['crew_all'] = test_data['crew'].map(lambda x:','.join([i['name'] for i in x]))
    test_data['crew_department_all'] = test_data['crew'].map(lambda x:','.join([i['department'] for i in x]))
    
    list_crew = list(train_data['crew'].map(lambda x:[i['name'] for i in x]))
    
    list_crew_count = []
    for i in list_crew:
        for j in i:
            list_crew_count.append(j)
    
    crew_top = Counter(list_crew_count).most_common()
    
    list_department = list(train_data['crew'].map(lambda x:[i['department'] for i in x]))
    
    list_department_count = []
    for i in list_department:
        for j in i:
            list_department_count.append(j)
    
    department_top = Counter( list_department_count).most_common()
    
    list_crew_count = [i[0] for i in Counter(list_crew_count).most_common(30)]
    
    list_department_count = [i[0] for i in Counter(list_department_count).most_common()]
    
    for i in list(list_crew_count):
        train_data['crew_'+i] = train_data['crew_all'].map(lambda x:1 if i in x else 0)
    
    for i in list(list_crew_count):
        test_data['crew_'+i] = test_data['crew_all'].map(lambda x:1 if i in x else 0)
    
    for i in list(list_department_count):
        train_data['department_'+i] = train_data['crew_department_all'].map(lambda x:1 if i in x else 0)
    
    for i in list(list_department_count):
        test_data['department_'+i] = test_data['crew_department_all'].map(lambda x:1 if i in x else 0)
    
    train_data['gender_0_crew'] = train_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 0]))
    train_data['gender_1_crew'] = train_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 1]))
    train_data['gender_2_crew'] = train_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 2]))
    
    test_data['gender_0_crew'] = test_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 0]))
    test_data['gender_1_crew'] = test_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 1]))
    test_data['gender_2_crew'] = test_data['crew'].apply(lambda x: sum([1 for i in x if i['gender'] == 2]))
    

    3.3 renvenue列处理

    renvenue列使我们的因变量列,经常会遇到的一个问题就是如果这列的波动很大,会导致模型过拟合,我们可以先来看下renvenue列的分布。

    sns.distplot(train_data['revenue'])
    
    image.png

    可以看到,数据波动还是蛮大的,所以这里我们将其进行对数转化,再看看下分布情况。

    train_data['revenue_log'] = np.log(train_data['revenue'])
    sns.distplot(train_data['revenue_log'])
    
    image.png

    可以看到,数据的波动明显变小了,这对我们的模型是有益的。

    4.0 机器学习建模与调参

    4.1 建模

    这里我分别尝试了RandForest和XGBoost进行类建模,但是发现RandForest调参之后的分数比直接使用默认参数的XGBoost分数还要低,所以这里直接使用 XGBoost进行建模调参。

    train_data.shape
    test_data.shape
    

    output:

    (3000, 271)
    (4398, 269)
    

    训练集和测试卷的json数据格式的列都进行了分类处理,还有些像id,homepag,overview...这种对票房影响不大,所以这里直接将这些列找出来,然后删掉。

    train_drop_col = ['id','belongs_to_collection','genres','homepage','imdb_id','overview','original_language','original_title',
               'poster_path','production_companies','production_countries','release_date','spoken_languages',
               'status','tagline','title','Keywords','cast','crew','revenue','revenue_log','genres_all','companies_all','countries_all',
                     'spoken_languages_all','Keywords_all','cast_all','crew_all','crew_department_all']
    
    test_drop_col = ['id','belongs_to_collection','genres','homepage','imdb_id','overview','original_language','original_title',
               'poster_path','production_companies','production_countries','release_date','spoken_languages',
               'status','tagline','title','Keywords','cast','crew','genres_all','companies_all','countries_all','spoken_languages_all',
                    'Keywords_all','cast_all','crew_all','crew_department_all']
    

    设定模型的训练集和测试集,

    X_train = train_data.drop(train_drop_col,axis=1)
    y_train = train_data.loc[:,'revenue_log']
    
    X_test = test_data.drop(test_drop_col,axis=1)
    

    使用XGBoost进行建模,并拟合预测。

    xgbrfr = XGBRegressor()
    xgbrfr.fit(X_train,y_train)
    y_pred = xgbrfr.predict(X_test)
    
    submission=pd.DataFrame({'id':test_data['id'] , 'revenue':[max(0,x) for x in np.exp(y_pred)]})
    submission.to_csv('moviebox_predictions_2.0.csv',index=False)
    
    svc_rfr = cross_val_score(estimator=xgbrfr,X=X_train,y=y_train,cv=4)
    svc_rfr.mean()
    

    output:

    0.5162551932810016
    

    提交到Kaggle上得分:

    image.png

    4.1 调参

    这里解释下调参为什么没有从n_estimators开始,因为我的电影性能问题,每次从n_estimators开始调参就卡死,所以调完其他参数后最后才敢调n_estimators参数。一般的话,都是从n_estimators开始调参。

    第一轮调参:

    params_1 ={'min_child_weight':[1,2,3,4,5,6]}
    gs = GridSearchCV(xgbrfr, params_1, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    

    output:

    0.5251436587021792
    {'min_child_weight': 5}
    

    确定min_child_weight=5

    第二轮调参

    xgbrfr = XGBRegressor(min_child_weight=5)
    params_2 ={'max_depth':[3,4,5,6,7,8,9,10]}
    gs = GridSearchCV(xgbrfr, params_2, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    

    output:

    0.5314080427515098
    {'max_depth': 6}
    

    确定max_depth=6

    第三轮调参

    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6)
    params_3 ={'gamma': [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]}
    gs = GridSearchCV(xgbrfr, params_3, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    

    output:

    0.530309470934862
    {'gamma': 0.1}
    

    确定gamma=0.1

    第四轮调参

    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6,gramma=0.1)
    params_4 ={'subsample': [0.6, 0.7, 0.8, 0.9], 'colsample_bytree': [0.6, 0.7, 0.8, 0.9]}
    gs = GridSearchCV(xgbrfr, params_4, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    
    0.5429990312580631
    {'colsample_bytree': 0.7, 'subsample': 0.9}
    

    确定colsample_bytree=0.7,subsample=0.9

    第五轮调参

    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6,gramma=0.1,subsample=0.9,colsample_bytree=0.7)
    params_5 ={'reg_alpha': [0.05, 0.1, 1, 2, 3], 'reg_lambda': [0.05, 0.1, 1, 2, 3]}
    gs = GridSearchCV(xgbrfr, params_5, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    
    0.546215111884258
    {'reg_alpha': 3, 'reg_lambda': 0.05}
    

    确定reg_alpha=3,reg_lambda=0.05

    第六轮调参

    
    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6,gramma=0.1,subsample=0.9,colsample_bytree=0.7,reg_alpha=3,reg_lambda=0.05)
    params_6 ={'learning_rate': [0.01, 0.05, 0.07, 0.1, 0.2]}
    gs = GridSearchCV(xgbrfr, params_6, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    
    0.546215111884258
    {'learning_rate': 0.1}
    

    确定learning_rate=0.1

    第七轮调参

    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6,gramma=0.1,subsample=0.9,colsample_bytree=0.7,reg_alpha=3,reg_lambda=0.05,learning_rate=0.1)
    params_7 ={'n_estimators': [100,200,300,400, 500, 600, 700, 800,900,1000]}
    gs = GridSearchCV(xgbrfr, params_7, cv=4)
    gs.fit(X_train,y_train)
     
    #查验优化后的超参数配置
    print(gs.best_score_)
    print(gs.best_params_)
    
    0.546215111884258
    {'n_estimators': 100}
    

    确定n_estimators=100

    进过N轮的调参,我们终于确定了最佳的超参数,现在我们就可以根据这些最佳超参数,重新建模并进行预测。

    xgbrfr = XGBRegressor(min_child_weight=5,max_depth=6,gramma=0.1,subsample=0.9,colsample_bytree=0.7,reg_alpha=3,reg_lambda=0.05,learning_rate=0.1)
    
    xgbrfr.fit(X_train,y_train)
    y_pred = xgbrfr.predict(X_test)
    
    submission=pd.DataFrame({'id':test_data['id'] , 'revenue':[max(0,x) for x in np.exp(y_pred)]})
    submission.to_csv('xgb_moviebox_predictions_2.0.csv',index=False)
    
    image.png

    可以看到,分数还是有一定的提升的,后续我们还可以在特征工程,特征选择,特征融合,模型融合等方面做出进一步改进,多尝试下,冲击前200指日可待!

    相关文章

      网友评论

          本文标题:kaggle电影票房预测(特征工程与建模调参)

          本文链接:https://www.haomeiwen.com/subject/wfnhwqtx.html