Python大数据分析实战:豆瓣人的电影口味重吗?

在上一篇文章中,我们实战使用urllibBeautifulSoup抓取了关于豆瓣电影TOP250的非常丰富的信息,包括导演、编剧、演员、上映时间和地区、语言、别名、短评数、影评数、多少人想看、多少人看过等多达23个字段。

接下来,我们要做的就是对这些数据进行分析、挖掘,得到尽可能多的信息。毕竟有价值的不是数据,而是从数据中发掘而出的洞见。

一、数据清洗

我们先读取我们的数据,观察一下:

import pandas as pd
df = pd.read_csv('douban_top250.csv')
df.loc[0]

输出为:

image
df.loc[0, 'director']

输出为:

"['弗兰克·德拉邦特']"

通过观察我们迅速得知,这些数据并不能简单地直接使用,因为有些数据的格式还不符合我们的要求,比如:

  • 评分人数部分为字符串,且包含了汉字;
  • 多个字段的列表实际上被读取为字符串;
  • 百分比以字符串形式存储等
  • ……

因此我们要先对这些数据进行一番清洗,再开始做后续的分析。

# 评分人数:x人评价清洗为x,并调整为int类型
df['score_cnt'] = df['score_cnt'].map(lambda x: int(x[:-3]))
df.describe()['score_cnt']

# 将字符串改为列表
df_tmp = df[['director', 'writers', 'actors', 'types', 'dates', 'play_location', 'rating_per', 'betters', 'tags']]
df[['director', 'writers', 'actors', 'types', 'dates', 'play_location', 'rating_per', 'betters', 'tags']] = df_tmp.applymap(lambda x: eval(x))

# 上映年份由字符串转换为int类型
df['dates'] = df['dates'].map(lambda x: [int(i) for i in x])
df['year'] = df['dates'].map(lambda x: min(x))

# 五星比例/好评比例
df['five_star_rate'] = df['rating_per'].map(lambda x: float(x[0][:-1])/100)
df['favor_rate'] = df['rating_per'].map(lambda x: (float(x[0][:-1]) + float(x[1][:-1]))/100)

# 比百分之多少同类电影好
df['better_than'] = df['betters'].map(lambda x: sum([int(i.split('%')[0]) for i in x])/len(x))

好了,我们先简单清洗下,后续分析过程中有什么需要再额外添加。

可以看到,除了解决格式不符合要求的问题之外,我们还额外创建了一些字段,比如五星比例、好于多少同类电影等,这些后续都会为我们的分析提供帮助。

二、简单分析

1. 谁是豆瓣最佳导演?

我们先划定一个标准,那就是在TOP250中有不少于3部电影的导演,才可以参与最佳导演评选。

我们先看一下一共有多少位导演:

from functools import reduce

# 消灭空格
df['director'] = df['director'].map(lambda x: [i.strip() for i in x])

# reduce迭代获取所有导演的列表
director_list = reduce(lambda x, y: x + y, df.director)

print(len(director_list))

结果返回281,也就是说这250部影片有281位导演,存在联合执导的情况。那我们接着看一下影片数量大于3部的有哪些导演:

from collections import Counter

dire_counter = Counter(director_list)
dire_counter = sorted(dire_counter.items(), key=lambda x: x[1], reverse=True)
top_directors = list(filter(lambda x: x[1] >= 3, dire_counter))
print(top_directors)

输出为:

[('宫崎骏', 7),
 ('克里斯托弗·诺兰', 7),
 ('史蒂文·斯皮尔伯格', 6),
 ('王家卫', 5),
 ('李安', 4),
 ('大卫·芬奇', 4),
 ('詹姆斯·卡梅隆', 3),
 ('朱塞佩·托纳多雷', 3),
 ('刘镇伟', 3),
 ('弗朗西斯·福特·科波拉', 3),
 ('姜文', 3),
 ('彼得·杰克逊', 3),
 ('彼特·道格特', 3),
 ('昆汀·塔伦蒂诺', 3),
 ('理查德·林克莱特', 3),
 ('李·昂克里奇', 3),
 ('理查德·柯蒂斯', 3),
 ('吴宇森', 3),
 ('是枝裕和', 3)]

哇,大神宫崎骏和诺兰独一档,各有7部入榜;斯皮尔伯格、王家卫、李安、大卫芬奇分别以6、5、4、4的入榜数目分列3至6位,剩下的13位均有3部入榜电影,其中有刘镇伟、姜文、吴宇森三位中国导演。

