文本数据挖掘实践

数据预处理

1. 煮粥之前先淘米——预处理

其实我们应该先寻找算法,有了方向再对数据进行相应的预处理,不过刚好最近在学习正则表达式,以及文件读写,就顺便练手,对数据进行“粗加工”。

也就是简单地:

  1. 去除坏值,比如编码有问题的。我觉得这种认为打了label的数据应该很可靠不应该有太多的问题,但还是有30多个编码有问题的。

  2. 舍弃冗余内容。比如:

    id = {D10-1003}
    author = {Cheung, Jackie Chi Kit; Penn, Gerald}
    title = {Utilizing Extra-Sentential Context for Parsing}
    venue = {EMNLP}
    year = {2010}
    

    year这个信息再id中有表现( - 前的数字),title的作用就是这个作品的名字,里面的内容我们并不关心,所以只需要用id这一个信息来表示就可以。我们就需要舍弃year和title这两个冗余信息。

  3. 从信息中提取出有效信息。比如:

    author = {Cheung, Jackie Chi Kit; Penn, Gerald}

    其中有效信息是 ’Cheung, Jackie Chi Kit‘ 和 ‘Penn, Gerald’ 这两个字符串。其他的等号,分号,大括号都要去掉。

  4. 整合信息。

    比如:需要知道作者之间的关系,但其实作者是无序的,可以用集合来存放作者;id和author,venue关系紧密,我们可以用字典来表示id和他们之间的关系,便于查询。

需要注意的一点是,一些外国作者名字中会出现西语,法语,或者德语字符。比如:Nria 显示成 N &uacute ;ria;Iaki显示成I&ntilde ;aki; Germn显示成Germ &aacute ; n。对于这种情况,我们在此不做处理,因为作者的名字不会变,此处编码成如此,在别处也会编码成如此。

2. 求求你了别报错——编码

之前从来没有接触过文件编码的问题,遇到之后一直报错,根本停不下来,这就让人觉得有点头大。

代码如下:

with open('acl-metadata.txt','r') as data:
    print(data.readlines(),'\n')

报错:

UnicodeDecodeError: 'gbk' codec can't decode byte 0x94 in position 394157: illegal multibyte sequence

因为这个项目提供的文本文件不是 UTF8 编码的(例如,可能是 GBK 编码的),而系统默认采用 UTF8 解码。在网上搜索之后找到了解决方法,应该将文件读取进来的时候改为对应的解码方式。

with open('acl-metadata.txt','rb') as data:

这种方式可以解决文件编码的问题,因为 ‘rb’ 关键字表示read as binary data,故读进来之后,储存每一行信息的多维list里面的内容就是二进制格式,不会将读取的字节转换成字符。

但如果他是二进制信息,我们之后想用正则表达式,和split方法处理字符串,就只能成为美梦了。所以我们需要找到另一种处理文件的方法!

total_aclmetadata.append(str(line, encoding = "utf-8") )

先强制转换编码格式,再将获取到的内容转为字符串格式就OK了。

3. 删除坏值像狙击——定位,出击

删除坏值的原则有两个:

  1. 这条数据(id,venue,title等)不能为我所用。

  2. 如果这条数据要删,就要把相对应的五条数据(id,author,venue,year,title)全部删掉。

  3. 保留删去的数据组对应的id。

    我们现在是拿着aclmetadata.txt这个数据集说事,我们如果要删去这个文件里的某些数据(或者说数据组),就要把相对应的ppraletionship.txt里面的文章之间的索引信息也给删掉。

有一点需要强调的是,也是我绕的一点弯路:坏值处理要先于获取有效信息!

我最初的想法是先处理有效信息:

先用正则表达式,和split获得有效信息(前提是再找到坏值之后在对应位置填上提示信息“abnormal data = {'坏值待处理'}”,这样就可以用同样的正则规则来处理坏值的信息),将id作为key,author和venue作为value合成为一个字典。

那怎么把对应的内容都删了呢:

  1. 先查看坏值之前或之后一行信息,split获取是id,author,year之类的。

  1. 再找到对应的id。

  1. 找到id之后,再把对应字典中的内容删掉!

凭借badvalue找到对应的id,然后用它做key去author里ramove掉对应元素。

问题是这个abnormal data你根本不知道他是什么信息,如果是author,venue肯定会造成字典中的元素不对应,更不要提坏值是id的情况了——怎么去找对应的id?

也尝试了一些其他想法,比如设置一个标志为,如果这组数据里坏值出现的标志位abdata被置1了,则这组数据不添加到字典中。但都太不优雅了,会使程序的可读性变得十分糟糕。

那我们现在来老老实实地进行坏值删除,两步走:

1> 定位:

定位这一步在读取数据的时候就要进行了,记录下来坏值出现的位置,非常简单故不再赘述。遇到一个坏值,就将其索引存放到badvalue里。

