Identification des auteurs de packages

Nous allons récupérer, pour chaque communauté, la liste des auteurs de packages. Nous essayerons ensuite de faire un merging des identités, et d'identifier la taille de chaque communauté en terme d'auteurs, ainsi que les auteurs qui sont présents dans plusieurs communautés.


In [1]:
import pandas

github = pandas.read_csv('../data/github-description.csv')
cran = pandas.read_csv('../data/cran-description.csv')
bioconductor = pandas.read_csv('../data/bioconductorsvn_description.csv')
r_forge = pandas.read_csv('../data/r-forge_description.csv')
rforge = pandas.read_csv('../data/rforge_description.csv')

Nous récupérons, pour chaque package de chaque communauté, le champ Author. A noter que le champ Authors existe aussi dans certains cas (CRAN, GitHub) mais en faible proportion (8 packages sur CRAN, 42 sur GitHub par exemple).


In [2]:
# Drop packages belonging to "cran" user on GitHub
github = github[github['owner'] != 'cran']
github = github[github['owner'] != 'rpkg']

# Pivot on key->value
_packages = github[github['key'] == 'Package'].rename(columns={'value': 'Package'})[['owner', 'repository', 'Package']]
_author = github[github['key'] == 'Author'].rename(columns={'value': 'Author'})
_github = _packages.merge(_author, how='left', on=('owner', 'repository'))
github = _github.set_index('Package')[['Author']]

# Indexes
bioconductor = bioconductor.set_index('Package')[['Author']]
rforge = rforge.set_index('Package')[['Author']]
r_forge = r_forge.set_index('Package')[['Author']]

# Pivot & index
_cran = cran.drop_duplicates(('package', 'key'), take_last=True)
_author = _cran[_cran['key'] == 'Author'].rename(columns={'value': 'Author'})

_cran = _cran.merge(_author, how='left', on='package')
cran = _cran.rename(columns={'package': 'Package'}).drop_duplicates('Package').set_index('Package')[['Author']]

In [3]:
sources = [
    ('github', github),
    ('cran', cran),
    ('bioconductor', bioconductor),
    ('rforge', rforge),
    ('r-forge', r_forge)
]

Nous allons devoir parcourir les champs Author et récupérer la liste des auteurs. Ensuite, il conviendra de "nettoyer" chaque auteur pour obtenir une sorte d'identité canonique qui servira de base pour le matching. L'approche est discutable, mais a le mérite d'être relativement simple. Nous pourrons manuellement attester de la qualité de cette approche.

La fonction split_author_list se base sur l'expression régulière suivant pour "décomposer" la liste des auteurs (format textuel) en une liste d'auteurs. Les séparateurs sont listés dans RE_SPLIT_AUTHORS.


In [4]:
import re

RE_SPLIT_AUTHORS = re.compile(r'(,|;|( \- )|  | & |(and)|(with)|(contributions from))')

def split_author_list(authors):
    candidates = re.split(RE_SPLIT_AUTHORS, authors)
    return map(lambda x: x.strip(), filter(lambda x: x is not None and len(x) > 5, candidates))

La fonction clean_name utilise l'expression RE_GREP_AUTHOR pour identifier les auteurs. Grosso-modo, on procède comme suit :

  • On découpe la chaîne correspondant potentiellement à un auteur avec le séparateur ' ' et '\n'.
  • Chaque morceau est passé en minuscules.
  • Au sein de chaque morceau, on récupère la plus longue suite consécutive de caractères autorisés ('a' à 'z', plus '-') d'au moins 3 caractères, en début de chaîne et se terminant soit en fin de chaîne, soit par un '.'. Cela permet d'éliminer les initiales, tout en conservant les listes d'auteurs séparés par un '.'.
  • Ces différents morceaux ainsi récupérés sont triés par ordre alphabétique (pour obtenir une sorte de version "canonique").

In [5]:
RE_GREP_AUTHOR = re.compile(r'^([a-z\-]{3,}).?$')
def clean_name(name):
    name = name.replace('\n', ' ')
    _ = name.lower().split(' ')
    names = []
    for e in _:
        for f in re.findall(RE_GREP_AUTHOR, e):
            names.append(f)
    names.sort()
    return names

Le bloc suivant va composer le dictionnaire name. Ce dictionnaire contiendra comme clés les noms canoniques des auteurs. Chaque auteur est associé à un dictionnaire contenant, pour chaque source, l'ensemble des packages pour lesquels cet auteur est repris. Une entrée supplémentaire names contient également la liste des noms qui ont été associés à cet auteur (pour vérification manuelle, par exemple).


In [6]:
names = {}
for source_name, source in sources:
    for index, value in source.fillna('').iterrows():
        for dirty in split_author_list(value['Author']):
            author = ' '.join(clean_name(dirty))
            if len(author) > 0:
                d_author = names.setdefault(author, {})
                d_author.setdefault('names', set()).add(dirty)
                d_author.setdefault(source_name, set()).add(index)

La fonction suivante (un peu moche, et qui nécessite des variables globales) va retourner un dictionnaire avec comme clés chaque source (CRAN, GitHub, etc.). Comme valeur associé à cette clé, un ensemble des auteurs présents dans cette source. Le paramètre N permet de définir un "nombre minimum de packages distincts à développer pour être considéré comme auteur".


In [24]:
def _author_sets(N = 1):
    authors = {source[0]: set() for source in sources}
    for name, dname in names.iteritems():
        pkg = set()
        for k,p in dname.iteritems():
            if k != 'names':
                pkg = pkg.union(p)
        if len(pkg) >= N:
            for key in dname:
                if key != 'names':
                    authors[key].add(name)
    return authors

In [25]:
%matplotlib inline

from matplotlib import pyplot as plt
from matplotlib_venn import venn3

figure, axes = plt.subplots(5, 2, figsize=(15,30))

for i in range(5):
    authors = _author_sets(i+1)
    venn3((authors['github'], authors['cran'], authors['bioconductor']), ('github', 'cran', 'bioconductor'), ax=axes[i][0])
    venn3((authors['github'], authors['cran'], authors['r-forge']), ('github', 'cran', 'r-forge'), ax=axes[i][1])