但这样我们无法确定谁才是最佳导演,接下来我们用两种方法对他们进行排序:

  • 以平均豆瓣评分来进行排序
  • 以平均榜单位置进行排序
from collections import defaultdict

top_dire_score = defaultdict(list)
top_dire_ind = defaultdict(list)
for name, cnt in top_directors:
    for index, row in df.iterrows():
        if name in row['director']:
            top_dire_score[name].append(row['score'])
            top_dire_ind[name].append(row['top_no'])
print(top_dire_score)
print(top_dire_ind)

看下结果:

# 评分
defaultdict(list,
            {'宫崎骏': [9.3, 9.1, 9.0, 8.9, 8.8, 8.8, 8.5],
             '克里斯托弗·诺兰': [9.3, 9.2, 9.1, 8.8, 8.6, 8.6, 8.9],
             '史蒂文·斯皮尔伯格': [9.5, 8.9, 8.8, 8.7, 8.6, 8.5],
             '王家卫': [8.8, 8.7, 8.6, 8.6, 8.5],
             '李安': [9.0, 9.1, 8.7, 8.8],
             '大卫·芬奇': [9.0, 8.8, 8.8, 8.7],
             '詹姆斯·卡梅隆': [9.3, 8.6, 8.6],
             '朱塞佩·托纳多雷': [9.2, 9.1, 8.8],
             '刘镇伟': [9.2, 8.9, 8.7],
             '弗朗西斯·福特·科波拉': [9.2, 9.1, 8.8],
             '姜文': [9.2, 8.7, 8.8],
             '彼得·杰克逊': [9.1, 9.0, 8.9],
             '彼特·道格特': [8.9, 8.6, 8.7],
             '昆汀·塔伦蒂诺': [8.8, 8.6, 8.5],
             '理查德·林克莱特': [8.7, 8.8, 8.8],
             '李·昂克里奇': [8.6, 9.0, 8.8],
             '理查德·柯蒂斯': [8.5, 8.7, 8.6],
             '吴宇森': [8.6, 8.7, 8.4],
             '是枝裕和': [9.1, 8.7, 8.8]})

# 榜单位置           
defaultdict(list,
            {'宫崎骏': [7, 19, 36, 43, 88, 112, 191],
             '克里斯托弗·诺兰': [9, 18, 27, 65, 137, 145, 192],
             '史蒂文·斯皮尔伯格': [8, 70, 83, 118, 171, 222],
             '王家卫': [80, 91, 132, 159, 181],
             '李安': [30, 54, 94, 131],
             '大卫·芬奇': [35, 62, 64, 104],
             '詹姆斯·卡梅隆': [6, 96, 210],
             '朱塞佩·托纳多雷': [13, 29, 66],
             '刘镇伟': [15, 38, 101],
             '弗朗西斯·福特·科波拉': [17, 50, 155],
             '姜文': [32, 69, 87],
             '彼得·杰克逊': [33, 51, 52],
             '彼特·道格特': [37, 127, 173],
             '昆汀·塔伦蒂诺': [73, 174, 218],
             '理查德·林克莱特': [105, 113, 217],
             '李·昂克里奇': [127, 129, 158],
             '理查德·柯蒂斯': [140, 154, 231],
             '吴宇森': [141, 151, 223],
             '是枝裕和': [153, 206, 208]})

接下来我们求一下均值,并将入榜电影数作为一个权重加进去:

from math import log2
from math import sqrt
rank_score = []
rank_ind = []

for name, scores in top_dire_score.items():
    rank_score.append([name, sum(scores) / len(scores) * sqrt(log2(len(scores)))])

for name, indexes in top_dire_ind.items():
    rank_ind.append([name, sum(indexes) / sqrt(log2(len(scores))) /len(indexes)])
    
rank_score = sorted(rank_score, key=lambda x: x[1], reverse=True)
rank_ind = sorted(rank_ind, key=lambda x: x[1])
print(rank_score[:10])
print(rank_ind[:10])

输出为:

