Gene Coordinate Annotation#
Attaches genomic coordinates to a molecular phenotype matrix, and emits the region and gene lists that downstream steps split work by.
Overview#
Molecular phenotype matrices arrive keyed by feature ID alone – an ENSEMBL gene ID, a UniProt accession, a leafcutter intron-cluster label – but every downstream QTL step needs a genomic position to define a cis window. This module attaches those coordinates, turning a plain matrix into a coordinate-sorted, bgzipped and tabix-indexed bed.gz file.
Coordinates come from a collapsed gene-model GTF, following the GTEx pipeline convention, so the annotation used here matches the one used to build the GTF in the first place. Feature IDs that the GTF does not contain are dropped rather than guessed at, which is why the row count of the output can be lower than that of the input.
Which route applies depends on how the features are named. Gene-level expression matrices match the GTF directly. Protein matrices carry gene_id|UniProt composite IDs and need an auxiliary mapping to reach a gene. Leafcutter intron clusters are not genes at all: they are genomic intervals that must first be mapped onto the genes they overlap, which also produces the phenotype-group file that grouped multiple-testing correction later consumes. Psichomics PSI output is already grouped by gene symbol, so it only needs that symbol translated to an ENSEMBL ID.
An older biomaRt route is retained for reference. It places a single-base TSS interval rather than the full gene body and is obsolete; new analyses should use the GTF-based route.
When to run it. After a molecular phenotype matrix has been assembled and normalised, and before any QTL association scan, since the association modules read the coordinates from this bed.gz.
Input#
--phenoFile(required): the molecular phenotype matrix to annotate. Feature IDs live in the column named by--phenotype-id-column; all remaining columns are samples.--coordinate-annotation(required): the collapsed gene-model GTF (or, for thebiomaRtroute, the reference the annotation is drawn from) supplying chromosome, start and end per feature.--phenotype-id-column(defaultgene_id): name of the feature-ID column in--phenoFile.--molecular-trait-type(defaultgene):genefor expression,proteinfor proteomics; selects how the feature ID is parsed before the GTF lookup.--auxiliary-id-mapping(default.): two-column map used for theproteintrait type to reach a gene from a UniProt accession.--sample-participant-lookup(default.): optional two-column sample-to-participant map; supply it to rename matrix columns to participant IDs in the output.--ensembl-version: ENSEMBL release passed tobiomaRt; only theannotate_coord_biomartworkflow reads it, and it must be set explicitly there.--intron-count: leafcutter intron-count table, read bymap_leafcutter_cluster_to_geneto enumerate the clusters that need a gene assignment.--map-stra/--cwd/--container/--entrypoint: intron-to-gene matching mode for cluster mapping (defaultsite), output directory (defaultoutput), and the software environment.
Example inputs shipped with the protocol:
input/rnaseq/protocol_example.rnaseq.bed.gz– toy gene-level bulk RNA-seq matrix.input/rnaseq/protocol_example.rnaseq.gene_ID.tsv– the same matrix with agene_IDcolumn, for thebiomaRtexample.input/proteomics/protocol_example.protein.no_coord.tsv– toy protein matrix,gene_id|UniProtIDs, no coordinates.input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene– the collapsed gene model.
First rows of the expression matrix:
#chr start end ID SAMPLE_001
chr22 10939387 10961337 ENSG00000283047 37.077
chr22 15528191 15529138 ENSG00000130538 0.0
Output#
<phenoFile stem>.bed.gzplus its.tbiindex – the annotated matrix, coordinate-sorted, with#chr start end IDprepended to the sample columns. Written by every annotation workflow.<phenoFile stem>.region_list.txt– one row per retained feature giving its coordinates, ID and the path of thebed.gzit came from; QTL modules read this to enumerate cis regions.<phenoFile stem>.gene_list.tsv– the feature IDs that were successfully matched to the GTF.*.leafcutter.clusters_to_genes.txtand*.exon_list– frommap_leafcutter_cluster_to_gene: the cluster-to-gene assignment and the exon table it was derived from.*.phenotype_group.txt– from the leafcutter and psichomics isoform workflows: the isoform-to-gene grouping used by grouped multiple-testing correction.
output/gene_annotation/protocol_example.rnaseq.bed.bed.gz:
#chr start end ID strand SAMPLE_001
chr22 10939387 10961337 ENSG00000283047 - 37.0768403309204
chr22 15528191 15529138 ENSG00000130538 + 0.0
output/gene_annotation/protocol_example.rnaseq.bed.region_list.txt (the path column holds the absolute location of the bed.gz, abbreviated here):
#chr start end ID strand path
chr22 10939387 10961337 ENSG00000283047 - .../output/gene_annotation/protocol_example.rnaseq.bed.bed.gz
chr22 15528191 15529138 ENSG00000130538 + .../output/gene_annotation/protocol_example.rnaseq.bed.bed.gz
Minimal Working Example#
Each block below is a self-contained route: pick the one that matches how the features in the matrix are named. All of them read from input/ and write to output/gene_annotation/, and none depends on the others having been run.
Gene Expression#
Annotate a gene-level expression matrix. Each feature ID is an ENSEMBL gene ID that is matched directly against the GTF to obtain its chromosome, start and end coordinates.
Timing: ~1 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb annotate_coord \
--cwd output/gene_annotation \
--phenoFile input/rnaseq/protocol_example.rnaseq.bed.gz \
--coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf \
--phenotype-id-column gene_id
The output protocol_example.rnaseq.bed.gz is written to output/gene_annotation/, with genomic coordinates (#chr start end ID) prepended to the expression matrix.
Proteins#
Annotate a protein matrix. Here each feature ID has the form gene_id\|UniProt; the gene ID portion is matched against the GTF for coordinates and the UniProt portion is retained in the output feature ID. Use --molecular-trait-type protein to select this behaviour.
Timing: ~1 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb annotate_coord \
--cwd output/gene_annotation \
--phenoFile input/proteomics/protocol_example.protein.no_coord.tsv \
--coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.reformatted.collapse_only.gene.ERCC.gtf \
--phenotype-id-column gene_id \
--molecular-trait-type protein
Leafcutter clusters to genes#
For leafcutter splicing data, intron clusters first need to be assigned to genes. The input is a leafcutter intron-count table whose feature IDs have the form chr:start:end:clu_N_strand; --coordinate-annotation is a full gene-model GTF (with exon records). The default --map-stra site maps introns to genes by matching splice-site coordinates to exon boundaries.
Timing: ~2 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb map_leafcutter_cluster_to_gene \
--cwd output/gene_annotation \
--phenoFile input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz \
--intron-count input/rnaseq/protocol_example.leafcutter.intron_count.tsv \
--coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf \
--map-stra site
Leafcutter isoforms#
This example turns the raw leafcutter intron-excision output into a phenotype matrix that TensorQTL can use. Building on the cluster-to-gene mapping from the previous example, it attaches genomic coordinates and a gene assignment to every intron, then writes a coordinate-annotated phenotype BED together with a phenotype-group file that links each intron back to its gene. The same --intron-count table and --coordinate-annotation GTF used above are supplied here.
Along the way the step produces three intermediate files, shown below with a few example rows from the toy data.
1. Exon list — exon records parsed from the annotation GTF, used to match intron splice sites to genes (<intron_count>.exon_list):
chr |
start |
end |
strand |
gene_id |
gene_name |
|---|---|---|---|---|---|
1 |
111869 |
112227 |
+ |
ENSG00000223972 |
DDX11L1 |
1 |
112613 |
112721 |
+ |
ENSG00000223972 |
DDX11L1 |
2. Clusters-to-genes — each leafcutter cluster mapped to the gene it belongs to (<intron_count>.leafcutter.clusters_to_genes.txt):
clu |
genes |
|---|---|
22:clu_10_+ |
ENSG00000236052 |
22:clu_11_- |
ENSG00000185264 |
3. Phenotype group — links each annotated intron feature to its gene, so introns from the same gene are grouped together (<phenotype>.phenotype_group.txt):
ID |
gene |
|---|---|
22:19149095:19149663:clu_14_-:ENSG00000063515 |
ENSG00000063515 |
22:19149916:19150025:clu_14_-:ENSG00000063515 |
ENSG00000063515 |
Timing: ~2 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb annotate_leafcutter_isoforms \
--cwd output/gene_annotation \
--phenoFile input/rnaseq/protocol_example.leafcutter.phenotype.bed.gz \
--intron-count input/rnaseq/protocol_example.leafcutter.intron_count.tsv \
--coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf \
--map-stra site
Psichomics isoforms#
For psichomics splicing quantifications, each event ID ends in _<gene_id>; the gene ID is matched against the GTF to obtain coordinates. The output is a coordinate-annotated phenotype BED plus a phenotype-group file.
Timing: ~2 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb annotate_psichomics_isoforms \
--cwd output/gene_annotation \
--phenoFile input/rnaseq/protocol_example.psichomics.phenotype.tsv \
--coordinate-annotation input/reference_data/Homo_sapiens.GRCh38.103.chr.gtf
Alternative coordinate annotation with biomaRt#
Instead of a local GTF, coordinates can be fetched from the Ensembl biomaRt web service. The phenotype matrix must have a gene_ID column of ENSEMBL gene IDs. --ensembl-version selects the Ensembl release; use a release whose server is reachable from your network (e.g. a current release). This step requires internet access to Ensembl.
Timing: ~2 min (on toy dataset)
sos run pipeline/gene_annotation.ipynb annotate_coord_biomart \
--cwd output/gene_annotation \
--phenoFile input/rnaseq/protocol_example.rnaseq.gene_ID.tsv \
--ensembl-version 115
chr |
start |
end |
strand |
gene_id |
gene_name |
|---|---|---|---|---|---|
chr1 |
29554 |
30039 |
+ |
ENSG00000243485 |
MIR1302-2HG |
chr1 |
30564 |
30667 |
+ |
ENSG00000243485 |
MIR1302-2HG |
chr1 |
30976 |
31097 |
+ |
ENSG00000243485 |
MIR1302-2HG |
chr1 |
35721 |
36081 |
- |
ENSG00000237613 |
FAM138A |
chr1 |
35277 |
35481 |
- |
ENSG00000237613 |
FAM138A |
chr1 |
34554 |
35174 |
- |
ENSG00000237613 |
FAM138A |
chr1 |
65419 |
65433 |
+ |
ENSG00000186092 |
OR4F5 |
chr1 |
65520 |
65573 |
+ |
ENSG00000186092 |
OR4F5 |
chr1 |
69037 |
71585 |
+ |
ENSG00000186092 |
OR4F5 |
phenotype_group
X1 |
X2 |
|---|---|
7:102476270:102478811:clu_309_-:ENSG00000005075 |
ENSG00000005075 |
7:102476270:102478808:clu_309_-:ENSG00000005075 |
ENSG00000005075 |
X:47572961:47574002:clu_349_-:ENSG00000008056 |
ENSG00000008056 |
X:47572999:47574002:clu_349_-:ENSG00000008056 |
ENSG00000008056 |
8:27236905:27239971:clu_322_-:ENSG00000015592 |
ENSG00000015592 |
8:27239279:27239971:clu_322_-:ENSG00000015592 |
ENSG00000015592 |
8:27241262:27241677:clu_323_-:ENSG00000015592 |
ENSG00000015592 |
8:27241262:27242397:clu_323_-:ENSG00000015592 |
ENSG00000015592 |
8:27241757:27242397:clu_323_-:ENSG00000015592 |
ENSG00000015592 |
1:35558223:35559107:clu_4_+:ENSG00000020129 |
ENSG00000020129 |
Command Interface#
sos run pipeline/gene_annotation.ipynb -h
usage: sos run pipeline/gene_annotation.ipynb
[workflow_name | -t targets] [options] [workflow_options]
workflow_name: Single or combined workflows defined in this script
targets: One or more targets to generate
options: Single-hyphen sos parameters (see "sos run -h" for details)
workflow_options: Double-hyphen workflow-specific parameters
Workflows:
annotate_coord
annotate_coord_biomart
map_leafcutter_cluster_to_gene
annotate_leafcutter_isoforms
annotate_psichomics_isoforms
Global Workflow Options:
--modular-script-dir code/script (as path)
--cwd output (as path)
Work directory & output directory
--phenoFile VAL (as path, required)
Molecular phenotype matrix
--job-size 1 (as int)
For cluster jobs, number commands to run per job
--walltime 5h
Wall clock time expected
--mem 16G
Memory expected
--numThreads 1 (as int)
Number of threads
--container ''
--entrypoint ''
Sections
annotate_coord:
Workflow Options:
--sample-participant-lookup . (as path)
A file to map sample ID from expression to genotype,
must contain two columns, sample_id and participant_id,
mapping IDs in the expression files to IDs in the
genotype (these can be the same).
--molecular-trait-type gene
Options: gene, protein, atac
--coordinate-annotation VAL (as path, required)
gtf annotation for RNA-seq data; other types of
annotation index for protein and atac-seq
--phenotype-id-column 'gene_id'
gene_id or gene_name for RNA-seq data, SOMAseqID for
proteins for example
--auxiliary-id-mapping . (as path)
Optional mapping file (protein_name_index for protein,
etc.)
--[no-]strip-id (default to False)
Optional processing flag
--sep '\t'
annotate_coord_biomart:
Workflow Options:
--ensembl-version VAL (as int, required)
map_leafcutter_cluster_to_gene:
Workflow Options:
--intron-count VAL (as path, required)
Extract the code in case psichromatic needs to be
processed the same way PheoFile in this step is the
intron_count file
--coordinate-annotation VAL (as path, required)
gtf annotation with exons (the full gene model), used to
map intron splice sites to genes
--map-stra site
Defines the mapping strategy options: 'site' or
'region', with 'site' as the default. The 'site'
strategy maps introns to the start and end of each exon.
The 'region' strategy, to be recommended in leafcutter2,
maps each intron based on it overlapping more than
overlap_ratio of a gene's region.
--overlap-ratio 0.8 (as float)
Define the overlap ratio as the proportion of the
cluster length that intersects with a gene, used to
determine mapping to the gene.
annotate_leafcutter_isoforms:
Workflow Options:
--sample-participant-lookup . (as path)
--coordinate-annotation VAL (as path, required)
gtf annotation with exons (the full gene model)
annotate_psichomics_isoforms:
Workflow Options:
--sample-participant-lookup . (as path)
--coordinate-annotation VAL (as path, required)
gtf annotation with exons (the full gene model)
Workflow implementation#
[global]
parameter: modular_script_dir = path('code/script') # override with --modular-script-dir
# Work directory & output directory
parameter: cwd = path("output")
# Molecular phenotype matrix
parameter: phenoFile = path
# For cluster jobs, number commands to run per job
parameter: job_size = 1
# Wall clock time expected
parameter: walltime = "5h"
# Memory expected
parameter: mem = "16G"
# Number of threads
parameter: numThreads = 1
parameter: container = ""
parameter: entrypoint= ""
Implementation using pyqtl#
Implementation based on GTEx pipeline.
Following step serves to annotate coord for gene expression file.
[annotate_coord]
# A file to map sample ID from expression to genotype, must contain two columns, sample_id and participant_id, mapping IDs in the expression files to IDs in the genotype (these can be the same).
parameter: sample_participant_lookup = path()
# Options: gene, protein, atac
parameter: molecular_trait_type = "gene"
# gtf annotation for RNA-seq data; other types of annotation index for protein and atac-seq
parameter: coordinate_annotation = path
# gene_id or gene_name for RNA-seq data, SOMAseqID for proteins for example
parameter: phenotype_id_column = "gene_id"
# Optional mapping file (protein_name_index for protein, etc.)
parameter: auxiliary_id_mapping = path()
# Optional processing flag
parameter: strip_id = False
parameter: sep = "\t"
input: phenoFile, coordinate_annotation
output: f'{cwd:a}/{_input[0]:bn}.bed.gz', f'{cwd:a}/{_input[0]:bn}.region_list.txt'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \
--step annotate_coord \
--phenoFile "${_input[0]}" \
--coordinate-annotation "${_input[1]}" \
--sample-participant-lookup "${sample_participant_lookup}" \
--molecular-trait-type "${molecular_trait_type}" \
--phenotype-id-column "${phenotype_id_column}" \
--auxiliary-id-mapping "${auxiliary_id_mapping}" \
--sep "${sep}" \
${"--strip-id" if strip_id else ""} \
--output-bed "${_output[0]}" \
--output-region-list "${_output[1]}" \
--gene-list-output "${cwd:a}/${_input[0]:bn}.gene_list.tsv"
Implementation using biomaRt#
This workflow adds the annotations of chr pos(TSS where start = end -1) and gene_ID to the bed file. This workflow is obsolete.
[annotate_coord_biomart]
parameter: ensembl_version=int
input: phenoFile
output: f'{cwd:a}/{_input:bn}.bed.gz',
f'{cwd:a}/{_input:bn}.region_list.txt'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \
--step annotate_coord_biomart \
--phenoFile "${_input[0]}" \
--ensembl-version ${ensembl_version} \
--output-bed "${_output[0]}" \
--output-region-list "${_output[1]}"
Annotation of leafcutter isoform#
The gtf used here should be the collapsed gtf, i.e. the final output of reference_data gtf processing and the one used to called rnaseq.
[map_leafcutter_cluster_to_gene]
## Extract the code in case psichromatic needs to be processed the same way
## PheoFile in this step is the intron_count file
parameter: intron_count = path
# gtf annotation with exons (the full gene model), used to map intron splice sites to genes
parameter: coordinate_annotation = path
# Defines the mapping strategy options: 'site' or 'region', with 'site' as the default.
# The 'site' strategy maps introns to the start and end of each exon.
# The 'region' strategy, to be recommended in leafcutter2, maps each intron based on it overlapping more than overlap_ratio of a gene's region.
parameter: map_stra = "site"
# Define the overlap ratio as the proportion of the cluster length that intersects with a gene, used to determine mapping to the gene.
parameter: overlap_ratio = 0.8
input: intron_count, coordinate_annotation
output: f'{cwd}/{_input[0]:b}.exon_list', f'{cwd}/{_input[0]:b}.leafcutter.clusters_to_genes.txt'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \
--step map_leafcutter_cluster_to_gene \
--intron-count "${_input[0]}" \
--annotation-gtf "${_input[1]}" \
--map-stra "${map_stra}" \
--overlap-ratio ${overlap_ratio} \
--output-exon-list "${_output[0]}" \
--output-cluster-map "${_output[1]}"
[annotate_leafcutter_isoforms]
parameter: sample_participant_lookup = path()
# gtf annotation with exons (the full gene model)
parameter: coordinate_annotation = path
input: phenoFile, coordinate_annotation, output_from("map_leafcutter_cluster_to_gene")
output: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \
--step annotate_leafcutter_isoforms \
--phenoFile "${_input[0]}" \
--annotation-gtf "${_input[1]}" \
--cluster-map "${_input[3]}" \
--sample-participant-lookup "${sample_participant_lookup}" \
--output-bed "${_output[0]}" \
--output-phenotype-group "${_output[1]}"
Processing of psichomics output#
It occurs that the psichomatic by default grouped the isoforms by gene name, so only thing needs to be done is to extract this information and potentially renamed the gene symbol into ENSG ID
[annotate_psichomics_isoforms]
parameter: sample_participant_lookup = path()
# gtf annotation with exons (the full gene model)
parameter: coordinate_annotation = path
input: phenoFile, coordinate_annotation
output: f'{cwd:a}/{_input[0]:bn}.formated.bed.gz', f'{cwd:a}/{_input[0]:bn}.phenotype_group.txt'
task: trunk_workers = 1, trunk_size = job_size, walltime = walltime, mem = mem, tags = f'{step_name}_{_output[0]:bn}'
bash: expand= "${ }", stderr = f'{_output[0]:n}.stderr', stdout = f'{_output[0]:n}.stdout', container = container, entrypoint = entrypoint
Rscript ${modular_script_dir}/data_preprocessing/phenotype/gene_annotation.R \
--step annotate_psichomics_isoforms \
--phenoFile "${_input[0]}" \
--annotation-gtf "${_input[1]}" \
--sample-participant-lookup "${sample_participant_lookup}" \
--output-bed "${_output[0]}" \
--output-phenotype-group "${_output[1]}"