Skip to content

Scanning

Scanning sequences for motif hits. Sequences can be strings, lists or dicts of strings, PackedSequences, or the path to a FASTA file, as described under scan_sequences().

scan_sequences

scan_sequences(
    motifs: Motif | Iterable[Motif],
    sequences: str
    | Sequence[str]
    | Mapping[str, str]
    | PathLike[str],
    pvalue: float = 0.0001,
    rc: bool = True,
    nthreads: int = 1,
    no_overlaps: bool = False,
    no_overlaps_by: str = "pvalue",
    no_overlaps_by_strand: bool = False,
    no_overlaps_by_motif: bool = False,
    use_gaps: bool = True,
    correct_gaps: bool = False,
) -> pl.DataFrame

Scan sequences for motif hits.

Motifs and sequences of any alphabet can be scanned. Each motif's score threshold comes from the pvalue cutoff, and every window scoring at least that much is reported with its P-value. Only DNA and RNA have a reverse strand, which is scanned with rc.

A motif too short or too degenerate to reach the pvalue cutoff can have no hits, and is named in a warning with the smallest P-value it can give. There are warnings too when DNA motifs meet sequences that look like RNA or the other way round, and when a string that names a file is given as the sequence.

Parameters:

Name Type Description Default
sequences str | Sequence[str] | Mapping[str, str] | PathLike[str]

A sequence, a list of sequences, or a mapping from names to sequences such as the result of read_fasta(). DNA, RNA and protein letters are matched case-insensitively and custom alphabets exactly; anything else, such as N or X, never forms part of a hit. PackedSequences are scanned without decoding them, and a gzipped or plain FASTA file given as a path object such as a pathlib.Path is read and scanned a batch at a time, so a genome can be scanned in a few hundred MB. A Genome is scanned one chromosome at a time.

required
rc bool

Also scan the reverse strand.

True
no_overlaps bool

Remove overlapping hits with dedup_hits(), keeping the best by no_overlaps_by, which is "pvalue" or "score". no_overlaps_by_strand and no_overlaps_by_motif also compare hits across strands and across motifs.

False
nthreads int

Threads to use; 0 means all available.

1
use_gaps bool

Scan gapped motifs, such as those made with add_gap(), with every combination of their gap sizes; otherwise their gaps are ignored. A gapped hit's match shows the gap as dots, and its P-value is that of its score under the motif without the gap. Gapped motifs are scanned on both strands like any other.

True
correct_gaps bool

Bonferroni-correct gapped motifs' P-values for the number of combinations of gap sizes tried: hits must reach pvalue divided by that number, and their P-values are multiplied by it and capped at 1.

False

Returns:

Type Description
DataFrame

A DataFrame with one row per hit and the columns motif, motif_i, sequence, sequence_i, start, end, strand, score, score_pct, match and pvalue. score_pct is the score as a percentage of the motif's maximum, and match is the matched sequence, reverse complemented on the - strand. motif_i and sequence_i are 0-based indices into the inputs; start and end are positions on the forward strand, counted from 0 with the end left out, so a hit's letters are sequence[start:end]. Rows are ordered by motif, sequence and start.

Examples:

>>> import universalmotif as um
>>> wbox = um.example_motifs()[5]
>>> hits = um.scan_sequences(wbox, um.example_sequences())
>>> hits.shape
(30, 11)
>>> hits.select("sequence", "start", "end", "strand", "match").row(0)
('target_004', 448, 457, '-', 'GTTTGACCG')
>>> round(hits["pvalue"][0], 7)
3.43e-05

dedup_hits

dedup_hits(
    hits: DataFrame,
    by: str = "pvalue",
    reverse: bool = False,
    ignore_strand: bool = False,
    ignore_motif: bool = False,
    seq: str = "sequence",
    motif: str = "motif",
    strand: str = "strand",
) -> pl.DataFrame

Remove overlapping hits.

Hits are grouped by sequence, motif and strand. Within a group, overlapping hits are taken in order of the by column and each is kept unless it overlaps one already kept. Lower values come first for columns whose name starts with pval and higher values otherwise; reverse flips this, and ties go to the earlier row. ignore_strand and ignore_motif compare hits across strands and motifs. start and end columns are required, with the end left out as in a slice.

Examples:

A palindromic G-box site matches on both strands at the same place:

>>> import universalmotif as um
>>> gbox = um.example_motifs()[3]
>>> hits = um.scan_sequences(gbox, "TTACACGTGTTAAACCACGTGGTT", pvalue=1e-3)
>>> hits.select("start", "end", "strand").rows()
[(2, 10, '+'), (2, 10, '-'), (14, 22, '+'), (14, 22, '-')]
>>> um.dedup_hits(hits, ignore_strand=True).select("start", "end", "strand").rows()
[(2, 10, '+'), (14, 22, '+')]

