金融科技之情感分析(一):股民情緒指數與股市價格的相關性分析

金融科技之情感分析(一):股民情緒指數與股市價格的相關性分析


本文是我在一家金融機構實習時做的第一個項目的整理。蒟蒻一枚,是金融科技和人工智能的初學者,寫下本篇博文意在記錄自己的足跡並與和我一樣想要從事金融科技行業的小夥伴分享我遇到的問題和解決方案。如果本文有錯誤的地方,請大佬們指正。

前言

在投資者社區中有許多文本型數據,我們可以對這些文本型數據進行處理,挖掘出有價值的信息。比如,東方財富股吧中有很多用戶的發言、評論,我們可以藉助自然語言處理中的情感分析技術,挖掘出市場的情緒指數,並研究情緒指數與金融市場的相關性。這個項目做的就是這個工作。

主要涉及爬蟲自然語言處理相關性分析以及有限的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語句下的代碼並沒有出現異常,但存儲的數據並非我們想要的內容。這需要我們在實踐的過程中不斷總結、尋找規律、判斷並排除這類情況。
至此,獲取數據我們已經介紹完了。

數據存儲和查詢

在利用爬蟲技術獲取到我們需要的數據後,便需要將數據存儲以待後續處理。數據存儲有很多方法,在這裏,我們將其存儲到數據庫中。
這裏使用的是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,代表呈現出強的相關性。從圖像可以更直觀看出:
在這裏插入圖片描述
至此,我們已經完成了所有的步驟並得出了比較好的結果。

如有不足之處,歡迎指教。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章