任务简介:

现有两份针对同一主题的数据,但是在人物的属性名称及格式上有所不同,需要对两份数据进行匹配来确定是同一个人。

匹配属性:

1 人名
2 出生日期
3 国籍

原始数据举例

1 数据源1(以下简称S1)

id name date of birth nationality
282577 lukas-klunter 1996-05-26 Germany

nationality or place of birth?
应该还是用nationality,不过分析数据过程中发现存在诸如' Morocco|Germany '的字段,考虑用分隔后,多国籍分别分组

2 数据源2 (以下简称S2)

player_id first_name last_name date_of_birth country
18679 Lukas Klünter 26/05/1996 Germany

需求

  • 将两个不同数据源中的相匹配的记录关联起来

问题分析

  • 匹配的格式问题,包括:

    • 字段合并,比如S1中的name对应S2中的first_name+last_name

    • 语言的格式问题,就人名而言,此例中S1中的klunter与S2中的Klünter就有区别

    • 此外,在浏览S2记录时发现两边记录中的first_name和last_name以及name的格式有多种

    • 日期格式问题以及大小写格式问题

  • 效率问题,S1数据量达到40+W,S2大约是4200+,如何进行两者的匹配是个问题


思路

  • 格式匹配

    • 字段合并的话应该可以将S2中的first_name与last_name用'-'连接,进一步分析数据发现,S1中的name与对应的S2中的first_name及last_name之间并无规律,考虑匹配的话,一种思路是在构建多级字典后因为范围已经缩小很多考虑直接提取first_name进行匹配,另一种思路是用S2中的first_name+last_name来contains S1中的name进行匹配,初步考虑第二种思路(更准确)

    • 语言格式化,经过google知,德语字母源于拉丁字母,且有四个变形字母,从stackexchange上我发现可通过Latin->ASCII进行初步转换,然后进行大小写的统一即可进行转换

    • 日期上应该修改格式即可

  • 效率方面,设想是构造多级字典实现数据查找范围的缩小,以期提高效率,具体而言分以下几步:

    • 在对S1的csv文件构建多级字典以后,先从S2中取出country字段对应的值,与S1中nationality对应的值经处理后进行匹配从而缩小范围

    • 再从S2中取date_of_birth字段的值与S1同一nationality下的date of birth(处理过后)进行匹配进一步缩小范围

    • 最后是从S2中取出first_namelast_name的值拼接处理后与S1中同一name下的值进行处理匹配,将成功匹配的S1中的记录输出到新的csv文件

实现

  • 格式匹配

    • 语言格式化,调用unihandecode中的unidecode方法来对Latin字符进行处理转化为ASCII字符,代码实现如下

    # _*_ coding: utf-8 _*_
    import unihandecode
    print(unihandecode.unidecode(u"Lukas;Klünter"))
    • 日期格式转换
      调用time包然后直接转换输出格式即可

    import time
    
    t='1996-05-26'
    timeArray=time.strptime(t,"%Y-%m-%d")
    print time.strftime("%d/%m/%Y",timeArray)
    • 国籍字段的格式化,前面提到进一步分析数据的过程中我发现S1中存在诸如' Morocco|Germany '(多国籍)的现象,而对应的S2中的同一球员却只拥有一个国籍作为Country的值,所以这里我们的想法是对建立好的多级字典进行清洗,将值为多国籍的项的键进行分割然后添加到已有重名国籍字段或是新建一个当前字典中没有的国籍字段,且两者的值相同
      通过个人的思考加上stackoverflow上的提问,找到了解决方法,代码如下:

    def country_format(dictionary):
       new_dict = {}
       for item in dictionary:
           # print item
           for index in str.split(item, '|'):
               if not index in new_dict:  # if not exists, create, and then insert or insert directly
                   new_dict[index] = {}
               new_dict[index].update(dictionary[item])
       return new_dict
    
    • 姓名字段匹配

  • 构建多级字典
    思路是先以两个数据源的国籍为键构造字典将数据分组,缩小范围,然后在每个一级字典里再次以出生日期为键构造字典(这里有个优先级的问题,是以出生日期还是国籍作为第一级字典比较好?以国籍的话则分组相对少,但每组对应的元素多,而以日期来分的话则是分组较多,每组元素相对少),最后再以名称进行匹配,参照着stackoverflow上的方法,假设我有如下数据(imitate.csv)

    id,name,date of birth,nationality
    227,alexander-zickler,1974-02-28,Germany,
    229,abdelaziz-ahanfouf,1978-01-14,Morocco|Germany,
    233,perry-brautigam,1963-03-28,Germany,
    232,christian-brand,1972-05-23,Germany,
    455,chen-yang,1974-01-17,China,
    35214,leilei-li,1977-06-30,China,
    35228,yunfei-liu,1978-05-08,China,
    218,paulo-sergio,1969-06-02,Brazil,
    1263,marcio-amoroso,1974-07-05,Brazil,

经过处理后输出结果如下

D:\Anaconda\python.exe E:/PythonWorkspace/PlayerTransfer/nested_dict.py
{'Brazil': {'1969-06-02': {'id': '218', 'name': 'paulo-sergio'}, '1974-07-05': {'id': '1263', 'name': 'marcio-amoroso'}},
'Germany': {'1972-05-23': {'id': '232', 'name': 'christian-brand'}, '1974-02-28': {'id': '227', 'name': 'alexander-zickler'}, '1963-03-28': {'id': '233', 'name': 'perry-brautigam'}}, 
'China': {'1978-05-08': {'id': '35228', 'name': 'yunfei-liu'}, '1977-06-30': {'id': '35214', 'name': 'leilei-li'}, '1974-01-17': {'id': '455', 'name': 'chen-yang'}},
'Morocco|Germany': {'1978-01-14': {'id': '229', 'name': 'abdelaziz-ahanfouf'}}}

Process finished with exit code 0

对应的代码如下

import csv
from collections import defaultdict

def build_dict(source_file):
  projects=defaultdict(dict)
  #headers=['id','name','date of birth','nationality']
  with open(source_file,'rb') as fp:
      reader=csv.DictReader(fp,dialect='excel',skipinitialspace=True)   #如果原始csv文件里不含标题时,需要添加fieldnames=headers以及上述注释掉的headers
      for rowdict in reader:
          if None in rowdict:
              del rowdict[None]
          nationality=rowdict.pop("nationality")
          date_of_birth=rowdict.pop("date of birth")
          projects[nationality][date_of_birth]=rowdict
  return dict(projects)

source_file='imitate.csv'
print build_dict(source_file)

关于这部分代码的理解需要学习Python的dictionary类型相关章节以及关于内置包csv的文档阅读

也有另一种思路,直接嵌套着构建,代码如下,参考


def build_dict(source_file):
    new_dict = {}
    with open(source_file, 'rb')as csv_file:
        data = csv.DictReader(csv_file, delimiter=",")
        for row in data:
            item = new_dict.get(row['nationality'], dict())
            item[row['date of birth']] = {k: row[k] for k in ('id', 'name')}
            new_dict[row['nationality']] = item
    return new_dict


source_file='imitate.csv'

playerInfo = build_dict(source_file)
print playerInfo

输出结果如下

{'Brazil': {'1969-06-02': {'id': '218', 'name': 'paulo-sergio'}, 
            '1974-07-05': {'id': '1263', 'name': 'marcio-amoroso'}}, 
'Germany': {'1972-05-23': {'id': '232', 'name': 'christian-brand'}, 
            '1974-02-28': {'id': '227', 'name': 'alexander-zickler'}, 
            '1963-03-28': {'id': '233', 'name': 'perry-brautigam'}}, 
'China': {'1978-05-08': {'id': '35228', 'name': 'yunfei-liu'}, 
        '1977-06-30': {'id': '35214', 'name': 'leilei-li'}, 
        '1974-01-17': {'id': '455', 'name': 'chen-yang'}}, 
'Morocco|Germany': 
        {'1978-01-14': {'id': '229', 'name': 'abdelaziz-ahanfouf'}}}
  • 上述结果经过之前提到的国籍的分离操作后得到如下输出结果

{'Brazil': {'1969-06-02': {'id': '218', 'name': 'paulo-sergio'}, 
            '1974-07-05': {'id': '1263', 'name': 'marcio-amoroso'}}, 
'Germany': {'1972-05-23': {'id': '232', 'name': 'christian-brand'}, 
            '1974-02-28': {'id': '227', 'name': 'alexander-zickler'}, 
        '1963-03-28': {'id': '233', 'name': 'perry-brautigam'}, 
        '1978-01-14': {'id': '229', 'name': 'abdelaziz-ahanfouf'}}, 
'China': {'1978-05-08': {'id': '35228', 'name': 'yunfei-liu'}, 
          '1977-06-30': {'id': '35214', 'name': 'leilei-li'}, 
       '1974-01-17': {'id': '455', 'name': 'chen-yang'}}, 
'Morocco': {'1978-01-14': {'id': '229', 'name': 'abdelaziz-ahanfouf'}}}

可以看到之前的' Morocco|Germany '已经成功的分离为两个字段,Germany是添加到已有的字典中,而Morocco则是新建立了一个字典

  • csv文件格式规范的处理
    再进一步将要对两个数据源进行匹配工作之前,我准备开始读入S2的数据,这个时候发现S2的csv文件格式有些不一样,其header是以,分割,而常规的row则是以;分割,为了解决这个问题,采用的方法是先读取header部分作为fieldnames,然后对rows通过以;为分隔符进行csv文件的读取以及逐行转化为字典,代码如下:

    with open('test.csv') as src_csv:
       reader=csv.reader(src_csv,delimiter=',')
       fieldnames=next(reader)
    
       reader=csv.DictReader(src_csv,fieldnames=fieldnames,delimiter=';')
    
       for row in reader:
           print row
    

参考

  • 初步测试在根据S1构建的多级字典中查找S2的键值对应的(S1中的)值,测试的数据集如下:

    • S2中数据test.csv

