Automatiser le parcours et la manipulation d’arbres phylogénétiques avec le module Bio.Phylo de BioPython

La géno­mique com­pa­ra­tive per­met d'étudier l'évolution d'organismes par com­pa­rai­son de leur génome. La repré­sen­ta­tion de la proxi­mi­té entre les orga­nismes, élé­ment essen­tiel de la géno­mique com­pa­ra­tive, repose sur des arbres phy­lo­gé­né­tiques. Mais com­ment mani­pu­ler ces arbres faci­le­ment ? Quand il n’y en a qu’un, pas de pro­blème : on uti­lise un visua­li­sa­teur comme ceux pro­po­sés dans l’article sur les arbres phy­lo­gé­né­tiques. Cepen­dant, avec plu­sieurs cen­taines ou mil­liers d’arbres, les trai­te­ments manuels doivent être rem­pla­cés par des rou­tines. Pour cela, des boîtes à outils pour tra­vailler avec l’information phy­lo­gé­né­tique sont dis­po­nibles et lar­ge­ment uti­li­sées : Ape de R, Bio::Phylo (Vos et al, 2011) de Bio­Perl, … En Python, plu­sieurs librai­ries ont été conçues pour la phy­lo­gé­nie : PyCogent (Knight et al, 2007), Den­dro­Py (Suku­ma­ran & Hol­der, 2010) , ETE (Huer­ta-Cepas et al, 2010), p4 (Fos­ter, 2003) et Bio.Phylo (Tale­vich et al, 2012). Cette der­nière, contrai­re­ment aux autres, a pour objec­tif d’être géné­ra­liste, facile à uti­li­ser et com­pa­tible avec d'autres pro­grammes, per­met­tant ain­si le déve­lop­pe­ment rapide de scripts pour l’automatisation de la mani­pu­la­tion des arbres. Tota­le­ment inté­gré à Bio­Py­thon et codé en Python, ce module n’a pas besoin de librai­rie sup­plé­men­taire et fonc­tionne sur des implé­men­ta­tions alter­na­tives de Python comme Jython ou Pypy.

biopython
Logo offi­ciel de Bio­Py­thon

 

Ce module est de plus en plus uti­li­sé grâce au déve­lop­pe­ment conti­nu de ses fonc­tion­na­li­tés et de sa docu­men­ta­tion. Ce module a, par exemple, ser­vi dans une étude de phy­lo­gé­no­mie micro­bienne (Bei­ko, 2010) où les topo­lo­gies de plus de 100 000 arbres ont été per­mu­tées grâce à des rou­tines. Pour ma part, j’ai uti­li­sé Bio.Phylo pour com­pa­rer des vitesses d’évolution entre des familles de gènes, récu­pé­rer des familles mono­phy­lé­tiques, récon­ci­lier des arbres de familles de gènes avec un arbre d’espèces …

