文章数据分析与自动分类

  • Post author:
  • Post category:其他




一、相关说明

  1. 文章难免有不足的地方,欢迎讨论和斧正,文章可以对文本类数据分析有所启发;
  2. 文章数据获取参见我的另一篇文章,https://blog.csdn.net/weixin_51749229/article/details/115558292
  3. 数据可以自己替换或者去相关网站查找;
  4. 预测分类处理思路:由于sklearn 学习的时候需要的是数值型的数据,而文本是类别型的,所以需要将文本向量化,然后找出合适的特征来拟合,最后利用特征结合逻辑回归算法求出模型,就可以利用训练好的模型预测未知文章的分类了。
  5. 由于主要目标是进行分类预测,所以在文本数据分析的过程中主要从文章的词的角度进行拆解数据指标。



二、项目任务

根据文章文本内容对文本预处理,建模等操作对校内网站文章进行综合性评估并实现自动将文章划分到最可能的类别中。

  1. 对文本数据进行清洗、分词,去除停用词,文本向量化等操作;
  2. 运用描述性统计分析方法分析文章现状,使用python统计词频生成词云图;
  3. 运用推断统计分析方法,如方差分析进行特征选择;
  4. 根据文本内容,对文本数据进行分类,完成数据建模。



三、文章数据综合评价

import numpy as np 
import pandas as pd 
import matplotlib.pyplot as plt 
import seaborn as sns

sns.set(style= "darkgrid", font_scale=1.2)
plt.rcParams["font.family"] = "SimHei"
plt.rcParams["axes.unicode_minus"]=False

news = pd.read_csv("news.csv",sep=" ",index_col=0)
print(news.shape)
display(news.head())
date headline content 详情页 tag
2 2021-03-19 13:14:17 ND高级技工学校专业设置专题研讨会 NaN http://www.gdnjsxy.com/home/article/detail/id/… 教学研究
3 2021-03-18 22:52:20 劳动最光荣,公益在行动——ND高级技工学校公益日活动 劳动创造美好。劳动者用辛勤的双手,编织了这个五彩斑澜的世界,创造了人类的文明。 为了… http://www.gdnjsxy.com/home/article/detail/id/… 其他
4 2021-03-13 06:48:29 ND高级技工学校开始报名了! ND高级技工学校开始报名了! http://www.gdnjsxy.com/home/article/detail/id/… 其他



数据处理



空值处理

news.isnull().sum()
date         0
headline     0
content     31
详情页          0
tag          0
dtype: int64
news.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 192 entries, 0 to 191
Data columns (total 5 columns):
 #   Column    Non-Null Count  Dtype 
---  ------    --------------  ----- 
 0   date      192 non-null    object
 1   headline  192 non-null    object
 2   content   161 non-null    object
 3   详情页       192 non-null    object
 4   tag       192 non-null    object
dtypes: object(5)
memory usage: 9.0+ KB
#用标题填充空值内容
index = news[news.content.isnull()].index
# news["content"][index] = news["headline"][index]
news.content[index] = news.headline[index]
news.isnull().sum()
date        0
headline    0
content     0
详情页         0
tag         0
dtype: int64
#随机取样 sample
news.loc[index].sample(5)
date headline content 详情页 tag
85 2019-07-18 13:03:47 喜 报 喜 报 http://www.gdnjsxy.com/home/article/detail/id/… 教学研究
30 2020-11-09 10:01:33 ND高级技工学校 2020年(秋)免学费初审名单公示 ND高级技工学校 2020年(秋)免学费初审名单公示 http://www.gdnjsxy.com/home/article/detail/id/… 其他
87 2019-06-29 11:23:00 青春不散场…..我们毕业啦 青春不散场…..我们毕业啦 http://www.gdnjsxy.com/home/article/detail/id/… 其他
47 2020-07-09 23:02:15 喜 报 喜 报 http://www.gdnjsxy.com/home/article/detail/id/… 技能培训



重复值处理

print(news.duplicated().sum())
display(news[news.duplicated()])
0

<

