3.1 Text Mining文本挖掘python练习

本章的重点是使用python进行自然语言处理(NLP)。

我会结合具体案例——使用机器学习算法对电子邮件进行分类,看看是不是垃圾邮件。因此这些习题涉及到supervised learning过程。在数据集里面,每个电子邮件的标签都已经给定,我们希望使用这个数据集训练模型,以便能够将代码逻辑嵌入到应用程序里。

先看一眼数据

spam.csv

作为数据分析师,工作的第一步就是看数据,以便对数据有初步了解。
首先我们看到csv文件中有两列,分别是label标签,和文本text。由此可知这是个典型的有监督机器学习问题。
然后看仔细,在文件中两列是由波浪线分隔开的,因此在导入数据的时候要注意设置这个参数。

以下是本章的大纲,我会对解决问题的思路和常见问题以及某些函数的用法进行阐述:

Outline

  1. 使用pandas读入数据并进行初步的data understanding
  2. 进行data preprocessing,如果发现重复的文本内容,则进行去重
  3. 在原有两列的基础上新增一个特征列,用于表达每个电子邮件文本的字数,并进行可视化以加深对数据集的理解,这里可以用直方图,分别用不同 的颜色表示正常邮件(ham)和垃圾邮件(spam),通过两种邮件的频数分布,我们大致可以得到一个基于字数的分类标准
  4. 对电子邮件的文本text进行处理,以便能更好地适应后面用到的模型。包括:去掉停止词stopwords,去掉特殊符号,文本统一改成小写,以及把所有单词转化成其词根的形式
  5. 把垃圾邮件和正常邮件中,各自最频繁出现的10个单词进行可视化,可以使用条形图或词云
  6. 对单词的频率进行分析,创建一个tfidf矩阵,并把标签中的“spam”和“ham”编码,用数字形式表示标签的内容
  7. 训练模型,用于训练的数据占原始数据的7成,并保证每次执行代码,输出的结果相同。我在这里选择逻辑回归LogisticRegressionMultinomial Naive Bayes

导入需要的库

# basics
import random
import string
import pandas as pd

# visualization
import matplotlib.pyplot as plt
import seaborn as sns
import plotly_express as px

# Natural Language Processing
from nltk.corpus import stopwords
import nltk
# nltk.download('stopwords')
# nltk.download('wordnet')
# nltk.download('punkt')
from wordcloud import WordCloud

# Machine Learning
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import MultinomialNB
from sklearn.metrics import accuracy_score,confusion_matrix,precision_score, f1_score, recall_score

*注意:我在导入nltk之后,注释掉了三行download,这三行指令只需要执行一次就好,用于下载stopwords,wordnet和punkt三个文件。网上很多教程教我们自己手动下载,然后配置环境变量,这样太复杂了,我们只需要在import nltk后面跟上这三句指令就好

1. 读入数据和Data Understanding

data = pd.read_csv("spam.csv", sep='~')

*这里只需要注意波浪线,要使用sep参数

然后输出前几行,看看读入数据的效果怎样:


看上表,数据集是由两列组成,一列是标签,另一列是文本。我们把ham标签看作是我们需要的正常邮件,而spam标签表示垃圾邮件

还可以使用data frame 的shape属性和info函数:



describe()

*注意:在describe函数的输出中,有每列的总行数count和独一无二的行数unique,这二者的差异给我们提供了信息。此外,top表示列中出现次数最多的行

要想获取垃圾邮件和正常邮件的数量,也可以用value_counts()函数

接下来用上一步的输出做一个饼图,看看数据集里垃圾邮件和正常邮件的比重分别是多少,这样更直观:

fig = plt.figure(figsize=(8, 5))
plt.pie(data['label'].value_counts(), labels = ['ham', 'spam '])  # 查pie函数的用法
plt.show()

pie() 方法语法格式如下:

matplotlib.pyplot.pie(x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=0, radius=1, counterclock=True, wedgeprops=None, textprops=None, center=0, 0, frame=False, rotatelabels=False, *, normalize=None, data=None)[source]

参数说明:

x:浮点型数组,表示每个扇形的面积。

explode:数组,表示各个扇形之间的间隔,默认值为0。

labels:列表,各个扇形的标签,默认值为 None。

colors:数组,表示各个扇形的颜色,默认值为 None。

autopct:设置饼图内各个扇形百分比显示格式,%d%% 整数百分比,%0.1f 一位小数, %0.1f%% 一位小数百分比, %0.2f%% 两位小数百分比。

labeldistance:标签标记的绘制位置,相对于半径的比例,默认值为 1.1,如 <1则绘制在饼图内侧。

