python文本数据分析——以《射雕英雄传》为例

本次受华师和武大研会共同举办的“python训练营”之邀,来给大家做一期“python数据分析”主题的分享。疫情期间,研会的朋友为大家组织一次python训练营也是十分的不容易,那我这次就尽量把我所掌握的分享给大家,不过较为基础,大佬绕道哈哈!本次分享的内容以这篇文章为课件,这篇文章写的很详细,在直播中我带大家过一遍,如果因时间问题分享不完的话,自行参照代码学习,也能很快掌握!直播回访地址,我是第四期的分享!本次课程分享文件可在公众号回复“20200419”获得。

前言

python无疑是当下最火爆的语言,你从微信朋友圈时不时的python广告和地产大佬潘石屹的学python微博就可窥见一斑。这次研会的同学组织了四次python主题,我个人觉得这刚好是学习python的四个不同阶段的主题,再换一下顺序可能就更贴切了:python入门分享、python爬虫入门、python数据分析、python深度学习。python以其简洁的语言吸引了很多同学的学习,但相信大多数同学可能就止步于一二步了吧,我也一样哈哈,由于本科阶段做了一点项目,所以相较于大多数同学,我在python上花的时间多一点,但我也仅仅是到了python爬虫入门这个阶段,如果这次是来邀请我分享爬虫的话,我想我会很得心应手的,但这次给我的主题是数据分析,可是把我难倒了好久,我的python数据分析生涯仅仅在于爬完数据后对数据的一个简单的清理和平常自己论文里的一些实证实现。python数据分析应用非常广泛,在金融、数理统计、文本处理等方面都有很好的应用,也具备很高的商业价值,但是相对而言它对涉及的理论知识也有一定的要求。我本来打算这周恶补一些python数据分析的numpy、scipy、pandas、matplotlib这些python包,然后讲一下这些包的基础操作,但后来意识到这样的分享会很枯燥无聊,而且关于包的基础操作大家完全可以在网上自行搜索。于是我就斗胆决定分享一下一些我做过的关于python文本数据分析的工作,用《射雕英雄传》这个例子把它们串起来。我的专业是情报学,我的数据分析经历大多数在于给自己或者师兄、师姐、老师论文里的实证部分实现的过程,所以我今天就只分享一些我做过的一些涉及情报学理论的文本信息处理,相信可能会对大家使用python应用于自己论文中有一点点的帮助。本次分享过程没有从《射雕英雄传》中获得任何的理论或者发现(哈哈),只是单纯根据个人喜好借用一下金庸老爷子的名作,重点是利用python代码复现情报学领域部分数据分析方法。

目录

  • 1.采集语料
  • 2.利用jieba分词进行数据预处理
    • 2.1 分词
    • 2.2 载入自定义词典和去除停用词
    • 2.3 关键词提取
  • 3.cos余弦值计算
    • 3.1 文本余弦相似度原理
    • 3.2 以简单的例子来说明一下文本余弦相似度的计算过程
    • 3.3 python代码实现
  • 4.基于朴素贝叶斯的书评情感判断
    • 4.1 朴素贝叶斯介绍
    • 4.2 利用朴素贝叶斯进行情感判断
    • 4.3 代码实现

正文

1.语料采集

语料采集也就是之前一次“python爬虫入门”那次主题的内容,这里我就直接展示一下本次要使用到的语料。我们本次使用射雕英雄传前三回的文本和一部分由我杜撰的书评。

射雕英雄传节选

首先看到上图的文本我们就发现了第一个问题,分行不太对,这样会导致分词过程出现错误(这可能源于这个文本是爬取自某个在线小说阅读的数据),所以先来解决它。
在解决这个错误之前,我们先来写一个函数,这个函数的作用是去掉文本中的所有分行和空格,利用一下这个方法获取一下文本然后重新存到新的文档里面,主函数这样写。其中line.replace("", "")是一个文本替换函数,\n就是文本文档中的换行; f = open(after, 'w', encoding='utf-8')里面的参数要注意,w是指覆盖写入,a为覆盖,r为只读。