players.first_name players.last_name players.vis_name players.player_id players.date_of_birth players.role players.team players.country
Dusan Svento Svento 8658 01/08/1985 Midfielder 1. FC Köln Slovakia
Markus Henriksen Henriksen 7687 25/07/1992 Midfielder AZ Norway
Lukas Klünter Klünter 18679 26/05/1996 Defender 1. FC Köln Germany
Roque Santa Cruz Santa Cruz 547 16/08/1981 Forward Málaga Paraguay
Benjamin Kirsten Kirsten 19078 02/06/1987 Goalkeeper N.E.C. Germany
  • S1中数据test2.csv (小插曲,在用markdown构建表格时出现'|'冲突,参考stackoverflow|替换本身的'|'即可)

id name complete name date of birth place of birth age height nationality position foot player's agent agent id current club club id in the team since contract until outfitter
46415 duje-cop 1990-02-01 Jugoslawien (SFR) 26 1.87 m Croatia Striker - Centre Forward right pharos-sport-agency 1358 malaga-cf 1084 Jul 16. 2015 30.06.2016
34543 dusan-svento 1985-08-01 CSSR 30 1.78 m Slovakia Midfield - Left Wing left stars-amp-friends-international-holding-gmbh 10 1-fc-koln 3 Jul 1. 2014 30.06.2016
122011 markus-henriksen 1992-07-25 Norway 23 1.87 m Norway Midfield - Attacking Midfield right jim-solbakken 1292 az-alkmaar 1090 Aug 31. 2012 30.06.2017
49327 bradley-johnson 1987-04-28 England 29 1.78 m England | United States Midfield - Central Midfield right derby-county 22 Sep 1. 2015 30.06.2019
282577 lukas-klunter 1996-05-26 19 1.87 m Germany Defence - Right-Back right sportstotal 199 1-fc-koln-ii 438 Jul 1. 2015 30.06.2017
215 roque-santa-cruz 1981-08-16 Paraguay 34 1.93 m Paraguay | Spain Striker - Centre Forward right jb-sports2business 1043 malaga-cf 1084 Aug 26. 2015 30.06.2016
29903 benjamin-kirsten 1987-06-02 DDR 28 1.82 m Germany Goalkeeper right l-concept-sports-gmbh 2559 unattached 515 Dec 6. 2015 -

代码如下:

import csv
import date_format
import csv2dict
import Country_Format

# read the dest csv file
dest_file = 'test2.csv'
# create the nested dict
playerInfo = csv2dict.build_dict(dest_file)
# print playerInfo
S1=Country_Format.country_format(playerInfo)
# print S1

with open('test.csv') as src_csv:
    reader=csv.reader(src_csv,delimiter=',')
    fieldnames=next(reader)

    reader=csv.DictReader(src_csv,fieldnames=fieldnames,delimiter=';')

    for row in reader:
        # print row
        print S1[row['players.country']][date_format.date_convert(row['players.date_of_birth'])]['name']

输出结果如下:

dusan-svento
markus-henriksen
lukas-klunter
roque-santa-cruz
benjamin-kirsten

可见现在国籍和出生日期的匹配初步测试没有问题,现在问题集中在名称匹配和处理上,以S1中的jose-gimenez对应S2中的José María;Giménez de Vargas为例,分析知大约需要以下几步:

  • 对S2的操作

    • 抽取first_name和last_name并以空格为分隔符分割后存于列表L2

    • Latin->ASCII

    • 转换为全小写

  • 对S1的操作

    • 抽取name的值并以'-'为分隔符分割后存于列表L1

  • 判断L1是否为L2的子集,若是则认为匹配成功,否则认为失败

实现代码如下:

# _*_ coding: utf-8 _*_
import Latin2ASCII

def name_match(s1,s2):
    l1 = []
    l2 = []

    l1 = str.split(str.lower(Latin2ASCII.ud(s1.decode('utf-8', 'ignore'))), ' ')
    l2 = str.split(s2, '-')

    return set(l2).issubset(set(l1))

这其中遇到之前的名称中拉丁文与字符串的转换问题,最终通过google,结合stackoverflow以及Python文档得到解决

  • 输出S1中匹配记录到csv文件

    • 在匹配成功后,将所匹配的记录所对应于S1中的id集中到一个列表white_list

    • 根据white_list中的id值从S1中进行匹配并导出该id所对应的记录到文件white_list.csv
      代码实现如下:参考

import csv

def csv_match(id_list,input_file,output_file):
    with open(input_file, 'rb') as f:
        reader = csv.DictReader(f)
        rows = [row for row in reader if row['id'] in set(id_list)]

    header = rows[0].keys()
    with open(output_file, 'w') as f:
        f.write(','.join(header))
        f.write('\n')
        for data in rows:
            f.write(",".join(data[h] for h in header))
            f.write('\n')