# 加权得分榜
[['克里斯托弗·诺兰', 14.959967098817579],
 ['宫崎骏', 14.936031151459467],
 ['史蒂文·斯皮尔伯格', 14.202073072976324],
 ['王家卫', 13.165523290477429],
 ['李安', 12.586500705120548],
 ['大卫·芬奇', 12.480434687942564],
 ['朱塞佩·托纳多雷', 11.372541542166006],
 ['弗朗西斯·福特·科波拉', 11.372541542166006],
 ['彼得·杰克逊', 11.330576444224434],
 ['刘镇伟', 11.24664624834129]]
 
# 加权位置榜
[['朱塞佩·托纳多雷', 28.59519121510834],
 ['彼得·杰克逊', 36.008759307914204],
 ['刘镇伟', 40.774624510432254],
 ['姜文', 49.776814337410805],
 ['大卫·芬奇', 52.6230949444702],
 ['宫崎骏', 56.282598582118],
 ['弗朗西斯·福特·科波拉', 58.77900416438936],
 ['李安', 61.36051448241997],
 ['克里斯托弗·诺兰', 67.28947774031447],
 ['詹姆斯·卡梅隆', 82.60833017697963]]

可以看到,在我们的加权得分算法下,诺兰以微弱优势胜出,夺得豆瓣最佳导演奖。然而在我们的加权榜单位置算法中,朱塞佩·托纳多雷的电影平均能获得更靠前的豆瓣排名,夺得桂冠,而宫崎骏和诺兰的排名则分列6、9位。

具体哪个排名更靠谱呢?我倾向于第一个,因为这里使用的是连续数据,而位置榜的数据是离散数据,本身有了一些信息损耗,榜首和末尾的数据差了249倍,但他们的实际表现并没有那么大的差距。同时这一数据容易受到离群值的影响,比如某位导演绝大多数电影排名都很靠前,但某一部偏偏特别靠后,这时他的加权位置得分就会较大(位置越靠后),那他的总排名就会向后排。

因此我个人倾向于由拍出了数部经典大片的诺兰获得豆瓣最佳导演金奖(说得好像真的有这个奖似的……)。

2. 谁是豆瓣最佳演员?

我们将上述代码中的列名调整下就可以得到演员的榜单,这一部分大家自己尝试,我来直接放出结果和作图代码。

# 入榜电影数排行
import matplotlib.pyplot as plt
names = [i[0] for i in top_actors]
cnt = [i[1] for i in top_actors]

fig, ax = plt.subplots(figsize=(16, 8))
plt.bar(names, cnt, color='c')
ax.set_xticklabels(labels=names, 
                   fontdict={
                       'verticalalignment': 'bottom', 
                       'y': 0.1, 
                       'fontsize': 18, 
                       'rotation': 'vertical'
                   })
plt.ylabel('上榜电影数', fontsize=20)
plt.title('豆瓣电影TOP250入榜最多演员情况', fontsize=24)
image
# 加权得分榜
names = [i[0] for i in rank_score]
score = [i[1] for i in rank_score]
fig, ax = plt.subplots(figsize=(16, 8))
plt.bar(names, score, color='c')
ax.set_xticklabels(labels=names, 
                   fontdict={
                       'verticalalignment': 'bottom', 
                       'y': 0.1, 
                       'fontsize': 18, 
                       'rotation': 'vertical'
                   })
plt.ylim([10,16])
plt.ylabel('加权得分', fontsize=20)
plt.title('豆瓣电影TOP250演员加权得分榜', fontsize=24)
image

好,恭喜“哥哥”张国荣喜提豆瓣最佳演员奖!

3、TOP250分数及评分人数分布

df.score.hist(color='green')
plt.title('豆瓣电影TOP250评分分布')
image

9分以上的比例不算很高,在8.7、8.8附近有较多电影集中。

df.score_cnt.hist(bins=20, color='green')
plt.title('豆瓣电影TOP250评分人数分布')
image

大多数电影的评分人数在40万以下。

4、哪些类型的电影更受豆瓣用户欢迎?

# 调整数据
type_list = reduce(lambda x, y: x + y, df.types)
type_counter = Counter(type_list)
type_counter = sorted(type_counter.items(), key=lambda x: x[1], reverse=True)
types = [i[0] for i in type_counter]
cnts = [i[1] for i in type_counter]

# 作图
fig, ax = plt.subplots(figsize=(16, 8))
ax.bar(types[1:], cnts[1:], color='c')
ax.set_xticklabels(
    labels = types[1:],
    fontdict={
        'rotation': 'vertical',
        'fontsize': 18
    })
ax.set_title('豆瓣电影TOP250分类电影分布', fontsize=24)
image