news.drop_duplicates(inplace=True)
print(news.duplicated().sum())
0
display(news.content.sample(5))
91                          品小布茗茶,赏文学佳作本期由小布金叶茶业专业合作社协办。
94     地域上虽相隔千里,但对技工教育事业共同的追求跨越了界限。 紧张又充实的5天结束了,参赛老师纷...
123    \n2018年11月23日,为时三天的首届“骏亚杯”田径运动会圆满落幕。本次运动会共有47个...
Name: content, dtype: object



数据清洗

  1. 去除文章里的标点符号和特殊符号


import re 

pattern =  r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~!,。?、¥……【】《》‘’“”\s]+"

re_obj = re.compile(pattern)

def clear(text):
    return re_obj.sub("",text)

news.content = news.content.apply(clear)
news.sample(5)
date headline content 详情页 tag
180 2017-08-18 22:48:07 ND技工学校选派教师赴河北邢台技师学院参加“一体化”教学培训 关于做好定点帮助ND技工学校的通知精神扎实做好帮助对接工作河北邢台技师学院… http://www.gdnjsxy.com/home/article/detail/id/… 教学研究
49 2020-07-07 20:52:18 ND高级技工学校第二届精品小课比赛 为加强我校师资建设提高教师综合素养提升教师业务能力促进一体化教学改革特开展精品小课比赛为广大… http://www.gdnjsxy.com/home/article/detail/id/… 其他
50 2020-06-23 16:13:00 ND高级技工学校2020年面向社会公开招聘教师笔试时间和地点公告 根据ND高级技工学校2020年面向社会公开招聘教师公告要求现将笔试时间地点及有关事项公告如下… http://www.gdnjsxy.com/home/article/detail/id/… 其他
  1. 中文分词
import jieba

def cut_word(text):
    return jieba.cut(text)

news.content = news.content.apply(cut_word)
display(news.sample(10))
date headline content 详情页 tag
153 2018-04-28 19:34:46 ND技工学校和ND卫校面向社会公开招聘教师公告 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
138 2018-07-21 11:20:18 ND技工学校2018年火热招生报名中…… <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
95 2019-06-03 23:48:33 采风专稿 ▎自信的力量 ——ND县高级技工学校采风见闻(郭斌) <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
6 2021-03-06 10:39:32 “践行雷锋精神,开展志愿服务”——ND高级技工学校学雷锋日活动 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 精神建设工作
52 2020-06-19 21:09:00 关于举办2020届毕业生校园招聘双选会的通知 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
117 2019-01-14 11:07:59 校企联谊谋发展 合作双赢谱新篇 ——ND技工学校举行2019年校企合作座谈会 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
114 2019-03-11 23:27:36 我校举行庆三八妇女节烘焙活动 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 其他
157 2018-03-31 23:59:00 第45届世界技能大赛江西省选拔赛重型车辆维修项目圆满落幕 ND技工学校学生夺冠 <generator object Tokenizer.cut at 0x0000023D4… http://www.gdnjsxy.com/home/article/detail/id/… 技能培训
  1. 去除停用词
def get_stopword():
    s = set()
    with open("stopword.txt",encoding="utf-8") as f :
        for line in f :
#             去除回车符
            s.add(line.strip())
    return s 

def remove_stopword(words):
    return [word for word in words if word not in stopword]

stopword = get_stopword()
news.content = news.content.apply(remove_stopword)

news.content.sample(10)
9      [一技, 在手, 就业, 愁, ND, 依托, ND, 高级技工, 学校, 培养, 高技能,...
137                                                 [喜报]
103    [专家, 介绍, 张文涛, 湖北, 东风汽车, 技师, 学院, 机电, 工程系, 副, 主任...
116    [建设, 知识型, 技能型, 创新型, 劳动者, 大军, 加速, 我校, 技能, 人才培养,...
40     [热烈祝贺, 我校, 教师, 首届, 江西省, 技工, 院校, 教师职业, 能力, 大赛, ...

Name: content, dtype: object



描述统计

# 每个类别包含的文章数量
sns.countplot(x="tag",data=news)
<matplotlib.axes._subplots.AxesSubplot at 0x23d48e97a58>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dPNV0kLG-1617970996424)(output_19_1.png)]

其他类文章数量占比大,而教学研究和技能培训的文章占比少,需要增加相应工作内容和文章产出频次。



文章数量分布统计

# 年份数量分布
#日期切分,expend是新建列进行存储
t = news.date.str.split(" ",expand=True)
t = t[0].str.split("-",expand=True)
t.columns=["年","月","日"]
t.head()
0 2021 04 03
1 2021 03 23
2 2021 03 19
3 2021 03 18
4 2021 03 13
#按年统计
sns.countplot(x=t.)
plt.xlabel("年份")
plt.ylabel("文章数量")
Text(0, 0.5, '文章数量')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kT01aqLp-1617970996428)(output_22_1.png)]