pctdistance::类似于 labeldistance,指定 autopct 的位置刻度,默认值为 0.6。

shadow::布尔值 True 或 False,设置饼图的阴影,默认为 False,不设置阴影。

radius::设置饼图的半径,默认为 1。

startangle::起始绘制饼图的角度,默认为从 x 轴正方向逆时针画起,如设定 =90 则从 y 轴正方向画起。

counterclock:布尔值,设置指针方向,默认为 True,即逆时针,False 为顺时针。

wedgeprops :字典类型,默认值 None。参数字典传递给 wedge 对象用来画一个饼图。例如:wedgeprops={'linewidth':5} 设置 wedge 线宽为5。

textprops :字典类型,默认值为:None。传递给 text 对象的字典参数,用于设置标签(labels)和比例文字的格式。

center :浮点类型的列表,默认值:(0,0)。用于设置图标中心位置。

frame :布尔类型,默认值:False。如果是 True,绘制带有表的轴框架。

rotatelabels :布尔类型,默认为 False。如果为 True,旋转每个 label 到指定的角度。

2. 检查重复数据,并删除数据集里面的重复数据

*注意:我们想做的是对邮件进行分类,因此重复的数据对我们来说没用。我们只关心某个邮件是否是垃圾邮件

# 这里的思路就是看看删除重复项之前和之后,重复项的数量的变化
print(data.duplicated().sum())
print(data.duplicated())
data = data.drop_duplicates(keep='first')
print(data.duplicated().sum())
df.dublicated()

df.dublicated()函数会检查调用该函数的dataFrame中是否包含重复的行,如果有重复,则在该位置返回True

*注意:在执行完data.drop_duplicates(keep='first')之后,数据集就不在包含重复的行了,并且只要发现有重复,则保留第一个

3. 创建一个新的特征feature,用来表示文本的长度(df中增加一列)

data['len'] = data['text'].apply(len)
print(data.head())

现在我想看看data中的文本长度的分布情况,因此进行以下操作,发现最长的邮件居然有910个单词,我想读一下这么长的邮件


有以下三种输出方式,第三种最好:

print(data[data.len == 910])  # print all the entry
print(data[data.len == 910]['text'])
print(data[data.len == 910]['text'].iloc[0]) 

再来输出第一个长度是155的文本:


然后是所有长度为155 的文本:


继续进行data understanding。写一个函数,按照以下格式进行输出:标签:文本

def printtxts(df):
    txts = df['text']
    lab = df['label']

    for ind in range(len(txts)):
        print(f'{lab.iloc[ind]}: {txts.iloc[ind]}', end='\n\n')


# 输出数据集里面,文本长度等于155 的标签与文本信息
printtxts(data[data.len == 155][: 10])
# 不难发现,这里出现了很多垃圾邮件标签

较长的文本中,属于垃圾邮件的比较多。那么试试稍短一些的文本,

printtxts(data[data['len'] == 25][: 10])
# 在较短的邮件集合中,不难发现几乎没有垃圾邮件。这对数据分析工作来说是个很重要的趋势

这10个数据都不是垃圾邮件,说明短一些的邮件是正常邮件的几率比较大。

做个直方图,更直观:

px.histogram(data, 'len', nbins= 182, color='label')

# 看下图,大多ham邮件都较短,而spam垃圾邮件则普遍较长

现在尝试在仅仅考虑文本长度的情况下,创建一个分类器:长的邮件是垃圾,短的不是.这可能是一个很好的分类标准:

print(data[(data.len >= 90) & (data.len <= 170)]['label'].value_counts()["spam"])
print(data.label.value_counts()['spam'])

前面说的长度是字符串的长度,即一个字符串中有多少个字符(空格和标点也算),现在再来增加一个新的属性,words,表示一个邮件中包含多少个单词。要实现这一步,需要用到nltk.word_tokenize()函数。:

# split strings into section is tokenization
data['words'] = data['text'].apply(lambda x: len(nltk.word_tokenize(x))) # to produce a list of words
round(data['words'].describe(), 2)

# 注意这里的最小值min,最短的邮件只有一个单词
单词数量

现在再来看一下我们的数据data:


按照单词数的频数分布,制作直方图。因为在语义学中,意思的最小单位是词汇。因此单词数量在识别垃圾邮件方面是个更好用的指标。不难看出,这个直方图的分布情况和基于字母数量的直方图类似。

px.histogram(data, "words", nbins=44, color="label")

