金融科技之情感分析(一):股民情绪指数与股市价格的相关性分析

金融科技之情感分析(一):股民情绪指数与股市价格的相关性分析


本文是我在一家金融机构实习时做的第一个项目的整理。蒟蒻一枚,是金融科技和人工智能的初学者,写下本篇博文意在记录自己的足迹并与和我一样想要从事金融科技行业的小伙伴分享我遇到的问题和解决方案。如果本文有错误的地方,请大佬们指正。

前言

在投资者社区中有许多文本型数据,我们可以对这些文本型数据进行处理,挖掘出有价值的信息。比如,东方财富股吧中有很多用户的发言、评论,我们可以借助自然语言处理中的情感分析技术,挖掘出市场的情绪指数,并研究情绪指数与金融市场的相关性。这个项目做的就是这个工作。

主要涉及爬虫自然语言处理相关性分析以及有限的MySQL的技术。

文本数据源介绍

首先我们打开http://guba.eastmoney.com/list,zssh000001,f.html,进入上证指数吧。进入后的界面如下图所示,同时我们选择排序方式为“发帖时间”。
在这里插入图片描述
可以看到,在股吧的界面中,有财经评论、东方财富网、股吧访谈等官方发布的内容,也有普通用户发表的评论。在这里,我们只研究普通用户发表的评论。
接着我们点击鼠标右键,选择网页源代码,可以看到该页面的html代码如下图。
在这里插入图片描述
我们发现在页面上看到的用户评论信息都被写在了HTML页面中,这为我们后面的爬虫提供了方便。进一步观察该源代码,我们发现源代码还同时包含了评论详细信息的网址、评论作者页面的网址。如果点击打开评论详细信息的网址,并打开其网页源代码,可以发现,我们可以在源代码中找到该条评论的**“发表时间”、”点赞数“**等数据。同样的,我们可以在评论作者的用户页面的源代码中找到该用户的”粉丝数“等数据。这些数据在我们后面情绪指数计算时都会用到。

评论数据爬取

我们通过分析数据源——股吧,发现我们所需要的数据基本上都被包含在了相关页面的HTML源代码中。那下一步便可以设计我们的爬虫了。我们这里只使用requests和BeautifulSoup两个库便可以完成数据爬取的任务了。
首先,引入这两个库。若还未安装这两个库,只需通过pip安装即可。

import requests
from bs4 import BeautifulSoup

首先把requests进一步封装成根据url获取HTML的函数

def getHtml(url):#下载网页源代码
    header = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; LCTE; rv:11.0) like Gecko'}
    try:
        r=requests.get(url,headers=header)
        r.encoding='utf-8'
        #print(r.status_code)
        r.raise_for_status()
        return r.text
    except:
        getHtml(url)

现在我们可以根据url地址获取对应网页的HTML源代码。下一面要做的便是解析HTML的结果,定位我们需要的数据的位置。
通过分析股吧页面的HTML源代码我们发现,每条评论、公告、新闻等数据都存储在一个div标签下,且该div标签的class属性为articleh。
在这里插入图片描述
而对于每一个div标签,其又有5个span子标签,分别存储阅读量、评论数、标题、作者和发表时间。在第三个span子标签,即class属性值为”l3 a3“的span标签中,有一个a标签,a标签存在两个属性,其中href属性是该条评论详细页面的地址,title属性是该条评论的标题。我们可以发现多数散户的评论标题和正文的内容是一致的,所以这里保存title的值作为用户的评论文本。
分析完网页源代码后,我们便可以使用BeautifulSoup来获取对应的数据了。主要代码如下:(关于BeautifulSoup的用法,读者只需知道其可以通过标签类型和标签属性、属性值查询到相应的标签即可,具体的用法可自行在网络中查询。)