这里我们去除了最多的“剧情”类电影,因为这个分类实在是太多了,大部分的电影都是有剧情的……这两个字不能明显地告诉我们这个电影到底是讲哪一类的内容,所以我们选择剔除掉它。

可以看到爱情、喜剧、犯罪、冒险四类是豆瓣用户最喜欢的分类。

这里提出一个问题,上边的数据,能说明我上边的论述可以看到爱情、喜剧、犯罪、冒险四类是豆瓣用户最喜欢的分类吗?这个问题我暂且不答,留给大家思考。有想法的可以在下方留言沟通。

5、哪些变量最相关?

df.corr()

我们可以通过.corr()方法看一下各变量之间的相关系数,但是看一下下边这张表,一堆数字,非常不直观。

image

我们来做个热力图:

import seaborn as sns
plt.figure(figsize=(16,10))
sns.heatmap(df.corr(), cmap='coolwarm')
plt.show()

这样就方便多了,我们可以快速定位到相关性强的那些变量。首先,对角线的相关性都为1,这个可以忽略,因为同样的数据之间的相关性始终为1;然后我们注意到,五星比例和好评率与评分之间有非常强的相关性,这个也符合预期,五星比例、好评比例越高,分数自然更高;然后就是榜单排名和分数等变量之间的负相关,这个也正常,因为排名数字越低代表排名越靠前,分数高是符合预期的;然后就是评分人数和已看人数、短评、影评数量等之间的正相关,这个也是符合我们预期的。看起来这里我们发现不了什么,因为这些都是符合我们的经验的。未来如果想要拿这些数据训练模型,这些高度相关的特征可能需要通过主成分分析等方法进行降维,因为他们包含的信息很可能是一致的。

image

6、评分人数和已看人数之间什么关系?

前边我们已经知道了,它们两个高度正相关,我们现在拿这两个变量做一个简单的线性回归,用评分人数推测已看人数。

from sklearn.linear_model import LinearRegression
from sklearn.cross_validation import train_test_split

lr = LinearRegression(fit_intercept=True)
X = df['score_cnt'].values
y = df['had_seen'].values
X.shape = (len(X), 1)

# 将数据拆分为训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y)

# 训练模型
lr.fit(X_train, y_train)

# 预测
y_pred = lr.predict(X_test)

# 画图
plt.scatter(X_test, y_test, color='green', marker='+')
plt.plot(X_test, y_pred, '-', color='orange')
plt.show()

可以看到,我们的预测和实际情况非常接近,基本上实际表现的那些点都围绕着我们预测的直线上下波动。

image

看下我们预测的直线:

print(lr.coef_, lr.intercept_)

也就是说,假设y代表已看人数,X代表评分人数,那么我们预测他们之间的关系为y = 1.33 × X - 1726

[1.32705913] -1726.1045794936945

7. 哪些年头的电影更受豆瓣人喜爱?

这种分析,使用直方图最方便。

sns.set()
df.year.hist(bins=20)

可以看到,当代的电影还是绝对主力,毕竟老旧的电影在视觉效果上会差很多,只有极少数非常经典、优秀的剧本才可以超越视觉。

image

8. 豆瓣人喜欢的电影有哪些标签?

from functools import reduce
from wordcloud import WordCloud

# 生成数据,每个词之间以空格分离
tags = reduce(lambda x, y: x + y, df.tags)
result = ' '.join(tags)

# 设置词云属性,注意要指定中文字体
wc = WordCloud(
    font_path='msyh.ttf',     #字体
    background_color='white',   #背景颜色
    width=1000,
    height=500,
    max_font_size=100,            #字体大小
    min_font_size=10,
    max_words=100
)

# 生成词云对象
wc.generate(result)

# 展示词云
plt.figure('豆瓣电影标签', figsize=(16,8))
plt.imshow(wc)
plt.axis('off')
plt.show()
image

可以看到,在豆瓣人喜欢的电影中,有那么几类格外突出,比如美国、经典、剧情、爱情、人性、犯罪、温情、悬疑等。看起来豆瓣人喜欢的电影既不小清新,也不重口味,网民们给豆瓣人贴的标签有些片面了哦。

好,我们今天演示了如何简单地探索数据并进行展示,后续我们会尝试更加丰富的数据分析手段。

你可能感兴趣的:(Python大数据分析实战:豆瓣人的电影口味重吗?)