目标:通过编写Python程序自动抓取豆瓣电影Top 250的数据,之后使用可视化技术对这些数据进行深入分析,最后将分析结果以直观的方式展示出来。
意义:
目标网站:豆瓣电影 Top 250
注意事项:如个人实现时发现自己写的代码语法错误,请调试至正确后再运行,避免在短时间内大量进行爬取,给服务器带来负担(状态码会变成403,只有状态码等于200时,爬虫才能进行)
检查和网络如下图(检查是左边第一个图标)
import osimport reimport pandas as pdimport requestsfrom bs4 import BeautifulSoupimport csv#设置请求头,模拟浏览器行为,避免被服务器拒绝#网页中右键检查,点击顶上的网络(附图在上方),刷新之后出现的内容随便点击一样,拖动出现变化的页面到最下方就能看见自己的User-Agentheaders={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"}#初始化数据列表title=[]year=[]score=[]type=[]review=[]director=[]#循环遍历所有页面for star_num in range(0,250,25):#总共250部电影,每页25部电影 response=requests.get(f"https://movie.douban.com/top250?start={ star_num}",headers=headers)#f-string,格式化字符串字面量,允许在字符串中嵌入表达式 if response.status_code==200:#响应体正常 print(f'Page { star_num // 25 + 1} Ok') else: print(response.status_code)#返回状态码,查看具体异常原因 html=response.text soup=BeautifulSoup(html,'html.parser') #找到所有电影的条目,一个条目对应一部电影 #右键,检查,检查,点击自己想要获取的页面元素,可以得到对应的html结构。以下提取部分根据网页对应结构编写,不止一种方法。 item=soup.findAll('div',class_='item')#与python中class区分 for movie in item: # 提取标题 title.append(movie.find('span',class_='title').text) #提取年份 year.append(re.findall('\d+',movie.find('p').text)[0]) #提取评分 score.append(movie.find('span',class_='rating_num').text) #提取类型 type.append(movie.find('div',class_='bd').find('p').text.split('/')[-1].split(';')[-1].split()) #提取评论数 review.append(re.findall('\d+',movie.find('div',class_='star').text)[-1]) #提取导演 director.append(movie.find('div',class_='bd').find('p').text.split('/')[0].split(':')[1].split(';')[0].split('\xa0')[0])print('title:',title)#查看获取元素是否是自己想要的元素print('year:',year)print('score:',score)print('type:',type)print('review:',review)print('director',director)#构建输出路径out_path='./douban250.csv'#创建电影信息字典film_dict={ 'title':title, 'year':year, 'score':score, 'type':type, 'review':review, 'director':director}film_250=pd.DataFrame(film_dict)if os.path.exists(out_path):#如果该地址存在文件就读取文件,否则在该地址创建csv文件 pd.read_csv(out_path)else: film_250.to_csv(out_path,index=False,encoding='utf-8')#不包含原索引print(film_250.dtypes)
成功啦!然后你就获得了豆瓣前250部电影信息的csv表格,如下:
在这段代码中,使用了一些需要额外安装的Python包,可在python编辑器终端中下载。以下是这些包及其对应的pip安装命令:
pandas - 用于数据分析和操作表格数据。
pip install pandas
numpy - 用于科学计算,提供了大量的数学函数和数组操作功能。
pip install numpy
matplotlib - 用于绘图和可视化数据。
pip install matplotlib
seaborn - 基于matplotlib的高级接口,用于更美观的数据可视化。
pip install seaborn
adjustText - 用于调整matplotlib图形中的文本标签位置,避免重叠。
pip install adjustText
scikit-learn - 用于机器学习任务,如聚类和降维。
pip install scikit-learn
wordcloud - 用于生成词云图。
pip install wordcloud
Pillow (PIL Fork) - 用于图像处理。
pip install pillow
simhei.ttf
字体文件也需要准备,通常系统字体库中已有,如没有可以从互联网上下载适合的中文字体文件。
在原项目文件夹下新开一个python文件
#导入数据#导入常用库import pandas as pdimport numpy as npimport matplotlib.pyplot as pltfrom adjustText import adjust_textimport seaborn as snsplt.style.use('ggplot')from datetime import datetime#读取数据df=pd.read_csv('./douban250.csv')print(df.head())print(df.info())
得到如下数据
year列数据看不出类型,进行下一步
#查看索引,数据类型,非空数值数量,内存使用情况print(df.info())
结果如下:
print(df.describe())
设置全局字体为支持中文的字体,并允许负号正常显示
plt.style.use('ggplot')plt.rcParams['font.sans-serif']=['SimHei']#使用黑体显示中文plt.rcParams['axes.unicode_minus']=False#正常显示负号
# # 历年平均分折线图# avg_score=df.groupby('year')['score'].mean().reset_index()# plt.figure(figsize=(10,5))# plt.plot(avg_score['year'],avg_score['score'],marker='o',linestyle='-')#折线图,用圆形标注# plt.title('历年平均分折线图')# plt.xlabel('年份')# plt.ylabel('平均分')# #显示每个点的值# for year,score in zip(avg_score['year'],avg_score['score']):#将年份和对应的评分配对# plt.text(year,score,f'{ score:.1f}',ha='center',va='bottom')#确定坐标点位置并水平垂直对齐,确保文本不会覆盖数据点# #确保x轴上每一个年份都有刻度标记,并将文本旋转90度以防止重叠# plt.xticks(avg_score['year'],rotation=90)# # 显示网格线# plt.grid(True)# plt.show()
plt.figure(figsize=(15,10))plt.hist(df['score'],bins=10,color='blue')plt.title('不同评分段电影数量直方图')plt.xlabel('评分')plt.ylabel('电影数量')plt.show()
结果如图,大部分的电影区间确实在8.7-9.3之间
前文提到,评论数数据标准差大,现在我们绘制评论数和平分之间的散点图,探寻它们之间是否存在某种规律。
#2评分和评论量间的相关性#使用皮尔逊相关系数来衡量评分和评论数之间的线性关系,1表示完全正相关,-1 表示完全负相关,0 表示没有线性关系。correlation=df['score'].corr(df['review'])#返回单一相关系数,如果有多个数值变量的话可以绘制相关矩阵,这个例子中数值变量不多,所以只返回单一相关系数print(f'评分与评论数之间的皮尔逊相关系数:{ correlation:.2f}')#f-string可在字符串中嵌入表达式并输出表达式结果#绘制散点图plt.figure(figsize=(15,10))sns.scatterplot(x='score',y='review',data=df)plt.title('评分与评论的散点图')plt.xlabel('评分')plt.ylabel('评论数')plt.show()
0.31 的相关系数确实属于弱相关的范畴。这表明评分和评论数之间存在一定的正向关系,但这种关系不是特别强烈。换句话说,虽然评分较高的电影往往也可能会有更多的评论,但这并不是一个确定的趋势;有很多例外情况,且其它因素也可能影响评论数量。
从散点图中也可以看出,评分高的作品也可能评论数并不高。并且图中数据可以再一次看出,评论数数据比较分散。
类型列值较复杂,有多个不同值在一个列表中,不同类型电影之间可能有潜在模式及关系,所以对电影类型进行聚类分析
#3.根据type聚类分析df_copy=df.copy()'''将类型列表转化为独热编码,使每种类型对应一个独立的二进制特征使用explode()将type中的列表内容拆分为多行使用 str.get_dummies() 将每个唯一的类型转换成一列,并用0或1表示该类型是否存在特定行中(独热编码)使用 groupby(level=0) 按照原来的索引级别进行分组,并通过 .sum() 对每组内的值求和。这样做可以合并同一原始行(即相同的索引)的所有独热编码结果,得到最终的汇总表'''type_dummies=df['type'].explode().str.get_dummies().groupby(level=0).sum()#独热编码#将编码后的类型数据与原始df合并df_encoded=pd.concat([df.drop('type',axis=1),type_dummies],axis=1)#axis=1,表示按列方向操作#使用k-Means算法,找到最佳的簇数量from sklearn.cluster import KMeansfrom sklearn.metrics import silhouette_score#初始化存储轮廓系数的列表,注意最后有个s,避免变量名冲突silhouette_scores=[]#轮廓系数,考虑簇内样本间的紧密度和不同簇之间的分离度,评估聚类效果,反映聚类质量'''轮廓系数接近 +1:表示样本很好地被分类到当前簇中,并且远离其他簇。接近 0:表示样本接近簇间的边界,可能被错误分类。接近 -1:表示样本可能被分配到了错误的簇。'''#尝试2到10个簇for k in range(2,11): #创建并训练模型 kmeans = KMeans(n_clusters=k, random_state=42)#创建KMeans实例,random_state=42,固定随机性,确保实验结果可以精确复现 clusters = kmeans.fit_predict(type_dummies)#返回每个样本所属的簇标签数组 sscore=silhouette_score(type_dummies,clusters)#计算轮廓系数,此处没有s silhouette_scores.append(sscore)#独热编码和簇的轮廓系数#找出轮廓系数最高的k值optimal_k=np.argmax(silhouette_scores)+2#返回最大值索引位置,从0开始,因为我们是从k=2开始的,所以+2print(f'基于轮廓系数的最佳k值为:{ optimal_k}')#应用选定k值进行聚类kmeans=KMeans(n_clusters=optimal_k,random_state=42)clusters=kmeans.fit_predict(type_dummies)#将聚类结果添加回dfdf_encoded['cluster']=clusters#输出每个簇中的电影数量print('每个簇中的电影数量:',df_encoded['cluster'].value_counts())#可视化聚类结果print(df_encoded.head())#指定保存路径save_path=('./fenlei.csv')#如果目录存在,删除文件,如果不存在,保存文件if os.path.exists(save_path): os.remove(save_path) print(f'已删除现有文件:{ save_path}')else: df_encoded.to_csv(save_path, index=False, encoding='utf-8')
结果如下:
应用方向:
电影推荐系统:
根据电影类型聚类的结果,可以构建推荐系统,为用户提供与其观看历史相匹配的高分电影建议,提高用户粘合度和依赖性。电影分类管理:
影院或流媒体平台可以根据电影类型的聚类结果优化电影分类和展示方式,提升用户体验。市场调研:
通过分析不同类型的电影受欢迎程度及其观众特征,可以帮助电影制作公司了解市场需求,指导新电影的开发。观众画像构建:
聚类分析可以帮助构建观众画像,了解不同观众群体的偏好和行为模式,从而制定更有针对性的推广策略。将聚类结果可视化,以便更直观的理解聚类结果
#使用PCA进行降维并可视化聚类结果from sklearn.decomposition import PCA#使用PCA进行降维pca=PCA(n_components=2)#降到2维reduced_data=pca.fit_transform(type_dummies)'''使用PCA进行降维,第一列(第一个主成分)代表原始数据中最大的信息量或变化量。每一个样本在这个方向上的投影值就构成了 reduced_data[:, 0] 中的元素第二个主成分则捕捉到了与第一个主成分正交方向上的最大方差。换句话说,它解释了在去除第一个主成分影响后,剩余数据中的最大变化量。'''#计算每个簇的中心centers=kmeans.cluster_centers_#获取每个簇的类型clusters_types=[]for cluster_id in range(optimal_k): cluster_df=df_encoded[df_encoded['cluster']==cluster_id] types_in_cluster=','.join(cluster_df['type'].explode().unique()) clusters_types.append(types_in_cluster)#创建散点图,切片[m:n]m行n列。[:,0]表示所有行,第一列plt.figure(figsize=(15,10))scatter=plt.scatter(reduced_data[:,0],reduced_data[:,1],c=clusters,cmap='viridis')#用cluster决定数据点颜色#标记每个簇的中心并添加类型标签texts=[]for i,center in enumerate(centers):#遍历元素及其索引 text=plt.text(center[0],center[1],f'{ i}簇\n类型:{ clusters_types[i]}',fontsize=8,ha='right')#ha设置文本右对齐 texts.append(text)#自动调整文本位置以避免重叠adjust_text(texts,arrowprops=dict(arrowstyle='->',color='red'))#获取图例legend1=plt.legend(*scatter.legend_elements(),title='簇')#*用来解包,将对象中元素单独传递给函数#获取当前Axes对象ax=plt.gca()#将图例添加到Axes对象中ax.add_artist(legend1)#设置标题和轴标签plt.title('电影类型聚类分析PCA降维散点图')plt.xlabel('主成分1')plt.ylabel('主成分2')plt.show()
从图中可以看出,电影的分类并不像我们想的那么简单,并不是类型相同就分作一类。如0簇主要包含喜剧、家庭、剧情等类型的电影;3簇主要包含犯罪、剧情等类型的电影。就算类型不同,电影间在某种维度上也可能有紧密的关联性。找出它们之间的关联,并据此分类,可能比单一的类型或主观臆测的分类更准确。
不同类型的电影在PCA降维后的空间中被分成了不同的簇,说明聚类算法能够有效地将相似类型的电影聚集在一起。
例如,喜剧和家庭类型的电影集中在右上角,而犯罪和剧情类型的电影集中在左下角。
哪怕不是同一个簇,在空间中更靠近的电影相关性更强,流媒体平台可据此推荐。
图中有一些单独的数据点,可能代表异常值或特殊类型的电影,这些电影没有被归入任何簇中。
4.4中可以看出,某一个簇的电影数量占比很高,为了探究这个簇可能是什么类型或什么类型组合,我们绘制类型词云图
import matplotlib.pyplot as pltfrom wordcloud import WordCloud,ImageColorGeneratorfrom PIL import Image#合并所有电影类型为一个长字符串,并删除单引号all_types=''.join(df['type'].explode()).replace("'","")#感兴趣的小伙伴们可以自行给词云图设置形状,代码比较复杂,我就不做了(鬼脸),这里采用最普通的方法绘制#生成词云wordcloud=WordCloud( width=800, height=400, background_color='white', colormap='tab20c', max_words=100, font_path='simhei.ttf'#指定字体文件路径为微软雅黑).generate(all_types)#显示词云图plt.figure(figsize=(12,8))plt.imshow(wordcloud,interpolation='bilinear')#将生成的词云图像以双线性插值的方式显示。双线性插值会在图像缩放时提供平滑的效果,使得图像看起来更加柔和和清晰。plt.axis('off')#关闭坐标轴plt.title('豆瓣排名前250电影类型词云图',fontsize=15)plt.show()
分析结论:
常见类型识别:
词云图中较大的字体代表出现频率较高的电影类型。例如,如果“剧情”、“喜剧”、“冒险”等类型出现频率较高,则表明这些类型在豆瓣前250电影中非常普遍。反映观众偏好及市场趋势。类型多样性:
观察词云图中各类型的分布情况,可以判断豆瓣前250电影的类型多样性。如果词云图中有多个明显突出的类型,说明电影类型较为多样化;反之,则可能某些类型占主导地位。探究最容易拍摄出受欢迎电影的导演,绘制导演玫瑰图
#4.导演玫瑰图from collections import Counter#统计每个导演的作品数量director_counts=Counter(df['director'])#取前10个导演top_directors=director_counts.most_common(10)#提取导演名称和作品数量directors,counts=zip(*top_directors)#*用来解包,将对象中元素单独传递给函数#创建极坐标系fig,ax=plt.subplots(figsize=(10,12),subplot_kw=dict(polar=True))#figsize先宽后高,可据此调整#计算角度,不包括停止值2π,避免起点和终点重叠angles=np.linspace(0,2*np.pi,len(directors),endpoint=False).tolist()#生成一个长度为directors长度的从0到2π的等间距角度列表#添加最后一个角度完成封闭环形图angles+=angles[:1]#切片获取列表中第一个结果counts+=counts[:1]#绘制玫瑰图ax.fill(angles,counts,color='skyblue',alpha=0.7)#alpha设置透明度,0表示完全透明ax.set_xticks(angles[:-1])ax.set_xticklabels(directors)#设置标题,并增加标题与图表之间的距离plt.title('豆瓣前250电影导演作品数量玫瑰图',va='bottom',fontsize=15,pad=20)#pad参数增加距离plt.show()
玫瑰图面积表示相应导演的作品数量,面积越大表示该导演的作品越多。从图中可以看出,宫崎骏导演的作品占比最高,其次是克里斯托弗.诺兰导演和史蒂文,这表明这些导演在豆瓣前250电影中占据了重要的地位,受到了更多的关注和喜爱,一定程度上代表了市场风向。