2> 进攻策略

我一开始写了一个函数,自我感觉非常良好:

def data_erasing(badvalue, dataset):
    for i in badvalue:
        for j in range(6):
            dataset.pop(i-(i%6)+j)

一运行就发现了问题,pop这个方法每pop出一个内容,该位置之后的所有元素的索引都减1!!!,所以我的这个:for j in range(6):循环,其实都是删一个元素,然而元素整体上已经向前移动一位了,索引再加一,那就完全乱了套。

我心想:在删除了数据之后,数据整体向前移动,那我后面坏值的索引是不固定的。那如果我不想让剩下的坏值的索引变化,该怎么办呢?

有了!咱倒着删不就成了吗,从最后一个开始处理,剩余的坏值都在它前面,那不就不用考虑坏值索引的问题了嘛!当然,对于每一个坏值对应的数据组,我们也要按照索引从后往前删,所以就有了下面的代码:

def data_erasing(badvalue, dataset):
    for i in reversed(badvalue):
        for j in range(5,-1,-1):
            dataset.pop(i-(i%6)+j)

可问题层出不穷,你经历过的东西越少,你犯的错误就越多,错误也越白痴。只想着删除一个数据组,忘了怎么样才算一个数据组了。坏值在year这条信息的位置上,按照上面的代码,就删掉了year及其后面五行(个)元素。这当然是不合理的,我们最终想要删除的是这个坏值所在的这组数据。

所以我们需要第三步,实际上是我们第一步就该做好的。

3> 再次定位

这次定位不是定“坏值在全局中的索引”了,而是“坏值在对应数据组中的位置”。因为这些数据在给的时候已经处理过了,都是按照id,author,title,venue,year的顺序排列的,我们只需要找出坏值前后的信息各是什么,就可以对坏值所对应的数据组做出推测,我们找到了该数据组的id对应的索引,在用上面的方式删除就好了。

区别就在于:之前删除坏值的基础位置是badvalue(坏值本身索引),这种方法是基于坏值所在数据组的id所在的位置。

4. 字符串雌雄双煞——RE & split方法

处理字符串有两个神器:split方法,和正则表达式(regular expression,缩写为regexp)

split方法,用于拆分字符串。通过指定分隔符对字符串进行切片,并返回分割后的字符串列表。比如,A.split(' = '),将A按“空格+等号+空格”切分成n个字符串,返回一个列表。B.split(';'),将B按“分号+空格”切分成n个字符串,返回一个列表。

正则表达式就更有趣了,我举几个例子:

匹配中文字符的正则表达式:
[\u4e00-\u9fa5] 

匹配Email地址的正则表达式:
\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*

匹配信用卡卡号校验,检查16位四组格式的,以空个或-分开的卡号,或者连在一起的:
^(\d{4}[- ]){3}\d{4}|\d{16}$

匹配任何大写或小写字母组成,以逗号或空格分开的文本,空格的数量不限。
^\s*[a-zA-Z,\s]+\s*$

有了这两个神器,我们从字符串中获取有效信息就游刃有余了。

5. label团队扣鸡腿——获取信息

之前的想法(version 1)是:观察到信息包括id, author, title, venue, year一共五项,外加一个空格。那么我可以按照六个一循环来读取数据。

for i in range(len(dataset)):
    temp = []
    if i%6 == 0:
        ids.append((re.match(pattern_allinfor, dataset[i])).groups()[0])
    elif i%6 == 1:
        temp.append((re.match(pattern_allinfor, dataset[i])).groups()[0])
        splitedname = temp[-1].split('; ')
        authors.append(splitedname)
    elif i%6 == 3:
        venues.append((re.match(pattern_allinfor, dataset[i])).groups()[0])
        infors[ids[j]] = np.array([authors[j], venues[j]]) 
        j += 1
    else:
        continue

注意到上式代码,按索引对6取模来分类,模为0的都是六个一循环中的第一位,即id;模为1的都是六个一循环中的第二位,即author;模为3的都是六个一循环中的第四位,即venue。而且我们只需要这几个信息。

但在运行之后问题出现了!2012年一整年的信息的year的格式都有问题:最右侧的 } 被分到了下一行。不仅我们的正则表达式要进行改进,而且整个数据集就不能再用“六个一循环”这个策略读取了。我想把这个锅给打label的小组,午餐扣一个鸡腿!

不过方法总比问题多,我还是找到了解决方案。我们下面用伪代码来表示一下:

for 在datasset的长度范围内,从第一行遍历到最后一行:
    用split(' = ')获取信息的类型:
    if 类型是id:
        id.append()
    elif 类型是author:
        author.append()
    elif 类型是venue:
        venue.append()
        在这个if块里将id,author,venue的最后一个元素合成一个字典元素对。
    else:
        continue

这样就不存在要求数据集必须六个一循环了,是不是更简洁优雅了?呵呵。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值