Motif match matrices

Which motifs match which sequences, as a sparse matrix for single-cell ATAC analyses, without keeping the hits themselves. These need the scipy extra, and AnnData output the anndata extra.

match_motifs

match_motifs(
    motifs: Motif | Iterable[Motif],
    sequences: str
    | Sequence[str]
    | Mapping[str, str]
    | PackedSequences,
    pvalue: float = 5e-05,
    value: str = "matches",
    rc: bool = True,
    no_overlaps: bool = False,
    use_gaps: bool = True,
    nthreads: int = 1,
) -> MotifMatches

Which motifs match which sequences, as a sparse matrix.

This is the motif annotation used by chromVAR and similar single-cell ATAC methods: the sequences, usually peaks, are scanned as by scan_sequences() with the pvalue cutoff, and only a summary of the hits is kept for each sequence and motif, so that hundreds of thousands of peaks and hundreds of motifs take no more memory than the matrix itself. value is "matches" for 1 where a motif has a hit, "counts" for the number of hits, or "scores" for the best score of any hit. Palindromic sites are hit on both strands, so no_overlaps drops overlapping hits of each motif, on either strand, before they are counted, keeping the one with the lowest P-value.

Needs the scipy extra.

Returns:

Type Description
MotifMatches

The matrix with one row per sequence and one column per motif, and their names.

Examples:

>>> import universalmotif as um
>>> motifs = um.example_motifs()
>>> counts = um.match_motifs(motifs, um.example_sequences(), value="counts")
>>> counts.matrix.shape, counts.motifs[3]
((100, 12), 'G-box')
>>> int(counts.matrix[:, 3].sum()), int(counts.matrix[:, 3].max())
(18, 2)

The G-box is close to a palindrome, so some of its sites are hit on both strands, which no_overlaps counts once:

>>> once = um.match_motifs(motifs, um.example_sequences(), value="counts", no_overlaps=True)
>>> int(once.matrix[:, 3].sum()), int(once.matrix[:, 3].max())
(12, 1)

MotifMatches dataclass

MotifMatches(
    matrix: Any,
    sequences: list[str],
    motifs: list[str],
    value: str,
)

A sparse matrix of motif matches in sequences, from match_motifs().

matrix is a SciPy CSR array with one row per sequence and one column per motif, named by sequences and motifs. Its values are those asked for with value: 1 for a match, the number of hits, or the best score, and pairs without a hit are left out.

Examples:

>>> import universalmotif as um
>>> matches = um.match_motifs(um.example_motifs()[3:6], um.example_sequences())
>>> matches
MotifMatches(100 sequences x 3 motifs, 32 matches)
>>> matches.matrix.sum(axis=0).tolist()
[12, 3, 17]

to_polars

to_polars() -> pl.DataFrame

The matches as a long DataFrame with columns sequence, motif and value, one row per sequence and motif with a hit.

Examples:

>>> import universalmotif as um
>>> matches = um.match_motifs(um.example_motifs()[3:6], um.example_sequences())
>>> matches.to_polars().head(2).rows()
[('target_004', 'W-box', 1.0), ('target_005', 'W-box', 1.0)]

to_anndata

to_anndata()

The matches as an AnnData object, with the sequences as observations and the motifs as variables. Needs the anndata extra.

Examples:

>>> import universalmotif as um
>>> matches = um.match_motifs(um.example_motifs()[3:6], um.example_sequences())
>>> adata = matches.to_anndata()
>>> adata.shape
(100, 3)

add_motif_matches

add_motif_matches(
    adata: Any,
    motifs: Motif | Iterable[Motif],
    sequences: Sequence[str]
    | Mapping[str, str]
    | None = None,
    key: str = "motif_match",
    names_key: str = "motif_name",
    dense: bool = True,
    **kwargs: Any,
) -> None

Add motif matches to an AnnData object of peaks, as pychromVAR expects.

adata has the peaks as its variables, as in a cells by peaks matrix. Their sequences come from sequences, either a list in the order of the peaks or a mapping from peak names, and by default from adata.uns["peak_seq"], where pychromVAR's add_peak_seq() keeps them. The peaks by motifs matrix from match_motifs(), given kwargs, is stored in adata.varm[key], as a dense array unless dense is false, and the motif names in adata.uns[names_key]. With the defaults, the result can go straight to pychromVAR's compute_deviations().

Needs the scipy extra.

Examples:

>>> import universalmotif as um
>>> um.add_motif_matches(adata, um.example_motifs())
>>> adata.varm["motif_match"].shape
(100, 12)