任务要求
情绪理解是文本处理里最常见任务之一。现提供一个五类情绪字典(由情绪词组成,5 个文件,人工标注),实现一个情绪分析工具,并利用该工具对10000条「某社交平台」内容进行测试和分析(一行一条「某社交平台」内容)。字典数据见公开数据中的emotion lexicon (https://doi.org/10.6084/m9.figshare.12163569.v2)。
STEP1 数据清洗与转换
观察原始数据的特点可以发现:
每日必做一事要越来越多哈,第三日[做鬼脸]刚刚差点忘了QQ空间@何凯文 每日一句,filial duty[爱你] 我在这里:http://t.cn/z8AG60D Fri Oct 11 01:09:49 +0800 2013 [39.979173530384, 116.43293726453]
内涵图 我在:http://t.cn/z8FTqat Fri Oct 11 14:51:38 +0800 2013 [39.856444, 116.357902]
💋感谢一路有你💓💛💙💜💚❤💗💘💞💖💕 我在:http://t.cn/zRqGqkQ Sat Oct 12 20:23:18 +0800 2013 [39.97234, 116.312958]
补贴家用之兔子幼儿园和小伙伴冰箱贴[带着微博去旅行][熊猫] 我在:http://t.cn/zRG6lGY Fri Oct 11 21:35:28 +0800 2013 [39.925411, 116.338135]
也许有时我们脚步太快,忘却了身边简单的快乐 我在:http://t.cn/zRGCcE5 Fri Oct 11 23:57:00 +0800 2013 [39.999714, 116.385483]
我在这里:#清华大学第六教学楼#大家近来可好呀? @蓝蓝嫣然 @Kakakakakaka_kaka @麦田里的塔木德 @徐_小敏敏敏 @姑奶奶何叶叶 @西城佩玉 @小羊在成长 @嘻哈糊 @Eason彤 @徐_小敏敏敏 @smile要阳光 @1990图腾 @onenudtambition @苏苏Sue杰声慧影 @佳木朽 @于文小思 @Miss_绚 我在:http://t.cn/zRbr415 Fri Oct 11 09:00:28 +0800 2013 [40.004493, 116.32875]
我们可以发现微博里存在的几种特别格式:
- 位置网址链接(同时包括
我在: / 我在这里:
) - emoji表情💓💛💙💜💚❤与微博自带表情
[]
(需要注意的是表情符号通常带有一定的情绪倾向,时间原因本次作业对表情影响暂不考虑,数据清洗时直接将表情去除) @
其他用户信息- 话题
#
(需要注意的是,根据微博用户的使用习惯,话题通常为句子的一个必要组成部分,不能直接删除,选择去除符号#
) - 文字和时间、时间和经纬度之间用
\t
区隔。
根据后续分析的需求,计划构建一个字典列表,为:
[{原文wb_word, 处理后wb_text, 时间wb_time, 地点wb_loca, 分词wb_cut}, ..., {...}]
1.1数据清洗
- 读取文件:
def document_to_list(path):
"""
读取文档,按行划分,返回列表
:param path: 目标文档路径
:return doc_l: 文档内容列表
"""
print("正在将%s转换为文本列表, 请稍等..." % path)
doc_list = []
with open(path, 'r', encoding='utf-8') as doc_f:
for line in doc_f.readlines():
line = line.strip('\n')
doc_list.append(line)
return doc_list
- 在网上查找到利用正则表达式清洗微博数据的库
HarvestText
,得到微博清洗函数:
def clean_text(text):
"""
根据提供的微博数据集,进行各种文本清洗操作,微博中的特殊格式,网址,email,html代码,等等
:param text: 输入文本
:return: 清洗后的文本
"""
text = re.sub(r"(回复)?(//)?\s*@\S*?\s*(:| |$)", " ", text) # 去除 @ 和转发链 //
text = re.sub(r"\[\S+?]", "", text) # 去除微博表情 [文字]
addr_URL = re.compile(
r'(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))',
re.IGNORECASE)
text = re.sub(addr_URL, "", text) # 去除地址和 URL
text = re.sub("转发微博", "", text)
text = re.sub("我在:", "", text)
text = re.sub("我在这里:", "", text) # 去除无意义的字样
text = re.sub(r"\s+", " ", text) # 去除多余的空格
text = re.sub(r"#", "", text)
emoji_pattern = re.compile("["
u"\U0001F600-\U0001F64F" # emoticons
u"\U0001F300-\U0001F5FF" # symbols & pictographs
u"\U0001F680-\U0001F6FF" # transport & map symbols
u"\U0001F1E0-\U0001F1FF" # flags (iOS)
u"\U00002702-\U000027B0"
"]+", flags=re.UNICODE)
text = emoji_pattern.sub(r'', text)
return text.strip()
- 测试数据清洗结果:
ts = clean_text("🇨🇳🇨🇳又#失眠#了[抓狂][抓狂]。每天发生每一件很细小的事情,晚上都会静静的思考一些种种问题。明天早晨然后完美的解决。我在进步[阴险][阴险] 😄")
---------------
>>>print(ts)
又失眠了。每天发生每一件很细小的事情,晚上都会静静的思考一些种种问题。明天早晨然后完美的解决。我在进步
1.2jieba 分词,并实现前文所述的数据结构
def cut_words(doc, stopwords):
"""
给出语句和停用词集合,使用 jieba 分词,返回分词列表
:param doc: 语句
:param stopwords: 停用词集合
:return wordlist: 分词后列表
"""
wordlist = []
for word in jieba.cut(doc):
if word not in stopwords:
wordlist.append(word)
return wordlist
def get_weibo_info(doc_list):
"""
根据输入的每行文本列表,对信息进行初步处理,包括获取微博内容、发送时间、发送位置、并用jieba进行分词
:param doc_list: 微博文档列表
:return weibo_list: 列表,元素为单条微博的字典
"""
weibo_list = []
jieba.load_userdict(r"0_Res/anger.txt")
jieba.load_userdict(disgust_path)
jieba.load_userdict(fear_path)
jieba.load_userdict(joy_path)
jieba.load_userdict(sadness_path)
for doc in doc_list:
weibo_rough = doc.split("\t")
wb_text = clean_text(weibo_rough[0])
wb_time = weibo_rough[1]
wb_loca = weibo_rough[2]
wb_cut = cut_words(wb_text, stopwords)
weibo = {"wb_word": weibo_rough[0], "wb_text": wb_text, "wb_time": wb_time, "wb_loca": wb_loca, "wb_cut": wb_cut}
weibo_list.append(weibo)
return weibo_list
[{原文wb_word, 处理后wb_text, 时间wb_time, 地点wb_loca, 分词wb_cut}, ..., {...}]
Ref.
https://blog.csdn.net/zln_whu/article/details/103439905
https://github.com/blmoistawinde/HarvestText
https://www.runoob.com/python/python-reg-expressions.html
https://blog.csdn.net/blmoistawinde/article/details/103648044
https://www.jb51.net/article/157029.htm
http://c.biancheng.net/view/5335.html
STEP2 情绪区分
- 我们认为评论只存在最重要的一项(或多项)情绪,为情绪值最大值所对应的情绪
2.1 关于闭包
- 在本题中,题目要求为:
❗注意,这里要求用闭包实现,尤其是要利用闭包实现一次加载情绪词典且局部变量持久化的特点。
闭包的作用是固化并私有化某些参数,在本例子中是情绪字典,避免反复加载
- 根据题目中的局部变量持久化我们可以知道,在闭包模型中:
def a():
A
def b():
B
return
return b
f1 = a()
f2 = f1()
- 局部变量应该处于
A
位置。
2.2 情绪向量计算
def count_emo():
anger = document_to_list(anger_path)
disgust = document_to_list(disgust_path)
fear = document_to_list(fear_path)
joy = document_to_list(joy_path)
sadness = document_to_list(sadness_path)
def count(cut_list):
emotion = [0, 0, 0, 0, 0]
for word in cut_list:
if word in anger:
emotion[0] += 1
if word in disgust:
emotion[1] += 1
if word in fear:
emotion[2] += 1
if word in joy:
emotion[3] += 1
if word in sadness:
emotion[4] += 1
return emotion
return count
- 测试其有效性:
ce = count_emo()
for weibo in weibo_list:
weibo["wb_emo"] = ce(weibo["wb_cut"])
if weibo["wb_emo"] != [0, 0, 0, 0, 0]:
print(weibo)
- 发现能够正常生成情绪向量:
- 发现会出现这样的情况:
- 情绪向量为
[0, 0, 0, 0, 0]
,这时认为情绪是“无情绪” - 两个多个情绪值相同,这时认为是“多种情绪并存”
- 情绪向量为
- 为适应这样的变化,我们将完善字典列表:
[{原文wb_word, 处理后wb_text, 时间wb_time, 地点wb_loca, 分词wb_cut, 情感向量emo_vec = {<"情绪类型">: 情绪值} , 情感wb_emo = [情绪1[, 情绪2, ...]]}, ..., {...}]
- 其中,情感向量以字典形式存储
- 对前述代码进行调整:
def count(cut_list):
"""
闭包函数B,构建情绪向量 emo_cnt, 判断微博情绪(存在无情绪 None 和复杂情绪 [..., ...]情况)
:param cut_list:
:return emo_cnt: 情绪向量
wb_emo: 判断的本条微博情绪
"""
emo_cnt = {"anger": 0, "disgust": 0, "fear": 0, "joy": 0, "sadness": 0}
wb_emo = []
for word in cut_list:
if word in anger:
emo_cnt["anger"] += 1
if word in disgust:
emo_cnt["disgust"] += 1
if word in fear:
emo_cnt["fear"] += 1
if word in joy:
emo_cnt["joy"] += 1
if word in sadness:
emo_cnt["sadness"] += 1
emo_max = max(emo_cnt.values())
if emo_max == 0:
wb_emo.append("none")
else:
for key, value in emo_cnt.items():
if value == emo_max:
wb_emo.append(key)
if len(wb_emo) == 1:
wb_emo = wb_emo[0]
else:
wb_emo = "complex"
return emo_cnt, wb_emo
return count
- 测试情绪判断有效性:
ce = count_emo()
for weibo in weibo_list:
weibo["emo_vec"], weibo["wb_emo"] = ce(weibo["wb_cut"])
if weibo["wb_emo"] != ["None"]:
print(weibo)
Ref.
https://zhuanlan.zhihu.com/p/88054666
https://blog.csdn.net/weixin_43717839/article/details/98073726
https://blog.csdn.net/weixin_43487902/article/details/88407311
STEP3 时间分布
- 数据集中的时间表达方式为:
Fri Oct 11 13:34:55 +0800 2013
- 且日期时间集中于2013年10月
- 首先考虑利用正则等方法提取时间日期:
def converse_time(st):
"""
利用正则,将字符串转化为 datetime 格式的时间并返回
:param st: 字符串格式的时间
:return t: datetime 格式的时间
"""
get_month = re.compile("\w{3}\s(\w{3}).*")
get_year = re.compile(".*(\d{4})$")
get_day = re.compile("\w{3}\s\w{3}\s(\d{1,2})\s.*")
get_time = re.compile(".*(\d{2}:\d{2}:\d{2}).*")
month_abb = re.findall(get_month, st)[0]
year_str = re.findall(get_year, st)[0]
day_str = re.findall(get_day, st)[0]
time_str = re.findall(get_time, st)[0]
month_str = (list(calendar.month_abbr).index(month_abb))
times = "%s-%s-%s %s" % (year_str, month_str, day_str, time_str)
t = datetime.datetime.strptime(times, "%Y-%m-%d %H:%M:%S")
return t
- 将前述字典转换为
DataFrame
结构
df = pd.DataFrame(weibo_list)
df = df.set_index(pd.to_datetime(df["wb_time"]))
dfgrouped = df.groupby(df["wb_emo"])
dfds = dfgrouped.describe()
df.to_csv("wb_info.csv")
dfds.to_csv("describe.csv")
- 绘制时间分布图函数:
def gdf(df, emotion, fre):
emo_df = df[:][df["wb_emo"] == emotion]
cnt = emo_df.resample(fre)["wb_emo"].count()
# 进行绘图
cnt.plot(marker='*')
plt.rcParams['font.sans-serif'] = ['KaiTi_GB2312'] # 步骤一(替换sans-serif字体)
plt.rcParams['axes.unicode_minus'] = False # 步骤二(解决坐标轴负数的负号显示问题)
plt.title("%s 情绪时间分布图 %s视图" % (emotion, fre))
plt.ylabel("%s 情绪出现频次" % emotion)
plt.show()
return cnt
- 绘制堆叠图代码:
data = pd.read_csv("/0_Res/111.csv")
# data = np.array(data)
t = data["wb_time"].tolist()
anger = data["anger"].tolist()
disgust = data["disgust"].tolist()
fear = data["fear"].tolist()
joy = data["joy"].tolist()
sadness = data["Sadness"].tolist()
plt.stackplot(t, anger, disgust, fear, joy, sadness, colors=["#640902", "#529830", "#5b297d", "#eb9f00", "#052e84"])
plt.rcParams['font.sans-serif'] = ['KaiTi_GB2312'] # 步骤一(替换sans-serif字体)
plt.rcParams['axes.unicode_minus'] = False # 步骤二(解决坐标轴负数的负号显示问题)
plt.title("情绪时间分布图 H视图")
plt.ylabel("情绪出现频次")
plt.xticks(rotation = 45, color = "w")
plt.legend()
plt.show()
https://www.delftstack.com/zh/howto/python-pandas/pandas-column-to-list/
https://blog.csdn.net/sinat_36219858/article/details/79800460
https://blog.csdn.net/mighty13/article/details/115473283
STEP4 空间分布
- 微博使用的是高德地图,需要进行火星坐标的逆运算(GCJ-02->WGS-84)
- 网上存在
coordinate_conversion.py
,加入目录
def converse_loc(lc):
get_loc = re.compile("([1-9]\d*\.?\d*|0\.\d*[1-9]\d)")
loc = re.findall(get_loc, lc)
lat, lng = float(loc[0]), float(loc[1])
lng, lat = coordinate_conversion.gcj02towgs84(lng, lat)
return [lng, lat]
def get_distance(df):
# 计算重心,并当作城市中心
centre_lng = df["wb_lng"].mean()
centre_lat = df["wb_lat"].mean()
# 计算城市中心和各点之间的距离(欧式距离)
df["distance"] = df[["wb_lng", "wb_lat"]].apply(
lambda x :
math.sqrt((x["wb_lng"] - centre_lng) ** 2 + (x["wb_lat"] - centre_lat) ** 2), axis=1)
df_describe = df.describe()
df_describe.to_csv("des.csv")
- 据此,手动划分出10个空间距离区间:
[0, 0.02], [0.02, 0.04], ..., [0.18, 0.20]
- 计算各个区间内情绪微博数,并画图
def plot_distance(df, emotion):
emo_df = df[:][df["wb_emo"] == emotion]
disi = []
discnt = []
for i in range(10):
cnt = emo_df[:][(emo_df["distance"] >= 0.02*i) & (emo_df["distance"] < 0.02*(i+1))]
cnt = cnt["distance"].count()
disi.append(i*0.02)
discnt.append(cnt)
plt.plot(disi, discnt, marker='*')
plt.rcParams['font.sans-serif'] = ['KaiTi_GB2312'] # 步骤一(替换sans-serif字体)
plt.rcParams['axes.unicode_minus'] = False # 步骤二(解决坐标轴负数的负号显示问题)
plt.title("%s 情绪空间分布图(距离)" % (emotion))
plt.ylabel("%s 情绪出现频次" % emotion)
plt.show()
return
- 同样地,绘制情绪空间分布图
data = pd.read_csv("/Users/indecreasy/Desktop/212_MPD/P2/dis_c.csv")
# data = np.array(data)
t = data["dis"].tolist()
anger = data["anger"].tolist()
disgust = data["disgust"].tolist()
fear = data["fear"].tolist()
joy = data["joy"].tolist()
sadness = data["sadness"].tolist()
plt.figure(figsize=(8,7))
plt.stackplot(t, anger, disgust, fear, joy, sadness, colors=["#640902", "#529830", "#5b297d", "#eb9f00", "#052e84"])
plt.rcParams['font.sans-serif'] = ['KaiTi_GB2312'] # 步骤一(替换sans-serif字体)
plt.rcParams['axes.unicode_minus'] = False # 步骤二(解决坐标轴负数的负号显示问题)
plt.title("情绪空间分布图 距离-比例视图")
plt.ylabel("情绪出现比例")
plt.xlabel("距中心点距离")
plt.xticks(rotation = 45)
plt.show()
Ref.
https://blog.csdn.net/u013233097/article/details/102719229
https://blog.csdn.net/summer_dew/article/details/80723434
库管理
import re
import math
import jieba
import calendar
import datetime
import pandas as pd
import coordinate_conversion
import matplotlib.pyplot as plt
main()函数
def main():
global anger_path, disgust_path, fear_path, joy_path, sadness_path
anger_path = "0_Res/anger.txt"
disgust_path = "0_Res/disgust.txt"
fear_path = "0_Res/fear.txt"
joy_path = "0_Res/joy.txt"
sadness_path = "0_Res/sadness.txt"
emotions = ["anger", "disgust", "fear", "joy", "sadness", "none", "complex"]
global stopwords
stopwords = document_to_list("0_Res/stopwords_list.txt")
document_list = document_to_list("0_Res/weibo.txt")
weibo_list = get_weibo_info(document_list)
ce = count_emo()
for weibo in weibo_list:
weibo["emo_vec"], weibo["wb_emo"] = ce(weibo["wb_cut"])
df = pd.DataFrame(weibo_list)
df = df.set_index(pd.to_datetime(df["wb_time"]))
df = get_distance(df)
aa = []
for emotion in emotions:
aa.append(plot_distance(df, emotion))
return
反思总结
关于扩充词典
- 扩充词典有利于模型对微博的进一步了解,但根据边际效用递减,可以推断其效果远不如以下方法:
- 正确处理并理解微博中表情所包含的情绪
- 在情感词判断的基础上考虑句式的影响(原因见下)
- 只针对情绪词进行了分析,但是语料中有可能存在复杂句式,让判断出现误差甚至完全相反,类似句式为:
我真的不能说幸福
————实为消极,但是判断为积极
我之所以走出了痛苦,是因为我不再认为这些事情是折磨
————实为积极,但是判断为消极
......
- 甚至存在更大的误差,例如:
人脉是彼此对对方的价值,是你能帮到他,他能帮到你,这种双向的被需要,而不只是你认识多少人,你有很多朋友但他们都不能帮到你,这不算人脉广。而他能帮你的前提,首先是你能帮他,对他有价值,这种价值包括工作、生活等各各方面,单一的索取帮助是不能成为健康长久的人脉资源。
——————————————————————
{'anger': 0, 'disgust': 0, 'fear': 0, 'joy': 6, 'sadness': 0}
- 据此判断为
'joy'
,就贻笑大方了。
情绪时空模式的管理意义
从社会治理角度,情绪的时空模式为舆论分析提供了可靠的依据。
应用在热点事件上,结合传播链分析等,可以快速判断社会热点话题的酝酿、爆发、传播模型,预判其中潜在的造传谣、带节奏等不稳定因素,避免酿成群体社会事件。
社会治理的各个主体,需要在重视舆论的同时,认识并超越其中的感性因素,进入更为理性和科学的层面,时空模式视图较大程度地让管理者、决策者走出了信息茧房,在判断决策中兼顾宏观和微观。