Análise de homologias por BLAST

  1. blast.local_blastp(tag_to_files, db)
  2. blast.docker_blastp(directory, db)
  3. blast.expasy_blastp(tag_to_files)
  4. blast.blastp(tags_and_proteins, db, type)
  5. blast.write_queries_to_dir(tags_and_proteins, directory)
  6. blast.extract_blast_info(tag_to_files, type)
  7. add_info_to_blast_results(blast_results, uniprots)
  8. main()

O objectivo desta fase era, dadas as proteínas que retirámos do ficheiro genbank, procurar sequências homólogas com a ferramenta BLAST. Esta fase foi iterada várias vezes à procura da melhor estratégia, e no final acabámos com várias alternativas para a execução do BLAST, que podem ser encontradas no módulo util.blast. As alternativas são:

  • Local
  • Docker
  • ExPASy

Os resultados desta fase podem ser encontrados aqui.

Local

Esta função assume que o BLAST está instalado localmente, e recebe como parâmetro a base de dados que deve usar. O primeiro argumento desta função, tag_to_files, é um dicionário no qual as chaves são os locus_tag de cada gene, e os valores são pares em que a primeira componente é o ficheiro, onde se encontra a sequência de aminoácidos para ser usada na query, (in_file), e a segunda componente é o ficheiro onde deve ser guardado o resultado do BLAST (out_file).

def local_blastp(tag_to_files, db):
    """
    Corre o blast localmente.
    """

    for tag in tag_to_files:
        (in_file, out_file) = tag_to_files[tag]

        blastp_cline = NcbiblastpCommandline(
            query=in_file,
            db=db,
            evalue=10,
            outfmt=5,
            out=out_file
        )
        blastp_cline()

Docker

Para evitar a instalação do BLAST, o download de bases de dados e a sua adição como base de dados a ser usada pelo BLAST, criámos uma imagem Docker (vitorenesduarte/swissprot_blast) com a base de dados UniProtKB - Swiss-Prot retirada do site da UniProt.

def docker_blastp(directory, db):
    """
    Corre o blast numa instância docker.
    """
    cmd = "docker run -e QUERY_DIR=" + directory \
                  + " -e DB=" + db \
                  + " -v $PWD/" + directory + ":/" + directory \
                  + " -ti vitorenesduarte/swissprot_blast"

    p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    p.wait()

ExPASy

A terceira opção não corre o BLAST localmente, mas utiliza o serviço disponibilizado pelo site ExPASy.

def expasy_blastp(tag_to_files):
    """
    Corre o blast na expasy.
    """
    for tag in tag_to_files:
        (in_file, out_file) = tag_to_files[tag]
        query = rw.read_file(in_file)
        blast_result = www.expasy_blast(query)
        rw.write_file(blast_result, out_file)

Esta função usa uma outra função definida no módulo util.www que corre o BLAST contra a base de dados UniProtKB = Swiss-Prot + TrEMBL, usando o serviço da ExPASy.

def expasy_blast(query):
    """
    Corre o blast na expasy.
    """

    url = "http://web.expasy.org/cgi-bin/blast/blast.pl"
    data = {
        "format": "xml",
        "prot_db1": "UniProtKB",
        "matrix": "BLOSUM62",
        "showsc": 50, # best scoring sequences
        "showal": 50, # best alignments
        "seq": query
    }

    response = requests.post(
        url,
        data=data
    )

    return response.text

Esta última opção permitiu-nos encontrar sequências com maior homologia do que as que estávamos a encontrar, utilizando apenas a Swiss-Prot localmente.

BLAST

A função que engloba estas três opções recebe como argumento uma lista de pares em que a primeira componente é a locus_tag da proteína, e a segunda componente é a respectiva sequência de aminoácidos. Recebe ainda qual o tipo de BLAST, local, docker ou expasy, a utilizar.

def blastp(tags_and_proteins, type="local"):
    """
    Corre o blast para a proteínas passadas como argumento.

    O argumento type é opcional e pode ter dois valores:
        - local
        - docker
        - expasy

    É retornado um dicionário com os resultados.
    """

    directory = ".query_dir"
    tag_to_files = write_queries_to_dir(tags_and_proteins, directory)

    # correr o blast
    if type == "local":
        local_blastp(tag_to_files, "swissprot")
    elif type == "docker":
        docker_blastp(directory, "swissprot")
    elif type == "expasy":
        expasy_blastp(tag_to_files)
    else:
        raise Exception("Unsupported type: " + type)

    blast_results = extract_blast_info(tag_to_files, type)
    return blast_results

Esta função usa duas outras funções:

  • write_queries_to_dir que recebe uma lista de pares, locus_tag e sequência de aminoácidos protein, e retorna um dicionário que mapeia locus_tag para pares (in_file, out_file).
def write_queries_to_dir(tags_and_proteins, directory):
    """
    Grava as proteínas passadas como argumento na
    diretoria também passada como argumento.
    """

    tag_to_files = {}

    # Apagar a diretoria e criar uma nova
    if os.path.exists(directory):
        shutil.rmtree(directory)
    os.makedirs(directory)

    for (tag, protein) in tags_and_proteins:
        # Gravar a proteína num ficheiro
        in_file = directory + "/" + fasta_it(tag)
        rw.write_file(protein, in_file)

        # registar esta informação num dicionário
        out_file = directory + "/" + xml_it(fasta_it(tag))
        tag_to_files[tag] = (in_file, out_file)

    return tag_to_files

  • extract_blast_info que dado este dicionário retorna um outro dicionário que mapeia locus_tag para a lista de resultados do BLAST. Cada um destes resultados é um dicionário com:
    • uniprot_id
    • evalue
    • score
    • identity