html=getHtml(url)
soup = BeautifulSoup(html, "html.parser")
contain = soup.find_all("div", {"class": "articleh"})#获取存有数据的div标签,存在contain中,因为一个页面有多条评论,所以contain是一个列表。
for i in contain:#遍历contain
	content = i.find("span", {"class": "l3 a3"}).find("a")#获取一个div便签中第三个span标签下的a标签,其有href和title两个属性
	contentUrl="http://guba.eastmoney.com"+content["href"]#content["href"]是该评论的详细界面网址,因为其是相对地址,所以需要在前添加网址的前缀,得到完整的界面网址
	commentId=content["href"][-14:-5]#我们观察content["href"]属性的值,发现其是具有规则的字符串,从该字符串的倒数第14个位置到倒数第5个位置是该条评论的id
	text=content.attrs["title"]#获取评论文本(标题)
	userUrl = i.find("span", {"class": "l4 a4"}).find("a").attrs["href"]#用同样的方法获取用户主页链接
	userId=userUrl[23:]#获取用户ID

通过上面的代码段,我们已经从股票的评论列表页面中获取了我们需要的数据。下面我们将根据我们获取的conteUrl和userUrl获取其他我们需要的信息,方法和上面类似。

commentHtml=getHtml(contentUrl)#获取评论详细信息源代码
soup = BeautifulSoup(commentHtml, "html.parser")
date = soup.find("div", {"class": "zwfbtime"}).text[4:14]#获取评论发表时间
likeCount=int(soup.find("div", {"data-like_count": True}))#获取评论点赞数,并转换成整数类型。(因为从html中获取会认为是字符串类型)
userHtml=getHtml(userUrl)#获取用户主页源代码
soup = BeautifulSoup(userHtml, "html.parser")
fans=int(soup.find("a", {"id": "tafansa"}).find("span").text)#获取用户粉丝数

至此,我们已经能够获取我们所需要的数据了。但目前我们只是从评论列表的第一页获取数据,如果获取其他页的数据呢?很简单,当我们在网页上点击第二页时,可以发现浏览器的地址栏变成了
http://guba.eastmoney.com/list,zssh000001,f_2.html。所有我们只需要改变该地址字符串的”f_2”和“.html“之间的数字即可。
不过,上述代码只是简化后的结果。没有考虑如果该条”评论“是新闻、讨论、问答等的情况。事实上,这些情况经常出现,且因为不同类型的数据其html得到标签结构往往不同,所有可能导致程序异常终止。一个简单粗暴的方法便是在程序段外面,在循环语句下加上try-except异常处理,若出现异常,则执行continue语句,跳过这一层循环,即跳过该条”评论“。

