Sequences
EncodedArray
Sequence data in BioNumPy are are represented by EncodedArray objects. These are basically numpy arrays of integers, that have an encoding that specifies which character each integer represent. This representataion allows us to do fast numpy operation on the sequences, while still allowing for human readable representation of them. The easiest way to create an EncodedArray is to use the as_encoded_array function.
>>> import bionumpy as bnp
>>> encoded_array = bnp.as_encoded_array("actggtcc")
>>> encoded_array
encoded_array('actggtcc')
We see that the encoded array represents the text “actggtcc”, but under the hood these are just integers with an ASCII encoding. You seldom have to think about this internal representation, but it is what allow us to write for instance:
>>> print(encoded_array == "g")
[False False False True True False False False]
And get numpy-fast performance for the query. We can also use numpy-like indexing on encoded arrays, so that we can for instance trim the first and last two characters from the sequence:
>>> encoded_array[2:-2]
encoded_array('tggt')
EncodedRaggedArray
When working with multiple sequences we usually have to use EncodedRaggedArray objects. These are much like EncodedArray objects, but instead uses npstructures.RaggedArray to store the integers. This allows us to store seqeunces of differing lengths. The easiest way to create an EncodedRaggedArray is to use the as_encoded_array function on a list of strings:
>>> encoded_ragged_array = bnp.as_encoded_array(["ctt", "actg", "ag"])
>>> encoded_ragged_array
encoded_ragged_array(['ctt',
'actg',
'ag'])
These objects also behave very much like numpy arrays, in indexing and broadcasting. For instance, to get the 2nd character of the first and third seqeunce:
>>> encoded_ragged_array[[0, 2], 1]
encoded_array('tg')
Absolute basics of EncodedRaggedArray
As shown above, sequences are represented as EncodedRaggedArray objects. Below are some absolute basics of EncodedRaggedArray objects:
For this example, instead of creating a EncodedRaggedArray object directly, we will simulate some sequences using the BioNumpy’s simulate_sequences function:
>>> import bionumpy as bnp
>>> import numpy as np
>>> from bionumpy.simulate import simulate_sequences
>>> rng = np.random.default_rng(seed=1)
>>> np.random.seed(1)
>>> aa_alphabet = bnp.encodings.alphabet_encoding.AminoAcidEncoding.get_labels()
>>> named_seqs = simulate_sequences(aa_alphabet, {f's{i}': np.random.randint(5,20) for i in range(10)}, rng)
>>> my_seqs = named_seqs.sequence
>>> named_seqs # print the sequences
SequenceEntry with 10 entries
name sequence
s0 LMSYAEVYGH
s1 WKGVGKQNCAWSVNVH
s2 LTDHDL*DKKWFMGASC
s3 GMMD*S*CSHNYG
s4 SEH*KMHDKQLTIP
s5 TYKASNWLICLQTVFP
s6 TGIVPMRM*S
s7 CENVC
s8 RSTWF
s9 NTIFMC
Indexing and slicing of EncodedRaggedArray objects
We can index and slice EncodedRaggedArray objects in a similar way to numpy arrays. Below are some examples:
>>> my_seqs[0:2] # first 2 sequences
encoded_ragged_array(['LMSYAEVYGH',
'WKGVGKQNCAWSVNVH'], AlphabetEncoding('ACDEFGHIKLMNPQRSTVWY*'))
>>> my_seqs[-4:] # last 4 sequences
encoded_ragged_array(['TGIVPMRM*S',
'CENVC',
'RSTWF',
'NTIFMC'], AlphabetEncoding('ACDEFGHIKLMNPQRSTVWY*'))
Some basic properties of EncodedRaggedArray objects
>>> my_seqs.shape # number of sequences and length of each sequence
(10, array([10, 16, 17, 13, 14, 16, 10, 5, 5, 6]))
>>> my_seqs.lengths # lengths of each sequence
array([10, 16, 17, 13, 14, 16, 10, 5, 5, 6])
>>> my_seqs.size # total number of elements (amino acid residues across all sequences in the encoded ragged array)
112
>>> my_seqs.encoding # the encoding used for the sequences
AlphabetEncoding('ACDEFGHIKLMNPQRSTVWY*')
Concatenation of EncodedRaggedArray objects
>>> np.concatenate([my_seqs, my_seqs[-2:]]).shape # concatenate two encoded ragged arrays and get the shape
(12, array([10, 16, 17, 13, 14, 16, 10, 5, 5, 6, 5, 6]))
Getting unique elements and counting occurrences
>>> bnp.count_encoded(my_seqs.get_column_values(0)) # count the number of occurrences of each amino acid at the first position (similar to numpy.unique)
EncodedCounts(alphabet=['A', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'Y', '*'], counts=array([0, 1, 0, 0, 0, 1, 0, 0, 0, 2, 0, 1, 0, 0, 1, 1, 2, 0, 1, 0, 0]), row_names=None)
Counting the number of occurrences of a specific element in each sequence
>>> np.sum(my_seqs == "F", axis=-1) # count the number of occurrences of the amino acid "F" in each sequence
array([0, 0, 1, 0, 0, 1, 0, 0, 1, 1])
Filtering EncodedRaggedArray objects based on a mask
>>> mask = my_seqs.lengths < 8
>>> short_seqs = my_seqs[mask]
>>> short_seqs
encoded_ragged_array(['CENVC',
'RSTWF',
'NTIFMC'], AlphabetEncoding('ACDEFGHIKLMNPQRSTVWY*'))
Broadcasting and one-hot encoding
>>> short_seqs[1][..., np.newaxis] == "ACDEFGHIKLMNPQRSTVWY" # one-hot encoding of the second sequence
array([[False, False, False, False, False, False, False, False, False,
False, False, False, False, False, True, False, False, False,
False, False],
[False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, True, False, False,
False, False],
[False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, True, False,
False, False],
[False, False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False, False,
True, False],
[False, False, False, False, True, False, False, False, False,
False, False, False, False, False, False, False, False, False,
False, False]])
Reading sequences from file
Usually we get sequences directly from file. BioNumPy supports a range of file formats containing sequence data including fasta, fastq, indexed fasta and bam files.
Reading sequence entries (.fa, .fq, .bam)
To read in a set of sequence entries, we can just use the bnp.open method (here we read a fastq file, but this works the same for fasta and bam files):
>>> entries = bnp.open("example_data/reads.fq").read()
>>> entries
SequenceEntryWithQuality with 2 entries
name sequence quality
headerishere CTTGTTGA [2 2 2 2 2 2 2 2]
anotherheader CGG [93 93 93]
We see we get all the entries in the file, with the corresponding fields. The sequence field here is an EncodedRaggedArray and thus supports numpy-like indexing etc:
>>> (entries.sequence=="T").sum(axis=-1) # Count the number of T's
array([4, 0])
Reading indexed files (.fa.fai)
When reading a reference genome, we often can’t read in the whole file (using .read()) and it doesn’t make sense to read in the chromosome-sequences as entries in chunks (using .read_chunks()) but we rather want to read specific parts of the genome. In these cases we can use an index to be able to read specific regions. We then use bnp.open_indexed function:
>>> reference_sequence = bnp.open_indexed("example_data/small_genome.fa")
>>> reference_sequence["2"][10:20]
encoded_array('atattagcca')
Functions workin on sequences
A set of functions working on sequences are gathered in the bnp.seqeunce module.