社交平台内容情感分析|数据方法 – 情绪字典、绘图、火星坐标转换

任务要求

情绪理解是文本处理里最常见任务之一。现提供一个五类情绪字典(由情绪词组成,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]

我们可以发现微博里存在的几种特别格式:

  1. 位置网址链接(同时包括我在: / 我在这里:
  2. emoji表情💓💛💙💜💚❤与微博自带表情[](需要注意的是表情符号通常带有一定的情绪倾向,时间原因本次作业对表情影响暂不考虑,数据清洗时直接将表情去除)
  3. @其他用户信息
  4. 话题#(需要注意的是,根据微博用户的使用习惯,话题通常为句子的一个必要组成部分,不能直接删除,选择去除符号#
  5. 文字和时间、时间和经纬度之间用\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)
  • 发现能够正常生成情绪向量:
请添加图片描述
  • 发现会出现这样的情况:
    1. 情绪向量为[0, 0, 0, 0, 0],这时认为情绪是“无情绪”
    2. 两个多个情绪值相同,这时认为是“多种情绪并存”
  • 为适应这样的变化,我们将完善字典列表:
[{原文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',就贻笑大方了。

情绪时空模式的管理意义

从社会治理角度,情绪的时空模式为舆论分析提供了可靠的依据。

应用在热点事件上,结合传播链分析等,可以快速判断社会热点话题的酝酿、爆发、传播模型,预判其中潜在的造传谣、带节奏等不稳定因素,避免酿成群体社会事件。

社会治理的各个主体,需要在重视舆论的同时,认识并超越其中的感性因素,进入更为理性和科学的层面,时空模式视图较大程度地让管理者、决策者走出了信息茧房,在判断决策中兼顾宏观和微观。

Related Posts