2020年文章数量下降,需要加强文章产出。

#按月统计
sns.countplot(x=t.)
plt.xlabel("月份")
plt.ylabel("文章数量")
Text(0, 0.5, '文章数量')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RrTM6lH7-1617970996432)(output_23_1.png)]

2月份文章少是因为春节放假,6月文章多是因为毕业季,符合日常教学

#按日统计
plt.figure(figsize=(15.,5))
sns.countplot(t.)
plt.xlabel("日期")
plt.ylabel("文章数量")
Text(0, 0.5, 文章数量')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CGR9ZcIg-1617970996438)(output_25_1.png)]

根据文中文章分布情况可知在每月月初和月末文章达到最多,符合学校月初计划和月末总结的基本要求。

print(news.content.shape)
(192,)



文章词汇统计

# 词汇统计

from itertools import chain # 二维列表转一维列表
from collections import Counter # 统计每个不同的字符串的数量
# import numpy as np
# numpy 可以用flatten 和revel将多维转换成一维 或者用reshape改变维度
# np.array(li_2d).flatten
# print(np.array(li_2d).shape)
li_2d = news.content.tolist()

#二维列表扁平化为一维列表
# chain.from_iterable 可以把二维列表里面的维度拆开保持外层维度不变
li_1d = list(chain.from_iterable(li_2d))
print(f"总词汇量:{len(li_1d)}")
c=Counter(li_1d)
print(f"不重复词汇量:{len(c)}")
common = c.most_common(15) # 找出频数最高的15个词
print(common)
总词汇量:38477
不重复词汇量:7518
[('学校', 676), ('ND', 557), ('技能', 407), ('教师', 314), ('学生', 290), ('工作', 271), ('学院', 255), ('高级技工', 247), ('技师', 238), ('技工学校', 237), ('月', 227), ('我校', 210), ('活动', 202), ('教育', 199), ('专业', 197)]
d = dict(common)
plt.figure(figsize=(15,5))
sns.barplot(list(d.keys()),list(d.values()))
<matplotlib.axes._subplots.AxesSubplot at 0x23d4d84c5c0>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TX1mNOh4-1617970996443)(output_29_1.png)]

由图可知文章主要内容是关于技能和师生的,符合学校办学、教学的要求。

# 频率统计

#最多的15个词占总词汇多少
total = len(li_1d)
percentage = [v*100/total for v in d.values()]
print([f"{v:.2f}%" for v in percentage])
plt.figure(figsize=(15,5))
sns.barplot(list(d.keys()),percentage)
['1.76%', '1.45%', '1.06%', '0.82%', '0.75%', '0.70%', '0.66%', '0.64%', '0.62%', '0.62%', '0.59%', '0.55%', '0.52%', '0.52%', '0.51%']





<matplotlib.axes._subplots.AxesSubplot at 0x23d4d917198>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aCwhm4l5-1617970996446)(output_30_2.png)]

# # 频数分布统计

# # 绘制所有词汇频数分布直方图
# # Counter 本身结果是一个类字典形式的,可以通过values取值keys取键,结果转变成List 后才好进行随机访问
# # print(type(c))
# # print(c.values())
# # print(c.keys())
# print(list(c.values()))

plt.figure(figsize=(15,5))
v = list(c.values())
end = np.log10(max(v))
#logspace 等比数列 log:True 代表纵坐标使用对数进行显示 ked True 绘制核密度和直方图 ,false 只绘制直方图
ax = sns.distplot(v,bins=np.logspace(0,end,num=10),hist_kws={"log":True},kde=False) 
ax.set_xscale("log") #x轴对数形式显示
plt.xlabel("词的在文章中的频数")
plt.ylabel("词的频次")
Text(0, 0.5, '词的频次')

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q6aGsDUz-1617970996448)(output_31_1.png)]