def extract_blast_info(tag_to_files, type):
    """
    Extrai a informação que necessitamos dos resultados do blast.
      - uniprot_id
      - evalue
      - score
      - identity
    """

    blast_results = {}

    for tag in tag_to_files:
        (_, out_file) = tag_to_files[tag]

        ### Alguns resultados do blast, ao serem lidos de disco
        ### na função rw.read_blast
        ### estavam a dar erro ao fazer parsing nesta linha.
        ### Como não necessitamos deste resultado,
        ### optamos por removê-la.
        regex = ".*Hsp_midline.*"
        rw.filter_file(regex, out_file)

        handle = rw.read_blast(out_file)

        result = []

        for a in handle.alignments:
            # extrair o uniprot id
            if type in ["local", "docker"]:
                # nos blast locais, o uniprot_id está no hit_def
                uniprot_id = a.hit_def.split("|")[1]
            elif type == "expasy":
                # nos blast expasy, o uniprot_id está no hit_id
                uniprot_id = a.hit_id.split("|")[1]

            # escolher sempre o primeiro hsp
            hsp = a.hsps[0]
            evalue = hsp.expect
            score = hsp.score
            identities = hsp.identities
            align_length = hsp.align_length
            identity = (identities * 100) / align_length

            hit = {}
            hit["uniprot_id"] = uniprot_id
            hit["evalue"] = evalue
            hit["score"] = score
            hit["identity"] = identity
            result.append(hit)

        blast_results[tag] = result

    return blast_results

Nesta fase ainda precisávamos de mais informação para além daquela que é possível extrair dos resultados do BLAST, que foi conseguida utilizando um serviço do site UniProt descrito aqui.

  • status (reviewed ou unreviewed)
  • organism
  • translation (sequência de aminoácidos)
  • molecular_functions (GO - Molecular functions)

Esta informação foi adicionada a cada um dos resultados provenientes do BLAST.

def add_info_to_blast_results(blast_results, uniprots):
    """
    Esta função adiciona aos blast results informação
    retirada do site da uniprot.
    """

    for tag in blast_results:
        for i in range(len(blast_results[tag])):
            uniprot_id = blast_results[tag][i]["uniprot_id"]

            # esta proteína aparece nos resultados do blast como
            # Q8TC84-2 mas a uniprot retorna a sua informação
            # como Q8TC84
            if uniprot_id == "Q8TC84-2":
                uniprot_id = "Q8TC84"

            properties = [
                "status",
                "organism",
                "translation",
                "molecular_functions"
            ]

            for p in properties:
                value = uniprots[uniprot_id][p]
                blast_results[tag][i][p] = value

    return blast_results

Por fim, automatizámos o processo de inferência da função, dados os resultados do BLAST. Uma função é considerada provável se aparecer em pelo menos 50% dos resultados do BLAST. Este inferência foi feita tanto para os 10 primeiros resultados como para os resultados todos.

def infer_function(blast_results):
    """
    Infere qual as funções mais prováveis tendo em consideração
    os primeiros 10 resultados do blast,
    e quais as funções mais prováveis tendo em consideração
    todos os resultados do blast.
    """

    all = {}

    for tag in sorted(blast_results.keys()):
        results = blast_results[tag]
        # inferir função para os primeiros 10 resultados
        inferred_top_ten = infer(results[0:10])
        # inferir função para todos os resultados
        inferred_all = infer(results)

        all[tag] = {}
        all[tag]["top_ten"] = inferred_top_ten
        all[tag]["all"] = inferred_all

    return all

def infer(results):
    """
    Esta função tenta inferir a função das proteínas baseando-se
    numa lista de resultados do blast.
    """
    inferred = []
    leaderboard = defaultdict(int)

    for result in results:
        functions = result["molecular_functions"]

        # atualizar a leaderboard
        for function in result["molecular_functions"]:
            leaderboard[function] += 1

    # As funções que aparecem em pelo menos 50% dos resultados
    # são consideradas potenciais funções
    min = len(results) / 2

    for function in leaderboard:
        if leaderboard[function] >= min:
            inferred.append(function)

    return sorted(inferred)

Todos estes passos podem ser encontrados no ficheiro second.py e resumem-se a:

import util.rw as rw
import util.blast as blast
import util.www as www
import util.util as util

ncbi_json_path = ".ncbi.json"
blast_results_json_path = ".blast_results.json"
inferred_json_path = ".inferred.json"

dictionary = rw.read_json(ncbi_json_path)
tags_and_proteins = util.get_tags_and_proteins(dictionary)

blast_results = blast.blastp(
    tags_and_proteins,
    "expasy"
)

uniprot_ids = set()
for tag in blast_results:
    for result in blast_results[tag]:
        uniprot_ids.add(result["uniprot_id"])

uniprot_ids = list(uniprot_ids)
uniprots = www.fetch_uniprots(uniprot_ids)

blast_results = add_info_to_blast_results(
    blast_results,
    uniprots
)
rw.write_json(blast_results, blast_results_json_path)

inferred = infer_function(blast_results)
rw.write_json(inferred, inferred_json_path)