第十届“泰迪杯”数据挖掘挑战赛 C题:疫情背景下的周边游需求图谱分析 - 微信公众号文章无监督分类
我已在AIStudio将赛题方案上传,可以一键Fork运行。
步骤: 未注册的小伙伴需要先去AI Studio进行注册,完善资料后,访问下面方案链接(觉得好的话,记得给我点个关注和小红心哦):
竞赛地址:https://www.tipdm.org:10010/#/competition/1481159137780998144/question
- 一、项目介绍
- 1.1 赛题背景
- 1.2 第一问拟解决问题
- 1.3 数据展示
- 二、基于LDA的微信公众号分类
- 2.1 解决方案
- 2.2 数据预处理
- 2.3 LDA分析
- 2.4 词云展示
- 2.5 结果展示
- 三、总结
- 四、后续的任务
一、项目介绍
1.1 赛题背景
随着互联网和自媒体的繁荣,文本形式的在线旅游(Online Travel Agency,OTA)和游客的用户生成内容(User Generated Content,UGC)数据成为了解旅游市场现状的重要信息来源。OTA和UGC数据的内容较为分散和碎片化,要使用它们对某一特定旅游目的地进行研究时,迫切需要一种能够从文本中抽取相关的旅游要素,并挖掘要素之间的相关性和隐含的高层概念的可视化分析工具。
为此本赛题提出本地旅游图谱这一概念,它在通用知识图谱的基础上加入了更多针对旅游行业的需求。本地旅游图谱采用图的形式直观全面地展示特定旅游目的地“吃住行娱购游”等旅游要素,以及它们之间的关联。图 1所示为我国西藏阿里地区的本地旅游图谱,中心位置节点为旅游目的地“阿里”,它的下层要素包括该地区的重要景点如“冈仁波齐”和“玛旁雍错”,以及“安全”、“住宿”等旅游要素。旅游要素分为多个等级,需要从文本中挖掘出面对不同要素游客所关注的下一级要素。如阿里地区的“安全”要素下包括“高反”、“天气”和“季节”等下一级要素,这个组合是西藏旅游所特有的。旅游要素之间会存在关联关系,如“冈仁波齐”和“玛旁雍错”这两个景点通过“神山圣湖”这一高层概念产生联系,在本地旅游图谱中使用连接两个节点的一条边来表示。
在近年来新冠疫情常态化防控的背景下,我国游客的旅游消费方式已经发生明显的转变。在出境游停滞,跨省游时常因为零散疫情的影响被叫停的情况下,中长程旅游受到非常大的冲击,游客更多选择短程旅游,本地周边游规模暴涨迎来了风口。疫情防控常态化背景下研究分析游客消费需求行为的变化,对于旅游企业产品供给、资源优化配置以及市场持续开拓具有长远而积极的作用。本赛题提供收集自互联网公开渠道的2018年至2021年广东省茂名市的OTA和UGC数据,期待参赛者采用自然语言处理等数据挖掘方法通过建立本地旅游图谱的方式来分析新冠疫情时期该市周边游的发展。
1.2 第一问拟解决问题
构建文本分类模型,对附件1提供的微信公众号的推送文章根据其内容与文旅的相关性分为“相关”和“不相关”两类,并将分类结果以表1的形式保存为文件“result1.csv”。与文旅相关性较强的主题有旅游、活动、节庆、特产、交通、酒店、景区、景点、文创、文化、乡村旅游、民宿、假日、假期、游客、采摘、赏花、春游、踏青、康养、公园、滨海游、度假、农家乐、剧本杀、旅行、徒步、工业旅游、线路、自驾游、团队游、攻略、游记、包车、玻璃栈道、游艇、高尔夫、温泉等等。
- 该数据文本长度长,且没有任何标签
1.3 数据展示
共有8000多条微信公众号的文章,字数在4000+
import pandas as pd
wechat=pd.read_excel('./data/data175696/_5_微信公众号文章.xlsx')
wechat
二、基于LDA的微信公众号分类
2.1 解决方案
** 首先对疫情前发布的394篇和疫情后发布的5665篇公众号文章,以文旅这一大主题为目标,建立模型判断公众号文章与文旅的相关性。先对原始数据集进行数据清洗,避免因为存在较大的噪声干扰模型的判断。然后用TF-IDF算法进行特征关键词提取,并将处理好的数据输入LDA模型中进行主题词和主题生成。考虑到细粒度的问题,最终对疫情前的公众号文章生成16个主题,疫情后的公众号文章生成40个主题,通过人工标注的方法对这56个主题进行相关性标注,最后生成公众号文章与文旅相关性表格。**
# 安装pyLDAvis和词云库
"""
TypeError: import_optional_dependency() got an unexpected keyword argument 'errors',
感谢桨友发现在执行分词遇到错误,原因是因为pandas库版本太低和pyLDAvis不匹配,将pip更新安装最新的pandas即可,安装完后记得重启内核。
"""
!pip install pyLDAvis
!pip install wordcloud
!pip install --upgrade pip
!pip install --upgrade pandas
import os
import pandas as pd
import numpy as np
import re
import jieba
import jieba.posseg as psg
from tqdm import tqdm
# 画图
import warnings
warnings.filterwarnings(action='ignore')
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib import rcParams
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签(动态设置)
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
warnings.filterwarnings("ignore", category=DeprecationWarning) #忽略警告信息
warnings.simplefilter("ignore")
import os
print (os.path.abspath('.'))
2.2 数据预处理
# 加载停用词表
# dic_file = "./lda_t/stop_dic/dict.txt"
stop_file = "./lda_t/stop_dic/stopword.txt"
2.2.1 采用停词表过滤,并且从深蓝字典以及搜狗字典找了相关的一些字典,导入jieba进行分词
def chinese_word_cut(mytext):
jieba.load_userdict('./lda_t/字典/常用水果.txt')
jieba.load_userdict('./lda_t/字典/旅游常用词汇.txt')
jieba.load_userdict('./lda_t/字典/茂名.txt')
jieba.load_userdict('./lda_t/字典/茂名市.txt')
jieba.load_userdict('./lda_t/字典/茂名市城市信息精选.txt')
jieba.load_userdict('./lda_t/字典/茂名市信息大全.txt')
jieba.load_userdict('./lda_t/字典/甜品.txt')
jieba.initialize()
try:
stopword_list = open(stop_file,encoding ='utf-8')
except:
stopword_list = []
print("error in stop_file")
stop_list = []
flag_list = ['n','nz','vn']
for line in stopword_list:
line = re.sub(u'\n|\\r', '', line)
stop_list.append(line)
word_list = []
#jieba分词
seg_list = psg.cut(mytext)
for seg_word in seg_list:
word = re.sub(u'[^\u4e00-\u9fa5]','',seg_word.word)
#word = seg_word.word #如果想要分析英语文本,注释这行代码,启动下行代码
find = 0
for stop_word in stop_list:
if stop_word == word or len(word)<2: #this word is stopword
find = 1
break
if find == 0 and seg_word.flag in flag_list:
word_list.append(word)
return (" ").join(word_list)
import jieba
import jieba.analyse
def jiebacut(content):
content_S = []
for line in content:
# jieba分词 精确模式。返回一个列表类型,建议使用
current_segment = jieba.lcut(line) #每一行都做分词
if len(current_segment) > 1 and current_segment != '\r\n':
content_S.append(current_segment)
return content_S
这里的2个文件,是按照疫情前后进行分开的
2018-2019微信公众号.xlsx;2020-2021微信公众号.xlsx
data = pd.read_excel('./lda_t/data/2018-2019微信公众号.xlsx',sheet_name=0,header=0).astype(str)
data.columns=['id','title','time','content']
data.head()
2.2.2 数据清洗
data['title_cut'] = data['title'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
data['content_cut'] = data['content'].apply(lambda x:''.join(filter(lambda ch: ch not in ' \t◆#%', x)))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('&', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('"', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('"', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('&', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('"', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('"', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub(' ', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub(' ', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('>', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('>', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('<', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('<', ' ', x))
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub('hr/', ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub('hr/', ' ', x))
strinfo = re.compile('······')
data['content_cut'] = data['content_cut'].apply(lambda x: re.sub(strinfo, ' ', x))
data['title_cut'] = data['title_cut'].apply(lambda x: re.sub(strinfo, ' ', x))
data.head()
data['content_cutted'] = data['title_cut'] + data['content_cut']
content_cutted_list = []
for content in tqdm(data.content):
content_cutted_list.append(chinese_word_cut(content))
data["content_cutted"] = content_cutted_list
data.head()
from tqdm import tqdm
content_cutted_list = []
for content in tqdm(data.content):
content_cutted_list.append(chinese_word_cut(content))
data["content_cutted"] = content_cutted_list
data["content_cutted"] = ""
for index, row in tqdm(data.iterrows(), total=data.shape[0]):
data.at[index, "content_cutted"] = chinese_word_cut_with_progress(row["content"])
data.head()
2.3 LDA分析
我们用LDA将上述处理好的content_cut进行LDA分析,生成Topk个主题。
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import LatentDirichletAllocation
def print_top_words(model, feature_names, n_top_words):
tword = []
for topic_idx, topic in enumerate(model.components_):
print("Topic #%d:" % topic_idx)
topic_w = " ".join([feature_names[i] for i in topic.argsort()[:-n_top_words - 1:-1]])
tword.append(topic_w)
print(topic_w)
return tword
n_features = 1000 #提取1000个特征词语
tf_vectorizer = CountVectorizer(strip_accents = 'unicode',
max_features=n_features,
stop_words='english',
max_df = 0.5,
min_df = 10)
tf = tf_vectorizer.fit_transform(data.content_cutted)
# print((tf_vectorizer.get_feature_names()))
# print(tf_vectorizer.vocabulary_) # 索引
n_topics = 16 #主题的数量
lda = LatentDirichletAllocation(n_components=n_topics
, max_iter=50
,learning_method='batch',
learning_offset=50,
# doc_topic_prior=0.1, #α 若不指定,模型会自动选择
# topic_word_prior=0.01,#β
random_state=0)
lda.fit(tf)
n_top_words = 25
tf_feature_names = tf_vectorizer.get_feature_names()
topic_word = print_top_words(lda, tf_feature_names, n_top_words)
设置主题词的个数
n_top_words = 25
2.3.1 每个topic对应的关键词所占的比例图
def plot_top_words(model, feature_names, n_top_words, title):
fig, axes = plt.subplots(4, 4, figsize=(60, 50), sharex=True)
axes = axes.flatten()
for topic_idx, topic in enumerate(model.components_):
top_features_ind = topic.argsort()[: -n_top_words - 1 : -1]
top_features = [feature_names[i] for i in top_features_ind]
weights = topic[top_features_ind]
ax = axes[topic_idx]
ax.barh(top_features, weights, height=0.7)
ax.set_title(f"Topic {topic_idx +1}", fontdict={"fontsize": 30})
ax.invert_yaxis()
ax.tick_params(axis="both", which="major", labelsize=20)
for i in "top right left".split():
ax.spines[i].set_visible(False)
fig.suptitle(title, fontsize=40)
plt.subplots_adjust(top=0.90, bottom=0.05, wspace=0.90, hspace=0.3)
plt.show()
feature_names = tf_vectorizer.get_feature_names()
plot_top_words(lda, feature_names, n_top_words, "Topics in LDA model")
topics=lda.transform(tf) #获得每个文章对应的主题概率
labels = topics.argmax(-1)
# vect = CountVectorizer(max_df=0.95, min_df=2, max_features=n_features, stop_words="english")
# X-tf = vect.fit_transform(df.text.tolist())
data['topic'] = labels
data.head()
lda.components_[0].shape
2.3.2 输出主题对应的example
for topic_idx in range(n_topics):
topic = lda.components_[topic_idx]
top_features_ind = topic.argsort()[: -n_top_words - 1 : -1]
top_features = [feature_names[i] for i in top_features_ind]
samples = data[data['topic'] == topic_idx].sample(5)
print("=========================================================")
print(f"TOPIC {topic_idx + 1}")
print(f" Top words: {top_features}")
print("=========================================================")
for sample_idx, sample in enumerate(samples['content'].tolist(), 1):
print(f"Example {sample_idx}:")
print(sample)
print()
print('---------------------')
print()
print()
print()
print()
2.3.3 输出每篇文章对应主题
topic = []
for t in topics:
topic.append("Topic #"+str(list(t).index(np.max(t))))
data['概率最大的主题序号']=topic
data['每个主题对应概率']=list(topics)
data.to_excel("data_topic_2018_2019.xlsx",index=False)
2.3.4 可视化
import pyLDAvis
import pyLDAvis.sklearn
pyLDAvis.enable_notebook()
pic = pyLDAvis.sklearn.prepare(lda, tf, tf_vectorizer)
pyLDAvis.display(pic)
pyLDAvis.save_html(pic, 'lda_pass'+str(n_topics)+'.html')
# pyLDAvis.display(pic)
# pyLDAvis.show(pic)
生成后在目录中的html文件,打开可以对每个主题进行拖拽查看
]
2.3.5 困惑度计算调优
import matplotlib.pyplot as plt
plexs = []
scores = []
n_max_topics = 20
for i in range(1,n_max_topics):
print(i)
lda = LatentDirichletAllocation(n_components=i, max_iter=50,
learning_method='batch',
learning_offset=50,random_state=0)
lda.fit(tf)
plexs.append(lda.perplexity(tf))
scores.append(lda.score(tf))
n_t=19#区间最右侧的值。注意:不能大于n_max_topics
x=list(range(1,n_t+1))
plt.plot(x,plexs[0:n_t])
plt.xlabel("number of topics")
plt.ylabel("perplexity")
plt.show()
2.4 词云展示
word_name=tf_vectorizer.get_feature_names()
word_freq=tf.toarray().sum(axis=0)#每个词对应的词频
dic=dict(zip(word_name,word_freq))
from wordcloud import WordCloud
#开始画图
font_paths = './lda_t/SimHei.ttf' #词云词的字体目录
# picture_path = 'a.jpg' #画词云的背景图
# mask = plt.imread(picture_path)
# wc = WordCloud(font_path=font_paths, mask=mask, background_color='white') #这里换上字体的路径
wc = WordCloud(font_path=font_paths,background_color='white') #这里换上字体的路径
wc.fit_words(dic)
plt.imshow(wc)
wc.to_file('微信公众号文章的词云图.png') #保存词云图
2.5 结果展示
2.5.1生成分类结果
我们先将topk的主题,这里我们生成了16个主题,通过主题词判断是否与文旅相关;
然后将对应的主题进行替换
gx = pd.read_excel('./lda_t/18-19主题对应关系.xlsx',header=None)
gx.head()
relation=dict(zip(gx[0],gx[1]))
result=pd.DataFrame(data.loc[:,'id'])
result['文章id']=pd.DataFrame(data.loc[:,'id'])
result=result.drop('id',axis=1)
result.head()
result=result.drop('文章id',axis=1)
result.head()
def to_result():
for index in range(len(data["概率最大的主题序号"])):
# print(i)
data.loc[index,'分类标签']=relation[data.loc[index,"概率最大的主题序号"]]
result['文章ID']=pd.DataFrame(data.loc[:,'id'])
result['分类标签']=pd.DataFrame(data.loc[:,'分类标签'])
result.to_excel("result1_2018-2019.xlsx",index=False)
data=to_result()
2.5.2 输出总结果
最后分别将疫情前和疫情后的数据进行合并生成最终的结果
result1=pd.read_excel('result1_2018-2019.xlsx',header=0)
result2=pd.read_excel('result1_2020-2021.xlsx',header=0)
result_1=pd.concat((result1,result2),axis=0,join='inner')
result_1.head()
result_1.info()
result_1.to_excel("result1.xlsx",index=False)
三、总结
在微信公众号分类这个问题上,我们通过在网上查阅资料和对历年赛题的优秀作品的研究,了解了许多主题模型算法,比如LDA、BERTopic、Author-Topic。通过对各个主题模型分析和对比,最后选择了使用LDA模型,相比之下LDA模型能够更好的解决长文本的主题分类问题,并且LDA能够很好的对接LDAvis可视化工具。首先对文章进行分词特征抽取,之后进行主题词生成和主题生成,最后对生产的关键词进行了分析并进行了可视化。
四、后续的任务
由于篇幅有限,后续2个任务写在了另外一篇
第二问拟解决的问题 周边游产品热度分析
第三问拟解决的问题 本地旅游图谱构建与分析