Mostrando entradas con la etiqueta GO. Mostrar todas las entradas
Mostrando entradas con la etiqueta GO. Mostrar todas las entradas

24 de abril de 2015

Diferentes formas de poblar una lista en R

Hola,

trabajando con Gene Ontologies (GOs) he empezado a hacer unas pruebas con una biblioteca de Bioconductor llamada topGO. Esta sirve para comprobar si los términos de GOs de un conjunto de genes son diferentes de los que encontramos en otro conjunto.

Por ejemplo, si tenemos los genes de un organismo y nos interesan los que en nuestro tratamiento aumentan su expresión, podemos comprobar si los términos GO asociados a estos últimos son diferentes de los que tenemos para el conjunto de todos los genes del organismo.

Pero uno de los pasos previos, antes de hacer tests estadísticos con topGO, es asociar nuestros genes a términos GO. En Bioconductor y otras bases de datos hay listas ya precalculadas que podemos utilizar. Sin embargo, por distintos motivos (por ejemplo, al trabajar con un organismo para el cual no está tan estudiada la anotación funcional), podemos querer cargar nuestras propias asociaciones para que topGO trabaje sobre ellas.

Ya que un gen puede tener asociado más de un término GO, los desarrolladores de topGO han decidido que la estructura de datos a utilizar sea una lista de vectores de caracteres (técnicamente una "named list of character vectors"). Dicho de otra forma, una lista de genes, con cada una de las entradas de la lista asociada a un vector de nombres de términos GO. Una opción, si queremos trabajar con nuestra propia anotación, es utilizar la función 'readMappings' de topGO, que crea esta estructura si le damos un formato de fichero adecuado:

"It consists of one line for each gene with the following syntax:
gene_ID/TAB/GO_ID1, GO_ID2, GO_ID3, ...."

La otra opción es que creemos nosotros mismos la lista de vectores a partir de los datos que queramos. A veces, cuesta menos transformar un fichero de datos a otro formato (y usar la función 'readMappings', por ejemplo), y otras es más conveniente mantener nuestro formato de fichero y manipularlo como estructura de datos, sin crear otro fichero. En este caso, es además una buena oportunidad para probar algunas estructuras de datos y sentencias de R, que es lo que haremos nosotros.

¿Cómo es el formato de datos que tenemos en el fichero inicial? Obviamente no es el formato requerido por topGO (pues usaríamos la función 'readMappings' sin más). Nosotros vamos a partir de una tabla con 2 columnas (separadas por tabulador): una para el identificador del gen, la otra para un identificador de término GO. De esta forma, los distintos términos GO de cada gen quedan realmente en varias líneas:

gene_1    GO:000001
gene_1    GO:000004
gene_2    GO:000003
...

Esto obviamente puede cambiar según el origen de los datos, con qué programa se hayan obtenido las anotaciones, etc.

No vamos a entrar en detalles aquí sobre cómo leer el fichero con R o en dar ejemplos concretos de los datos. Es muy fácil generarlos y no es el objetivo de esta entrada. Lo importante para entender el código es que vamos a trabajar con la salida de read.table, en la variable 'go_tab', que será de tipo data.frame. En 'go_tab', los campos de las dos columnas se llaman 'identifier' (la columna con genes) y 'GOterm' (la columna con términos GO).

Vamos a ver varias alternativas para transformar éste 'data.frame' en una lista de vectores. En primer lugar, probaremos creando directamente una lista que iremos poblando con un bucle 'for'. Éste código es muy legible e intuitivo y programadores de muchos lenguajes diferentes pueden entenderlo fácilmente con solo echarle un vistazo.

El código completo de éste fragmento:

 gene2GO = list()  
 for (i in seq(1:nrow(go_tab))) {  
  identifier = go_tab$identifier[i]  
  go_term = go_tab$GOterm[i]  
  if (identifier %in% names(gene2GO)){  
   gene2GO[[identifier]] = c(gene2GO[[identifier]], as.character(go_term))  
  } else {  
   gene2GO[[identifier]] = as.character(go_term)  
  }  
 }  

En éste ejemplo, por tanto, vamos a recorrer la tabla de datos (go_tab) importada del fichero, e iremos construyendo nuestra lista final (gene2GO) concatenando nuevos términos GO para cada gen:

 gene2GO[[identifier]] = c(gene2GO[[identifier]], as.character(go_term))  