html=getHtml(url)
soup = BeautifulSoup(html, "html.parser")
contain = soup.find_all("div", {"class": "articleh"})
for i in contain:
	try:
		content=i.find("span", {"class": "l3 a3"}).find("a")
  		contentUrl="http://guba.eastmoney.com"+content["href"]
  		commentId=content["href"][-14:-5]
  		text=content.attrs["title"]
  		userUrl=i.find("span", {"class": "l4 a4"}).find("a").attrs["href"]
  		userId=userUrl[23:]
  		commentHtml=getHtml(contentUrl)
  		soup=BeautifulSoup(commentHtml, "html.parser")
  		date=soup.find("div", {"class": "zwfbtime"}).text[4:14]
  		likeCount=int(soup.find("div", {"data-like_count": True})
  		userHtml=getHtml(userUrl)
  		soup=BeautifulSoup(userHtml, "html.parser")
  		fans=int(soup.find("a", {"id": "tafansa"}).find("span").text)
 	except:
  		continue

这样可以解决大多数情况的问题,但也并非万无一失。可能有的”评论“标签(这里指新闻讨等类型的数据)执行try语句下的代码并没有出现异常,但存储的数据并非我们想要的内容。这需要我们在实践的过程中不断总结、寻找规律、判断并排除这类情况。
至此,获取数据我们已经介绍完了。

扫描二维码关注公众号,回复: 9987092 查看本文章

数据存储和查询

在利用爬虫技术获取到我们需要的数据后,便需要将数据存储以待后续处理。数据存储有很多方法,在这里,我们将其存储到数据库中。
这里使用的是MySQL数据库和DataGrip数据库图形界面操作工具。读者可参考网络上的MySQL安装教材进行安装。若读者无法使用DataGrip(收费软件,在校大学生和教师可申请免费使用)也无妨,我只是使用该软件方面查看数据库的内容。

成功安装数据库,并输入密码登录后。便可以创建数据库和数据表了。
首先我们创建数据库。注:windows操作系统下不区分大小写

create database dfcf;

其中,dfcf是自定义的数据库名。dfcf即东方财富。
接着,我们在dfcf数据库中创建数据表。

use dfcf;
create table tb_comment
(
  id         int auto_increment
    primary key,
  comment_id varchar(20)  null,
  content    varchar(300) null,
  like_count int          null,
  date       date         null,
  user_id    varchar(20)  null,
  share_code varchar(15)  null,
);
create table tb_user
(
  id   char(18) not null
    primary key,
  fans int      null
);

通过上面的代码,我们便在dfcf数据库中创建了两个表。一个存储评论的相关信息,另一个存储用户的相关信息。在tb_comment表中,id字段为从1开始递增的整数,用来方便查看存储的数据总数。share_code字段存储的是该条评论所针对的股票,在这里,因为我们研究的是上证指数吧,所以share_code为zssh000001.

现在我们已经打建好了数据库。下一个问题便是如何向数据库中存储和从数据库中查询数据了。只需要简单的了解一下SQL语句即可。对数据库的操作基本上可以归为增删改查四大块。每一块常用的语句很少也很简单,读者自行在网络上了解即可,后面我也会给出用到的SQL语句。
关于数据存储和查询的最后一个问题便是:如何使用Python对数据库进行操作。
事实上,我们只需要安装pymysql库,再按照一定格式调用即可。下面给出例子:

import pymysql
def  storeCommentInf(comment):#存储评论
    db = pymysql.connect("localhost", "root", "你的登录密码", "dfcf")
    cur=db.cursor()
    sql = "INSERT INTO TB_COMMENT(comment_id,content,like_count,date ,user_id,share_code) values (%(comment_id)s,%(content)s,%(like_count)s,%(date)s,%(user_id)s,%(share_code)s)"
    sql1 = "SELECT * FROM TB_COMMENT where COMMENT_ID=%s"
    if  cur.execute(sql1,(comment["comment_id"])):#去重
        cur.close()
        print("评论已经存在")
        db.close()
    else:
        cur.execute(sql, (comment))
        db.commit()
        cur.close()
        db.close()
        print("插入评论成功")

在上面的代码中,我们首先导入了pymysql库,并定义了一个函数。函数中有一个参数comment,为字典类型,存储的是一条评论的有关数据。上述代码段的第3行,我们连接了dfcf数据库。第4行,我们引入了一个会话指针,这是固定写法,读者不必深究。随后,我们定义个两个字符串sql和sql1来定义要执行的数据库操作。sql用来插入comment,其values()内定义的%(xxx)s中的”xxx"是参数comment中的键,执行该语句可以将参数comment对应键的值存入数据库对应字段中。sql1用来根据comment_id来查询数据,避免同一条评论被重复插入。cur.execute()便是用来执行设置好的sql语句。
下面给出全部的用到的关于数据库操作的函数:

import pymysql

def  storeCommentInf(comment):#存储评论
    db = pymysql.connect("localhost", "root", "你的登录密码", "dfcf")
    cur=db.cursor()
    sql = "INSERT INTO TB_COMMENT(comment_id,content,like_count,date ,user_id,share_code) values (%(comment_id)s,%(content)s,%(like_count)s,%(date)s,%(user_id)s,%(share_code)s)"
    sql1 = "SELECT * FROM TB_COMMENT where COMMENT_ID=%s"
    if  cur.execute(sql1,(comment["comment_id"])):#去重
        cur.close()
        print("评论已经存在")
        db.close()
    else:
        cur.execute(sql, (comment))
        db.commit()
        cur.close()
        db.close()
        print("插入评论成功")

def storeUserInf(user):#储存用户数据
    db = pymysql.connect("localhost", "root", "你的登录密码
", "dfcf")
    cur = db.cursor()
    sql = "INSERT INTO TB_USER(id,fans) values (%(id)s,%(fans)s)"
    sql1="SELECT * FROM TB_USER where ID=%s"
    if  cur.execute(sql1,(user["id"])):#去重
        cur.close()
        db.close()
        print("用户已经存在")
    else:
        cur.execute(sql, (user))
        db.commit()
        cur.close()
        db.close()
        print("插入用户成功")

def selectCommentOrderByDate(share_code,method):#查询评论信息
    db = pymysql.connect("localhost", "root", "你的登录密码", "dfcf")
    cur = db.cursor()
    if  method==0:#按照日期升序
        sql = "SELECT * FROM TB_COMMENT WHERE SHARE_CODE=%s ORDER BY DATE"
    else:#按照日期降序
        sql="SELECT * FROM TB_COMMENT WHERE SHARE_CODE=%s ORDER  BY DATE DESC "
    cur.execute(sql,(share_code))
    db.commit()
    cur.close()
    return  cur.fetchall()

def selectFansByUserId(userId):#查询用户粉丝数
    db = pymysql.connect("localhost", "root", "你的登录密码", "dfcf")
    cur = db.cursor()
    sql = "SELECT FANS FROM TB_USER where ID=%s"
    cur.execute(sql,userId)
    db.commit()
    cur.close()
    return cur.fetchall()

至此,我们已经解决了获取数据和存储、查询数据的问题了。

计算情绪指数

现在,我们便可以计算投资者的情绪指数了。我们需要借助自然语言处理中的情感分类技术。按照正常的处理流程,我们需要搭建模型、准备语料库、训练模型、测试模型然后得到一个情感分类的模型。但这里,我们直接使用现有的模型。snownlp是一个中文的开源的自然语言处理的Python库,可以进行分词、情感分类等。但snownlp库有一个缺陷,便是其模型的训练语料是商品购物评论语料,用来做金融领域的情感分类效果一般,但目前还并没有关于金融领域的中文自然语言处理的开源库、语料库。所以这里我们暂时使用snownlp库来完成我们的实验。若想进一步提高准确率,还需自己搭建语料库进行模型训练。
下面介绍用snownlp进行情感分析的方法:
首先,需要安装snownlp库,直接用pip安装即可。安装完毕之后,按照下列方法调用。

from snownlp import SnowNLP
text="大牛市来啦,发财啦"
nlp=SnowNLP(text)
print(nlp.sentiments)

运行上述代码,我们可以得到一个浮点数0.7343040563996935。nlp.sentiments是一个在【0,1】之间的浮点数,这个数越接近1,代表该文本表达的积极情绪越强,反之,则代表该文本表达的消极情绪越强。
现在我们已经可以计算一个评论文本的情感得分了,下一步便是量化出某一日市场投资者的整体情绪。量化的方法有许多种,可以将某一日所有的评论情绪得分得分相加再求评价,也可以求某一日情绪得分大于0.5的评论所占的比例。

本蒟蒻采用的方法是:

①将情绪得分>0.6的评论当作积极评论,小于0.4的评论当作消极评论。

②设置变量neg和pos,存储某一日市场的积极情绪因子和消极情绪因子。关于neg和pos的计算方法,以neg为例: 初始化为0
若某一日的某一评论comment的情绪得分<0.4 neg=neg+1+log(该条评论的点赞数+该条评论作者的粉丝数+1,2)
其中log(x,2)表示以2为低的x的对数。考虑该条评论的点赞数和该条评论作者的粉丝数是因为考虑到不同的评论的质量不同。取对数是为了让数据更加平滑,防止极值过大。+1是为了防止该条评论的点赞数和该条评论作者的粉丝数都为0。

③计算某一日市场的总体情绪得分score。我设计的模型是
score=log((pos/(pos+neg+0.0001)-0.5)*(该日评论总数+1))
(pos/(pos+neg+0.0001)-0.5)的意思是计算市场的情绪倾向,大于0表明市场积极情绪情绪较强,越接近0.5越强。小于0反之。后面的(该日评论总数+1),是因为本人认为某一日投资者的评论越多,代表市场投资者情绪的波动越大。

该部分的核心代码如下:

def quantilizeSentiments(data,date):
    pos=neg=0
    for comment in data[date]:
        try:
            nlp = SnowNLP(comment['comment'])
            sentimentScore = nlp.sentiments
        except:
            print(traceback.format_exc())
            continue
        if(sentimentScore>0.6):
            fans=SQL.selectFansByUserId(comment['user_id'])
            pos+=1+math.log(comment['like_count']+fans[0][0]+1,2)
        if(sentimentScore<0.4):
            fans=SQL.selectFansByUserId(comment['user_id'])
            neg+=1+math.log(comment['like_count']+fans[0][0]+1,2)
    print("负:"+str(neg)+"  正:"+str(pos))
    return math.log((pos/(pos+neg+0.0001)-0.5)*(len(data[date])+1),2)

相关性分析和可视化

通过上面的步骤,我们已经可以计算某一日市场的情绪指数了。下面便是最后一步,分析情绪指数与股票走势的相关性。这里以平安银行近两年的股票价格走势为例。

关于股票历史价格数据,读者可以前往tushare.pro :https://tushare.pro/了解其相关接口,这是一个免费的社区,这里不过多介绍了。

我将其计算出的情绪指数和股票历史价格都存储在了data.xlsx excel文件中,部分数据如下:全部数据是从2017-06到2019-06的交易日的情绪指数和股票价格。
在这里插入图片描述
下面进行相关性分析。在这里,我们使用MIC最大互信息系数来表示socre和price的相关性。关于MIC的具体算法和原理,我在做的时候没有去深究,但最近正好信息论学了互信息,后面可能会去研究一下。现在我们需要知道其能衡量线性、非线性数据的相关性即可。MIC的值越接近1,则表明两者的相关性越强。
我们可以直接使用Python的minepy包来计算MIC值。minepy需要使用pip安装,若读者在安装时出现错误,可在CSDN的其他博客上寻找解决方案。
安装完minepy包后,我们按照调用规则调用即可。同时,我们可以利用matplotlib绘制出两者的曲线图。代码如下:

import pandas as pd
import  matplotlib.pyplot as plt
from pandas.plotting import register_matplotlib_converters
import numpy as np
from minepy import MINE
register_matplotlib_converters()

plt.rcParams['font.sans-serif']=['SimHei']
plt.rcParams['axes.unicode_minus']=False

data=pd.read_excel('data.xlsx')#读取数据

mine = MINE(alpha=0.6, c=15)
mine.compute_score(data['score'], data['price'])
print(mine.mic())//相关性技术

plt.plot(data['date'],data['score'],linestyle='--',label='情绪')
plt.xticks(rotation=45)
plt.legend(loc='upper left')
plt.twinx()
plt.plot(data['date'],data['price'],color='red',label='股价')
plt.xticks(rotation=45)
plt.legend(loc='upper right')
plt.show()

运行,得到的结果是0.22005529890307557,表现出较弱的相关性,绘制出的折线图如下:
在这里插入图片描述
从图片上可以看出情绪指数与股票价格的走势大致相同,但MIC的结果和我们的直觉不太一致。

下面,我们对数据进行平滑处理,消除一部分的噪音。我采用的方法是计算score和price的移动平均值

import numpy as np
data['avg_Score'] = np.round(data['score'].rolling(window=30,min_periods=1).mean(), 2)
data['avg_Price'] = np.round(data['price'].rolling(window=30,min_periods=1).mean(), 2)

上面的代码求出了socre和price的30日均线。我们再用计算出的data[‘avg_Score’]和data[‘avg_Price’]代替前面的data[‘score’]和data[‘price’],重新计算会绘制。得到如下结果:
MIC系数为0.5477048122148983,代表呈现出强的相关性。从图像可以更直观看出:
在这里插入图片描述
至此,我们已经完成了所有的步骤并得出了比较好的结果。

如有不足之处,欢迎指教。

发布了3 篇原创文章 · 获赞 6 · 访问量 192

猜你喜欢

转载自blog.csdn.net/weixin_43915798/article/details/104977533
今日推荐