微博情绪分析,时空可视化
库的调用
import math
import numpy as np
import jieba
from harvesttext import HarvestText #从harvesttext中引入HarvestText,调用专门用于处理微博评论的函数
import re#用于正则表达式的替换
from datetime import datetime
import matplotlib.pyplot as plt
from pyecharts.charts import Geo
from pyecharts import options as opts
生成一万条评论,并且生成情绪文件,便于做分词字典
因为两百万条评论跑起来实在是太费劲了,于是只跑了10000条评论,我将前一万条评论按行读入的同时将行导入一个新的test.txt文件中。同时将五个文件中的情绪词导入了同一个txt文件,以便于后面做评论的清洗和分词
def create_test():#生成一个一万条的评论进行测试,同时将情绪词合并成一个,作为后续停用词的使用
f=open("\\weibo.txt","r",encoding="utf-8")
f.readline()#将第一行的标题行读取,但是不加入
new=open("\\test.txt","w",encoding="utf-8")
for i in range(10000):
x=f.readline()
new.write(x)
new.close()
f.close()
anger=open("\\Anger\\data\\emotion_lexicon\\anger.txt","r",encoding="utf-8")
disgust=open("\\Anger\\data\\emotion_lexicon\\disgust.txt","r",encoding="utf-8")
fear=open("\\Anger\\data\\emotion_lexicon\\fear.txt","r",encoding="utf-8")
sadness=open("\\Anger\\data\\emotion_lexicon\\sadness.txt","r",encoding="utf-8")
joy=open("\\Anger\\data\\emotion_lexicon\\joy.txt","r",encoding="utf-8")
anger=anger.readlines()
disgust = disgust.readlines()
fear = fear.readlines()
sadness = sadness.readlines()
joy = joy.readlines()
emotion=open("\\emotion.txt","w",encoding="utf-8")
for i in anger:
emotion.write(i)
for i in disgust:
emotion.write(i)
for i in fear:
emotion.write(i)
for i in joy:
emotion.write(i)
for i in sadness:
emotion.write(i)
emotion.close()
微博评论清洗
def filter(path1,path2):
weibo=open(path1,"r",encoding="utf-8")
#emotion=open(path2,"r",encoding="utf-8")
jieba.load_userdict(path2)#对情感按行读入字典
lis=[]#按照地点坐标,text,id,时间点作为一行列表合并到一起
for item in weibo:#对每条评论进行处理,text在第二部分
item=item.strip().split("\t")
lis.append(item)
for item in lis: #对噪音进行处理
item[1] = HarvestText().clean_text(item[1])
item[1] = re.sub("我在:", "", item[1]) #不知道什么原因,函数并没有将 我在 我在这里这两个词删去
item[1] = re.sub("我在这里:", "", item[1])
item[1]=jieba.lcut(item[1],cut_all=False)#由于只需要分析情绪词,因此我们不对其做停用词过滤
return lis
函数的参数path1, path2分别表示10000条评论,path2表示所有的情绪词。
我才用了jieba库的内置函数load_userdict(file)生成了一个分词字典。
通过观察评论的格式,我发现评论被分为四个部分local,text,id,time,且每个部分由\t字符隔开,且字符串最后有\n换行符。因此先提取weibo的每条评论,先用strip()函数去除头尾的空白符,然后用split()函数将评论分成四部分。然后使用harvesttext库的HarvestText().clean_text()每条评论的文本部分进行url清洗。但是不知道为什么并没有把“我在:”,“我在这里:”删去,因此采用re库里的re.sub()函数对字符串匹配部分替换,re.sub也是使用正则表达式匹配字符串的常用函数。
接着对评论部分进行精确分词
对评论进行情绪分析,并且处理时间和经纬度
def time_emotion_local(filename):
path="\\Anger\\data\\emotion_lexicon\\"
emo=[]#用来分类保存情绪词
for i in range(len(filename)): #这一步将情绪词分开方便处理,anger,disgust,joy,fear,sadness
fp=open(path+filename[i],"r",encoding="utf-8")
item=[i.strip() for i in fp.readlines()]
emo.append(item)
fp.close()
def wordsplit(lis):
nonlocal emo
time,emotion_lis,local=[],[],[]#保存每条评论的时间,情绪词典,地址
for i in range(len(lis)):
a=lis[i][0][1:-2].split(",")
b=[float(a[0]),float(a[1])]
local.append(b)
time.append(lis[i][3])
for i in range(len(time)):
tm=time[i].strip().split(" +0800")#剔除+0800,接着对时间标准化为时间类型
tm=" ".join(tm)
time[i]=datetime.strptime(tm,"%c")#datetime.datetime(2013, 10, 11, 22, 8, 28)为年 月 日 小时 分钟 秒数
for item in lis:
emotion_dict = {"anger": 0, "disgust": 0, "fear": 0, "joy": 0, "sadness": 0}
for it in item[1]:#判断评论里是否带有情绪词
if it in emo[0]:
emotion_dict["anger"]+=1
elif it in emo[1]:
emotion_dict["disgust"]+=1
elif it in emo[2]:
emotion_dict["fear"]+=1
elif it in emo[3]:
emotion_dict["joy"]+=1
elif it in emo[4]:
emotion_dict["sadness"]+=1
set_emo=set(list(emotion_dict.values()))#生成一个集合,如果说无情感或者情感数目相同,则集合长度为1
if len(set_emo)==1:
emotional="no emotion"
else:
emotional=max(emotion_dict,key=emotion_dict.get)#返回键值最大的键
emotion_lis.append(emotional)
return time,emotion_lis,local
return wordsplit #
该函数使用了闭包,虽然没有必要,但还是试了试
首先是对时间的处理:首先要把+0800删除,然后就是一种标准化的时间类型,在时间类型中有专门的表达格式%c。先提取评论信息的时间部分,这部分是一个字符串,然后我将他们通过split函数删去" +0800"之后再用空格符连接起来,也可以采用正则表达式 match=re.sub(r"+\d{4}“,”",str)。然后,调用函数datetime.striptime()对时间进行标准化处理,并转为时间格式.
之后是对地点的处理:我需要将地点转化为经度和维度的浮点数保存在地址列表中,a=lis[i][0][1:-2].split(“,”)提取每条评论的数字部分,并对其关于逗号分割,在利用float函数对数字进行浮点化。
对情绪的处理,我们利用for语句对每条评论做了分析,字典用来统计每条评论的情感,利用if语句判断判断带有情感的分词出现次数。我需要对字典值进行判断,如果都为0或者相等记为no emotion,所以我将所有键值转为列表后,在转为集合(利用set函数),如果说值都一样,那么set的长度应该只为1。否则,返回键值最大的键作为评论情绪的代表。
情绪关于时间可视化
def ptime(emotion,time_lis,emotion_dic,foun):
month_dict = {}
month = [i+1 for i in range(12)] # 生成一个0到11的列表,表示月份
month_dict = month_dict.fromkeys(month, 0)
week_dict={}
week=[i for i in range(7)]#生成一个0到6的列表,从0到6为周一到周日
week_dict=week_dict.fromkeys(week,0)
hour_dict = {}
hour = [i for i in range(24)] # 生成一个0到23的列表,小时
hour_dict = hour_dict.fromkeys(hour, 0)
if foun=="week":
for i in range(len(emotion_dic)):
if emotion_dic[i]==emotion:#当发现情绪匹配时,将对应的星期加一
we=time_lis[i].weekday()
week_dict[we]+=1
week_value=list(week_dict.values())#生成键值列表
plt.plot(week,week_value,color="green",marker="*",label="week_{}".format(emotion))
plt.xlabel("week")
plt.ylabel("numbers")
for a, b in zip(week, week_value):#zip函数,将元素一一对应,打包成元组
plt.text(a, b + 1, b,fontsize=10)#分别表示坐标,打印的值
plt.show()
elif foun=="hour":
for i in range(len(emotion_dic)):
if emotion_dic[i]==emotion:
ho=time_lis[i].hour
hour_dict[ho]+=1
hour_value=list(hour_dict.values())#生成键值列表
plt.plot(hour,hour_value,color="red",marker="*",label="hour_{}".format(emotion))#设置图像参数
plt.xlabel("hour")
plt.ylabel("numbers")
for a, b in zip(hour, hour_value):#zip函数,将元素一一对应,打包成元组
plt.text(a, b + 1, b,fontsize=10)#分别表示坐标,打印的值
plt.show()
elif foun=="month":
for i in range(len(emotion_dic)):
if emotion_dic[i]==emotion:
mo=time_lis[i].month
month_dict[mo]+=1
month_value=list(month_dict.values())#生成键值列表
plt.plot(month,month_value,color="blue",marker="*",label="month_{}".format(emotion))#设置图像参数
plt.xlabel("month")
plt.ylabel("numbers")
for a, b in zip(month, month_value):#zip函数,将元素一一对应,打包成元组
plt.text(a, b + 1, b,fontsize=10)#分别表示坐标,打印的值
plt.show()
首先是对函数传入参数的表示:所要表示的情绪,时间列表,情绪列表,时间匹配模式。
首先生成一个列表,以周为例,0到11,然后根据列表进行字典操作,对每周的匹配值进行初始化为0,具体使用的函数为dict.fromkeys(month,0)。(忽然发现好像并不需要这么做,直接用列表就行)。之后用for循环对每一条评论进行遍历,当发现找到需要匹配的情绪时,将对应的时间提取weekday,然后将对应星期加一。接着进行可视化,即对键和键值分别做为x,即可。
以下为joy情感的可视化图
空间半径变化对情绪的变化及其可视化
一开始我设置了所有的评论重心作为空间原点,但是发现有某些值对重心产生了很大的偏移,导致重心范围内几乎很难找到一条评论,于是我采用了天安们坐标作为在圆心。
def location_mid(local):#但是发现有某个坐标影响特别大,导致重心位置偏离过大
x=0
y=0
for i in range(len(local)):
x+=local[i][0]
y+=local[i][1]
x_ave=x/len(local)
y_ave=y/len(local)
mid=[x_ave,y_ave]
return mid
def location_bar(local,emotion_lis,start,r):#地点列表,感情列表,起点,终点r,r设为从0到0.5截取50段的列表便于之后画堆积图
start=np.array(start)
distance=[]
for dis in local:
dis=np.array(dis)
length=math.sqrt(sum((dis-start)**2))
distance.append(length)#保存每条评论距离重心的距离
#生成情感占比列表
anger_lis=[]
disgust_lis=[]
fear_lis=[]
joy_lis=[]
sadness=[]
no_emotion=[]
for i in range(len(r)):#50个间距
emo = {"anger": 0, "disgust": 0, "fear": 0, "joy": 0, "sadness": 0,"no emotion":0}#50组分布
for j in range(len(distance)):
if distance[j]<=r[i]:
emo[emotion_lis[j]]+=1
total=sum(emo.values())
for key in emo:
emo[key]=emo[key]/(total+1)#为了防止分母为0
#保存了200组
anger_lis.append(emo["anger"])
disgust_lis.append(emo["disgust"])
fear_lis.append(emo["fear"])
joy_lis.append(emo["joy"])
sadness.append(emo["sadness"])
no_emotion.append(emo["no emotion"])
#绘制堆积图
#堆积图预处理
y = np.sum([anger_lis,disgust_lis], axis=0).tolist()#列表对应元素相加
y2=np.sum([y,fear_lis], axis=0).tolist()
y3=np.sum([y2,joy_lis], axis=0).tolist()
y4=np.sum([y3,sadness], axis=0).tolist()
fig,ax = plt.subplots(figsize=(50, 8), dpi=200)
ax.bar(r, anger_lis, width=0.1, label='anger')
ax.bar(r, disgust_lis, width=0.1, bottom=anger_lis, label='disgust')#buttom指底部从什么位置开始
ax.bar(r, fear_lis, width=0.1, bottom=y, label='fear')
ax.bar(r, joy_lis, width=0.1, bottom=y2, label='joy')
ax.bar(r,sadness, width=0.1,bottom=y3,label="sadness")
ax.bar(r,no_emotion,width=0.1, bottom=y4,label='no emotion')
ax.legend()#生成图例
plt.show()
我利用柱形图绘制堆积图,x轴表示搜索半径的扩大,y轴表示各类情绪的百分比
函数的参数分别为:地点列表,感情列表,起点,终点r,r设为从0到0.5截取200段的列表便于之后画堆积图
首先将地点列表化为元组,利用sum,sqrt函数计算点到点的距离,并且保存距离将距离内所有的评论的情感加入到字典中,然后对其进行标准化化为百分比的形式。对每个r的取值都进行该项操作,生成五组列表。
对于绘图,np.sum().tolist()表示的是列表的对应值相加,因为对于bar函数,需要设置bottom参数,表示从什么位置开始绘制bar,否则会出现叠加相互掩盖的现象。当然,最后不能忘了plt.show()
空间分布的可视化之pyechart
def location_geo(local,emotion_lis):
geo = Geo()#生成一个用于打开查看的链接
geo.add_schema(maptype="北京")
emo = {'sadness': 5, 'joy': 15, 'fear': 25, 'disgust': 35, 'anger': 45}#为了便于后面画图分段用
data=[]#用来存储数值,之后会标在地图上
for k in range(len(emotion_lis)):
if emotion_lis[k] != "no emotion":
data.append((emotion_lis[k] + str(k), emo[emotion_lis[k]]))#生成单独的数值对,且不重复
geo.add_coordinate(emotion_lis[k] + str(k), local[k][1], local[k][0])#在地图上加点,分别为测试点,坐标经度,纬度
geo.add("北京微博情绪分布图", data, symbol_size=5)
geo.set_series_opts(label_opts=opts.LabelOpts(is_show=False))#去除坐标点的值的大小
pieces = [
{'min': 1, 'max': 10, 'label': 'sadness', 'color': 'blue'},
{'min': 10, 'max': 20, 'label': 'joy', 'color': 'yellow'},
{'min': 20, 'max': 30, 'label': 'fear', 'color': 'cyan'},
{'min': 30, 'max': 40, 'label': 'disgust', 'color': 'green'},
{'min': 40, 'max': 50, 'label': 'anger', 'color': 'red'}
]
geo.set_global_opts(
visualmap_opts=opts.VisualMapOpts(is_piecewise=True, pieces=pieces),
title_opts=opts.TitleOpts(title="北京-情绪分布"),
)#用颜色区别数据大小
return geo
首先生成这种可交互的图需要调用 from pyecharts.charts import Geo
对于geo.add_schema函数可以直接根据城市名称加入地图
对每个情绪赋值,对于之后的函数set_global_opts函数内的设置,需要根据区间选择点的颜色。
思考字典字典扩充
利用字典做情感分析我认为是简单可行的,但是,当一条评论里出现了多种情绪,单凭数量判断评论的情绪是不够的,因为他没有考虑各种情绪之间的联系,当出现负面情绪时,可能会有fear and sadness。也许可以采用机器学习的方法?寻找个词之间的关联。
以下是关于字典的自动扩充的一个思路:
首先将各个没有纳入字典的分词与五类情绪做相关性检验,如果某一分词出现的情况与joy高度正相关,但是与其他几类情绪出现情况相关性小,甚至负相关,那么可以认为该词表示了joy,就可以将其加入到字典中。这种方法也可以排除 “我”这类词汇被加入字典中。
情绪时空模式的管理意义
通过对时间模式的分析:
在营销上,可以探究人在不同情绪时,对于不同种类产品的消费增减,从而加强在这个时间点的广告投放。假设人在悲伤的时候会增加对饮食的摄入量,那么可以通过分析什么时段悲伤情绪最多,从而加强这个时段的广告推送。
对于社会治理方面,我觉得可以通过分析悲伤情绪的时间点,从而在这个时段密切关注网络的情绪,及时的救助有需要帮助的人,或者对网络进行降温,这样也许可以使得更少的人自杀或者做出一些过激行为。
通过对空间模式的分析:
在国家治理方面,可以分析一个地区的幸福度,对于负面情绪明显的地区,可以重点寻找原因,并且及时解决。
在市场方面,可以敏感的发现各地的舆情变化,从而对这种变化做出反映。例如,当上海、深圳等头部城市出现了fear、sadness大面积分布的情况时,也许可以对市场做出预警,提前做好防御措施。
产品的消费增减,从而加强在这个时间点的广告投放。假设人在悲伤的时候会增加对饮食的摄入量,那么可以通过分析什么时段悲伤情绪最多,从而加强这个时段的广告推送。
对于社会治理方面,我觉得可以通过分析悲伤情绪的时间点,从而在这个时段密切关注网络的情绪,及时的救助有需要帮助的人,或者对网络进行降温,这样也许可以使得更少的人自杀或者做出一些过激行为。
通过对空间模式的分析:
在国家治理方面,可以分析一个地区的幸福度,对于负面情绪明显的地区,可以重点寻找原因,并且及时解决。
在市场方面,可以敏感的发现各地的舆情变化,从而对这种变化做出反映。例如,当上海、深圳等头部城市出现了fear、sadness大面积分布的情况时,也许可以对市场做出预警,提前做好防御措施。