Vemos aquí una de las peculiaridades de R en el manejo de listas, que es el uso de corchetes dobles '[['. Si hay dudas de por qué se usa aquí '[[' en lugar de '[', la explicación rápida es que en R para acceder a datos en una lista hay que usar '[['. Para ampliar esto, está detallado en http://adv-r.had.co.nz/Subsetting.html

Sin embargo, éste método es bastante lento en mi equipo (Dell Optiplex 9010) y con mis datos (unas 90.000 filas en el fichero de entrada). Pongamos, por referencia y para comparar con métodos que veremos a continuación, que éste ejemplo en el que usamos una lista y un bucle 'for' tarda 1' (54'' en promedio, realmente).

Parte de por qué es así puede tener que ver con el bucle 'for'. Más adelante veremos alguna alternativa al bucle. Sin embargo, también tiene que ver el rendimiento de la búsqueda de los elementos en la lista.

Una alternativa al operador '%in%' podría ser:

 if (exists(identifier, gene2GO)){  

Pero en principio esta opción es más lenta, en mi caso unos 2' (1'57'').

En ambos casos, la comprobación la hacemos para acceder después al gen en cuestión. Por tanto, se está buscando 2 veces el mismo elemento en la lista: una en la comprobación, otra al obtener el elemento. Una forma de evitar esto sería cogiendo directamente el objeto (1 sola búsqueda) y comprobando si existe o no (NULL):

 current = gene2GO[[identifier]]  
 if (is.null(current)){  

(Nota: ojo que ahora el if comprueba si NO existe, no si existe como en los ejemplos anteriores).

Vemos que con esta otra forma tenemos un tiempo promedio de unos 26''. Aproximadamente la mitad que con el uso de '%in%', como esperaríamos.

Sin embargo, ya hemos comentado antes que el uso de un bucle 'for' en R también puede tener algo que ver con la velocidad de nuestro código (por ejemplo ver: http://faculty.nps.edu/sebuttre/home/R/apply.html). En R se suele preferir utilizar funciones que van recorriendo las estructuras de datos y aplicando una función sobre cada elemento de la estructura. Es el caso de la familia de funciones 'apply' (ver por ejemplo https://nsaunders.wordpress.com/2010/08/20/a-brief-introduction-to-apply-in-r/). Veamos el equivalente al código anterior usando la función 'apply':

 gene2GO = list()  
 add_to_list <- function(go_tab){   
  identifier = go_tab[[1]]  
  go_term = go_tab[[2]]  
  current = gene2GO[[identifier]]  
  if (is.null(current)){  
   gene2GO[[identifier]] <<- as.character(go_term)  
  } else {  
   gene2GO[[identifier]] <<- c(gene2GO[[identifier]], as.character(go_term))  
  }  
 }  
 invisible(apply(go_tab, 1, add_to_list))  

Ahora está tardando unos 21'' en mi máquina. Una ligera mejora respecto al uso del bucle 'for', que podría tener impacto en conjuntos de datos muy grandes o cuando queremos correr muchos análisis.

Vemos varias peculiaridades en el código anterior. La función 'apply' va a recorrer nuestra lista y llamará a la función 'add_to_list' para cada elemento. Sin embargo, en nuestro caso no queremos simplemente transformar los elementos de la lista (por ejemplo, formateando el nombre del gen), si no que queremos poblar una estructura de datos diferente a partir de los valores de la lista recorrida. Por eso se utiliza el operador <<-:

"The operators are normally only used in functions, and cause a search to made through parent environments for an existing definition of the variable being assigned."

Otra posible opción sería utilizar un parámetro opcional en la función, pero no me queda claro que vaya a funcionar para escribir en él, ya que el parámetro (la lista de vectores que estamos creando) necesitaría pasarse por referencia, y me suena que en R no he pasado parámetros así anteriormente ¿Os suena? ¿Algún consejo sobre esto? ¿Alguna otra opción?

La otra peculiaridad es la función 'invisible'. Ya que las funciones en R devuelven un valor y 'apply' lo propaga para obtener la posible transformación de 'go_tab' en otra cosa, si no asignamos el resultado de 'apply' a una variable tendremos el resultado en nuestra salida. Para evitar esto, podemos asignar el resultado a una variable "dummy" o utilizar el método 'invisible', que a mi me ha parecido más adecuado y legible.

Otra diferencia, más básica, es cómo accedemos a nuestros datos originales. En el primer ejemplo, lo que hacíamos es acceder a los campos del 'data.frame' y, en un campo dado, obtener el dato correspondiente según el bucle for:

identifier = go_tab$identifier[i]

Con 'apply' en cambio estamos recibiendo cada una de las líneas del 'data.frame' como un 'vector', por lo que accedemos directamente a los datos en éste vector:

identifier = go_tab[[1]]

Por ahora hemos mejorado nuestro algoritmo cambiando la forma de comprobar la existencia de un elemento en la lista para luego obtenerlo, y pasando de un bucle 'for' a usar la función 'apply'. Pero aún podemos ir más allá. ¿Por qué utilizar una lista? Quizás podamos mejorar el rendimiento utilizando un entorno con un mapeado "hash" (https://stat.ethz.ch/R-manual/R-devel/library/base/html/environment.html).

 gene2GO = new.env()  
 add_to_list <- function(go_tab){  
  identifier = go_tab[[1]]  
  go_term = go_tab[[2]]  
  current = gene2GO[[identifier]]  
  if (is.null(current)){  
   gene2GO[[identifier]] <<- as.character(go_term)  
  } else {  
   gene2GO[[identifier]] <<- c(gene2GO[[identifier]], as.character(go_term))  
  }  
 }  
 invisible(apply(go_tab, 1, add_to_list))  
 gene2GO = as.list(gene2GO)  

Aquí, en lugar de una lista creamos un entorno ('environment'), que por defecto utiliza una función "hash" para mapear su contenido. Por lo demás, como vemos, lo estamos manejando aquí de forma muy similar a la lista, aunque hemos añadido al final una conversión a formato de lista (en una sola línea pasamos de nuestro "hash" a una lista de vectores).

Éste código tarda unos 7'', incluída la conversión. Hay que tener en cuenta que el uso de un "hash" puede ser más o menos aconsejable según el volumen de datos con el que se trabaje. Desde luego, con nuestros datos, hemos mejorado bastante el rendimiento prestando atención a distintas alternativas que ofrece R y cuál sería la mejor combinación de las que hemos probado.

Y ya tendríamos nuestra lista creada. Nuestra intención era utilizar esta lista para informar a topGO de qué términos de GOs se relacionan con nuestros genes ¿recordáis? Le pasaremos esta lista a topGO como parámetro 'gene2GO' en la función en la creamos el primer objeto necesario con éste paquete, e indicaremos que la función de anotación es del tipo "annFun.gene2GO". Para eso, y terminar así la preparación de los datos, tendríamos que importar las listas de genes a comparar, o una sola lista con todos pero con algún criterio que permita diferenciar un subconjunto de otro. Crearíamos entonces el objeto con:

 GOdata <- new("topGOdata", description="whatever", ontology = "BP", allGenes = background, geneSel = test,   
          annot = annFUN.gene2GO, gene2GO = gene2GO, nodeSize = 10)  

Donde:
'background' es nuestro conjunto de genes de referencia.
'test' es el grupo de genes que estamos interesados en comparar con la referencia.
'gene2GO' la lista que hemos generado.

Seguiríamos entonces con las siguientes fases de topGO: los test de enriquecimiento y el análisis de los resultados. Si te ha entrado el gusanillo de probar, topGO está perfectamente documentado:

http://www.bioconductor.org/packages/release/bioc/vignettes/topGO/inst/doc/topGO.pdf

Un saludo!


30 de agosto de 2011

Decodificando la Gene Ontology (C/C++)

Hola,
como continuación de la entrada anterior os pongo hoy código en C/C++ para hacer exactamente la misma tarea, aprovechando las estructuras de datos e iteradores de la Standard Template Library (STL). En mis pruebas, esta implementación es aproximandamente el doble de rápida que la de Perl. Por cierto, se compila con:  
$ gcc -o get_go_annotation get_go_annotation.cpp myGOparser.cpp -lstdc++

El código fuente incluye 3 ficheros:


1) get_go_annotation.cpp

 #include <stdio.h>  
 #include <stdlib.h>  
 #include <string>  
 #include <map>  
 #include "myGOparser.h"  
 using namespace std;  
   
 #define DEFFLATFILE "/path/to/gene_ontology.1_2.obo";  
   
 int main(int argc, char *argv[])  
 {   
    if(argc == 1)  
    {  
       printf("# usage: ./get_go_annotation <GO:0046983>\n\n");  
       exit(-1);  
    }  
      
    string input_term = argv[1];  
    string flatfile_name = DEFFLATFILE;   
    int n_of_records = 0;  
    map <string, GOnode> parsedGO;  
      
    printf("# parsing GO flat file ... ");  
    n_of_records = parse_GO_flat_file(flatfile_name, parsedGO);  
    printf("done (%d records)\n\n",n_of_records);  
      
    string annot = get_full_annotation_for_term(input_term,parsedGO);  
    printf("%s\n",annot.c_str());  
      
    exit(0);  
 }  

2) myGOparser.h

 /* Bruno Contreras-Moreira, 2011 EEAD/CSIC */   
 #include <stdio.h>  
 #include <stdlib.h>  
 #include <string>  
 #include <vector>  
 #include <map>  
   
 using namespace std;  
   
 #define LINE 400  
 #define FIELD 200  
   
 struct GOnode   
 {  
    string name;  
    vector <string> parents;     
 };  
   
 int parse_GO_flat_file(string &filename, map <string, GOnode> &GO);  
   
 string get_full_annotation_for_term(string &term, map <string, GOnode> &GO);  

3) myGOparser.cpp

 // Bruno Contreras-Moreira, 2011 EEAD/CSIC  
 #include <string.h>  
 #include <map>  
 #include "myGOparser.h"  
   
 using namespace std;  
   
 int parse_GO_flat_file(string &filename, map <string, GOnode> &GO)  
 {  
    // 1) parse flat GO file  
    FILE * gofile = fopen(filename.c_str(),"r");  
    if(gofile == NULL)  
    {  
       printf("# parse_GO_flat_file : cannot read %s, exit ...\n",  
          filename.c_str());  
       return 0;  
    }  
      
    int n_of_records = 0;  
    char line[LINE], field[FIELD];  
    string term,name,alt_id,parent;  
    map <string, string> synonym;  
    while( fgets(line, LINE, gofile) != NULL )  
    {  
      /*[Term]  
       id: GO:0006355  
       name: regulation of transcription, DNA-dependent  
       namespace: biological_process  
       alt_id: GO:0032583  
       is_a: GO:0010468 ! regulation of gene expression */  
         
       if(strncmp(line,"id:",3) == 0)  
       {   
          sscanf(line,"id: %s", (char *) &field);  
          term = field;   
          n_of_records++;  
       }  
       else if(strncmp(line,"name:",5) == 0)   
       {  
          strncpy(field,line+6,FIELD-1);  
          field[strcspn (field,"\n")] = '\0'; // chomp  
          name = field;   
          GO[term].name = name;  
       }  
       else if(strncmp(line,"alt_id:",7) == 0)  
       {  
          sscanf(line,"alt_id: %s",(char *) &field);  
          alt_id = field;  
          synonym[alt_id] = term;   
       }  
       else if(strncmp(line,"is_a:",4) == 0)  
       {  
          sscanf(line,"is_a: %s",(char *) &field);  
          parent = field;     
          GO[term].parents.push_back(parent);  
       }  
    }  
    fclose(gofile);  
      
    // 2) link synonims  
    map <string, string>::iterator syn;  
    for(syn = synonym.begin(); syn != synonym.end(); ++syn )   
       GO[syn->first] = GO[syn->second];  
      
    return n_of_records;  
 }  
   
 string get_full_annotation_for_term(string &term, map <string, GOnode> &GO)  
 {  
    string annot, pterm;  
    vector <string>::iterator pit;  
      
    if(GO.find(term) == GO.end())  
    {  
       annot = "[cannot find GO term ";  
       annot += term;  
       annot += "]";  
    }  
    else  
    {  
       if(GO[term].parents.size() == 0)  
       {  
          annot += GO[term].name;  
          annot += "(";  
          annot += term;  
          annot += ") | ";  
       }  
       else  
       {  
          for(pit=GO[term].parents.begin();pit != GO[term].parents.end();++pit)  
          {      
             annot += GO[term].name;  
             annot += "(";  
             annot += term;  
             annot += "), ";  
             annot += get_full_annotation_for_term(*pit,GO);  
            
          }  
       }     
    }  
      
    return annot;  
 }  

