Pour chaque paquet collecté sur CRAN, Github, Bioconductor et R-Forge, nous allons récupérer la liste des dépendances dans les champs "Depends" et "Imports", et étudier la répartition de ces dépendances dans les différentes communautés. En plus de ces différentes communautés, certains packages (apparaissant dans les dépendances) sont des paquets fournis par défaut avec une distribution R. Ces paquets sont les suivants :
In [2]:
R_pkg = ('R MASS Matrix base boot class cluster codetools compiler datasets foreign grDevices ' +
'graphics grid lattice methods mgcv nlme nnet parallel rpart ' +
'spatial splines stats stats4 survival tcltk tools translations utils').split(' ')
"""
R_pkg = ('R, base, compiler, datasets, graphics, '
'grDevices, grid, methods, parallel, profile, splines, stats, stats4, '
'tcltk, tools, translations, utils').split(', ')
"""
sources = ['cran', 'bioconductor', 'github', 'rforge']
In [3]:
%matplotlib inline
from IPython.display import set_matplotlib_formats
import matplotlib.pyplot as plt
import os.path
#set_matplotlib_formats('pdf')
In [4]:
import pandas
df = pandas.DataFrame.from_csv('../data/R-Packages.csv')
df = df.fillna(value={'Depends': '', 'Imports': ''})
df = df.drop('profile')
La fonction suivante va récupérer les dépendances dans les champs "Depends" et "Imports" d'une série. Le parsing est naif, mais devrait matcher les paquets existants.
In [5]:
def parse_dependencies(item):
depends = item['Depends'] if item['Depends'] != pandas.np.nan else ''
imports = item['Imports'] if item['Imports'] != pandas.np.nan else ''
f = lambda lst: [dep.split('(')[0].strip() for dep in lst.split(',')]
return filter(lambda x: len(x) > 0, f(depends) + f(imports))
La classe suivante sert de base à notre graphe. Les attributs sont :
name
: nom du packagesources
: liste des sources où ce paquet peut être trouvésource
: source principale (dans l'ordre indiqué)dependencies
: dictionnaire nom --> Package instanceLa méthode add_source
permet d'ajouter une source (str
). La liste dependencies
peut être manipulée directement.
In [6]:
class Package(object):
sources = ['R', 'cran', 'bioconductor', 'biocDS', 'github', 'rforge']
def __init__(self, name):
self.name = name
self.sources = []
self.dependencies = {}
def add_source(self, source):
if source not in self.sources:
self.sources.append(source)
@property
def source(self):
for source in Package.sources:
if source in self.sources:
return source
return 'Unknown'
def installable_with(self, sources):
# List of packages needed for the current one
relies_on = set(self.dependencies.itervalues())
tested = set([self])
while len(relies_on) > 0:
p = relies_on.pop()
tested.add(p)
# Can p be installed using current sources?
if len(set(sources).intersection(set(p.sources))) == 0:
return False
# Add p's dependencies
for d in p.dependencies.itervalues():
if d not in tested:
relies_on.add(d)
return True
def pprint(self):
return str(self) + ' -> ' + ', '.join(map(str, self.dependencies.itervalues()))
def __unicode__(self):
return '{name} on [{sources}]'.format(name=self.name, sources=', '.join(self.sources))
__str__ = __repr__ = __unicode__
Pour chaque paquet identifié, nous ajoutons ces sources.
Le dictionnaire packages
vise à associer à chaque nom de paquet une instance de la classe Package
.
In [7]:
packages = {}
for name, data in df.iterrows():
p = Package(name)
for source in sources:
if data[source] == 1:
p.add_source(source)
packages[name] = p
Ajoutons R dans les sources avec les packages correspondants.
In [8]:
sources.append('R')
for name in R_pkg:
# new data:
p = Package(name)
# keep existing data:
# p = packages.setdefault(name, Package(name))
p.add_source('R')
packages[name] = p
Ajoutons maintenant les packages contenant un dataset sur BioConductor.
In [9]:
sources.append('biocDS')
biocDS1 = pandas.DataFrame.from_csv('../data/bioconductor_annotation_description.csv')
biocDS2 = pandas.DataFrame.from_csv('../data/bioconductor_experiment_description.csv')
biocDS1.fillna(value='', inplace=True)
biocDS2.fillna(value='', inplace=True)
for name, data in biocDS1.iterrows():
p = Package(name)
p.add_source('biocDS')
packages[name] = p
for name, data in biocDS2.iterrows():
p = Package(name)
p.add_source('biocDS')
packages[name] = p
#print len(filter(lambda p: 'biocDS' in p.sources, packages.itervalues()))
#print len(filter(lambda p: 'biocDS' == p.source, packages.itervalues()))
# Ajout des dépendances
for name, data in biocDS1.iterrows():
p = packages[name]
for dep in parse_dependencies(data):
p_dep = packages.setdefault(dep, Package(dep))
p.dependencies[p_dep.name] = p_dep
for name, data in biocDS1.iterrows():
p = packages[name]
for dep in parse_dependencies(data):
p_dep = packages.setdefault(dep, Package(dep))
p.dependencies[p_dep.name] = p_dep
Ajoutons les dépendances. Si le paquet n'est pas connu de notre liste de paquets, nous créons tout de même une instance de Package
. La source associée à ce paquet sera automatiquement Unknown et ce paquet pourra donc être utilisé de façon transparente lors des calculs sur le graphe.
In [10]:
for name, data in df.iterrows():
p = packages[name]
for dep in parse_dependencies(data):
p_dep = packages.setdefault(dep, Package(dep))
p.dependencies[p_dep.name] = p_dep
Enfin, il nous reste à créer le graphe dirigé. Ce graphe est construit classiquement pour les dépendances. A noter que les dépendances de la source Unknown sont également dans ce graphe (qui n'est naturellement pas une composante connexe).
In [12]:
import networkx
dg = networkx.DiGraph()
for package in packages.itervalues():
dg.add_node(package)
for dependency in package.dependencies.itervalues():
dg.add_edge(package, dependency)
Création d'un nouveau graphe dont les noeuds sont des strings et avec un attribut source pour l'export en GraphML.
In [14]:
filename = os.path.join("../data", "deps.graphml")
dg2 = networkx.DiGraph()
packages2 = (package for package in packages.itervalues() if type(package.name) is str)
for package in packages2:
dg2.add_node(package.name, package=package.name, source=package.source)
for dependency in package.dependencies.itervalues():
dg2.add_edge(package.name, dependency.name)
networkx.write_graphml(dg2, filename)
In [11]:
def d2df(gd):
m = {}
for package, value in gd.iteritems():
m[package.name] = {'source': package.source, 'value': value}
return pandas.DataFrame.from_dict(m, orient='index')
for name, group in d2df(networkx.in_degree_centrality(dg)).groupby(by='source'):
fig = plt.figure()
ax = fig.add_subplot(111)
group['value'].plot(kind='hist', figsize=(8,4), bins=10, title=name, ax=ax)
Pour chaque source de packages, nous allons regarder les différents paquets présents dans cette source. Pour chaque paquet, nous regardons les dépendances de ce paquet et la source principale de chacune de ces dépendances. Une liste $S(x,y)$ est calculée pour chaque paire $(x,y)$ de sources. Cette liste contient l'ensemble des paquets de $x$ qui ont au moins une dépendance dans $y$.
In [12]:
S = {s:{s2: set() for s2 in sources + ['Unknown']} for s in sources}
In [13]:
sources_needed_for = lambda p: set(map(lambda p: p.source, p.dependencies.itervalues()))
for p in packages.itervalues():
for source in sources_needed_for(p):
S[p.source][source].add(p)
In [14]:
scores = {s: {s2: len(S[s][s2]) for s2 in S[s].iterkeys()} for s in S.iterkeys()}
Le tableau suivant reprend, pour chaque ligne, le nombre de paquets ayant au moins une dépendance vers la source indiquée en colonne. Par exemple, la ligne bioconductor
en colonne cran
indique qu'il y a x paquets de BioConductor qui ont au moins une dépendance vers CRAN.
In [35]:
S['cran']['github']
packages['reports'].dependencies
Out[35]:
In [15]:
pandas.DataFrame.from_dict(scores).T
Out[15]:
Quels sont les paquets "inconnus" ?
In [16]:
# map(lambda p: p.name, filter(lambda p: p.source == 'Unknown', packages.itervalues()))
Cycles dans le graphe ?
In [17]:
list(networkx.simple_cycles(dg))
Out[17]:
Le tableau suivant reprend, pour chaque ligne, le nombre de dépendances dans la source indiquée en colonne. Par exemple, la ligne bioconductor
en colonne cran
indique qu'il y a x dépendances intervenant dans un paquet de BioConductor, et ces x dépendances sont présentes sur CRAN (un même nom est potentiellement comptabilisé plusieurs fois).
In [18]:
scores_all = {}
for p in packages.itervalues():
score = scores_all.setdefault(p.source, {})
for d in p.dependencies.itervalues():
score[d.source] = score.get(d.source, 0) + 1
pandas.DataFrame.from_dict(scores_all).T
Out[18]:
Le tableau suivant reprend, pour chaque ligne, le nombre de paquets distincts servant de dépendance pour la source indiquée en colonne.
In [19]:
distinct_scores_all = {}
for p in packages.itervalues():
score = distinct_scores_all.setdefault(p.source, {})
for d in p.dependencies.itervalues():
score.setdefault(d.source, set()).add(d.name)
# Get numbers
for s1, d1 in distinct_scores_all.iteritems():
for s2, d2 in d1.iteritems():
d1[s2] = len(d2)
pandas.DataFrame.from_dict(distinct_scores_all)
Out[19]:
In [20]:
dg2 = dg.copy()
dg2.remove_nodes_from(filter(lambda n: n.source == 'Unknown' or n.source == 'R', dg2.nodes_iter()))
in_degrees = [n for n in dg2.in_degree_iter()]
in_degrees.sort(key=lambda n: n[1], reverse=True)
La liste suivante reprend les dépendances apparaissant le plus fréquemment pour chaque source de packages. La liste primarily indique que ce package a comme source principale la source concernée. La liste also available indique que ce paquet est notamment disponible sur la source concernée. Notez bien que ce sont les dépendances de toutes les sources vers une source spécifique. Pour une liste des dépendances de chaque source, regardez (bien) plus bas.
In [21]:
for source in ['cran', 'bioconductor', 'biocDS', 'github', 'rforge']:
print 'Most frequent dependency, for all packages, primarily on', source
_ = filter(lambda x: x[0].source == source, in_degrees)[:10]
print '\n'.join([str(n[0]) + ' : ' + str(n[1]) for n in _])
print
In [22]:
for source in ['cran', 'bioconductor', 'biocDS', 'github', 'rforge']:
print 'Most frequent dependency, for all packages, available on', source
_ = filter(lambda x: source in x[0].sources, in_degrees)[:10]
print '\n'.join([str(n[0]) + ' : ' + str(n[1]) for n in _])
print
La liste suivante reprend, pour chaque communauté, les dépendances les plus fréquentes (toute source confondue).
In [23]:
from collections import Counter
for source in ['cran', 'bioconductor', 'biocDS', 'github', 'rforge']:
pkgs = filter(lambda x: x.source == source, packages.itervalues())
deps = []
for pkg in pkgs:
_ = filter(lambda x: x.source != 'R' and x.source != 'Unknown', pkg.dependencies.itervalues())
deps.extend(_)
print 'Most frequent dependency for packages in', source
print '\n'.join([str(n[0]) + ' : ' + str(n[1]) for n in Counter(deps).most_common(10)])
print
In [24]:
# Packages that are available in at least one of our true sources.
candidates = filter(lambda p: len(p.sources) > 0, packages.itervalues())
In [25]:
import itertools
combinations = [('R', )]
for i in range(1, 6):
for comb in itertools.combinations(filter(lambda x: x != 'R', sources), i):
e = ['R'] + list(comb)
e.sort()
e = tuple(e) # hashable
combinations.append(e)
In [26]:
installables = {}
installables['all'] = {}
for comb in combinations:
n = filter(lambda p: p.installable_with(comb), candidates)
installables['all'][comb] = len(n) # Change to n if you are interested in the packages list
for source in sources:
if source == 'R':
continue
installables[source] = {}
for comb in combinations:
n = filter(lambda p: p.installable_with(comb), filter(lambda p: source in p.sources, candidates))
installables[source][comb] = len(n)
A ce stade-ci du notebook, plusieurs éléments importants sont disponibles :
sources
est une liste des sources disponibles. combinations
est une liste des combinaisons de sources (type tuple
) utilisés pour le calcul de l'installabilité. candidates
est une liste de tous les paquets candidats à l'installation (autrement dit, tout ceux qui sont au moins disponibles sur l'une de nos sources). installables
est maintenant un dictionnaire dont les clés sont les sources disponibles, et la valeur est un dictionnaire reprenant, pour chaque combinaison de sources, le nombre de packages virtuellement installables depuis cette combinaison.
Par exemple, pour connaître les paquets de R-Forge qui sont installables avec R, GitHub et R-Forge, il convient d'utiliser ceci :
In [27]:
installables['rforge'][('R', 'github', 'rforge')]
Out[27]:
Notez que les éléments formant le tuple utilisé comme clé du dictionnaire sont triés par ordre alphabétique.
In [28]:
for source in installables.iterkeys():
if source == 'all':
n = len(candidates)
else:
n = len(filter(lambda p: source in p.sources, candidates))
print n, 'candidates on', source
for combination, value in installables[source].iteritems():
print value, 'installable packages using', str(combination)
print