其中抽取含有white_list中指定id的操作利用了set的子集判断,并最终输出所有符合条件的结果,参考

  • 异常处理
    在这一过程中遇到不少的异常情况,现小结如下:

    • key不存在的异常,由于之前进行测试时的数据是手工提取的能保证找得到的记录进行的沙盒测试,而对于实际数据在匹配过程中可能存在字典中无该键值的情形,所以前面所述的

    print S1[row['players.country']][date_format.date_convert(row['players.date_of_birth'])]['name']

    是行不通的,需逐级进行判断

    if nationality in S1:
               if date in S1[nationality]:
                   s2=S1[nationality][date]['name']
    • 进一步的,在进行数据匹配过程中,出现了日期格式不规范的异常,仔细分析发现是S2中存在诸如 '00/00/0000'以及‘22/00/1996’(月份错误)的错误数据,所以对于前面所写的date_convert函数需要优先进行判断,修改后如下 参考

    def date_convert(t):
       if validate_date(t):
           timeArray = time.strptime(t, '%d/%m/%Y')
           return time.strftime('%Y-%m-%d', timeArray)
       else:
           return '0001-01-01'
    
    def validate_date(d):
       try:
           time.strptime(d,'%d/%m/%Y')
           return True
       except ValueError:
           return False

    在对原始的S1,S2数据源进行测试得到最后的white_list以及id_list,经过统计发现,id_list的数目是2406而white_list的数目约为2393左右,然而原始的S2中的记录约有4213条,和期望有所差距,进一步考虑将S2中未找到的记录输出到文件black_list来进一步分析看是数据本身的问题还是匹配中有问题,思路如下

  • 在前面统计对应于S1中的id_list中加一步统计对应的S2中的id_list

  • 从文件中获取S2所有的id的集合列表

  • 从S2集合列表中刨去匹配成功的列表即为匹配失败的列表black_list

  • 将S2中匹配失败的记录输出到文件black_list.csv并进行分析

  • 问题分析:

    • 多级字典导致的名称覆盖问题,例如如下两个S1中的数据

    282577,lukas-klunter,,1996-05-26,,19,1.87 m,Germany,Defence - Right-Back,right,sportstotal,199,1-fc-koln-ii,438,Jul 1. 2015,30.06.2017,
    
    317484,hassib-sediqi,,1996-05-26,Germany,19,,Germany|Morocco,Striker,,,,unknown,75,Jul 1. 2014,-,

    位于较后面记录覆盖了前面的记录因为他们拥有相同的国籍和出生日期,考虑重构多级字典,在国籍,出生日期之下再加一级name

  • 重整旗鼓

    • 涉及文件

      • `csv2dict.py

      • core.py

      • CountryFormat.py

仍以imitate.csv文件为测试,则代码修改如下

import csv

def build_dict(source_file):
  new_dict = {}
  with open(source_file, 'rb')as csv_file:
      data = csv.DictReader(csv_file, delimiter=",")
      for row in data:
          print row
          item = new_dict.get(row['nationality'], dict())
          print item
          sub_item=item.get(row['date of birth'],dict())
          print sub_item
          # sub_item[row['name']]={k:row[k] for k in ('id')}
          sub_item[row['name']]={'id':row['id']}
          print sub_item
          item[row['date of birth']]=sub_item            
          print item
          new_dict[row['nationality']] = item
  return new_dict

由于imitate文件相对简单,只有四列,所以最深层的(name下一层仅有id属性),在这里对完整数据需要修改为comprehensions语句
测试结果如下

 {'Brazil': {'1969-06-02': {'paulo-sergio': {'id': '218'}}, 
          '1974-07-05': {'marcio-amoroso': {'id': '1263'}}}, 
  'China': {'1978-05-08': {'yunfei-liu': {'id': '35228'}}, 
      '1977-06-30': {'leilei-li': {'id': '35214'}}, 
      '1974-01-17': {'chen-yang': {'id': '455'}}}, 
  'Germany': {'1972-05-23': {'christian-brand': {'id': '232'}}, 
          '1974-02-28': {'alexander-zickler': {'id': '227'}}, 
          '1963-03-28': {'perry-brautigam': {'id': '233'}}}, 
  'Morocco|Germany': {'1978-01-14': {'abdelaziz-ahanfouf': {'id': '229'}}}}
  • core.py
    然而在对真实数据进行测试时发现,这一次white_list比上次的数据还要少,于是再次输出black_list进行比对分析,在对core.py中间对于姓名匹配的逐条分析过程中发现了两个问题:

    • 逻辑上的问题:在对国籍,出生日期进行匹配后,到了name这一层的匹配应该是先取出S2中的first_namelast_name并格式化然后遍历S1当前层级字典下存在的所有name逐一进行匹配判断;而之前的做法是没有遍历,直接取了一个读到的值,这是思维上的漏洞;

    • 多级字典造成出生日期重复而造成记录覆盖问题:类似于之前的国籍划分提取,在这一过程中构建多级字典会出现同名的键值,这时需要将记录插入已存在的键值的子字典中,而直接插入会造成覆盖,仿照着之前对country的操作对CountryFormat.py进行修改;
      比如,我用下述数据进行原有函数测试:

        D={'Germany': {'1972-05-23': {'danny':{'id':'1'}}, 
                        '1969-12-27': {'lancelot':{'id':'2'}}},
              'Morocco|Germany': {'1978-01-14':{'tony':{'id':'3'}},
                             '1969-12-27':{'lydia':{'id':'4'}}}}

原始代码如下

def country_format(dictionary):
 new_dict = {}
 for item in dictionary:
     # print item
     for index in str.split(item, '|'):
         if not index in new_dict:  # if not exists, create, and then insert or insert directly
             new_dict[index] = {}
         new_dict[index].update(dictionary[item])
 return new_dict

输出结果对比如下

 original dict
 {'Germany': {'1972-05-23': {'danny': {'id': '1'}},
             '1969-12-27': {'lancelot': {'id': '2'}}},
 'Morocco|Germany': {'1978-01-14': {'tony': {'id': '3'}}, 
                     '1969-12-27': {'lydia': {'id': '4'}}}}
 --------------------------------------
 after dict
 {'Morocco': {'1978-01-14': {'tony': {'id': '3'}}, 
             '1969-12-27': {'lydia': {'id': '4'}}},
 'Germany': {'1978-01-14': {'tony': {'id': '3'}}, 
             '1972-05-23': {'danny': {'id': '1'}}, 
         '1969-12-27': {'lydia': {'id': '4'}}}}

可以看到Germany中的lancelot记录被lydia覆盖了,所以在进行country的格式化时需要对于date of birth这一层级字典的构建进行修改
修改后代码

def country_format(dictionary):
 new_dict = {}
 for item in dictionary:
     for index in str.split(item, '|'):
         if not index in new_dict:  # if not exists, create, and then insert or insert directly
             new_dict[index] = {}
             new_dict[index].update(dictionary[item])
         else:
             for k in dictionary[item]:
                 if not k in new_dict[index]:
                     new_dict[index][k]={}
                 new_dict[index][k].update(dictionary[item][k])
 return new_dict

结果对比

    original dict
 {'Germany': {'1972-05-23': {'danny': {'id': '1'}}, 
             '1969-12-27': {'lancelot': {'id': '2'}}}, 
 'Morocco|Germany': {'1978-01-14': {'tony': {'id': '3'}}, 
                     '1969-12-27': {'lydia': {'id': '4'}}}}
 --------------------------------------
 after dict
 {'Morocco': {'1978-01-14': {'tony': {'id': '3'}},
             '1969-12-27': {'lydia': {'id': '4'}}},
 'Germany': {'1978-01-14': {'tony': {'id': '3'}}, 
             '1972-05-23': {'danny': {'id': '1'}}, 
         '1969-12-27': {'lydia': {'id': '4'}, 
                       'lancelot': {'id': '2'}}}}

其中需要注意的地方主要在于厘清层级之间的逻辑/流程关系,然后是对于dict.update()方法的理解,对于深层级的重复字段是覆盖,而不同字段是添加到上一层级子字典下,再者就是在外层再次用update会导致之前的记录被覆盖

  • 接下来就是对于core.py的修改,主要是添加遍历,对于没取出一个S2中的名称,需要遍历S1中符合国籍,出生日期条件的name列表中的所有名称,然后进行匹配;
    修改后中间的代码如下

    if nationality in S1:
          if date in S1[nationality]:
              s1 = row['players.first_name'] + ' ' + row['players.last_name']
              s2=S1[nationality][date].keys()
              for i in s2:
                  if Name_Match.name_match(s1, i):
                      id_list.append(S1[row['players.country']][date_format.date_convert(row['players.date_of_birth'])][i]['id'])
                      temp_list.append(row['players.player_id'])

对真实数据进行测试输出结果如下:

     length of id_list: 3578
   length of black list: 613
   length of all id 4213

大为改观,但仍难以让人满意,遂同样地输出black_list.csv进行分析,大约有以下几种情形

  • 国籍不规范

  • 名称不规范

  • 数据本身不一致

  • 特殊名称

  • 日期错误

国籍不规范,例如

    39471,brad-guzan,,1984-09-09,United States,31,1.93 m,United States|Poland,Goalkeeper,left,wasserman-media-group,440,aston-villa,405,Aug 1. 2008,30.06.2017,
  
     Guzan;Goalkeeper;USA;Guzan;409;Brad;09/09/1984;A. Villa
  
  265132,kevin-toner,,1996-07-18,Ireland,19,,Ireland,Defence - Centre Back,left,,,aston-villa-u21,12124,Jul 1. 2014,30.06.2016,

  Toner;Midfielder;Republic of Ireland;Toner;20367;Kevin;18/07/1996;A. Villa

可知其实是有其人,但是根据我们的判断规则,在两条记录的国籍无法匹配,原因在于S2中使用了USA,而S1中的记录是United States,所以针对此种特殊情形可能得加一步预处理;

名称不规范,例如

    314237,rushian-hepburn-murphy,,1998-08-28,England,17,,England,Striker - Centre Forward,,,,aston-villa-u18,6933,-,-,

  Hepburn-Murphy;Forward;England;Hepburn-Murphy;17821;Rushian;28/08/1998;A. Villa

S2中出现了Hepburn-Murphy这种形式,所以对于其名称匹配要再加一个对于S2中的名称的分隔符为-的判断

数据本身不一致,例如

  • 出生日期不一致

        39908,rudy-gestede,,1988-10-09,France,27,1.93 m,Benin|France,Striker - Centre Forward,right,gallea-gestion-s-a,1628,aston-villa,405,Jul 31. 2015,30.06.2020,
    
      Gestede;Forward;Benin;Gestede;5149;Rudy;10/10/1988;A. Villa
      
  • 姓名不一致,例如

        4315,johnny-heitinga,,1983-11-15,Netherlands,32,1.80 m,Netherlands|Indonesia,Defence - Centre Back,right,wasserman-netherlands-management,274,end-of-career,123,Feb 1. 2016,-,
    
      Heitinga;Defender;Netherlands;Heitinga;190;John;15/11/1983;Ajax

    而且对于这个记录在S2中重复了三次

可见两者应该是同一个人,但两个数据源的出生日期差了一天,这个就是数据录入的问题了

特殊名称:

其实也算是名称不规范,在S2中存在不少名称是拉丁文,当前采用的处理方法是转为ASCII并采取忽略字符中特殊标记强制转为对应的英文字母并全部转化为小写的方法,例如

     é    ñ    Á    á    ó    í    Ó
     e  n  a   a   o  i   o

然而对于以下情况这种方法就失效了,例如

    22165,charles-nzogbia,,1986-05-28,France,29,1.71 m,France|Congo DR,Midfield - Right Wing,left,dea-football-investment,3525,aston-villa,405,Jul 30. 2011,30.06.2016,adidas

  N'Zogbia;Midfielder;France;N'Zogbia;422;Charles;28/05/1986;A. Villa

对于其中的Charles N'Zogbia即使经过转化,得到的结果为charles n'zogbia是不会和charles-nzogbia匹配的,如果需要处理的话考虑进一步的对名称的规范化处理

日期错误

之前提到过的错误日期,例如'00/00/0000'以及'12/00/1992'之类的

    353935,edgar-alexandre,,1996-11-24,,19,1.80 m,France,Midfield - Central Midfield,right,,,sc-bastia-b,9652,-,-,

  Alexandre;Midfielder;France;Alexandre;17386;Edgar;00/00/0000;Bastia

这种根据判断规则也难以处理,除非用另一种匹配规则

优化

接下来考虑对于国籍不规范以及姓名不规范这两种情况考虑进一步进行优化

针对姓名不规范

  • 针对S2中存在的first_name或者last_name中存在字符-的情形,通过对于Name_Match.py中对于获取到的名称的分割操作进行修改,通过正则表达式加入其它分隔符划分,代码如下:参考

    # before
    # l1 = str.split(str.lower(Latin2ASCII.ud(s1.decode('utf-8', 'ignore'))), ' ')
    # after Latin -> ASCII -> lower case ->split by space, - etc.
    l1=re.findall(r"[\w']+", str.lower(Latin2ASCII.ud(s1.decode('utf-8', 'ignore'))))

测试结果

length of id_list: 3639
length of black list: 556
length of all id 4213

可以看到相较之前有小幅提升

  • 特殊名称,针对S2中某些姓名存在的'Latin2ASCII无法处理的情形,通过简单的替换操作除去后续得到的姓名分割后的列表中元素的'字符,代码如下,参考

l1 = [s.replace("'", '') for s in l1]   # remove the "'" in name

测试结果

length of id_list: 3659
length of black list: 536
length of all id 4213

匹配记录小幅提升

针对国籍不规范

初步对S1所构建的多级字典统计了下国籍数目,大约是246个,所以考虑在获得所构建的多级字典并对国籍进行分割后,将其中的一些名称表示不匹配的国家名建立字典然后循环替换,在拿去和S2进行匹配,代码如下:启发1,启发2

def transfer_country(dict):
    tr = {'United States': 'USA', 'Ireland': 'Republic of Ireland'}
    for row in tr:
        dict[tr[row]] = dict.pop(row)
    return dict

测试结果:

length of id_list: 3697
length of black list: 498
length of all id 4213

小幅提升,需要人工寻找更多可能的国籍字典来添加进去
重新分析black_list.csv可知出去之前的本身数据不一致或者数据错误以及记录找不到的情况,又发现如下情况:

57216,ismael-traore,,1986-08-18,France,29,1.86 m,Cote d'Ivoire|France,Defence - Centre Back,right,soccer-and-more-ltd-,496,sco-angers,1420,Jul 1. 2015,30.06.2017,

Ismael Traoré;Defender;Côte d'Ivoire;Traoré;1058;Ismael;18/08/1986;Angers

即其中的S2中部分国际名称存在拉丁字符例如Côte d'Ivoire,对这一点,类似之前对姓名的处理,需要进行转化,代码如下

def country_name_format(s):
    return Latin2ASCII.ud(s.decode('utf-8', 'ignore'))

结果如下,依然是小幅提升

length of id_list: 3733
length of black list: 466
length of all id 4213

为了将匹配失败的国籍一网打尽,我修改了部分代码,将所有S2中未通过国际匹配的国家名称输出到csv文件以便进行分析和类似于之前的映射处理,代码如下参考

import csv

def list2csv(list,file):
    wr=csv.writer(open(file,'wb'),quoting=csv.QUOTE_ALL)
    for word in list:
        wr.writerow([word])

对应的core部分,在原有基础上添加了输出country_list语句

        if nationality in S1:
            if date in S1[nationality]:
                s1 = row['players.first_name'] + ' ' + row['players.last_name']
                # print s1
                # print '+++++++'
                s2=S1[nationality][date].keys()
                # print S1[nationality][date]['name']
                # print s2
                for i in s2:
                    if Name_Match.name_match(s1, i):
                        # print S1[row['players.country']][date_format.date_convert(row['players.date_of_birth'])]['id']
                        # id_list.append(S1[row['players.country']][date_format.date_convert(row['players.date_of_birth'])][i]['id'])
                        id_list.append(S1[nationality][date][i]['id'])
                        # print id_list
                        temp_list.append(row['players.player_id'])
                #     print i
                # print '---------'
        else:
            country_list.append(nationality)

如此便可以收集到所有国籍不一致的国家,然后进行手动添加至字典进行映射处理,此外为使输出结果更加直观,除了输出匹配成功,失败,所有的记录数目,添加了输出匹配成功率的一项格式控制

print 'match rate:',"{:6.2f}%".format((qtt_white*1.0/qtt_all)*100)

再手动添加了剩余国籍映射问题后,输出的country_list.csv中仍有一条信息,仔细分析后,我将S1和S2中对应的这条记录放在下面

//record in S1
225339,queensy-menig,,1995-08-19,Netherlands,20,1.74 m,Netherlands|Suriname,Striker - Left Wing,right,forza-sports-group,2553,pec-zwolle,1269,Jul 25. 2015,30.06.2016,
//record in S2
Queensy;Menig;Menig;15538;19/08/1995;Forward;Zwolle;Netherlands

Queensy;Menig;Menig;15538;19/08/19918653;21/04/1991;Forward;Roda JC;Mexico

可以看到这条数据对应的球员在S2中有两条记录,而且从第二条的情况来看应该是数据错误,而我们捕捉到的Roda JC便来自于此,而对照S1中可知其实该球员的国际应该是Netherlands|Suriname,所以由此观之,S2中数据本身还存在一些问题,如重复和错误数据;
而当前的测试结果如下:

length of id_list: 3778
length of black list: 423
length of all id: 4213
match rate:  89.67%

所以我决定再匹配以前先对S2数据进行初步清晰,除去id重复的数据,对清洗后的数据进行匹配,代码如下,

import csv

# clean data of S2, eliminate data with same id
def clean_data(in_file,out_file):
    with open(in_file,'rb') as src_csv:
        reader = csv.reader(src_csv, delimiter=',')
        fieldnames = next(reader)
        # print fieldnames
        reader = csv.DictReader(src_csv, fieldnames=fieldnames, delimiter=';')
        writer=csv.writer(open(out_file,'wb'),delimiter=',')

        id=set()
        fieldnames=['players.vis_name','players.role','players.country',
                    'players.last_name','players.player_id', 'players.first_name',
                    'players.date_of_birth','players.team']
        writer.writerow(fieldnames)
        for row in reader:
            # print row

            if row['players.player_id'] not in id:
                # writer.writerow(row)
                # writer.writerow(row.keys())
                writer.writerow(row.values())
                id.add(row['players.player_id'])

in_file='2015players.csv'
out_file='2015players_clean.csv'

clean_data(in_file,out_file)

测试结果如下

length of id_list: 3536
length of black list: 423
length of all id: 3958
match rate:  89.34%

但是在除重的这一过程存在问题,因为我是直接根据id这一项来判断是否重复的,而除掉的重复项仅仅是按照逐行读取csv时的先后顺序,这样对于完全重复的记录还好,但是对于其他情况肯定是不科学的,所以我决定先把所有重复的记录导出来然后分析重复的情形;

于是在之前根据id判重的基础上用id_repeated这么一个列表来收集重复的id并仿照这之前的匹配操作将原始S2中对应列表中所有id的记录导出来得到repeated_id.csv,代码如下

def clean_data_byID(in_file,out_file):
    id_repeated=[]
    with open(in_file,'rb') as src_csv:
        reader = csv.reader(src_csv, delimiter=',')
        fieldnames = next(reader)
        # print fieldnames
        reader = csv.DictReader(src_csv, fieldnames=fieldnames, delimiter=';')
        writer=csv.writer(open(out_file,'wb'),delimiter=',')

        id=set()
        fieldnames = ['players.vis_name', 'players.role', 'players.country',
                      'players.last_name', 'players.player_id', 'players.first_name',
                      'players.date_of_birth', 'players.team']
        writer.writerow(fieldnames)
        for row in reader:
            # print row

            if row['players.player_id'] not in id:
                # writer.writerow(row)
                # writer.writerow(row.keys())
                writer.writerow(row.values())
                id.add(row['players.player_id'])
            else:
                id_repeated.append(row['players.player_id'])
    Csv_Match2.csv_match2(id_repeated,'2015players.csv','repeated_id.csv')

对应的csv_match2代码

# this is for the special csv file with ',' in header ';' in rows
def csv_match2(id_list,input_file,output_file):
    with open(input_file, 'rb') as f:
        # ------------------if the delimiter for header is ',' while ';' for rows
        reader = csv.reader(f, delimiter=',')
        fieldnames = next(reader)

        reader = csv.DictReader(f, fieldnames=fieldnames, delimiter=';')
        # reader = csv.DictReader(f)
        rows = [row for row in reader if row['players.player_id'] in set(id_list)]

    header = rows[0].keys()
    with open(output_file, 'w') as f:
        f.write(','.join(header))
        f.write('\n')
        for data in rows:
            f.write(";".join(data[h] for h in header))
            f.write('\n')

但是这样得到的文件不够直观,所以进一步的我对该文件进行了除去完全重复的记录以及根据id排序把所有所有类似的记录放到一起这么两个操作,代码如下:

  • 除去完全重复的记录

# eliminated the completely repeated record in repeated file for further analysis
def eliminate_repeated_row(in_file,out_file):
    with open(in_file,'rb') as in_file,open(out_file,'wb')as out_file:
        seen=set()
        for line in in_file:
            # print line
            if line in seen:continue

            seen.add(line)
            out_file.write(line)
  • 根据id排序把所有所有类似的记录放到一起

# sort the csv file by column 'id' to put the similar record together for further analysis
def sort_csv_byID(in_file,out_file):
    with open(in_file, 'rb') as f:
        reader = csv.reader(f, delimiter=',')
        fieldnames = next(reader)
        reader = csv.DictReader(f, fieldnames=fieldnames, delimiter=';')
        sorted_list=sorted(reader,key=lambda row:row['players.player_id'],reverse=True)
        # print sorted_list
        List2csv.nestedlist2csv(sorted_list,out_file)

对应的nestedlist2csv代码

# write nested list of dict to csv
def nestedlist2csv(list, out_file):
    with open(out_file, 'wb') as f:
        w = csv.writer(f)
        # it's very anoying that each time the field names changed
        # fieldnames = ['players.vis_name', 'players.first_name', 'players.date_of_birth', 'players.role',
        #               'players.player_id', 'players.team', 'players.country',
        #               'players.last_name']
        fieldnames=list[0].keys()  # solve the problem to automatically write the header
        # for row in list:
        #     print row.keys()
        # print list[0].keys()
        w.writerow(fieldnames)
        for row in list:
            w.writerow(row.values())

代码参考列表

测试结果部分截取如下:

vis_name,role,country,last_name,id,first_name,date_of_birth,team

Khazri,Midfielder,Tunisia,Khazri,989,Wahbi,08/02/1991,Bordeaux
Khazri,Midfielder,Tunisia,Khazri,989,Wahbi,08/02/1991,Sunderland

Kawaya,Forward,Belgium,Kawaya,9631,Andy,23/08/1996,Willem II

Lewis Baker,Midfielder,England,Baker,9574,Lewis,25/04/1995,Vitesse

Nordin Amrabat,Forward,Morocco,Amrabat,9425,Nordin,31/03/1987,Málaga
Nordin Amrabat,Midfielder,Morocco,Amrabat,9425,Nordin,31/03/1987,Watford

Marcelo Díaz,Midfielder,Chile,Diaz,9240,Marcelo,30/12/1986,Celta
Marcelo Díaz,Midfielder,Chile,Díaz,9240,Marcelo,30/12/1986,Hamburg

Vainqueur,Midfielder,France,Vainqueur,9102,William,19/11/1988,Dynamo
Vainqueur,Midfielder,France,Vainqueur,9102,William,19/11/1988,Roma

Kramaric,Forward,Croatia,Kramaric,8726,Andrej,19/06/1991,Hoffenheim
Kramaric,Forward,Croatia,Kramaric,8726,Andrej,19/06/1991,Leicester

Gullón,Midfielder,Spain,Gullón,8449,Marcos,20/02/1989,Roda JC

Iturbe,Forward,Paraguay,Iturbe,8364,Juan,04/06/1993,B'mouth
Iturbe,Forward,Argentina,Iturbe,8364,Juan,04/06/1993,Roma

可以看到除去完全重复的记录以及之前记录错误的情形,S2中存在不少记录显示的球员的队名不同,而其他信息一致;
进一步的,随机找了部分S1中对应的球员信息比对球队信息,例如:

S1-current_club S2-team
sunderland-afc Sunderland/Bordeaux
watford-fc Málaga/Watford
celta-de-vigo Celta/Hamburg
... ...

分析过程中我发现我犯了一个错误,这些重复的数据并不会对我们之前所构建的多极字典中的匹配造成影响,有以下几点原因:

  • 首先,我们是根据国籍出生日期姓名完成匹配,这中间并未涉及到球队名称的匹配,所以即使有两条记录在这里产生不一致也不会对于匹配结果产生影响,因为我们S1的数据相对规范,而且我们是遍历数据量相对较小的S2去与以S1为基础构建好的多级字典进行匹配,只要满足国籍出生日期姓名匹配我们就认为是匹配成功的,而这一过程中无论是无安全重复的记录亦或只是球队不同的记录都是能成功匹配的,只是匹配到的是S1中同一条记录而已;

  • 此外,对于匹配率的计算,回顾几个收集id的list,最后的操作都是通过set()来隐式除重所以无论是计算匹配率还是输出所有没有通过匹配的black_list(因为这里我是取出所有没有通过的S2中记录的id然后对原始S2文件进行遍历输出所有匹配id的记录,所以可以确保所有未通过匹配的记录都被导出到black_list.csv)

length of id_list: 3535
length of black list: 423
length of set(all id): 3958
length of all id: 4213
length of set(id matched in S2): 3535
length of id matched in S2: 3778
match rate:  89.31%

所以重心还是得放到对于未通过匹配的black_list.csv

进一步的,我们仿照着类似的情形对于S2中国籍匹配通过而日期未通过的数据导出字典(id为键,date of birth为值)到csv文件进行分析,代码如下,参照

def dict2csv(dict,file):
    with open(file,'wb') as f:

        w=csv.writer(f)
        # write each key/value pair on a separate row
        w.writerows(dict.items())

结果大约导出了49条记录,问题原因大概出在前面提到的错误日期以及日期不匹配上,所以,考虑用另一种方法对S1构建多级字典(nationality->position->name),这中间除了本身够贱多级字典可以参照之前的方法以外,对于position需要类似之前对于国籍不匹配的问题进行字典映射,因为S1中的position分类较细(约49种),而S2中对应的role只是简单分为了4中,所以需要将S1中的'postion'利用字典映射到S2中的'role'从而进行匹配;
此外,在对由于日期不匹配的部分数据(约49条)进行匹配后,除了本身需要将数据追加到前面匹配成功的各个数据如temp_list,id_list,white_list,id_map并从black_list里除去以外;对于数据的匹配率也只是小幅提升,难点在于对于姓名不一致的情形的匹配上;

通过对于姓名不匹配的记录的一些分析,我发现大概有以下2种情形

  • 情形一,S1中的姓名比S2中的长:

233782,aissa-bilal-laidouni,,1996-12-13,,19,,France,Midfield,,,,sco-angers-b,16672,-,-,

Laidouni;Midfielder;France;Laidouni;19906;Aissa;13/12/1996;Angers

这个与我之前的匹配规则有关,因为之前对于两边数据源的分析发现S1的name一般而言是截取的球员的部分姓名,是S2的first_name+last_name的一部分,也就是说短于S2的所以当时指定的规则是判断S1的姓名分割后的集合是否是S2的集合的子集;

  • 情形二:两个数据源中的姓名字段有部分字母不一样;最常见的不一致是yi的不一致;

4315,johnny-heitinga,,1983-11-15,Netherlands,32,1.80 m,Netherlands|Indonesia,Defence - Centre Back,right,wasserman-netherlands-management,274,end-of-career,123,Feb 1. 2016,-,

Heitinga;Defender;Netherlands;Heitinga;190;John;15/11/1983;Ajax

102045,izu-uzochukwu,,1990-04-11,,26,1.71 m,Nigeria,Midfield - Defensive Midfield,right,ohne-berater,96,odense-boldklub,173,Jan 26. 2016,30.06.2019,

Uzochukwu;Midfielder;Nigeria;Uzochukwu;18237;Izunna;11/04/1990;Amkar

212251,yuri-shafinskiy,,1994-05-06,Russia,21,1.90 m,Russia,Goalkeeper,right,sa-football-agency,2119,anzhi-makhachkala-ii,26567,Aug 12. 2015,30.06.2019,

Shafinsky;Goalkeeper;Russia;Shafinsky;15565;Yuri;06/05/1994;Anzhi

55396,evgeni-pomazan,,1989-01-31,UDSSR,27,1.93 m,Russia,Goalkeeper,right,prosports-management,1330,anzhi-makhachkala,2700,Sep 1. 2011,-,

Pomazan;Goalkeeper;Russia;Pomazan;7457;Evgeny;31/01/1989;Anzhi

对这种不一致我的想法是利用字符串的模糊匹配设置一个阈值来判断达到多少匹配即判断是匹配的;

下面就日期不匹配以及姓名的模糊匹配来进行优化:
结果在导出了两个数据源对应球员position的信息,我觉得还是建立二级字典直接匹配姓名比较合适...

l1 = ['Forward', 'Midfielder', '21/04/1991', 'Goalkeeper', 'Defender']

l2 = ['', 'Defence - Left Midfield', '- Central Midfield', 'Defence', 'Striker - Defensive Midfield',
      'Midfield - Centre Back', 'Defence - Left Wing', '- Attacking Midfield', 'Defence - Right Midfield',
      '- Centre Forward', 'Striker - Right Midfield', 'Striker - Secondary Striker', 'Midfield - Centre Forward',
      'Striker', '- Defensive Midfield', 'Striker - Left Midfield', 'Defence - Centre Back', 'Defence - Centre Forward',
      '- Centre Back', 'Striker - Right Wing', 'Midfield - Sweeper', 'Midfield - Defensive Midfield',
      'Striker - Centre Forward', 'Defence - Defensive Midfield', 'Midfield - Attacking Midfield', '- Left Midfield',
      'Striker - Central Midfield', 'Midfield - Secondary Striker', 'Midfield - Left Wing', 'Defence - Right Wing',
      'Striker - Attacking Midfield', 'Defence - Central Midfield', 'Striker - Right-Back', 'Midfield',
      'Midfield - Right-Back', 'Striker - Centre Back', '- Right Midfield', 'Defence - Right-Back',
      'Striker - Left-Back', 'Midfield - Right Wing', 'Defence - Attacking Midfield', 'Striker - Left Wing',
      'Midfield - Left Midfield', 'Midfield - Central Midfield', 'Midfield - Left-Back', 'Goalkeeper',
      'Defence - Left-Back', 'Midfield - Right Midfield', 'Defence - Sweeper']

我本来想手动将l2中数据映射到l1中,但是除去本身数据量比较大以外,发现很多l2中的字段在l1中无法找到对应的,例如Striker - Centre Back,而且也无法像对姓名那样利用子集匹配,所以我决定直接对这日期匹配失败的49条记录利用nationality->name的二级字典来进行匹配;

这里有两种思路,一种是另写方法来读入未匹配的日期文件与构建的二级字典匹配,将匹配成功的记录根据id分别添加如各个列表并将仍不成功的记录导出;
思路二是直接在原始匹配函数中对匹配不成功的日期除了收集并导出外,添加语句直接通过二级字典进行匹配完成对各个列表的添加,修改操作,同样的导出仍未匹配成功的记录;
(此外在这一过程中我通过对于S1的id的收集及处理判断其是否唯一,结果显示是唯一的;)
考虑到思路一还要再次读入文件,我决定尝试思路二
代码如下

if nationality in S1:
                if date in S1[nationality]:
                    s1 = row['players.first_name'] + ' ' + row['players.last_name']
                    s2 = S1[nationality][date].keys()
                    for i in s2:
                        if Name_Match.name_match(s1, i):
                            # get id for white list
                            id_list.append(S1[nationality][date][i]['id'])
                            # get matched id in S2
                            temp_list.append(row['players.player_id'])
                            id_dict[row['players.player_id']] = S1[nationality][date][i]['id']
                else:
                    # collect the date mismatched id
                    date_dict[row['players.player_id']] = row['players.date_of_birth']
                    date_list.append(row['players.player_id'])
                    # try to match the date mismatched record by 2-level nested dict
                    s1 = row['players.first_name'] + ' ' + row['players.last_name']
                    s2 = S1_2[nationality].keys()
                    for i in s2:
                        if Name_Match.name_match(s1, i):
                            id_list.append(S1_2[nationality][i]['id'])
                            ignore_date_list_s1.append(S1_2[nationality][i]['id'])
                            # get matched id in S2
                            temp_list.append(row['players.player_id'])
                            ignore_date_list_s2.append(row['players.player_id'])
                            id_dict[row['players.player_id']] = S1_2[nationality][i]['id']
                        # wrong logic here, traverse the data set one by one can only judge the matched one,you can only
                            #  say it's mismatch when traversed all element
                        # else:
                        #     date_still_list.append(row['players.player_id'])

这里犯了一个逻辑错误在于对于判断国籍相同的情形下忽略出生日期后,姓名匹配的记录的id的收集过程中,收集匹配成功的数据的逻辑是没问题的,在国际已成功匹配但是日期不匹配的数据集中遍历构建好的二级字典,判断其中姓名是否匹配,若匹配则追加到之前的两个数据源的id列表以及对应的映射字典中;但这里我犯的错误时对于不匹配的数据我直接也收集id,这是不对的,因为当前遍历的是根据S1构建好的二级字典,再逐一遍历的过程中,与S2中姓名匹配的理论上应该是一个,而在此过程中若统计不匹配的则循环下来我就把所有的id都添加进去了,即使当前id是能匹配到,但是只是这一次遇到的S1中的名称不匹配也会被加进去;
正确的做法是取反也就是从原始因为日期原因未匹配成功而收集的id列表中除去所有的现在忽略日期而在二级字典中匹配成功的记录即可;
对应的二级字典构建代码如下

# build specific nested dict from csv files(nationality->name)
def build_level2_dict(source_file):
    new_dict = {}
    with open(source_file, 'rb')as csv_file:
        data = csv.DictReader(csv_file, delimiter=",")
        for row in data:
            item = new_dict.get(row['nationality'], dict())
            item[row['name']] = {k: row[k] for k in ('id', 'complete name', 'place of birth',
                                                         'age','position','height', 'date of birth', 'foot',
                                                         "player's agent",'agent id','current club',
                                                         'club id', 'in the team since', 'contract until',
                                                         'outfitter')}
            new_dict[row['nationality']] = item
    return new_dict

然后这样一来匹配的结果如下:

length of id_list: 3841
length of set(id_list): 3594
length of black list: 379
length of set(all id): 3958
length of all id: 4213
length of set(id matched in S2): 3579
length of id matched in S2: 3841
match rate:  90.42%

现在有以下几个问题:

  • 国籍不一致

    • 本身两边的国籍信息不一致

    • S1中的同一国籍有两种不同写法(出现在不同记录中)

  • 重复数据造成的S1,S2中的的匹配id数目不一致

  • 姓名不一致

解决思路

  • 国籍不一致

    • 本身两边的国籍信息不一致
      经过当前的优化,原有的49条国籍匹配成功但是日期匹配失败的记录现在只剩下5条不匹配,逐一分析发现,其中有三条记录是由于国际信息不一致导致的,考虑对其进行(date of birth->name构建二级字典进一步匹配)

    • S1中的同一国籍有两种不同写法(出现在不同记录中)
      这个应该跟S1的数据录入有关,如下:

255930,aristote-ndongala,,1994-01-19,Zaire,22,1.86 m,Congo DR,Midfield - Left Wing,left,bestway-soccer-limited,2759,fc-nantes-b,10850,-,30.06.2017,

Ndongala;Midfielder;Congo;Ndongala;5258;Aristote;19/01/1994;Nantes

其中

S1-nationality S2-country
Congo DR Congo

起先我以为只是国际名称的不一致,后来通过对于S1中的Congo的搜索发现其中其他记录也有这种写法,所以根本原因是S1本身数据的录入不一致,考虑直接将Congo DR映射到Congo结果对S2一搜索发现其中也存在Congo DR的记录,仔细查了维基发现是我孤陋寡闻了,实际是非洲的两个不同国家,那问这个应该也归类到上面的不同数据源的国籍信息不一致问题

  • 重复数据造成的S1,S2中的的匹配id数目不一致
    之前提到过S2中存在四百多条数据重复的问题,除去完全重复以外还有仅仅因为球队名称不同导致的重复,由于预处理没有更多信息基础难以除重(除非再加一级判断球队名称,但是一来这样更加复杂导致匹配度会受影响而且本身球队名称类似人物名两边写法不一致,实际操作也不太好),所以当前的想法是我先把所有数据匹配起来,因为一来S1中不存在重复数据,二来由上面的结果可以看到,根据我们三级列表的判断,球队名的不一致所造成的重复是不影响匹配的,所以我们可以在匹配过后回过头来根据球队对匹配后的数据根据球队名进行再匹配除去S2中的不一致数据;

但是在上面看到这次匹配出现了之前未曾看到的新问题,之前数目一致的除重后的S1,和S2中的匹配id列表在分别追加忽略日期信息的匹配记录后出现了不一致,猜想是因为约束条件放松加上姓名匹配是判断子集可能多个S1中的姓名对应到了S2中相同的姓名,考虑先导出值相同键不同的重复的记录分析并改进姓名匹配规则

  • 姓名不一致
    考虑用模糊匹配进行处理

在此基础上我将经过二级字典匹配的之前因为日期原因未匹配成功的数据,由于统计结果显示存在多个S1中id对应同一个S2中id的情形,所以我猜测是跟姓名匹配有关,所以先是根据之前分别收集的id构建了一个字典,然后对该字典进行重构将其中值相同的元素的值取出来作为键,而多个对应的键构成列表作为值构建新的字典,这样我就能很直观地看到哪些S1中的id对应了S2中的同一个id;参考代码

# return a dict with the same value in original as new key and keys as value
def dict_same_value(original_dict):
    new_dict={}
    for k,v in original_dict.iteritems():
        new_dict.setdefault(v,[]).append(k)
    return new_dict

old_dict=core.ignore_date_dict
repeated_dict=dict_same_value(old_dict)
print {k:repeated_dict[k] for k in repeated_dict if len(repeated_dict[k])>=2}

而统计出的结果如下:

{'19559': ['172796', '38588', '354359'],
'18892': ['30846', '26734'],
'4095': ['199330', '349899', '70961', '113138'],
'15144': ['51518', '128702'],
'11506': ['263843', '232143'],
'3946': ['92947', '176427', '92781', '95433'],
'6395': ['150927', '74548', '292349', '270188'],
'18297': ['95497', '251876']}

以其中的第一组为例

Julen;López;Julen López;19559;00/00/0000;Midfielder;Eibar;Spain

172796,julen,Julen Macizo Adan,1989-03-02,Spain,27,,Spain,Goalkeeper,,,,unknown,75,Jul 1. 2011,-,

38588,lopez,Pedro Jesus de Tuleda Lopez,1983-08-25,,32,1.87 m,Spain,Defence - Centre Back,,,,unknown,75,Jul 1. 2008,-,

354359,julen-lopez,,,Spain,,,Spain,Defence - Centre Back,,Agent is known players under 18:,no id,cd-vitoria,50186,Jul 1. 2015,30.06.2016,

可见造成这种情形的原因是S1中的姓名录入不规范,考虑修改姓名匹配规则;
但是进一步分析其他数据发现,存在如下情形:

Lucas;Evangelista Santana de Oliveira;Evangelista Santana de Oliveira;6395;06/05/1995;Midfielder;Udinese;Brazil

150927,oliveira,Valmir Alves de Oliveira,1979-04-07,,37,,Brazil,Striker,,,,unknown,75,Jul 1. 2010,-,

74548,lucas,Lucas Marcolini Dantas Bertucci,1989-05-06,Brazil,26,1.76 m,Hungary|Brazil,Midfield - Attacking Midfield,

292349,lucas-oliveira,Lucas Dias Pires de Oliveira,1995-06-27,Brazil,20,1.80 m,Brazil,Striker - Left Wing,right,,,ad-estacao,10164,Aug 25. 2014,-,

270188,lucas-evangelista,Lucas Evangelista Santana de Oliveira,1995-02-06,Brazil,21,1.81 m,Brazil,Midfield - Attacking Midfield,right,familienangehoriger,1207,panathinaikos-athens,265,Jan 20. 2016,30.06.2016,

在这一组数据中一个S2中的id对应了4个S1中的id,如果仅仅这种情形我们可能可以通过控制姓名模糊匹配的阈值来控制;但是对于下面的情形就不好办了:

Antonio Jesús;Cotán Pérez;Cotán Pérez;4095;10/09/1995;Midfielder;Sevilla;Spain

199330,antonio-cotan,,1995-09-19,Spain,20,1.76 m,Spain,Midfield - Central Midfield,,you-first-sports,1406,sevilla-atletico,8519,Jul 1. 2012,30.06.2016,

349899,antonio-perez,,1993-05-24,Spain,22,1.77 m,Spain,Defence - Right-Back,right,,,sd-tarazona,41403,Jul 1. 2015,30.06.2016,

可以看到上述两天记录对应的阈值极有可能是相同的,所以仅仅是靠姓名来进行模糊匹配准确率还是欠佳,可能还是再加一层日期的模糊匹配,针对这个情形可以考虑取出年-月作为键,但是后续发现其他情形中有月份和日子都对不上的,考虑只用年来区分,但这样效果就又差了

此外在此过程中还发现了S2中数据的一个错误

Nolan Mbemba;Batina;Batina;19671;19/02/1995;Midfielder;Lille;France
Nolan;Mbemba;Nolan Mbemba;20158;19/02/1995;Midfielder;Lille;France

276768,nolan-mbemba,,1995-02-19,,21,1.81 m,France|Congo DR,Midfield - Defensive Midfield,,,,losc-lille-b,12765,-,-,

前面两条中有一条是不规范的,就大部分S2中数据来看,极有可能是第一条有问题,而这样的同一个人却对应了两个id,不得不说S2的数据实在是太粗糙了点

至此,接下来的思路是:

  • 姓名匹配的规则一定要改为模糊匹配,当前判断子集无论是匹配准确度还是匹配度上都不尽如人意

  • 前面试图追加后期二级字典匹配的的想法现在看来结果反而出现了重复对应的问题,所以考虑还是分步骤处理,数据是不平等的,所以需要制定规则划分类别;

  • 仅仅国籍-姓名的二级字典匹配准确率欠佳,考虑加入日期模糊匹配,思路有两种:构建三级字典或者后期判断

姓名模糊匹配

使用第三方库fuzzywuzzy,初步通过对于一些数据的测试将匹配率设置在60%,代码如下:(保留了之前对于名称Unicode以及分隔符等等的处理)

# modify the name match rule for fuzz name match,token_sort_ratio>=60
def name_match(s1,s2):
    # Latin -> ASCII -> lower case ->split by space, - etc.
    l1=re.findall(r"[\w']+", str.lower(Latin2ASCII.ud(s1.decode('utf-8', 'ignore'))))
    l1 = [s.replace("'", '') for s in l1]   # remove the "'" in name

    s1=' '.join(l1)
    s2=s2.replace('-',' ')
    # if fuzz.token_sort_ratio(s1,s2)>=60:
    #     return True
    return fuzz.token_sort_ratio(s1,s2)>=60

其中对于将列表中的字符串转化为空格为分割符的字符串,参照

重新进行第一阶段的匹配工作,仍以国籍->出生日期->姓名的三级字典以修改后的姓名匹配规则进行匹配,需要:

  • 导出匹配成功的S1,S2中记录分别到两个不同的csv文件

  • 构建以S1中id为键,S2中id作为值的映射列表,这里注意,虽然原始的S1中的id是唯一的,但是由于S2中存在重复记录以及部分重复记录造成可能同一个S1的id对应多个S2中的id(也有可能并不会,后续同级两个id列表长度进行分析看看);如果出现上述情况则对值构建嵌套列表,将不同的id放入其中,而不是覆盖

  • 将这一阶段匹配失败的记录提取出来保存为另一个文件以作为下一阶段匹配工作的数据源

在直接利用修改后的姓名匹配方法进行配对后输出结果如下:
name_match(60ratio)

表面上看到匹配率和匹配记录都大幅提升了,但是对于除重后的两个数据源的id构成的set的数目进行对比我发现与之前不同,前面两个数据源通过子集判断匹配姓名所匹配成功的id列表数目以及经过除重后所得的id集合数目都是一致的,这个基本上可以证明没有出现S1中多个姓名对应S2中同一人的错误匹配(当然我后面会进一步判断和处理),如下
name_match_by_subset

而反观对于以60%的比率的模糊匹配方法匹配的结果两个有几十条记录的差异,经过导出id映射字典(这里我对于字典的构建处理方法是以s2的id为键,然后值为列表,添加或创建s1的id,这样就保留了s1中的重复id对应s2中id的情况,可以看看是否出现错误匹配)

id_dict.setdefault(row['players.player_id'], []).append(S1[nationality][date][i]['id'])

而事实确实发生了错误匹配,以其中一条记录为例:

344,"['204298', '37217', '76050', '204298', '37217', '76050', '204298', '37217', '76050']"

而对这些记录的进一步观察发现由于匹配率过低导致不少不同s1中的人会对应s2中的同一个人,那么如果单纯的考虑提高匹配率呢,这样可以提升精确度,但是会漏掉不少实际是同一人但是由于仅仅输入部分姓名导致匹配率不高的情形,例如:

 s2='charles-nzogbia'
 s1="Charles N'Zogbia José  Hepburn-Murphy"

匹配率大约只有60%+
结果如下
name_match(90ratio)

而另外的仅仅通过子集判断但是加入逆子集判断(因为后来发现存在少许S1中姓名包含S2中姓名的情形)也做了一次测试,输出结果如下:
name_match_by_subset2

所以我决定把子集判断与模糊匹配(90%ratio)结合起来
name_match_mix

对应的代码如下

if Name_Match.name_match_by_set(s1, i):
                        # get id for white list
                        id_list.append(S1[nationality][date][i]['id'])
                        # get matched id in S2
                        temp_list.append(row['players.player_id'])
                        # id_dict[row['players.player_id']] = S1[nationality][date][i]['id']
                        id_dict.setdefault(row['players.player_id'],[]).append(S1[nationality][date][i]['id'])
                    elif Name_Match.name_match_by_set2(s1, i):
                        # get id for white list
                        id_list.append(S1[nationality][date][i]['id'])
                        # get matched id in S2
                        temp_list.append(row['players.player_id'])
                        # id_dict[row['players.player_id']] = S1[nationality][date][i]['id']
                        id_dict.setdefault(row['players.player_id'], []).append(S1[nationality][date][i]['id'])
                    elif Name_Match.name_match(s1,i):
                        # get id for white list
                        id_list.append(S1[nationality][date][i]['id'])
                        # get matched id in S2
                        temp_list.append(row['players.player_id'])
                        # id_dict[row['players.player_id']] = S1[nationality][date][i]['id']
                        id_dict.setdefault(row['players.player_id'], []).append(S1[nationality][date][i]['id'])

但是id集合也不完全相等,所以接下来我对id_map进行处理,逐一遍历其值形成的列表的集合,将其中长度大于1的记录输出出来看看
代码如下:

print {k:id_dict[k] for k in id_dict if len(set(id_dict[k]))>=2}

输出结果如下(一共三条记录(2条s1中id对应同一个s1中的id)):

Mikel;Oyarzabal;Oyarzabal;19641;21/04/1997;Forward;Sociedad;Spain

351478,mikel-oyarzabal,,1997-04-21,Spain,19,1.80 m,Spain,Midfield - Left Wing,left,df-sportmanagement-gmbh,3734,real-sociedad,681,Jan 1. 2016,30.06.2021,

376590,mikel-oyarzabal-ugarte,,1997-04-21,,19,,Spain,Midfield,,,,delete-player,10194,-,-,
------------
Danilo;D'Ambrosio;D'Ambrosio;3004;09/09/1988;Defender;Inter;Italy

120767,dario-dambrosio,,1988-09-09,Italy,27,1.89 m,Italy,Defence - Right-Back,right,,,bassano-virtus,9690,Jan 30. 2016,-,

55769,danilo-dambrosio,,1988-09-09,Italy,27,1.80 m,Italy,Defence - Right-Back,right,tullio-tinti,1708,inter-milan,46,Jan 30. 2014,30.06.2018,Nike
-------------
Rodrigue Casimir;Ninga;Ninga;19220;17/05/1993;Forward;Montpellier;Chad

392810,casimir-ninga,,1993-05-17,Chad,22,1.86 m,Chad,Striker - Centre Forward,right,new-star-sport-promotion,1119,hsc-montpellier,969,Aug 31. 2015,30.06.2020,

210948,rodrigue-ninga,,1993-05-17,,22,,Chad,Striker,,,,elect-sport-fc,48525,-,-,

第一条是根据逆子集匹配得到的,然而除去第二条数据可以根据姓名或者球队进行再次匹配来处理外,其余的两条数据就目前所得的记录的信息是难以分别哪条是正确的;

先保留记录留到后续处理,至此第一阶段的匹配完成,接下来开始就剩余的black_list进行处理,在此之前我需要将第一阶段所得的记录单独提取出来作为第一类匹配成功数据集,沿用之前的代码,根据id将S1,S2中对应的记录分别写到white_list.csv(3662)和white_list2.csv(3906)中,上述记录数差别在于S2中存在id重复数据,这个现在没有影响,后续根据其他信息反过来对其进行清洗;然后black_list.csv(309),接下来另写程序对于'black_list'进行处理

第二阶段

  • 首先通过建立二级字典(国籍->姓名)来对之前修改后的姓名匹配原则进行匹配看看测试结果:
    black_list_match(ratio90%)

可以看到,首先匹配率比较低毕竟在高达90的匹配要求下加上两个数据源的姓名录入不规范尤其s1中不少姓名只是部分姓名;另一方面还是存在不少错误匹配的结果,所以期望用这种方式来进一步匹配效果不太好;
那么对于先前的自己匹配呢,预计会出现大量错误匹配,毕竟失去日期这一强力的限制后,加上s1中许多仅仅录入了姓或者名的球员信息很容易被判断为匹配:
black_list_match(subset)

果然,出现大量错误匹配,看来直接进行二级字典的匹配效果很不理想,所以从一开始建立三级字典来精确结果提高查找效率是很有必要的;考虑仍构建三级字典,不过对于中间层日期适当放宽,仅取年-月或者仅仅取来构建三级字典进行匹配测试

三级字典(国籍->出生年份->姓名

在构造这种字典的过程中我发现之前我所构造的三级字典的一个漏洞,即默认的认为人名是唯一的或者说在加上国籍出生日期的限制下的人名是唯一的,所以在构造三级字典的最里层的name时是默认的唯一的,所以采取的是覆盖的构造方法,毫无疑问对于数据量较大的数据源这种方法,所以我对此进行了修改,将最里层设置为列表,对于出现上述国籍出生日期姓名全相同的记录,存到列表中,这样就不会造成覆盖而丢失记录,新的字典构造代码如下:

# build specific nested dict from csv files, relax the 'date' key regulation(nationality->date of birth(only year)->name)
def build_dict3(source_file):
    new_dict = {}
    with open(source_file, 'rb')as csv_file:
        data = csv.DictReader(csv_file, delimiter=",")
        for row in data:
            # print row
            item = new_dict.get(row['nationality'], dict())
            # print item
            # sub_item = item.get(row['date of birth'], dict())
            year = str.split(row['date of birth'], '-')[0]
            # print year
            sub_item = item.get(year, dict())
            # print sub_item
            # sub_item[row['name']] = {k: row[k] for k in ('id', 'complete name', 'place of birth',
            #                                              'age', 'height', 'position', 'foot',
            #                                              "player's agent",'agent id','current club',
            #                                              'club id', 'in the team since', 'contract until',
            #                                              'outfitter')}
            # consider the condition more than 1 person in the same nationality,same birth year and even same name
            sub_item.setdefault(row['name'], []).append({k: row[k] for k in ('id', 'complete name', 'place of birth',
                                                                             'age', 'height', 'position', 'foot',
                                                                             "player's agent", 'agent id',
                                                                             'current club',
                                                                             'club id', 'in the team since',
                                                                             'contract until',
                                                                             'outfitter')})
            # sub_item[row['name']]={'id':row['id']}
            # print sub_item
            item[year] = sub_item
            # print item
            # print '++++++++++++++++++++++++++++++++++'
            new_dict[row['nationality']] = item
    # print '+++++++++++++++++++++++++++++++++++++'
    # print new_dict
    return new_dict

其中在抽取出生日期中的年份主要是利用字符串的分割以及避免重复而利用的setdefault()方法;
进一步的,我对于S1进行了姓名和id的统计及其集合的统计来看看是否有重复数据
stat

==========上方是对姓名的统计,下方是对id的统计,可见S1中姓名重复的情况还是不少,id倒确实唯一,对应的统计代码如下:

# get certain column value of csv(for common csv file(',')),and judge if it's repeated
def get_column_value2(file,column_name):
    with open(file,'rb') as f:
        role_list=[]

        reader=csv.reader(f,delimiter=',')
        fieldnames=next(reader)
        reader=csv.DictReader(f,fieldnames=fieldnames,delimiter=',')
        for row in reader:
            # print row['players.role']
            role_list.append(row[column_name])

        role_set=set(role_list)
        print 'set statistics:',len(set(role_list))
        print '-------------'
        print 'list statistics',len(role_list)
        print '============='
        return list(role_set)

对于修改后的字典构建对第一阶段进行测试,结果如下:
modify_dict

可以看到其中确实存在一些记录按我们的规则是重复的,通过对其输出的重复记录(9条)进行分析,我们发现,大部分记录是类似这样的

Luke;Dreher;Dreher;20403;27/11/1998;Midfielder;C. Palace;England

432330,luke-dreher,,1998-11-27,,17,,England,,,,,delete-player,10194,-,-,
432293,luke-dreher,,1998-11-27,England,17,1.84 m,England,Midfield - Central Midfield,right,,,crystal-palace-u18,6950,-,-,

即其中存在delete-player的字眼,应该是同一球员,但是类似S2中存在同一球员不同球队的状况,这种情况只能留在后面处理,毕竟也确实是同一个球员,可以理解为只是信息冗余了,另一种情况是这样的:

Danilo;D'Ambrosio;D'Ambrosio;3004;09/09/1988;Defender;Inter;Italy

120767,dario-dambrosio,,1988-09-09,Italy,27,1.89 m,Italy,Defence - Right-Back,right,,,bassano-virtus,9690,Jan 30. 2016,-,
55769,danilo-dambrosio,,1988-09-09,Italy,27,1.80 m,Italy,Defence - Right-Back,right,tullio-tinti,1708,inter-milan,46,Jan 30. 2014,30.06.2018,Nike

匹配率在90%以上所以产生了重复,这个后面匹配完成后可以进一步清洗,还有一种难以分辨,考虑后期根据球队匹配配清晰

Rodrigue Casimir;Ninga;Ninga;19220;17/05/1993;Forward;Montpellier;Chad

392810,casimir-ninga,,1993-05-17,Chad,22,1.86 m,Chad,Striker - Centre Forward,right,new-star-sport-promotion,1119,hsc-montpellier,969,Aug 31. 2015,30.06.2020,
210948,rodrigue-ninga,,1993-05-17,,22,,Chad,Striker,,,,elect-sport-fc,48525,-,-,

放宽日期限制

在第二阶段,我试图放宽中间的出生日期的限制来对第一阶段为匹配的数据进行匹配(因为考虑到可能大部分数据是由于两边数据源的日期不一致所造成的)

  • 仅提取出生年份
    主要涉及的是一个对于出生日期中的字符串分割提取然后重构字典,代码如下:

def build_dict3(source_file):
    new_dict = {}
    with open(source_file, 'rb')as csv_file:
        data = csv.DictReader(csv_file, delimiter=",")
        for row in data:
            # print row
            item = new_dict.get(row['nationality'], dict())
            # print item
            # sub_item = item.get(row['date of birth'], dict())
            year = str.split(row['date of birth'], '-')[0]
            # print year
            sub_item = item.get(year, dict())
            # print sub_item
            # sub_item[row['name']] = {k: row[k] for k in ('id', 'complete name', 'place of birth',
            #                                              'age', 'height', 'position', 'foot',
            #                                              "player's agent",'agent id','current club',
            #                                              'club id', 'in the team since', 'contract until',
            #                                              'outfitter')}
            # consider the condition more than 1 person in the same nationality,same birth year and even same name
            sub_item.setdefault(row['name'], []).append({k: row[k] for k in ('id', 'complete name', 'place of birth',
                                                                             'age', 'height', 'position', 'foot',
                                                                             "player's agent", 'agent id',
                                                                             'current club',
                                                                             'club id', 'in the team since',
                                                                             'contract until',
                                                                             'outfitter')})
            # sub_item[row['name']]={'id':row['id']}
            # print sub_item
            item[year] = sub_item
            # print item
            # print '++++++++++++++++++++++++++++++++++'
            new_dict[row['nationality']] = item
    # print '+++++++++++++++++++++++++++++++++++++'
    # print new_dict
    return new_dict

其中核心部分是这里:

year = str.split(row['date of birth'], '-')[0]
            sub_item = item.get(year, dict())          
            sub_item.setdefault(row['name'], []).append({k: row[k] for k in ('id', 'complete name', 'place of birth',
                                                                             'age', 'height', 'position', 'foot',
                                                                             "player's agent", 'agent id',
                                                                             'current club',
                                                                             'club id', 'in the team since',
                                                                             'contract until',
                                                                             'outfitter')})

然后对第一阶段产生的black_list.csv进行测试,结果如下
year
乍一看似乎一下子解决了一半的不匹配状况,但是考虑到这样一下放宽可能会产生较多地重复匹配甚至错误匹配,所以故技重施导出之前S1中重复匹配S2中的数据记录,结果分析发现存在不少错误匹配的情形,所以这种匹配方法不太合适,那么提取年-月呢?
同样的这次是提取出生日期的年份+月份来进行过滤,代码核心就是用了一个字符串的切片,其余的和上面的类似就不贴了,测试结果如下:
year-month
可见一来匹配率不高,二来分析重复匹配数据发现居然也存在错误匹配的记录(即同国籍,同出生年月而且姓名满足匹配要求的人(通过了子集判断或者姓名字段模糊匹配率在88%以上)),所以看来这么一条死路不太可行,于是我们把目光投向了S2剩余的属性;

首先是球员位置,前面的分析发现这一属性较难匹配,需要结合足球知识手动构建映射字典来转换,后期没有办法的话再考虑;那么就只好看看俱乐部-球队的映射字典了;

构建俱乐部-球队映射字典颇费功夫,在对于两个数据源中对应俱乐部和球队的字段的分析过程中我发现,s1中对于俱乐部的记录更为详尽,包括了同一只俱乐部下不同‘梯队’的球队,例如:

s1中的

"celta-de-vigo"
"celta-vigo-b"
"celta-vigo-juvenil-a"
"celta-vigo-youth"

对应s2中的

Celta

此外就是s2中存在一些简写,所以需要手动匹配一些记录,例如

afc-bournemouth        ->      B'mouth  

特殊字符需要处理,例如

1-fc-koln        ->        1. FC Köln

所以类似于之前对于姓名中字符的处理,代码如下:

# format the row value of 'team', eliminate "'", decode the Latin char, replace the space with '-'
def simple_format_for_team(str_val):
    l_val = re.findall(r"[\w']+", str.lower(Latin2ASCII.ud(str_val.decode('utf-8', 'ignore'))))
    l_val = [s.replace("'", ' ') for s in l_val]
    return '-'.join(l_val)

而对于球队名的映射,则需要参照之前国家名的映射,而且更为复杂,因为此处球队名处于中间层的位置,对于字典传递值的需要准确你想要传递的部分,再就是需要考虑重名的情形,因为前面所述存在多种s1的不同梯队的同一俱乐部对应s2中的一个俱乐部,所以转换后需要判断当前俱乐部是否已存在,从而判断是创建还是添加元素,这里还用到了列表的嵌套所以层级比较复杂;
而且在代码书写的过程中还发现一个问题,就是可能经转换后s1与s2之间存在名称完全一致的俱乐部,这个时候我的做法是判断其值的变量类型是字典还是列表来进行不同的操作,但是以来这样代码不够优雅,二来不是很符合《learning python》中所述的python作为动态语言不应该将变量类型固定的python编码思维;但是一时却又想不出其他的好方法,而且这样一来后面对于数据源的匹配也需要进行分两种类型判断的操作:代码如下

def club2team(built_dict,club_team_dict):
    for row in built_dict:
        for sub_row in built_dict[row].keys():
            for key in club_team_dict:
                if sub_row==key:
                    list1=[]
                    if club_team_dict[sub_row] in built_dict[row].keys():
                        if isinstance(built_dict[row][club_team_dict[sub_row]],dict):
                            list1.append(built_dict[row][club_team_dict[sub_row]])
                        elif isinstance(built_dict[row][club_team_dict[sub_row]],list):
                            list1=list1+built_dict[row][club_team_dict[sub_row]]
                    list1.append(built_dict[row].pop(sub_row))
                    built_dict[row][club_team_dict[sub_row]]=list1
    return built_dict

但是这样进行最后的匹配发现匹配数量也只有80来条,仅仅达到零头,所以进一步分析发现问题还是在姓名上面,而对数据的分析发现不少记录可以通过s1中的complete name来进行匹配(本身complete name是作为姓名匹配的最佳字段,几乎和s2中拼接起来的first_namelast_name一致,遗憾的是s1中的complete name并不完全,大多数记录都是空值,只有少量名字出奇的长的人才会有这个字段的值),而且同样的对于前面第一阶段的姓名匹配加入complete name的辅助匹配,发现匹配率有所提升,代码如下:

# second data match by 3-level nested dict(nationality->club->name)
with open(src_file, 'rb') as src_csv:
    # reader = csv.DictReader(src_csv, delimiter=',')
    # ------------------if the delimiter for header is ',' while ';' for rows
    reader = csv.reader(src_csv, delimiter=',')
    fieldnames = next(reader)
    reader = csv.DictReader(src_csv, fieldnames=fieldnames, delimiter=';')

    for row in reader:
        total_list.append(row['players.player_id'])
        # eliminate the special Latin character in country name
        nationality = Country_Format.country_name_format(row['players.country'])
        team=Transfer_Team_Name.simple_format_for_team(row['players.team'])

        if nationality in S1_2:
            if team in S1_2[nationality]:
                s1 = row['players.first_name'] + ' ' + row['players.last_name']
                # s2 = S1_2[nationality][team].keys()
                s3=row['players.vis_name']      # or match by vis_name in s2
                for s2 in S1_2[nationality][team]:  # the structure changed by nested a list,traverse the team name
                    if isinstance(s2,dict):
                        s2_keys=s2.keys()
                        for i in s2_keys:
                            if Name_Match.name_match(s1, i):
                                for index in s2[i]:
                                    # print "index['id']=",index['id']
                                    id1_list.append(index['id'])
                                    id2_list.append(row['players.player_id'])
                                    id_dict.setdefault(row['players.player_id'], []).append(index['id'])
                            elif Name_Match.name_match(s3, i):
                                for index in s2[i]:
                                    id1_list.append(index['id'])
                                    id2_list.append(row['players.player_id'])
                                    id_dict.setdefault(row['players.player_id'], []).append(index['id'])
                    elif isinstance(s2,list):
                        s2_keys=[]
                        for key in s2:
                            s2_keys=s2_keys+key.keys()
                            # print key
                            # print '----------'
                        for i in s2_keys:
                            for key in s2:
                                if Name_Match.name_match(s1, i) and i in key.keys():
                                    for index in key[i]:
                                        # print index
                                        id1_list.append(index['id'])
                                        id2_list.append(row['players.player_id'])
                                        id_dict.setdefault(row['players.player_id'], []).append(index['id'])
                                elif Name_Match.name_match(s3, i) and i in key.keys():
                                    for index in key[i]:
                                        id1_list.append(index['id'])
                                        id2_list.append(row['players.player_id'])
                                        id_dict.setdefault(row['players.player_id'], []).append(index['id'])
        else:
            country_list.append(nationality)
            print row['players.first_name'] + ' ' + row['players.last_name']

其中对于姓名匹配我进行了修改,讲规则统一写到一个方法里,如下:

# put the name match rules together
def name_match(s1,s2):
    if name_match_by_fuzz(s1,s2)==True:
        return True
    elif name_match_by_set(s1,s2)==True:
        return True
    elif name_match_by_set2(s1,s2)==True:
        return True
    return False

接之前的实验记录

在加入complete name这一字段后得到如下实验结果
第一阶段匹配率达到 95.99%,剩余159条记录未匹配
而第二阶段则达到52.20%,剩余76条记录未匹配,最终是采取手工匹配,而其中有21条是匹配不到的


LancelotHolmes
51 声望20 粉丝

引用和评论

0 条评论