# 文章词汇长度统计

plt.figure(figsize=(15,5))
num = [len(li)  for li in li_2d]
length = 15
# print(sorted(num, reverse=True)) y值降序排列,只取最大的15个
sns.barplot(np.arange(1,length+1),sorted(num, reverse=True)[:length])
<matplotlib.axes._subplots.AxesSubplot at 0x23d4ddb6208>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vz8zcs8S-1617970996450)(output_32_1.png)]

# 文章词汇长度分布统计

plt.figure(figsize=(15,5))
sns.distplot(num, bins=15, hist_kws={"log":True}, kde=False)
<matplotlib.axes._subplots.AxesSubplot at 0x23d47adc710>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FAJnBRRv-1617970996453)(output_33_1.png)]



词云图

# 词云图
# 词云的图片最好是颜色区分度大的,而且除了要显示的部分其他部分为透明的,这样就能出形状
# 图片可以用PS处理一下
from  wordcloud import WordCloud, ImageColorGenerator


wc = WordCloud(background_color="white",
               font_path = "c:/Windows/Fonts/STFANGSO.TTF", 
               mask=plt.imread("中国地图.jpg"),scale=2
              )

back_color = plt.imread("中国地图.jpg")
join_words = " ".join(li_1d)
img = wc.generate_from_frequencies(c)
img_colors = ImageColorGenerator(back_color)
# img = wc.generate(join_words)
plt.figure(figsize=(15,10))
plt.axis('off')
plt.imshow(wc.recolor(color_func=img_colors))
<matplotlib.image.AxesImage at 0x23d4ea22630>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fkvHiNfp-1617970996455)(output_35_1.png)]



四、建立模型



构建训练集和测试集

注意 文本向量化需要传递空格分开的字符串数组类型,因此需要对文章文本进行预处理

#添加空格
def join(list_line):
    return " ".join(list_line)

news["content"] = news["content"].apply(join)
news.sample(10)
date headline content 详情页 tag
60 2020-03-27 13:26:06 备战国赛,疫情无阻 面对 突如其来 新冠 肺炎 疫情 全国上下 众志成城 奋战 我校 重型 车辆 技术 项目 教… http://www.gdnjsxy.com/home/article/detail/id/… 技能培训
129 2018-10-08 15:33:00 喜 报 喜报 http://www.gdnjsxy.com/home/article/detail/id/… 其他
52 2020-06-19 21:09:00 关于举办2020届毕业生校园招聘双选会的通知 营造 就业 环境 提升 毕业生 就业 服务质量 用人单位 交流 合作 社会 展示 学校 办学… http://www.gdnjsxy.com/home/article/detail/id/… 其他
147 2018-05-26 22:51:51 “六无”平安校园,我们在行动! 构建 和谐 平安 校园 增强 学生 安全意识 自我 保护意识 ND 技工学校 积极开展 平安… http://www.gdnjsxy.com/home/article/detail/id/… 其他
news.groupby("tag").count()
date headline content 详情页
tag
精神建设工作 15 15 15 15
其他 164 164 164 164
技能培训 7 7 7 7
教学研究 6 6 6 6
news.info()
# news.tag.value_counts()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 192 entries, 0 to 191
Data columns (total 5 columns):
 #   Column    Non-Null Count  Dtype 
---  ------    --------------  ----- 
 0   date      192 non-null    object
 1   headline  192 non-null    object
 2   content   192 non-null    object
 3   详情页       192 non-null    object
 4   tag       192 non-null    object
dtypes: object(5)
memory usage: 14.0+ KB
# 标签列离散化,方便切分数据集

news['tag'] = news['tag'].map({"教学研究":0,"技能培训":0,"精神建设工作":0,"其他":1})
news['tag'].values
array([1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1,
       0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1,
       1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
       0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
       1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1,
       0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1], dtype=int64)
#切分数据集
from sklearn.model_selection import train_test_split

x = news.content
y = news.tag
x_train, x_test, y_train, y_test = train_test_split(x,y,test_size=0.25)
print("训练集样本数:",y_train.shape[0],"测试集样本数:",y_test.shape[0])
训练集样本数: 144 测试集样本数: 48



