import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import random
from sklearn.preprocessing import LabelEncoder
from sklearn.linear_model import LinearRegression
from sklearn.metrics import recall_score
from sklearn.metrics import accuracy_score
from sklearn.metrics import mean_squared_error
from sklearn.metrics import mean_absolute_error
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from plotly import tools
import plotly.express as px
from plotly.offline import init_notebook_mode, iplot, plot
import plotly.figure_factory as ff
import plotly.graph_objs as go
import ast
之后,加载并打印抓取到的电视剧的数据集。
df = pd.read_csv("tv_rating.csv")
df
运行后可以看到:
这个数据集一共包含 3600 条记录,并且有三列:分别是 电视剧的标题、评分和演员表。
尝试基于目前手上已经有的数据,建立模型,来预测一个电视剧可能的评分。简单的说,需要基于一个电视剧标题和演员表的信息来预估它的评分。
由于只有一个数据集,所以从原始数据集中拆 80% 出来训练模型,然后用剩下的 20% 的数据来测试模型的效果。
第一印象来说,从标题和演员表来预测电视剧的评分,多少是有点不够的。信息比较有限。但标题和演员表也不是完全没用,比如是否存在以下可能:
接下来进行数据清洗,依然是从缺失值检测开始。
df.isna().sum()
输出如下:
title 0
rating 0
stars 0
dtype: int64
可以看出,数据表不存在缺失值。
下一步处理的是评分数据,评分数据目前还是字符串,因为数据中还包含了分数的“分”字。为了方便后续的处理,首先将其转换为数字。转换为数字有两个步骤:
df.rating = df.rating.apply(lambda x: x.replace("分", ""))
df.rating = pd.to_numeric(df.rating)
df
运行后可以看到:
可以看到,分数已经被转换为数字了。
接下来就是演员表的处理,从之前粗略的分析中,演员表是本节分析的重点。所以首先将其处理成列表。
由于现在是用逗号连接的字符串,所以用 split 函数将其拆为列表。另外,通过观察数据发现演员表开头都有"主演"这两个字,同样需要去除。代码如下:
def handleStars(stars):
stars = stars.replace("主演", "")
result_arr = []
base_arr = stars.split(",")
return base_arr
df.stars = df.stars.apply(handleStars)
df
运行后可以看到:
可以看到,演员表字段已经被处理成列表了。但细心的读者会发现,有记录的演员表字段没有被分割成功。比如《实习女捕快》这个电视剧,演员之间用空格隔开。所以当尝试用逗号分割时,这个列表还是被当成整个字符串处理了。本质就是很多演员表的格式可能是不标准的。
为了找出没有被成功分割的记录,创建一个新的列:star_count 来记录每一行记录 stars 字段的长度。
df["star_count"] = df.stars.apply(len)
df
之后,筛选长度为 1 的记录查看:
df[df.star_count==1].stars
运行后可以看到:
一共有 348 条长度为 1 的列表,当然这个列表并不全是异常值,比如有的电视剧确实就只有一个演员。由于 pandas 输出太多数据时会被截断,用 [] 索引器来输出某一段的记录,比如第 100 条到第 150 条。
df[df.star_count==1][100:150]
运行后可以看到(部分记录):
通过分析,发现未能被成功分割的主要有以下几种情况:
def handleStarStep2(stars):
spliters = [' ', '/',',','、']
result_arr = []
for item in stars:
for spliter in spliters:
temp_arr = item.split(spliter)
for sub_item in temp_arr:
temp_item = sub_item.strip()
if temp_item not in result_arr:
result_arr.append(temp_item)
return result_arr
df.stars = df.stars.apply(handleStarStep2)
df
运行后可以看到:
之后,再次刷新 star_count 字段的值并查看。
df["star_count"] = df.stars.apply(len)
df[df.star_count ==1]
运行后可以看到:
可以看到,演员个数为 1 的记录从 348 减少为 81。从结果中看,绝大多数都确实只有一个演员的电视剧,而也存在着极少数仍未被分开的记录,因为中间完全没有分隔符。由于个数较少,不做进一步的处理。
基本的数据处理完毕后,尝试可视化分析。目前数据表中可直接用作特征的数值字段只有 star_count,以及按照之前说的,标题的长度也应该是一个值得参考的值。
首先看一下目标变量的分布:
df.rating.hist()
运行后可以看到:
数据集中的评分分布还是比较平均的。
接下来看一下 title_len 字段。
df["title_len"] = df.title.apply(len)
df.title_len.hist()
运行后可以看到:
可以看到,绝大多数电视剧的标题都是 4~5 个字。由于数据太过集中,所以 title_len 可能并不是一个特别好的特征。
最后,来看一下 star_count。
df.star_count.hist()
运行后可以看到:
可以看到 title_len、star_count 的数量都集中在 0~5 的范围内。
接下来分析一下这两个字段与 rating 字段的相关性,首先是标题长度。
df["title_len"] = df.title.apply(len)
px.scatter(df, x="title_len", y = "rating")
运行后可以看到:
从上图中可以看到,这次的数据量比较小。这也导致做分析可能很难得出一个比较完美的结果,另一方面,title_len 和 rating 从图中反映的相关性也比较弱。
再看一下 star_count 。
px.scatter(df, x="star_count", y="rating")
运行后可以看到:
从上图中可以看到,数据尤其是高分的数据都集中在 star_count 3 到 10 这个区间。虽然也反映不出 star_count 与 rating 存在明显的关系,但至少从概率来看,不在这个区间的评分不高的概率就偏大了,可以说存在一定的弱相关性。
因为这次的任务并没有特别明显的特征,所以将特征功能和模型放到一起来讲,因为需要一边测试模型的表现一边筛选特征。
在本节的开头就描述了这次任务的性质:
这个均方误差就是基准结果,换句话来说,模型对于测试集中电视剧评分的预测需要超过这个基准结果,模型才能算是有用的。
首先第一步,拆分训练和测试集。
df_train,df_test = train_test_split(df, test_size=0.2, random_state=42)
df_test
运行后可以看到:
现在测试集中包含 720 行记录。接下来随机出 720 个 [0,5] 的评分,计算随机结果与测试集评分的 MSE。
random_result = np.random.uniform(0.0,5.0, 720)
mean_squared_error(random_result, df_test["rating"])
输出如下:
4.260423686305926
接下来进入模型的训练,目前两个特征 title_len 和 star_count,相关性都不是很强。但仍可以先试试用它们训练一版模型。代码如下:
from xgboost import XGBRegressor
features = ["title_len","star_count"]
target = "rating"
xgb = XGBRegressor(n_estimators = 2000 , random_state = 0 , max_depth = 27)
df_train_features = df_train[features]
df_train_target = df_train[target]
xgb.fit(df_train_features, df_train_target)
df_test_feat = df_test[features]
df_test_result = xgb.predict(df_test_feat)
mean_squared_error(df_test_result, df_test[target])
输出如下:
2.2631812293604723
通过 title_len 和 star_count 训练的 XGBoost 模型,在测试集上获得了 MSE 2.26 的成绩,已经大幅超过了之前准备的基准结果。这说明虽然这两个特征并不是非常的强,但也足够做出比乱猜靠谱得多的预测。
虽然第一版的模型就能够得出不错的结果,但仍然有优化的空间。比如优秀的演员对于评分的帮助显然比标题长度,或者演员个数来的直接。定义两个维度。
优秀演员:参演过 4 分及以上电视剧的演员;
资深演员:参与过 3 部以上电视剧的演员(数据集中为准)。 基于此,制定出三个新的特征。
电视剧包含优秀演员的个数:top_stars_count;
电视剧包含资深演员的个数:sr_stars_count;
电视剧包含的优秀演员共演过几部 4 分以上电视剧:top_stars_movie_count。 要实现上述三个特征,首先需要遍历数据表,筛选出普通演员、优秀演员和电视剧个数的对应关系。代码如下,用 all_stars 存储普通演员,all_top_stars 存储优秀演员。
all_stars={}
all_top_stars = {}
i = 0
for item in df.stars.values:
rating = df.loc[i, "rating"]
for sub_item in item:
if sub_item in all_stars:
all_stars[sub_item] = all_stars[sub_item] + 1
else:
all_stars[sub_item] = 1
# 该演员参演的电视剧评分大于等于 4,则记为优秀演员
if rating >= 4:
if sub_item in all_top_stars:
all_top_stars[sub_item] = all_top_stars[sub_item] + 1
else:
all_top_stars[sub_item] = 1
i = i+1
print("普通演员个数:", len(all_stars))
print("优秀演员个数:",len(all_top_stars))
执行之后输出如下:
普通演员个数: 8808
优秀演员个数: 1237
可以看到,数据集中一共有 8800 多个演员,其中有 1200+ 个优秀演员。
之后,基于这两个字典,来生成三个新的特征,代码如下:
def get_sr_stars_count(c,l):
result = 0
for item in l:
if all_stars[item] > c:
result = result+1
return result
def get_top_stars_count(l):
result = 0
for item in l:
if item in all_top_stars:
result = result + 1
return result
def get_top_stars_movie_count(l):
result = 0
for item in l:
if item in all_top_stars:
result = result + all_top_stars[item]
return result
df["sr_stars_count"] = df.stars.apply(lambda x:get_sr_stars_count(3,x))
df["top_stars_count"] = df.stars.apply(get_top_stars_count)
df["top_stars_movie_count"] = df.stars.apply(get_top_stars_movie_count)
df
运行后可以看到:
一切准备妥当,现在用三个新特征来训练模型。代码如下:
from xgboost import XGBRegressor
features = ["top_stars_count", "top_stars_movie_count","sr_stars_count"]
target = "rating"
df_train,df_test = train_test_split(df, test_size=0.2, random_state=42)
xgb = XGBRegressor(n_estimators = 2000 , random_state = 0 , max_depth = 27)
df_train_features = df_train[features]
df_train_target = df_train[target]
xgb.fit(df_train_features, df_train_target)
df_test_feat = df_test[features]
df_test_result = xgb.predict(df_test_feat)
mean_squared_error(df_test_result, df_test[target])
输出如下:
1.8756039712703974
可以看到,在使用三个新的特征后,模型的 MSE 相比初版模型降低了 17%,相比基准结果降低了 55.9%。
为什么第二次训练没有包含第一次的 title_len、star_count 这两个特征?
原因是数据集本身比较少,特征太多很容易导致模型无法收敛,进而出现更差的结果。
本节的数据分析案例实战总结如下。回顾一下本节所涉及的内容:
| 库 | 本文编写时 | 当前稳定版 | 升级要点 |
|---|---|---|---|
| Python | 3.8-3.12 | 3.14 | 3.12+ 起性能显著提升;3.14 PEP 649/750 |
| NumPy | 1.x/2.0 | 2.5.x | np.float_ 等别名移除;NEP 50 类型提升 |
| Pandas | 1.x/2.x | 3.0.x | Copy-on-Write 默认开启;inplace 移除;字符串 dtype 变化 |
| Matplotlib | 3.x | 3.x 稳定版 | API 兼容,样式更新 |
| Seaborn | 0.12/0.13 | 0.13.x | API 稳定 |
| scikit-learn | 1.x | 1.9.x | API 稳定,新算法持续加入 |
本文讲解的数据分析流程(读取→清洗→分析→可视化)与核心 API 在最新版本中成立;升级时重点关注 Pandas 3.0 的 Copy-on-Write 与 NumPy 2.x 的类型变化。