数据预处理data preprocessing

  • 文本处理的一个重要的库:string
    当我们对字符串进行操作时,如果感觉代码很复杂,可以借助string库,里面有很多实用的属性。
>>> import string
>>> dir(string)
['Formatter', 'Template', '_ChainMap', '_TemplateMetaclass', '__all__', '__built
ins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__packag
e__', '__spec__', '_re', '_string', 'ascii_letters', 'ascii_lowercase', 'ascii_u
ppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctua
tion', 'whitespace']
>>> string.ascii_lowercase  #所有的小写字母
'abcdefghijklmnopqrstuvwxyz'
>>> string.ascii_uppercase  #所有的大写字母
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
>>> string.hexdigits        #所有的十六进制字符
'0123456789abcdefABCDEF'
>>> string.whitespace       #所有的空白字符
' \t\n\r\x0b\x0c'
>>> string.punctuation      #所有的标点字符
'!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~'
  • 当我们统计一个字符串中所有的单词出现的次数时,由于句子中存在标点符号,直接对字符串进行分割(split)会把单词和标点分割到一起,例如:


    单词和符号被分割到一起
  • 如果指定标点符号进行切割,在句子很长或者有多个标点的情况下会非常复杂。解决方案如下:
    先把句子中的标点符号统一替换为空格,然后用split分割即可。替换操作可以用list comprehension来完成,判断标点符号则需要用到string.punctuation。



  • 去掉标点之后可以用split进行字符串分割。这里就不作 演示了。后面会讲一种更好的方法:tokenization

在对data进行数据预处理之前,我先来处理一个字符串,以便于理解数据预处理的流程。

# 4. 在充分了解数据集之后,我们需要做一些数据预处理data preprocessing ---NLP
# 首先用一个长字符串实验一下
ex = 'I am a student of Business Informatics at the TU Dresden!!:) I would like to learn more about NLP...'

punct = list(string.punctuation) # 第一步:移除ex中的特殊符号。需要掌握punctuation属性和list comprehension
print(punct)

# a) 移除特殊符号
nop = [lett for lett in ex if lett not in punct]
clean = ''.join(nop) # join 的返回值是什么

print(clean) # 特殊符号已经被移除了
print(type(clean))
# 现在开始处理data,
# b) 移除stopwords, 因为stopwords并不会增加任何信息. NLTK包中已经给出了stopwords,可以直接拿来用

print(random.sample(stopwords.words('english'), 10))
print(random.sample(stopwords.words('german'), 10))

clean = [word for word in str(clean).split(" ") if not word  in stopwords.words('english')]
print(clean)
# 上方单元格已经移除对于句子含义没用用处的停止词,现在进行第二步:
# c) 统一大小写
clean = [word.lower() for word in clean]

print(clean)
# d) 把字符串中的词汇转换成原型(包括但不限于,名词的单复数形式,动词的不同时态,形容词比较级等)。这样做的目的是为了让程序更好地理解句子的意思
ps = nltk.stem.PorterStemmer()
lemma = nltk.wordnet.WordNetLemmatizer()

print(ps.stem('walking'))


# import nltk
# nltk.download('omw-1.4')
print(lemma.lemmatize('walking', nltk.corpus.wordnet.VERB))
# 这里是对现在分词的lemmatization
# 如果使用过去式,则会有不同的结果。在实际使用中,需要结合实际需求

print(ps.stem('went')) 
print(lemma.lemmatize('went', nltk.corpus.wordnet.VERB))
# start lemmatize of ex sentence
print('stemming')
print([ps.stem(word) for word in clean])

print('lemmatize')
print([lemma.lemmatize(word, nltk.corpus.wordnet.VERB) for word in clean]) # using dictionary words
# 现在开始对我们的数据集也进行以上四个操作,提示:可以将四种操作按照顺序封装在一个函数中,然后把这个自定义的函数apply到数据集
def preproc(message):
    nostop = " ".join([word for word in message.split(' ') if word not in stopwords.words('english')])
    nopunct = ''.join([lett for lett in nostop if lett not in list(string.punctuation)])
    nocaps = [word.lower() for word in nopunct.split(' ')]
    norm = [ps.stem(word) for word in nocaps]

    return ' '.join(norm)

data['prc_text'] = data['text'].apply(preproc)
print(data.head())
# 5. 数据可视化,尝试输出垃圾邮件和正常邮件中最经常出现的单词
ham_text = " ".join(data[data.label == 'ham']['prc_text'])
spam_text = " ".join(data[data['label'] == 'spam']['prc_text'])

# 计算单词的频率
ham_freq = nltk.FreqDist(nltk.word_tokenize(ham_text))
spam_freq = nltk.FreqDist(nltk.word_tokenize(spam_text))
# 把上方的两个频率进行可视化
spam_freq_df = pd.DataFrame.from_dict(dict(spam_freq), 'index', columns=['count']).reset_index()
sns.barplot(data = spam_freq_df.sort_values('count', ascending=False)[: 15], x = 'index', y = 'count')
plt.show()
ham_freq_df = pd.DataFrame.from_dict(dict(ham_freq), 'index', columns=['count']).reset_index()
sns.barplot(data = ham_freq_df.sort_values('count', ascending=False)[: 15], x = 'index', y = 'count')
plt.show()
# 現在嘗試使用词云

wc = WordCloud(width = 500, height = 500, min_font_size = 10, background_color = 'white')

spam_wc = wc.generate(data[data['label'] == 'spam']['prc_text'].str.cat(sep = " "))

plt.figure(figsize = (20, 8))
plt.imshow(spam_wc)
plt.show()
wc = WordCloud(width = 500, height = 500, min_font_size = 10, background_color = 'white')

ham_wc = wc.generate(data[data['label'] == 'ham']['prc_text'].str.cat(sep = " "))

plt.figure(figsize = (20, 8))
plt.imshow(ham_wc)
plt.show()
# 6. tfidf
data.head()
# 构建一个tfidf矩阵,即二维数组,
tfidf = TfidfVectorizer(max_features = 3000)
x = tfidf.fit_transform(data['prc_text']).toarray()
print(x)
print(data.label)
encoder = LabelEncoder() # 把原本的文本标签spam和ham转换成数字0和1
data['label'] = encoder.fit_transform(data['label'])
print(data.label)
y = data['label'].values
print(y)
print(len(y))
# feature matrix with 3000 features, and 5169 rows, these rows are our text messages left
pd.DataFrame(x)
x.shape
y.shape # label matrix
# 7. split our dataset into train and test data set
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state = 1703)
# 原始数据集的3成作为test集合,余下的都是用于训练的数据。为了保证每次训练的模型都一样,需要设置random_state参数
# 使用logistic regression和naive bayes模型进行分类(预测)
lr = LogisticRegression(solver='liblinear', penalty='l1')
mnb = MultinomialNB()  # multi nomial naives bayes: feature matrix represents the frequencies
# train our model with train set and do some predictions