def get_current_txt(path):
    sentences = ''
    for line in open(path, encoding='utf-8'):
        line = line.replace("\n", "")
        line = line.replace(" ", "")
        sentences = sentences + line
    return sentences
if __name__ == '__main__':
    txt_name = [['射雕英雄传第一章.txt','1.txt'],
                ['射雕英雄传第二章.txt','2.txt'],
                ['射雕英雄传第三章.txt','3.txt']]
    for txt in txt_name:
        before = '添加你自己文档位置' + txt[0]
        after = '添加你自己文档位置' + txt[1]
        t = get_current_txt(before)
        f = open(after, 'w', encoding='utf-8')
        f.write(t)

运行之后的文件变成了这样


解决分行问题

这样一来,所有文本只有一行,这虽然对我们的阅读带来了极大的不便,但是对我们分析文本没有丝毫的影响。

2.1分词

这里我们利用一下jieba分词包,对于中文文本处理,结巴分词使用的还是挺多的,还有就是中科院的文本处理包。
jieba分词有四种分词模式,一般习惯用精确模式。

# encoding=utf-8
import jieba
seg_list = jieba.cut("日日夜夜无穷无休的从临安牛家村边绕过", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式
#输出结果:Default Mode: 日日夜夜/ 无穷/ 无休/ 的/ 从/ 临安/ 牛家村/ 边/ 绕过

2.2载入自定义词典和去除停用词

这里分词的词典使用的是jieba分词自带的词典,但有些时候jieba分词自带的词典可能不包含我们需要的词(例如网络专有名词),我们在这段话里以“无穷无休”为例,假设这个词我们需要他作为一个四字成语单独出现,那么这个时候需要我们自定义一个词典,我们新建一个自定义词典。


自定义词典userdict.txt

然后在代码中加载自定义词典,结果就会出现我们想要的无穷无休

# encoding=utf-8
import jieba
jieba.load_userdict("userdict.txt")
seg_list = jieba.cut("日日夜夜无穷无休的从临安牛家村边绕过", cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式
#输出结果:Default Mode: 日日夜夜/ 无穷无休/ 的/ 从/ 临安/ 牛家村/ 边/ 绕过

可以看到,分词结果里面的类似于“的”这样的没有实际意义的词,也就是属于我们所说的停用词范围,在进行文本信息处理的时候我们一般会选择过滤掉他们也就是除去停用词,jieba分词无自带停用词词典,所以一般自行构建停用词词典进行过滤。我这里整理了一个包含1894个中文停用词的停用词此表。


停用词表

用代码尝试一下

# encoding=utf-8
import jieba
jieba.load_userdict("userdict.txt")
filepath = "stopwords.txt"
stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]
seg_list = jieba.cut("日日夜夜无穷无休的从临安牛家村边绕过", cut_all=False)
for i in seg_list:
    if i not in stopwords:
        print(i)
#输出结果:
#日日夜夜
#无穷无休
#临安
#牛家村
#绕过

以上就是简单的分词和去停用词的步骤,这样看其实一点都不难,下面我们把去停用词封装成一个函数,这样以后你就可以那这个函数去处理你的任意的需要分词和去停用词的文本。get_current_txt()对上面的get_current_txt简化了一下,使其适用于分词和去停用词的程序;cutWithNoSW(fileToCut,resultFile)就是分词且去掉停用词,输入待分词文件,输出分词结果。

import jieba
def get_current_txt(path):
    sentences = ''
    for line in open(path, encoding='utf-8'):
        sentences = sentences + line
    return sentences
def cutWithNoSW(fileToCut,resultFile):
    content_cut = get_current_txt(fileToCut)
    content_result = ''
    jieba.load_userdict("/Users/chenjianyao/Desktop/python数据分析/文档/userdict.txt")
    filepath = "/Users/chenjianyao/Desktop/python数据分析/文档/stopwords.txt"
    stopwords = [line.strip() for line in open(filepath, 'r', encoding='utf-8').readlines()]
    seg_list = jieba.cut(content_cut, cut_all=False)
    for i in seg_list:
        if i not in stopwords:
            content_result = content_result + i + '\n'
    print(content_result)
    f = open(resultFile, 'w', encoding='utf-8')
    f.write(content_result + '\n')
if __name__ == '__main__':
    txt_name = [['1.txt','1_cut.txt'],
                ['2.txt','2_cut.txt'],
                ['3.txt','3_cut.txt']]
    for txt in txt_name:
        before = '文档/' + txt[0]
        after = '文档/' + txt[1]
        cutWithNoSW(before, after)

得到结果:


第一章cut

2.3关键词提取

jieba分词提供了基于tf-idf和textRank两种算法提取关键词,直接上代码解释:

import jieba.analyse
def get_current_txt(path):
    sentences = ''
    for line in open(path, encoding='utf-8'):
        sentences = sentences + line
    return sentences
if __name__ == '__main__':
    txt_name = ['1_cut.txt','2_cut.txt','3_cut.txt']
    for txt in txt_name:
        filePath = '/Users/chenjianyao/Desktop/python数据分析/文档/' + txt
        keyword1 = jieba.analyse.extract_tags(get_current_txt(filePath), topK=3, withWeight=False, allowPOS=())
        keyword2 = jieba.analyse.textrank(get_current_txt(filePath), topK=3, withWeight=False, allowPOS=('ns', 'n', 'vn', 'v'))
        print("基于TF-IDF的关键词:",txt,keyword1)
        print("基于textrank的关键词:", txt, keyword1)
#运行结果:
#基于TF-IDF的关键词: 1_cut.txt ['杨铁心', '包惜弱', '郭啸天']
#基于textrank的关键词: 1_cut.txt ['杨铁心', '包惜弱', '郭啸天']
#基于TF-IDF的关键词: 2_cut.txt ['丘处机', '段天德', '完颜洪烈']
#基于textrank的关键词: 2_cut.txt ['丘处机', '段天德', '完颜洪烈']
#基于TF-IDF的关键词: 3_cut.txt ['铁木真', '哲别', '郭靖']
#基于textrank的关键词: 3_cut.txt ['铁木真', '哲别', '郭靖']

3.cos余弦相似度计算

3.1文本余弦相似度原理

cos余弦相似度在文本数据分析领域经常用到,cos余弦相似度共识很好理解,如果能自行用python代码实现一遍cos余弦相似度的计算,相信你以后遇到类似的相似度计算都没有问题。


数学公式

转换到文本数据领域的话,公式就变成了


文本余弦相似度

把两个文本A和B向量化为由词语所表示的文档A=[x1,x2,x3,...,xn],B=[y1,y2,y3,...,yn],然后通过上述公式进行计算,但前提是必须要保证A,B向量的向量维度一致,这个很重要,后面讲。

3.2 以简单的例子来说明一下文本余弦相似度的计算过程

以《射雕英雄传》第三章里的部分段落“完颜洪熙见郭靖等许多蒙古小孩站在远处,睁大了小眼,目不转瞬的瞧着,便哈哈大笑,探手入怀” 为例。我们人工分词把这段文本切分成:
A = ['完颜洪熙','见','郭靖','蒙古','小孩','站','远处']
B = ['睁大','小眼','目不转瞬','瞧着','哈哈大笑','探手入怀','郭靖']
为了使最终的相似度不为0,手动在B向量后面加一个‘郭靖’。下一步构造两个文本向量的交集,C向量相当于包含A,B向量所有词汇的向量,这就把A,B向量统一到了一个维度里。
C = ['完颜洪熙','见','郭靖','蒙古','小孩','站','远处','睁大','小眼','目不转瞬','瞧着','哈哈大笑','探手入怀']
按照C向量把A,B向量按照词频数字化:
A = [1,1,1,1,1,1,1,0,0,0,0,0,0]
B = [0,0,1,0,0,0,0,1,1,1,1,1,1]
计算两者的余弦相似度


image.png

最后得到相似度为0.143.

3.3python代码实现

首先获取分词结果,这个get_current_txt和前两个get_current_txt又不一样了,它主要按行获取txt中的文本,存在列表里,返回一个list->sentences

def get_current_txt(path):
    sentences = []
    for line in open(path, encoding='utf-8'):
        line = line.replace("\n", "")
        sentences.append(line)
    return sentences

还记得上面的构造A,B向量的集合C吗?这里也要通过代码实现,set()集合有一个非常好用的优点,就是他可以实现类似于自动去重排序的功能,我们输入一个训练集集合dataSet(也就是我们这里所要用到的A+B),他就会返回一个一个对dataSet自动去重然后排序的结果,列表化之后返回vocabList词汇列表。

def createVocabList(dataSet):
    vocabSet = set()
    for doc in dataSet:
        vocabSet = vocabSet | set(doc)
        vocabList = list(vocabSet)
    return vocabList

好了下面就到了计算cos余弦相似度的过程了,这里我们利用了python的数据字典dict,数据字典可以统计词频出现的次数。首先获得词汇列表c

c = createVocabList(a+b)

然后把输入的两个文本词汇向量a,b转换为词典dict_a,dict_b

    dict_a = {}
    dict_b = {}
    for key in a:
        dict_a[key] = dict_a.get(key, 0) + 1
    for key in b:
        dict_b[key] = dict_b.get(key, 0) + 1
#字典形式:dict = {'江南七怪': 22, '颜烈': 27, '跨出': 1, '房门': 1}

抽取出数据字典中的词频形成词频向量vec_a,vec_b

    vec_a = []
    vec_b = []
    for key in c:
        if key in dict_a:
            vec_a.append(dict_a[key])
        else:
            vec_a.append(0)
    for key in c:
        if key in dict_b:
            vec_b.append(dict_b[key])
        else:
            vec_b.append(0)

下面是计算余弦值的代码:

    sum = 0
    sq1 = 0
    sq2 = 0
    for i in range(len(vec_a)):
        sum += vec_a[i] * vec_b[i]
        sq1 += pow(vec_a[i], 2)
        sq2 += pow(vec_b[i], 2)
    try:
        result = round(float(sum) / (math.sqrt(sq1) * math.sqrt(sq2)), 2)
    except ZeroDivisionError:
        result = 0.0

好了,计算余弦相似度的代码基本都已完成了,我们把上述零零散散的代码汇总一下

import math
def get_current_txt(path):
    sentences = []
    for line in open(path, encoding='utf-8'):
        line = line.replace("\n", "")
        sentences.append(line)
    return sentences
def createVocabList(dataSet):
    vocabSet = set()
    for doc in dataSet:
        vocabSet = vocabSet | set(doc)
        vocabList = list(vocabSet)
    return vocabList
def getCos(a,b):
    c = createVocabList(a+b)
    dict_a = {}
    dict_b = {}
    for key in a:
        dict_a[key] = dict_a.get(key, 0) + 1
    for key in b:
        dict_b[key] = dict_b.get(key, 0) + 1
    vec_a = []
    vec_b = []
    for key in c:
        if key in dict_a:
            vec_a.append(dict_a[key])
        else:
            vec_a.append(0)
    for key in c:
        if key in dict_b:
            vec_b.append(dict_b[key])
        else:
            vec_b.append(0)
    sum = 0
    sq1 = 0
    sq2 = 0
    for i in range(len(vec_a)):
        sum += vec_a[i] * vec_b[i]
        sq1 += pow(vec_a[i], 2)
        sq2 += pow(vec_b[i], 2)
    try:
        result = round(float(sum) / (math.sqrt(sq1) * math.sqrt(sq2)), 2)
    except ZeroDivisionError:
        result = 0.0
    # print(result)
    return result
if __name__ == '__main__':
    txt_A = get_current_txt('文档1.txt')
    txt_B = get_current_txt('文档2.txt')
    print(getCos(txt_A, txt_B))

用这个代码可以实现中英文任意文献的相似度计算,我们是用这个代码计算射雕前三章的相似度,得到下述结果:

文档 第一章 第二章 第三章
第一章 1 0.88 0.91
第二章 0.88 1 0.86
第三章 0.91 0.86 1

仔细研读一下这个结果会发现非常的有意思!



就我个人经验,相似度都是很稀疏的,往往很多文档之间的相似度只有0.0几或者不超过0.5这样的,遇到这种情况往往需要归一化去处理。但是你看看金庸老爷子的章节之间相似度有多大,达到了0.91,这就说明作者在语言习惯、写作手法、用词手法等方面都有自己的一套,所以才会得到这么高的相似度。或许可以拿cos去判断红楼梦后四十章到底是谁写的?

4.基于朴素贝叶斯的书评情感判断

4.1 为何分享朴素贝叶斯

按道理讲,朴素贝叶斯应该属于机器学习的行列,但是为什么今天把它放到数据分析环节呢?一方面我个人觉得其实我们对文本进行数据分析目的也是为了从中得到我们想要的信息或者是证明我们的理论,这就回到了情报学的初衷,从信息中获取更有价值的信息,从这一角度来看类似于朴素贝叶斯这类的机器学习算法都应该数据数据分析的行列;另一方面,从难易程度来讲,朴素贝叶斯绝对是大家了解数据分析或者机器学习算法最简单的入门算法,因此我就斗胆以此为例给大家分享一下

4.2 朴素贝叶斯简介

在讲公式原理之前,先举一个例子!
我就举一个嫁不嫁的例子!
假如现在有一男,已知特征为(帅,无车,无房,上进),各位女性听众面对这样一个男生会选择嫁吗?当你犹豫不决的时候,先去看看你身边的案例,列一个表,看看你的闺蜜or你的女性朋友都嫁给了什么样的男性?

帅不帅 有无车 有无房 是否上进 嫁不嫁
不帅
不嫁
不帅
不帅 不嫁
不嫁

根绝已有的案例,试着利用朴素贝叶斯公式决定你嫁不嫁。
朴素贝叶斯公式:




转换为特征表达式:



将困扰这位女生嫁不嫁的问题转换为概率问题:
p(嫁|帅,无车,无房,上进)
p(不嫁|帅,无车,无房,上进)
最后得到谁的概率大就怎么做。

计算过程我在word中写了好了截过来:


4.3 代码实现

上面觉得嫁不嫁的例子是表明了朴素贝叶斯利用特征进行分类的过程。
应用于文本分类稍微有点不同,可以去这里补习一下朴素贝叶斯文本分类
应用于文本分类的朴素贝叶斯公式是这样的

文本分类的朴素贝叶斯公式

简单的理解就是这个文本属于哪一个类别,就用它的先验概率乘以他每个单词的似然概率。我们从豆瓣书评中获取了几条训练集和测试集,利用朴素贝叶斯识别训练集的情感极性。

    train_post = [
        ['当然','我','最','热爱','还是','83','电视剧'],
        ['最爱','一部','靖蓉','之间','感情','真是','美好'],
        ['这部','是','最','好看','喜欢']

    ]
    train_nega = [
        ['不喜欢','郭靖','不喜欢','黄蓉','因为','先看了','神雕侠侣'],
        ['我','真的','很','讨厌','郭靖','很','讨厌','很','讨厌','还有','江南七怪'],
        ['一个','智商','低','一个','情商','低','这','俩','兄弟','真','绝了']
    ]
    test = [
        ['从小','就','热爱','射雕','喜欢','里面','人物','感情'],
        ['不喜欢','郭靖','江南七怪','尤其','柯镇恶','打架','没赢过','装逼','没输过'],
    ]

首先写一个获取文档数量和词汇数量的方法

def createVocabList(dataSet):
    vocabSet = set()  # 创建一个空的集合
    for doc in dataSet:  # 遍历dataSet中的每一条言论
        vocabSet = vocabSet | set(doc)  # 取并集,set()可执行去重功能
        vocabList = list(vocabSet)
    voc_num = len(vocabList)
    doc_num = len(dataSet)
    return voc_num, doc_num

然后写一个判断某个单词x在文档中出现的次数,以及这个文档的总词数

def count2list(list, x):
    show_num = 0
    word_num = 0
    for i in list:
        show_num = show_num + i.count(x)
        word_num = word_num + len(i)
    return show_num, word_num

文档的先验概率计算方法:

# 先验概率
    xianyan_post = doc_num_post / doc_num
    xianyan_nega = doc_num_nega / doc_num

计算测试集的每个单词的似然概率

    # 保存每个单词似然概率的列表
    per_word_post = []
    per_word_nega = []
    for i in test:
        # 判断i在post文档中出现了几次以及相应的文档出现的总词数
        show_num, word_num = count2list(train_post, i)
        per_word_post.append((show_num + 1) / (word_num + voc_num))
        show_num, word_num = count2list(train_nega, i)
        per_word_nega.append((show_num + 1) / (word_num + voc_num))

接着进行概率判断:

    # 概率判断
    p_post = xianyan_post * reduce(lambda x, y: x * y, per_word_post)
    p_nega = xianyan_nega * reduce(lambda x, y: x * y, per_word_nega)
    print('post概率', p_post)
    print('nega概率', p_nega)
    if p_post > p_nega:
        print("该文本情感极性是post")
    else:
        print("该文本情感极性是naga")

综合一下上述的代码就得到:

from functools import reduce

def createVocabList(dataSet):
    vocabSet = set()  # 创建一个空的集合
    for doc in dataSet:  # 遍历dataSet中的每一条言论
        vocabSet = vocabSet | set(doc)  # 取并集,set()可执行去重功能
        vocabList = list(vocabSet)
    voc_num = len(vocabList)
    doc_num = len(dataSet)
    return voc_num, doc_num

def count2list(list, x):
    show_num = 0
    word_num = 0
    for i in list:
        show_num = show_num + i.count(x)
        word_num = word_num + len(i)
    return show_num, word_num


def classifier(test, voc_num, doc_num, doc_num_nega,  doc_num_post):
    # 先验概率
    xianyan_post = doc_num_post / doc_num
    xianyan_nega = doc_num_nega / doc_num
    print(xianyan_post, xianyan_nega)
    # 保存每个单词似然概率的列表
    per_word_post = []
    per_word_nega = []
    for i in test:
        # 判断i在post文档中出现了几次以及相应的文档出现的总词数
        show_num, word_num = count2list(train_post, i)
        per_word_post.append((show_num + 1) / (word_num + voc_num))
        show_num, word_num = count2list(train_nega, i)
        per_word_nega.append((show_num + 1) / (word_num + voc_num))
    # 概率判断
    p_post = xianyan_post * reduce(lambda x, y: x * y, per_word_post)
    p_nega = xianyan_nega * reduce(lambda x, y: x * y, per_word_nega)
    print('post概率', p_post)
    print('nega概率', p_nega)
    if p_post > p_nega:
        print("该文本情感极性是post")
    else:
        print("该文本情感极性是naga")


if __name__ == '__main__':
    train = []
    train_post = [
        ['当然','我','最','热爱','还是','83','电视剧'],
        ['最爱','一部','靖蓉','之间','感情','真是','美好'],
        ['这部','是','最','好看','喜欢']

    ]
    train_nega = [
        ['不喜欢','郭靖','不喜欢','黄蓉','因为','先看了','神雕侠侣'],
        ['我','真的','很','讨厌','郭靖','很','讨厌','很','讨厌','还有','江南七怪'],
        ['一个','智商','低','一个','情商','低','这','俩','兄弟','真','绝了']
    ]
    test = [
        ['从小','就','热爱','射雕','喜欢','里面','人物','感情'],
        ['不喜欢','郭靖','江南七怪','尤其','柯镇恶','打架','没赢过','装逼','没输过'],
    ]
    train = train_post + train_nega
    print(train)
    voc_num, doc_num = createVocabList(train)
    voc_num_nega, doc_num_nega = createVocabList(train_nega)
    voc_num_post, doc_num_post = createVocabList(train_post)
    for i in test:
        print(i)
        classifier(i, voc_num, doc_num, doc_num_nega,  doc_num_post)

你可能感兴趣的:(python文本数据分析——以《射雕英雄传》为例)