Un saludo, Bruno


26 de agosto de 2011

Decodificando la Gene Ontology

Hola,
a estas alturas es difícil no haber oído hablar de la Gene Ontology (GO), el vocabulario controlado más utilizado en Biología para describir secuencias y sus funciones. Diría yo que la principal aplicación de la GO es facilitar la anotación, manipulación y comparación computacional de grandes volúmenes de secuencias. De hecho, los repositorios de referencia, como por ejemplo UniProt, llevan tiempo anotando sus secuencias usando la jerarquía de la GO. Por lo tanto, actualmente es fácil encontrarse con un montón de secuencias, por ejemplo procedentes de un experimento o generadas por un colaborador, que en vez de descripciones legibles tienen asociados códigos de la GO. Por ejemplo, puedes encontrarte con una proteína de A.thaliana,

> AT3G04220  GO:0005524  
ATGGATTCTT CTTTTTTACT CGAAACTGTT GCTGCTGCAA CAGGCTTCTT
CACACTTTTG GGTACAATAC TTTTTATGGT TTACAGAAAA TTCAAAGACC
ATCGAGAAAA CAAAGAAAAT GATTCTTCTT CTTCAACACA ...

y preguntarte, qué será? Cuál es su función? En el Taller de (bio)Perl ya apuntamos una manera de averiguar la genealogía del término GO:0005524, pero dadas las limitaciones del módulo GO::Parser aquí os muestro una manera más eficiente de extraer el significado y la genealogía de cualquier identificador de la GO, que puede pertenecer a una de las 3 ramas fundamentales de la jerarquía  (proceso biológico, función molecular y localización celular). Para ello es necesario descargar la versión más reciente de la GO en un archivo de texto en formato OBO, y guardar el siguiente módulo escrito en Perl:

 package myGOparser;  
 require Exporter;  
   
 @ISA = qw(Exporter);  
 @EXPORT = qw( $DEFGOFLATFILE parse_GO_flat_file get_full_annotation_for_id );   
 use strict;  
   
 our $DEFGOFLATFILE = "/path/to/GO/gene_ontology.1_2.obo";  
   
 sub parse_GO_flat_file  
 {  
    my $flatfile = $_[0] || $DEFGOFLATFILE;  
    my ($id,$alt_id,$name,$namespace,%synonim,%GO);  
      
    open(GOFILE,$flatfile) ||   
       die "# parse_GO_flat_file : cannot read $flatfile\n";  
    while(<GOFILE>)  
    {  
       #[Term]  
       #id: GO:0006355  
       #name: regulation of transcription, DNA-dependent  
       #namespace: biological_process  
       #alt_id: GO:0032583  
       #is_a: GO:0010468 ! regulation of gene expression  
       if(/^id: (GO:\d+)/){ $id = $1; }  
       elsif(/^name: (.+?)\n/){ $GO{$id}{'name'} = $1; }  
       elsif(/^alt_id: (GO:\d+)/){ push(@{$synonim{$id}},$1); }  
       elsif(/^is_a: (GO:\d+)/){ push(@{$GO{$id}{'parents'}},$1); }  
    }  
    close(GOFILE);  
      
    # link synonims  
    foreach $id (keys(%synonim))  
    {  
       foreach my $syn (@{$synonim{$id}}){ $GO{$syn} = $GO{$id} }  
    }  
      
    return \%GO;  
 }  
   
 sub get_full_annotation_for_id  
 {  
    my ($id,$ref_parsed_GO) = @_;  
    my $annot;  
      
    if(!$ref_parsed_GO->{$id})  
    {  
       return "[cannot find GO term $id]";  
    }  
    else  
    {  
       if(!$ref_parsed_GO->{$id}{'parents'})  
       {  
          $annot .= $ref_parsed_GO->{$id}{'name'}."($id) | ";  
       }  
       else  
       {  
          foreach my $pid (@{$ref_parsed_GO->{$id}{'parents'}})  
          {  
             $annot .= $ref_parsed_GO->{$id}{'name'}."($id)".', '.  
               get_full_annotation_for_id($pid,$ref_parsed_GO);  
          }  
       }     
    }  
      
    return $annot;  
 }  
 1;  

Ahora podemos crear un programa como éste, que invocado desde el terminal nos devuelve la descripción jerárquica completa de un termino GO:

 use strict;  
 use myGOparser;  
   
 my $input_id = $ARGV[0] || die "# usage: ./go_parser.pl \n\n";  
   
 print "# parsing GO flat file ... ";  
 my $ref_GO = parse_GO_flat_file();  
 print "done\n\n";  
   
 print get_full_annotation_for_id($input_id,$ref_GO)."\n";  

Para el identificador GO:0046983 obtendremos la siguiente descripción:

protein dimerization activity(GO:0046983), protein binding(GO:0005515), binding(GO:0005488), molecular_function(GO:0003674) |