# firstly, using lr
lr.fit(x_train, y_train)
y_pred_lr = lr.predict(x_test)
test_acc_lr = accuracy_score(y_test, y_pred_lr)
precision_lr = precision_score(y_test, y_pred_lr)
f1_lr = f1_score(y_test, y_pred_lr)
recall_lr = recall_score(y_test, y_pred_lr)

print(f'Test ACC LR: {round(test_acc_lr, 3)}')
print(f'Precision LR: {round(precision_lr, 3)}')
print(f'F1_Score LR: {round(f1_lr, 3)}')
print(f'Recall LR: {round(recall_lr, 3)}')
cf_matrix = confusion_matrix(y_test, y_pred_lr)

ax = sns.heatmap(cf_matrix, annot=True, cmap='Blues', fmt='.5g')
ax.set_title('LogReg Confusion Matrix\n')
ax.set_xlabel('\nPredicted values')
ax.set_ylabel('Actual values')
ax.xaxis.set_ticklabels(['False', 'True'])
ax.yaxis.set_ticklabels(['False', 'True'])

plt.show()
# secondly, using mnb

mnb.fit(x_train, y_train)
y_pred_mnb = mnb.predict(x_test)

test_acc_mnb = accuracy_score(y_test, y_pred_mnb)
precision_mnb = precision_score(y_test, y_pred_mnb)
f1_mnb = f1_score(y_test, y_pred_mnb)
recall_mnb = recall_score(y_test, y_pred_mnb)

print(f'Test ACC MNB: {round(test_acc_mnb, 3)}')
print(f'Precision MNB: {round(precision_mnb, 3)}')
print(f'F1_Score MNB: {round(f1_mnb, 3)}')
print(f'Recall MNB: {round(recall_mnb, 3)}')
cf_matrix = confusion_matrix(y_test, y_pred_mnb)

ax = sns.heatmap(cf_matrix, annot=True, cmap='Blues', fmt='.5g')
ax.set_title('LogReg Confusion Matrix\n')
ax.set_xlabel('\nPredicted values')
ax.set_ylabel('Actual values')
ax.xaxis.set_ticklabels(['False', 'True'])
ax.yaxis.set_ticklabels(['False', 'True'])
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容