TF-IDF文本向量化

from sklearn.feature_extraction.text import TfidfVectorizer

#参数代表使用1个和相邻的2个词进行组合
vec = TfidfVectorizer(ngram_range=(1,2))
x_train_trans = vec.fit_transform(x_train)
x_test_trans = vec.transform(x_test)
display(x_train_trans,x_test_trans)
<144x23690 sparse matrix of type '<class 'numpy.float64'>'
	with 36621 stored elements in Compressed Sparse Row format>



<48x23690 sparse matrix of type '<class 'numpy.float64'>'
	with 7817 stored elements in Compressed Sparse Row format>



特征选择

词袋模型产生的特征过多,使用方差分析选出与分类最相关的20000个特征就可以保证准确性

from sklearn.feature_selection import f_classif

f_classif(x_train_trans,y_train)
(array([0.13304674, 0.13304674, 0.26015102, ..., 0.13304674, 0.13304674,
        0.13304674]),
 array([0.71583641, 0.71583641, 0.61080829, ..., 0.71583641, 0.71583641,
        0.71583641]))

上述数组值分别代表F值和P值,原假设是不相关,备择假设是相关,因此F值越大,P值越小越好

from sklearn.feature_selection import SelectKBest

#减少内存压力
x_train_trans = x_train_trans.astype(np.float32)
x_test_trans = x_test_trans.astype(np.float32)
#选择最好的K个特征,从F值高到低排序
selector = SelectKBest(f_classif,k=min(20000,x_train_trans.shape[1]))
selector.fit(x_train_trans,y_train)

x_train_trans = selector.transform(x_train_trans)
x_test_trans = selector.transform(x_test_trans)

print(x_train_trans.shape,x_test_trans.shape)

(144, 20000) (48, 20000)



五、逻辑回归预测分类

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report

param = [{"penalty":["l1","l2",],"C":[0.1,1,10],
         "solver":["liblinear"]},
         {"penalty":["elasticnet",],"C":[0.1,1,10],
         "solver":["saga"],"l1_ratio":[0.5]}
        ]
gs = GridSearchCV(estimator=LogisticRegression(),param_grid= param,
                 cv =2,scoring="f1",n_jobs= -1,verbose=10)
gs.fit(x_train_trans,y_train)
#最好参数验证
# print(gs.best_params_)

#预测结果
y_hat = gs.best_estimator_.predict(x_test_trans)
#对训练结果进行评估
print(classification_report(y_test,y_hat))

Fitting 2 folds for each of 9 candidates, totalling 18 fits


[Parallel(n_jobs=-1)]: Using backend LokyBackend with 4 concurrent workers.
[Parallel(n_jobs=-1)]: Done   5 tasks      | elapsed:    3.7s
[Parallel(n_jobs=-1)]: Done  10 tasks      | elapsed:    3.8s
[Parallel(n_jobs=-1)]: Done  13 out of  18 | elapsed:    3.8s remaining:    1.4s
[Parallel(n_jobs=-1)]: Done  15 out of  18 | elapsed:    3.8s remaining:    0.7s


              precision    recall  f1-score   support

           0       0.00      0.00      0.00        11
           1       0.77      1.00      0.87        37

    accuracy                           0.77        48
   macro avg       0.39      0.50      0.44        48
weighted avg       0.59      0.77      0.67        48



[Parallel(n_jobs=-1)]: Done  18 out of  18 | elapsed:    4.1s finished
c:\users\wty_pc\anaconda3\lib\site-packages\sklearn\metrics\classification.py:1437: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples.
  'precision', 'predicted', average, warn_for)

模型训练结果有77%的置信度,要想提高训练效果需要更多的数据。



六、结论

  1. 数量方面:

    文章总数不足,需要加强文章产出和挂网;

    其他类文章数量占比大,而教学研究和技能培训的文章占比少,需要增加相应工作文章产出频次;

    每月月初和月末文章达到最多,符合学校月初计划和月末总结的基本要求;
  2. 内容方面:

    词汇使用符合学校办学要求;

    需要加强教学、技能培训方面的文章内容。
  3. 模型可以实现根据文章内容自动分类了,当文章数量达到500篇以上后再进行调优。



版权声明:本文为weixin_51749229原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。