一、预处理
先给源码和数据集
git:https://github.com/linkunxin/filmRvenuePred 百度网盘:https://pan.baidu.com/s/1_rpzLeTFf14x6KoxwWY4Lw
1、数据初步处理
先来看一下数据集样子
然后总览一下数据情况( info() ),在此之前,我们往往将测试集和训练集先合并,统一处理后再分开,就不用处理两遍了。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
train ='train.csv'
test ='test.csv'
dataTrain =pd.read_csv(train)
dataTest =pd.read_csv(test)
data =pd.merge(dataTrain,dataTest,how='outer') #合并测试训练集
data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 7398 entries, 0 to 7397
Data columns (total 23 columns):
id 7398 non-null int64
belongs_to_collection 1481 non-null object
budget 7398 non-null int64
genres 7375 non-null object
homepage 2366 non-null object
imdb_id 7398 non-null object
original_language 7398 non-null object
original_title 7398 non-null object
overview 7376 non-null object
popularity 7398 non-null float64
poster_path 7396 non-null object
production_companies 6984 non-null object
production_countries 7241 non-null object
release_date 7397 non-null object
runtime 7392 non-null float64
spoken_languages 7336 non-null object
status 7396 non-null object
tagline 5938 non-null object
title 7395 non-null object
Keywords 6729 non-null object
cast 7372 non-null object
crew 7360 non-null object
revenue 3000 non-null float64
dtypes: float64(3), int64(2), object(18)
memory usage: 1.4+ MB
- belongs_to_collection和homepage出现大量空值,数据集中将不采用
- imdb_id可以用ID来表示、poster_path暂时没必要
data.status.value_counts()
Released 7385
Rumored 6
Post Production 5
Name: status, dtype: int64
- status状态信息有三种值,其中released占绝大部分,所以特征并不明显,所以弃掉
cnt =data[data.budget==0].count()['budget']
print(cnt,cnt/len(data))
2023 0.2734522844011895
- budget有2023个0值,缺失占比近百分之30,暂时不作采用。
2、缺失值处理
- 采用平均值补充:
data.loc[col0.isnull(),col] = col0.mean() #用平均值来补缺失值
- 用到的离散型数据基本没有缺失值,有则返回空列表,下面将作介绍
3、异常值处理
1)数值型
revenue
print(np.sort(data.revenue)[:50])
np.array(data.budget[np.argsort(data.revenue)[:50].values])
[ 1. 1. 1. 1. 2. 3. 3. 3. 4. 5. 6. 7. 8. 8.
8. 10. 10. 11. 12. 12. 13. 15. 18. 18. 20. 23. 25. 25.
25. 30. 30. 32. 46. 60. 70. 79. 85. 88. 97. 100. 100. 121.
125. 126. 135. 198. 204. 241. 306. 311.]
array([ 12, 2, 592, 0, 1, 1,
750000, 0, 344, 1, 6400000, 0,
6, 130, 0, 0, 0, 0,
23000000, 16000000, 0, 0, 10000000, 0,
0, 12000000, 0, 4, 0, 2000000,
0, 0, 9000000, 0, 0, 0,
5, 0, 0, 1, 410000, 0,
0, 0, 5, 500000, 0, 0,
0, 0], dtype=int64)
- 第一个输出是revenue的排序后的最小的50名
- 第二个输出的是与其对应的budget,也就是预计开支。
从常识来考虑,这两个的关系是挺大的,budget中除了一些0值,其他的budget还是蛮高的,但再看回第一个输出,他们的revenue这么低是不填合理的,所以这肯定就是有不少的异常值,可能是revenue记录时打少了N个0等原因造成的。 - 所以我们需对其进行异常值处理
本框架采用两种方法查找异常值:
- 1、3sigma原则。
- 2、箱型图识别异常值。
即
dealAbnormal(self,col,method) # method:3sigma或box
发现3sigma效果好点(最后拟合的效果)
popularity
print(np.sort(data.popularity)[:50])
print(np.sort(data.popularity)[7350:])
pd.DataFrame({'a':list(data.popularity)}).boxplot()
data.popularity.describe()
[1.0000e-06 1.0000e-06 3.0800e-04 4.6400e-04 5.7800e-04 6.5700e-04
8.4400e-04 1.2230e-03 1.2720e-03 1.3930e-03 1.8800e-03 2.2290e-03
2.3540e-03 2.8170e-03 3.0130e-03 3.3050e-03 3.5680e-03 3.6260e-03
3.6470e-03 4.4250e-03 4.7060e-03 5.3510e-03 7.2940e-03 7.8290e-03
7.8770e-03 9.0570e-03 9.9610e-03 1.1427e-02 1.1574e-02 1.6219e-02
2.1580e-02 2.2347e-02 3.0576e-02 3.7986e-02 3.8560e-02 3.8876e-02
3.9793e-02 4.2036e-02 4.3519e-02 4.4048e-02 4.4561e-02 4.5860e-02
4.7875e-02 4.7908e-02 5.6270e-02 5.7737e-02 6.0645e-02 6.3442e-02
6.3568e-02 6.4310e-02]
[ 40.796775 41.048867 41.051421 41.109264 41.225769 41.613762
41.725123 42.061481 42.149697 42.965027 43.847654 44.251369
45.38298 47.326665 48.307194 48.573287 49.247505 50.903593
51.645403 52.854103 53.291601 54.581997 63.869599 64.29999
68.726676 72.884078 75.385211 76.93789 88.439243 88.561239
89.887648 96.272374 123.167259 133.82782 140.950236 145.882135
146.161786 147.098006 154.801009 183.870374 185.070892 185.330992
187.860492 213.849907 228.032744 287.253654 294.337037 547.488298]
count 7398.000000
mean 8.514968
std 12.165794
min 0.000001
25% 3.933124
50% 7.435844
75% 10.920002
max 547.488298
Name: popularity, dtype: float64
- 再来看看数值型的声望值(popularity)
- 我们看到极差不小,最关键的是平均值和最大值的差距,非常大。Q3也只是10而已,方差也不小。再从箱型图可看出,越界的数非常多。
- 所以非常有必要进行异常值处理,再结合后来的回归效果可知,处理后比不处理效果好很多,随后也是对他进行3sigma异常值处理。
runtime
print(np.sort(data.runtime)[7300:])
np.sort(data.runtime)[:50]
[172. 174. 174. 174. 174. 175. 175. 175. 175. 175. 176. 176. 177. 177.
178. 178. 178. 178. 178. 178. 178. 179. 179. 179. 179. 180. 180. 180.
180. 180. 181. 181. 181. 181. 182. 183. 183. 183. 183. 183. 184. 185.
185. 186. 186. 186. 187. 187. 188. 188. 188. 189. 189. 189. 189. 190.
191. 191. 192. 192. 193. 193. 193. 194. 195. 195. 197. 197. 199. 199.
200. 201. 201. 202. 206. 207. 207. 208. 208. 212. 213. 214. 216. 219.
220. 224. 225. 238. 248. 254. 320. 338. nan nan nan nan nan nan]
array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 11., 25., 26., 38., 40.,
40., 40., 42., 44., 46., 53., 57., 59., 60., 62., 63., 63., 64.,
65., 65., 65., 66., 66., 66., 66., 67., 68., 68., 69.])
- runtime也是进行了同样的操作
runtime和popularity做了归一化处理,为了和离散型的程度相似。
revenue做了对数平滑处理,缩小数据,减小数值的差距,特征值都是一些小于的数,目标变量不宜取得这么大。
2)离散型
- 离散型有genres、original_language、production_companies、production_countries、spoken_languages、Keywords、cast、crew
- 他们都是一些json格式的数据,我们要在里面提取出有代表性的数据,例如id\name等等
- 下面的函数是用来处理json格式的,把它变回字典的形式,我们就可以提取出里面的信息了
def jsonChange(strs,key): #改进后的处理json格式的方法,采用eval函数
'''
函数说明:处理json文本,使其可加载(loads)出来
:param strs: json文本
:param key: 要提取信息的字典中的key
:return: list或者NaT--存放目标信息的list
'''
if type(strs)==float:
return []
strs = eval(strs)
if strs:
return list(i[key] for i in strs)
else:
return []
np.array(data.genres.apply(jsonChange,key='id'))
array([list([35]), list([35, 18, 10751, 10749]), list([18]), ...,
list([18]), list([27, 53]), list([18])], dtype=object)
- 这样就可以把里面的类型的ID提取出来了
data['new_genres'] =data.genres.apply(jsonChange,key='id')
data['countries'] = data.production_countries.apply(jsonChange, key='iso_3166_1')
data['companies'] = data.production_companies.apply(jsonChange, key='id')
data['languages'] = data.spoken_languages.apply(jsonChange, key='iso_639_1')
data['cast_name'] = data.cast.apply(jsonChange,key='id')
data['keywords'] =data.Keywords.apply(jsonChange,key='id')
#data['crew'] = data.crew.apply(jsonChange,key='id')
- 创建新的列存放处理后的数据
- crew和cast这两列信息非常多,都是选取名字对应的ID来作为特征
二、特征工程
1、特征选择
- 选择离散型的genres、original_language、production_companies、production_countries、spoken_languages、Keywords、cast、crew
还有数值型型的popularity、runtime
电影时长和revenue的关系
plt.scatter(data.runtime,data.revenue.apply(np.log1p))
plt.title('对数化')
plt.show()
plt.scatter(data.runtime,data.revenue)
plt.title('没对数化')
plt.show()
欢迎度和revenue
plt.scatter(data.popularity,data.revenue.apply(np.log))
plt.title('对数化')
plt.show()
plt.scatter(data.popularity,data.revenue)
plt.title('没对数化')
plt.show()
从上面的数据可以看出,revenue取对数后和特征相关性变高
genres种类及频数
id =[]
for i in data.genres:
if type(i)!=float:
for j in eval(i):
id.append(j['id'])
print(np.array(pd.DataFrame({'a':id}).a.value_counts()))
np.array(set(id))
[3676 2605 1869 1735 1435 1116 1084 744 735 675 628 550 382 295
267 243 221 117 84 1]
array({10752, 12, 14, 16, 10769, 18, 10770, 27, 28, 10402, 35, 36, 37, 9648, 53, 80, 99, 878, 10749, 10751},
dtype=object)
原始语言及频数
original_language=[]
for i in data.original_language:
original_language.append(i)
print(np.array(pd.DataFrame({'a':original_language}).a.value_counts()))
np.array(set(original_language))
[6351 199 118 109 95 90 56 49 49 46 41 31 20 17
13 12 11 9 9 9 6 5 5 5 5 4 4 3
3 3 3 3 2 2 2 1 1 1 1 1 1 1
1 1]
array({'ka', 'he', 'ml', 'ur', 'vi', 'bm', 'ta', 'cs', 'tr', 'nb', 'ro', 'sr', 'is', 'th', 'te', 'nl', 'xx', 'ca', 'mr', 'it', 'de', 'af', 'cn', 'hi', 'ko', 'ja', 'zh', 'hu', 'en', 'ar', 'da', 'no', 'fr', 'pt', 'kn', 'bn', 'id', 'pl', 'el', 'ru', 'fa', 'fi', 'es', 'sv'},
dtype=object)
电影语言及频数
spoken_languages=[]
for i in data.spoken_languages:
if type(i)!=float:
for j in eval(i):
spoken_languages.append(j['iso_639_1'])
print(np.array(pd.DataFrame({'a':spoken_languages}).a.value_counts()))
np.array(set(spoken_languages))
[6449 711 559 419 364 321 204 163 160 102 100 89 78 69
66 64 57 55 52 50 41 36 32 27 23 23 23 23
20 19 17 17 17 15 15 12 11 9 9 9 8 8
8 7 7 7 6 5 5 5 5 5 4 4 4 4
4 4 4 3 3 3 3 3 3 3 2 2 2 2
2 2 2 2 2 2 2 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1]
array({'mt', 'mk', 'wo', 'to', 'th', 'ce', 'sa', 'uk', 'mr', 'kk', 'zh', 'si', 'hu', 'en', 'qu', 'ga', 'da', 'sw', 'hr', 'bs', 'hy', 'ru', 'fa', 'fi', 'eo', 'ka', 'he', 'pa', 'ml', 'cs', 'ro', 'gd', 'zu', 'af', 'cn', 'hi', 'iu', 'tl', 'ny', 'fr', 'pt', 'ps', 'nv', 'kn', 'id', 'pl', 'ne', 'sv', 'so', 'bm', 'ta', 'tr', 'km', 'te', 'nl', 'eu', 'xx', 'gl', 'ca', 'ms', 'la', 'sq', 'my', 'xh', 'no', 'sh', 'el', 'bg', 'et', 'lo', 'yi', 'am', 'as', 'ur', 'gn', 'ty', 'st', 'vi', 'kw', 'jv', 'ku', 'sr', 'is', 'bo', 'ln', 'it', 'br', 'de', 'ko', 'mi', 'ja', 'ar', 'mn', 'bn', 'sk', 'cy', 'es', 'gu'},
dtype=object)
- 还有keyword,crew等被使用的数据、上映日期也跟revenue有着不少的关系、就把年份和月份都做成了单独一列,作为回归的特征
在该部分(预测revenue)没用上title、tagline、original_title文本特征,后面附加的分类会用到。
- 从上面数据可看出,各种选取的特征都是挺明显的
2、特征转换
- 建立一个新的数据框架,将需要的数据都放进去
- 例如:
onehotDataFrame =pd.DataFrame()
genre_kinds =[10752, 12, 14, 16, 10769, 18, 10770, 27, 28, 10402,
35, 36, 37, 9648, 53, 80, 99, 878, 10749, 10751]
for i in genre_kinds:
onehotDataFrame['genre_'+str(i)]= data.new_genres.apply(lambda col:1 if i in col else 0)
- 创建二进制列,每个类型都作为一个列
- 有些特征有几百几千种值,也不可能全部在把他们加进去,后面将会谈到
三、模型构建
- 刚开始用普通最小二乘法回归,因特征过多,容易过拟合,后改用岭回归削弱低权重的特征
- 把特征工程得到的新的数据集将每个例子放进二维数组,也不是全部特征都适合加进去,虽然岭回归可以削弱一些特征的权重,但依然是对结果有影响
1、过拟合处理
下面来确定放进新数据集的特征的数目(这是个人的方法,当然可以用迭代得到正确率来决定,但有时候没能求出正确率,就可以用这种方法来评估)
- 在基于普通最小二乘法算法下,对所有特征进行拟合,就是说所有特征都加进去,拟合,比如公司种类有3千多间,全部都变成二进制列加进去,有该公司则天1,无则填0;
所以最后回归方程的X值有三千多个。
这样的做法在训练集上是可观的,训练得分0.92,但放在测试集时就不是这回事了,最终预测出来的又上千个0,几百个无穷大,这就是自变量太多造成的,就是特征太多,每个特征都同等重要。
- 于是想要找到最适合的特征加入量,拿电影出产公司举例子,公司数目3000多。
用for循环来改变公司数目range(30,600,20),得到训练集分数,和测试集revenue的无穷大数目,0值数目,随加进去拟合的公司数目的变化,如下图所示
横坐标乘以30就是公司的数目了,可以看出,训练集分数没什么变化,其实是在逐渐上升的,因为特征值数目越多分数就会越高,只不过纵轴值太大,很看出一个小于1的百分数的变化
然后在10之前,要不是出现无穷大就是出现0值,这都是不正常的,在10之后也就是公司数目为300左右,0值和无穷大值就没有了,座椅我们应该选取这个公司数目加加人作为特征。
- 接下来我们要确定岭回归的lambda值,就是惩罚值,图中列出训练集分数和测试集最大值(发现最大值可表示着整体的水平)(因为要让数据不走向无穷大)
- 可以看出在lambda取14左右的时候,分数来开始下降,最大值也开始下降,表明预测出来的revenue开始整体下降了
- 这个是各个特征的系数随lambda值的变化图,也可以看出从14,15左右开始变稳定
- 我们要求训练集分数的同时也要兼顾测试集最大值(因为发现训练出来的revenue是整体偏大的,需要让他们下降)
- 上边的是训练集的revenue,下边的是测试集的revenue,可以看出确实是整体变高了
最后取14作为lambda值。
2、评估模型
采用交叉验证,用一下方法随机分离测试集和训练集
test_size = 0.25
data_class_list = list(zip(x, y)) #zip压缩合并,将数据与标签对应压缩
random.shuffle(data_class_list) #将data_class_list乱序
index = int(len(data_class_list) * test_size) + 1 #训练集和测试集切分的索引值
train_list = data_class_list[index:] #训练集
test_list = data_class_list[:index] #测试集
train_data_list, train_class_list = zip(*train_list) #训练集解压缩
test_data_list, test_class_list = zip(*test_list) #测试集解压缩
同sklearn的
sklearn.model_selection.train_test_split
然后再在外围加个for循环就可以进行多次交叉验证,再求个平均值就可以很好地评估了该模型的正确率,可以对测试集的预测结果有个底。
我们也可以通过这个评估模型来训练本身,因为我们加入的特征数量都是自己定的,而定多少就会影响到最终结果,在3.1过拟合分析也讲到确定特征数量的问题,那个是个人方法,我们一般采用比较好的参考值(即正确率)来作为评估值,逼不得已才需要想出、观察出其他的参考值。如下面的代码所示,featureNum就是我们需要加入的特征数量。
#岭回归
for featureNumin range(320, 420, 5): # 调试用,调试加入==特征的数量、岭回归的lamada值==等
forecast = mySklearn(preprocess,featureNum)
'''岭回归
r2, RMSE ,coef = forecast.ridge()
r2list.append(r2)
RMSElist.append(RMSE)
'''
'''贝叶斯'''
forecast.dmTree()
在模型里面收集正确率,然后绘图,如下:
找出正确率高的区间,然后再细分,如此类推,最终找到满意的参数即可。
附加
前面提到的title、tagline、original_title文本特征,现在用于作分类,用朴素贝叶斯来处理文本特征,然后进行分类。此处的分类是电影类型的分类,一部电影可以属于多种类型,所以这个问题属于多标签分类,也就是multi—label .
学习贝叶斯原理的可以移步到这里
其中的是不调库实现前面数据处理的部分,包括句子分词,词汇量排序,去停用词(a, the, what, how 这种,什么文章都有的这种,不能够清晰地分辨一篇文章的类型),组成特征词,构成词汇稀疏矩阵,然后就OK了(1),如果依然不掉库做二分类的话,就是训练出贝叶斯词频,然后再作用于测试集就行。
但是我们的问题是多标签的分类,不掉库实现多标签的话较麻烦,所以就直接调库了,如下:
from sklearn.multiclass import OneVsRestClassifier
from sklearn.pipeline import Pipeline
def TextClassifier(train_feature_list, test_feature_list, train_class_list, test_class_list):
clf = OneVsRestClassifier(MultinomialNB())#多标签处理器
classifier = clf.fit(np.array(train_feature_list), train_class_list)#拟合
pre = clf.predict(np.array(test_feature_list))#预测
test_accuracy = classifier.score(test_feature_list, test_class_list)#得分
num = 0
denom =0
for i,j in zip(pre, test_class_list):#总体正确率:预测正确的标签数量/总标签数量
h = i+j#两个np.array相加,出现2的话,就表明预测正确
denom = denom + sum(j)
for k in h:
if 2 ==k:#出现2
num=num+1
single_accuracy = num/denom
num_0 =0
for i,j in zip(pre, test_class_list):#单标签正确率:凡是有一个类型预测正确,这部电影就算是预测正确了
h = i+j
for k in h:
if 2 ==k:
num_0=num_0+1
break
print("单标签正确率:", num_0 / len(pre))
return test_accuracy, pre, single_accuracy
这里的
单标签正确率:电影的一个类型预测正确,就属于预测正确了
总体正确率:一部电影往往有多个标签,比如总标签是4个,预测到的有2个,则正确率为0.5。
单计算title的话:
单计算tagline的话:
单计算original_title的话:
三个合并起来:
可见,团结就是力量,不过也不一定是越多越好。
具体实现就,参考源码吧。
git:https://github.com/linkunxin/filmRvenuePred
百度网盘:https://pan.baidu.com/s/1_rpzLeTFf14x6KoxwWY4Lw