Le module Bio.Phylo donne accès à des méthodes pour la mani­pu­la­tion et l’analyse simple d’arbres phy­lo­gé­né­tiques, comme la recherche et le par­cours d’arbres, l’extraction d’informations basiques et leur modi­fi­ca­tion. Ces méthodes peuvent être faci­le­ment uti­li­sées pour des rou­tines dans des work­flows bio­in­for­ma­tiques grâce à la docu­men­ta­tion du module (wiki et les exemples d'utilisation, manuel Bio­Py­thon et article de Tale­vich et al, 2012). Contrai­re­ment aux méthodes d’entrées/sorties et de visua­li­sa­tion des arbres, les méthodes pour mani­pu­ler et par­cou­rir les arbres sont peu docu­men­tées et ne sont sou­vent pas illus­trées. Ces lacunes rendent le déve­lop­pe­ment des rou­tines un peu com­plexe. Le but de cet article est donc de com­plé­ter la docu­men­ta­tion exis­tante afin de faci­li­ter votre tra­vail de déve­lop­pe­ment. Pour illus­trer les méthodes, je vais uti­li­ser l’arbre sui­vant :

ex_arbre

L’arbre est enre­gis­tré dans le fichier "tree" sous le for­mat Newick avec la chaine sui­vante : (((H:.1,G:.2)5:.5,(((F:.1,E:.2)7:.3,D:.1)6:.4,C:.1)4:.5)3:1.5,(B:.1,A:.1)2:2)1 ;

Objets pour gérer les arbres dans Bio.Phylo

Les arbres dans le module sont enre­gis­trés en sui­vant la struc­ture :

structure

Ain­si, quand un arbre est créé, un objet

Tree

 est créé. Cet objet pointe vers un objet

Clade

 correspondant à la racine, que l’arbre soit raciné ou pas. Cet objet

Clade

 contient les informations liées à chaque clade (ou sous-arbre démarrant à la branche courante et terminant avec les branches terminales liées à ce sous-arbre) ainsi que des références vers les clades fils. La structure d’enregistrement des arbres dans Bio.Phylo repose sur l’hypothèse de topologie d’arbre et ne permet pas une topologie de type réseaux avec plus d’un parent pour un clade.

Ainsi, les objets de la classe

Tree

 ont comme attributs :

  • name : Nom
  • id : Identifiant
  • root : Référence vers un objet 
    
    Clade correspondant à la racine
  • weight : Poids
  • rooted : Booléen indiquant si l’arbre est raciné ou pas

 

Ces attributs sont accessibles (en lecture et écriture) directement. Les attributs des instances de la classe

Clade

 sont :

  • name : Nom
  • branch_length 
    : Longueur de la branche conduisant au clade
  • clades 
    : Liste de références vers les clades fils
  • confidence 
    : Confiance
  • comment 
    : Commentaire
  • width 
    : Largeur de la branche pour la visualisation
  • _color 
    : Couleur pour la visualisation

 

Pour stocker l’information supplémentaire spécifiques à certains formats de fichiers (PhyloXML et Newick, par exemple), des sous-modules offrent des classes supplémentaires qui héritent des classes

Tree

 et

Clade

. Je ne vais pas présenter ici les spécificités de ces sous-modules.

Création d’arbres dans Bio.Phylo

Les arbres dans Bio.Phylo sont généralement crées par lecture de fichiers dans différents formats possibles : Newick, NEXUS, PhyloXML, NeXML, Comparative Data Analysis Ontology (CDAO). Une interface de programmation (API) unifiée permet de prendre en charge tous les formats d’entrée et renvoie un objet

Tree

, identique quel que soit le format d’entrée.

La méthode 

read

 permet de lire un arbre dans le fichier donné et le renvoie. Le format du fichier correspond au deuxième argument des méthodes précédentes.

>>> tree = Phylo.read("tree","newick")

Une erreur est générée lorsque le fichier passé en argument contient plus d’un arbre ou pas d’arbre. Cependant, les arbres contenus dans un même fichier peuvent être chargés avec la méthode

parse

 :

>>> trees = Phylo.parse("trees","newick")
>>> for tree in trees :
...          print tree.name
>>> tree_list = list(trees)

Il est aussi possible d’utiliser directement des chaines de caractères pour charger un arbre avec

StringIO

 :

>>> from StringIO import StringIO
>>> handle = StringIO("(((H:.1,G:.2)5:.5,(((F:.1,E:.2)7:.3,D:.1)6:.4,C:.1)4:.5)3:1.5,(B:.1,A:.1)2:2)1;")
>>> tree = Phylo.read(handle,"newick")

Des sous-modules spécifiques à chaque format de fichiers (PhyloXMLIO, NeXMLIO, NewickIO, NexusIO, CDAOIO) permettent de charger les arbres et toutes les informations spécifiques à chaque format.

Un arbre peut aussi être créé à partir d’un clade avec

from_clade

. L’arbre obtenu correspond alors au sous-arbre démarrant au clade sélectionné, avec ses descendants.

>>> tree2 = tree.from_clade(c3) # c3 est un clade

tree2

 Exportation des arbres

La méthode

write

 permet d’exporter le (ou les) arbre(s) dans un fichier ou une chaine de caractères

>>> Phylo.write(tree, "tree","newick")
>>> Phylo.write(trees, "trees","newick")
>>> Phylo.write([tree,tree,tree], "trees","newick")
>>> handle = StringIO()
>>> Phylo.write(tree, handle, "newick")

Pour convertir des fichiers entre les formats Newick, Nexus, PhyloXML, …, on utilise la méthode 

convert
>>> Phylo.convert("tree","newick","tree.nex","nexus")

Les arbres peuvent aussi être exportés vers d’autres formats pour être utilisables dans d’autres modules Python ou logiciels comme R. Ainsi, dans le wiki, sont présentées des méthodes pour exporter pour Ape avec RPy2, DendroPy ou PyCogent, mais aussi en matrice Numpy.

Visualisation des arbres

Dans Bio.Phylo, la visualisation des arbres peut se faire en :

  • Chaine de caractères avec la hiérarchie complète de l’objet
    >>> print(tree)
    
  • Dendogramme simple en ASCII
    >>> Phylo.draw_ascii(tree)
    
  • Phylogramme, si les libraires matplotlib ou PyLab sont installées
    >>> Phylo.draw(tree)
    
  • Cladogramme, si Graphviz, PyDot ou PyGraphviz, NetworkX et matplotlib ou PyLab installés
>>> import pylab
>>> Phylo.draw_graphviz(tree, prog = "dot")
>>> pylab.show()
>>> pylab.savefig(‘phylo-dot.png’)

Les représentations peuvent être modifiées avec l’ajout de couleurs par exemple, mais aussi avec des librairies comme matplotlib ou PyLab. Les méthodes permettant ces modifications sont bien décrites dans le tutoriel de BioPython ou le wiki.

 Récupération d’informations sur l’arbre et sa topologie

La topologie d’un arbre ou d’un sous-arbre peut être interrogée avec plusieurs méthodes :

  • is_monophyletic 
    teste si la liste des cibles fournies forme un sous-clade complet dans l’arbre à partir duquel la méthode est appelée. Cette méthode teste donc s’il existe un clade tel que ces nœuds terminaux soient exactement les cibles. Si c’est le cas, la méthode renvoie le clade correspondant à l'ancêtre commun le plus proche, ou Faux sinon.
  • is_bifurcating teste si un arbre est strictement bifurcant, c’est-à-dire que tous les nœuds ont 0 ou 2 enfants. La racine peut cependant avoir 3 descendants, l’arbre sera toujours considéré comme bifurcant.

 

La position d’un clade dans l’arbre est interrogeable avec :

  • is_terminal teste si un clade est terminal. C’est équivalent à 
    
    if c1.clades : … qui teste si un clade c1 a des descendants
  • is_preterminal teste si tous les descendants directs sont terminaux et renvoie Faux si au moins une des descendants directs ne l’est pas
  • is_semipreterminal 
    teste si au moins un des descendant est terminal et renvoie Faux si tous les descendants directs ne le sont pas ou si le clade est terminal
>>> def is_semipreterminal(clade):
...     for child in clade:
...             if child.is_terminal():
...                     return True
...     return False
  • if c1 in c2.clades :… teste si un clade c1 est le descendant direct d’un clade c2
  • is_parent_of 
    teste si une cible est un descendant (pas obligatoirement direct) du clade à partir de laquelle la méthode est appelée.

 

Les nœuds internes et externes d’un clade ou de l’arbre entier sont récupérés avec les méthodes

get_nonterminals

 et

get_terminals

, qui renvoient une liste avec des références sur les clades correspondants. Similairement, la méthode

count_terminals

 compte le nombre de nœuds terminaux au sein de l’arbre.

>>> terminals = tree.get_terminals()
>>> terminals
[Clade(branch_length=0.1, name='H'), Clade(branch_length=0.2, name='G'), Clade(branch_length=0.05, name='F'), Clade(branch_length=0.2, name='E'), Clade(branch_length=0.1, name='D'), Clade(branch_length=0.05, name='C'), Clade(branch_length=0.05, name='B'), Clade(branch_length=0.1, name='A')]
>>> non_terminals = tree.get_nonterminals()
>>> non_terminals
[Clade(branch_length=1.0), Clade(branch_length=3.0, confidence=3.0), Clade(branch_length=0.5, confidence=5.0), Clade(branch_length=0.5, confidence=4.0), Clade(branch_length=0.4, confidence=6.0), Clade(branch_length=0.3, confidence=7.0), Clade(branch_length=2.0, confidence=2.0)]
>>> tree.count_terminals()
8

Avec la structure d’arbres dans Bio.Phylo, les références vers les parents ne sont pas stockées pour chaque clade. On peut utiliser la méthode

get_path

 (décrite par la suite) pour retrouver le lien parent-fils avec la méthode suivante :

>>> def get_parent(tree, child_clade):
...          node_path = tree.get_path(child_clade)    
...          return node_path[-2]

Parcours et navigation dans les arbres

Parcours des arbres

Pour parcourir l’arbre phylogénétique, il faut partir de la racine avec

tree.root

, qui pointe vers un objet

Clade

. On peut ensuite parcourir les clades de l’arbre en profondeur, en visitant les clades fils avec 

for clade_fils in clade.clades:...

 dans une méthode récursive, par exemple :

>>> def parcours_en_profondeur(clade):
...     if not clade.is_terminal():
...             for clade_fils in clade.clades:
...                     parcours_en_profondeur(clade_fils)
...
>>> parcours_en_profondeur(tree.root)

Le chemin entre deux clades parentes peut être récupéré avec la méthode

get_path

. Celle-ci renvoie la liste des clades entre la racine ou le clade courant et un clade cible, en terminant par le clade cible et en excluant le clade racine. Cependant, le chemin renvoyé est toujours un chemin en profondeur. Ainsi, si le clade courant n’est pas parent du clade cible, la méthode ne renvoie rien. La liste des clades entre deux clades cibles, pas obligatoirement avec un chemin descendant, est accessible avec la méthode 

trace
>>> c1 = tree.get_terminals()[0]
>>> tree.get_path(c1)
[Clade(branch_length=1.5, confidence=3.0), Clade(branch_length=0.5, confidence=5.0), Clade(branch_length=0.1, name='H')]
>>> c2 = tree.get_nonterminals()[0]
>>> c2.is_parent_of(c1)
True
>>> c2.get_path(c1)
[Clade(branch_length=1.5, confidence=3.0), Clade(branch_length=0.5, confidence=5.0), Clade(branch_length=0.1, name='H')]
>>> c3 = tree.get_nonterminals()[3]
>>> c3.is_parent_of(c1)
False
>>> c3.get_path(c1)
>>> tree.trace(c1,c3)
[Clade(branch_length=0.5, confidence=5.0), Clade(branch_length=1.5, confidence=3.0), Clade(branch_length=1.0)]

Recherche dans les arbres

Pour rechercher des nœuds au sein de l’arbre ou dans des sous-clades, il existe 3 trois méthodes (

find_clades

,

find_elements

,

find_any

) qui reposent toutes les trois sur la même définition, avec les attributs :

  • Target (
    
    None par défaut) : spécifie les caractéristiques à rechercher, comme
    
    • Une instance de
      Clade qui pourrait correspondre par identité, pour rechercher, par exemple, la position d’un clade dans un arbre
    • Une chaine de caractères qui puisse correspondre à une représentation en chaine de caractères d’un attribut de
      Clade (nom, par exemple)
    • Une classe ou un type pour lequel tout élément de l’arbre du même type pourrait correspondre
    • Un dictionnaire où les clés sont des attributs de
      Clade (nom, longueur, …) et les valeurs sont testées aux attributs correspondant de chaque élément de l’arbre
    • Une fonction prenant un argument et renvoyant Vrai ou Faux
  • Terminal (
    
    None, par défaut) : valeur booléenne pour ou contre les nœuds terminaux.
    
    • True 
      : recherche uniquement des nœuds terminaux
    • False 
      : exclusion des nœuds terminaux
    • None 
      : recherche à la fois des nœuds terminaux et internes
  • Order (
    
    preorder, par défaut) : ordre de parcours de l’arbre
    
    • preorder : recherche selon un algorithme de recherche en profondeur
    • postorder : recherche selon un algorithme de recherche en profondeur où les nœuds fils précèdent les parents
    • level : recherche selon un algorithme de recherche en largeur

 

La méthode

find_elements

 cherche les éléments de l’arbre qui correspondent à la cible.

find_clades

 fonctionne sur le même principe, mais retourne les objets

Clade

  correspondants. Ces méthodes renvoient un objet itérable selon l’ordre défini par

order

 et qui n’est pas nécessairement le même ordre que celui d’apparition des éléments dans le fichier source. La méthode 

find_any

 renvoie le premier élément trouvé par

find_element

 ou

None

 , permettant de tester l’existence de l’élément « recherché » dans l’arbre et utilisable dans une expression conditionnelle.

>>> tree.find_any({'name' : 'H'})
Clade(branch_length=0.1, name='H')
>>> tree.find_elements({'name' : 'H'})
<itertools.ifilter object at 0x10a0d5690>
>>> tree.find_elements({'name' : 'H'}).next()
Clade(branch_length=0.1, name='H')
>>> resultat = tree.find_elements(lambda c : c.branch_length < 1)
>>> resultat.next()
Clade(branch_length=0.5, confidence=5.0)
>>> resultat.next()
Clade(branch_length=0.1, name='H')
>>> resultat.next()
Clade(branch_length=0.2, name='G')

Pour obtenir l’ancêtre commun le plus récent (MRCA, sous forme d’un objet

Clade

 ) de cibles données, il faut utiliser

common_ancestor

. Si aucune cible n’est fournie, la racine du clade à partir de laquelle la méthode est appelée est renvoyée.

>>> tree.common_ancestor(c1,c2)
Clade(branch_length=1.5, confidence=3.0)
>>> c3 = tree.common_ancestor(c1,c2)

 Métriques

Plusieurs métriques sont accessibles sur les clades et les arbres. La méthode

total_branch_length

 calcule la somme de toutes les longueurs de branche de l’arbre. La distance entre deux cibles, c’est-à-dire la somme des longueurs des branches entre les cibles, est obtenue avec la méthode

distance

. Cette méthode est appelée à partir de l’arbre et si une seule cible est spécifiée, l’autre cible est définie comme la racine de l’arbre. La méthode

depths

 crée un mapping des clades aux profondeurs. Le résultat est un dictionnaire où les clés sont tous les clades de l’arbre et les valeurs les distances entre la racine et les clades. Par défaut, la distance est la somme des longueurs des branches mais il est possible de compter seulement de nombre de branches (avec

unit_branch_length = True

).

>>> tree.depths()
{Clade(branch_length=1.0): 1.0, Clade(branch_length=0.1, name='B'): 3.1, Clade(branch_length=0.4, confidence=6.0): 3.4, Clade(branch_length=0.1, name='D'): 3.5, Clade(branch_length=1.5, confidence=3.0): 2.5, Clade(branch_length=0.2, name='E'): 3.9, Clade(branch_length=0.3, confidence=7.0): 3.6999999999999997, Clade(branch_length=2.0, confidence=2.0): 3.0, Clade(branch_length=0.5, confidence=5.5): 3.0, Clade(branch_length=0.5, confidence=4.0): 3.0, Clade(branch_length=0.1, name='C'): 3.1, Clade(branch_length=0.1, name='H'): 3.1, Clade(branch_length=0.1, name='A'): 3.1, Clade(branch_length=0.2, name='G'): 3.2, Clade(branch_length=0.1, name='F'): 3.8}

 Modifications d'arbres

Rotations

Les clades peuvent être triés selon le nombre de nœuds terminaux avec

ladderize

. Par défaut, les clades les plus profonds sont placés en dernier. L’inverse est possible avec

reverse = True

. Cette méthode peut être appelée pour l’arbre complet ou pour un sous-arbre dont la racine correspond au clade courant.

>>> tree.ladderize()

tree_ladderize

>>> c4 = tree.get_nonterminals()[4]
>>> c4.ladderize(reverse = True)

clade_ladderize

Raciner et reraciner

Pour raciner ou reraciner l’arbre, il existe deux méthodes. La première 

root_at_midpoint

 reracine l’arbre au milieu calculé entre les extrémités les plus distantes de l’arbre. La seconde méthode 

root_with_outgroup

 reracine l’arbre avec un clade outgroup contenant l’ancêtre commun de l’outgroup. Si l’outgroup est identique à la racine de l’arbre, il n’y a pas de changement. Si l’outgroup est terminal, un nouveau clade racine bifurcant est créé avec une branche de longueur nulle vers l’outgroup donné. Dans les autres cas, le nœud interne à la base de l’outgroup devient une racine trifurquante pour l’arbre entier. Si la racine originale était bifurcante, elle est éliminée de l’arbre.

>>> tree.root_at_midpoint()

tree_root_at_midpoint

>>> tree_ca = tree.common_ancestor('E','F')
>>> tree.root_with_outgroup(tree_ca)

tree_root_with_outgroup_ca

>>> c0 = tree.get_terminals()[0]
>>> tree.root_with_outgroup(c0)

tree_root_with_outgroup_terminal

Idéalement, il faudrait que l’outgroup soit monophylétique plutôt qu’un taxon seul. Ce n’est pas vérifié automatiquement. Il est donc préférable d’utiliser

is_monophyletic

 avant de reraciner un arbre.

Ajout de clades

Pour ajouter des descendants, on peut utiliser la méthode

split

. Celle-ci ajoute n descendants (2 par défaut) à la racine ou au clade courant. Les nouveaux clades ont des longueurs de branches définies (1 par défaut) et le même nom que la racine du clade avec l’ajout d’un entier en suffixe.

>>> tree.split()

tree_split

>>> ca = tree.common_ancestor('E','C')
>>> ca.split(3)

clade_split

Suppression de clades

La suppression de clade peut se faire avec différentes méthodes.

prune

 élimine un clade terminal de l’arbre, soit à partir de la référence du clade, soit en cherchant le clade avec

find_any

. Si le taxon est issu d’une bifurcation, le nœud correspondant est éliminé et la longueur de branche est ajoutée à celle du nœud terminal restant.

collapse

 supprime la cible de l’arbre, en liant les clades fils au clade parent de la cible.

collapse_all

 élimine tous les descendants de l’arbre, laissant seulement les nœuds terminaux. Les longueurs de branches de la racine à chaque nœud terminal sont conservées. Si une cible est fournie, seuls les nœuds internes correspondant à la cible sont touchés.

>>> c0 = tree.get_terminals()[0]
>>> tree.prune(c0)
Clade(branch_length=0.4, confidence=6.0)

tree_prune

>>> ca = tree.common_ancestor('F0','C')
>>> tree.collapse(ca)
Clade(branch_length=3.5, confidence=3.0)

tree_collapse

>>> tree2.collapse_all()

tree_collapse_all

Conclusion

Bio.Phylo est un module facile d’utilisation et permettant le développement rapide de routines pour la manipulation et le parcours d’arbres phylogénétiques. Ce module est toujours en développement et j’espère que de nouvelles fonctionnalités, comme la comparaison d’arbres, viendront s’y greffer.

Merci aux relecteurs pour leurs commentaires : Clem_, Darkgilou, Waqueteu et Yoann M.

Références

Beiko (2010). Telling the Whole Story in a 10,000-GenomeWorld. Biol Direct, 6:34.

Foster (2003). p4: A Python package for phylogenetics. [http://code. google.com/p/p4-phylogenetics/].

Huerta-Cepas et al (2010). ETE: a python Environment for Tree Exploration. BMC Bioinformatics. 11:24.

Knight et al (2007). PyCogent: a toolkit for making sense from sequence. Genome Biol, 8(8):R171.

Sukumaran & Holder (2010). DendroPy: a Python library from phylogenetic computing. Bioinformatics. 26(12) : 1569-1571.

Talevich et al (2012). Bio.Phylo: A unified toolkit for processing, analyzing and visualizing phylogenetic trees in Biopython. BMC Bioinformatics, 13:209

Vos et al (2011). Bio: Phylo – phyloinformatic analysis usiing Perl. BMC Bioinformatics, 12:63.



Pour continuer la lecture :


Commentaires

Laisser un commentaire