Skip to content

Recurrence dotplot

A text plotted against itself: the sentence × sentence self-similarity grid.

lexograph.presets.recurrence.recurrence_plot(text, *, threshold=0.6, shingle=None, distances=None, mode='binary', figsize=(8.0, 8.0), background='white')

Draw the sentence × sentence recurrence dotplot of a text.

Parameters:

Name Type Description Default
text str

The source text.

required
threshold float

In "binary" mode, cells with distance <= threshold are lit.

0.6
shingle int | None

Passed to :func:lexograph.layout.recurrence.recurrence_distancesNone for word-token Jaccard, or k for character k-gram Jaccard. Ignored when distances is given.

None
distances FloatArray | None

A precomputed (N, N) distance matrix to use instead of the built-in Jaccard (e.g. an embedding distance from the [graph] extra). Must match the sentence count.

None
mode RecurrenceMode

"binary" draws the thresholded dotplot; "distance" draws the full similarity heatmap.

'binary'
figsize tuple[float, float]

Figure size in inches.

(8.0, 8.0)
background str

Figure background colour.

'white'

Returns:

Name Type Description
A Figure

class:matplotlib.figure.Figure with one axes. Never calls show().

Raises:

Type Description
ValueError

If the text has fewer than two sentences, or a supplied distances matrix does not match the sentence count.

Contract
  • Returns a Figure with exactly one axes.
  • The grid is square (sentence count on each axis) and symmetric.

Examples:

>>> from lexograph import load_demo_text
>>> fig = recurrence_plot(load_demo_text())
>>> type(fig).__name__
'Figure'
Source code in lexograph/presets/recurrence.py
def recurrence_plot(
    text: str,
    *,
    threshold: float = 0.6,
    shingle: int | None = None,
    distances: FloatArray | None = None,
    mode: RecurrenceMode = "binary",
    figsize: tuple[float, float] = (8.0, 8.0),
    background: str = "white",
) -> Figure:
    """Draw the sentence × sentence recurrence dotplot of a text.

    Args:
        text: The source text.
        threshold: In ``"binary"`` mode, cells with distance ``<= threshold`` are
            lit.
        shingle: Passed to :func:`lexograph.layout.recurrence.recurrence_distances`
            — ``None`` for word-token Jaccard, or ``k`` for character ``k``-gram
            Jaccard. Ignored when ``distances`` is given.
        distances: A precomputed ``(N, N)`` distance matrix to use instead of the
            built-in Jaccard (e.g. an embedding distance from the ``[graph]``
            extra). Must match the sentence count.
        mode: ``"binary"`` draws the thresholded dotplot; ``"distance"`` draws the
            full similarity heatmap.
        figsize: Figure size in inches.
        background: Figure background colour.

    Returns:
        A :class:`matplotlib.figure.Figure` with one axes. Never calls ``show()``.

    Raises:
        ValueError: If the text has fewer than two sentences, or a supplied
            ``distances`` matrix does not match the sentence count.

    Contract:
        - Returns a Figure with exactly one axes.
        - The grid is square (sentence count on each axis) and symmetric.

    Examples:
        >>> from lexograph import load_demo_text
        >>> fig = recurrence_plot(load_demo_text())
        >>> type(fig).__name__
        'Figure'
    """
    units = split_sentences(text)
    n = len(units)
    if n < 2:
        msg = f"need at least two sentences for a recurrence plot, got {n}"
        raise ValueError(msg)

    if distances is not None:
        dist = np.asarray(distances, dtype=float)
        if dist.shape != (n, n):
            msg = f"distances must have shape ({n}, {n}), got {dist.shape}"
            raise ValueError(msg)
    else:
        dist = recurrence_distances(units, shingle=shingle)

    fig = Figure(figsize=figsize, facecolor=background)
    ax = fig.subplots()
    ax.set_facecolor(background)

    if mode == "distance":
        # Low distance (similar) renders dark.
        ax.imshow(dist, cmap="Greys_r", origin="upper", interpolation="nearest")
    else:
        grid = recurrence_matrix(dist, threshold=threshold)
        ax.imshow(grid, cmap="Greys", origin="upper", interpolation="nearest")

    ax.set_xlabel("sentence index")
    ax.set_ylabel("sentence index")
    ax.set_title("Recurrence dotplot")
    fig.tight_layout()
    return fig

lexograph.layout.recurrence.recurrence_distances(units, *, shingle=None)

Return the pairwise Jaccard distance matrix between units.

Parameters:

Name Type Description Default
units Sequence[str]

The sentences (or any strings) to compare against each other.

required
shingle int | None

If None, compare lowercased word-token sets. If a positive integer k, compare sets of character k-grams instead (robust to short or morphologically varied sentences).

None

Returns:

Type Description
FloatArray

An (N, N) symmetric float array of Jaccard distances in [0, 1]

FloatArray

with a zero diagonal. Two units with no features in common are at

FloatArray

distance 1; two empty units are at distance 0.

Contract
  • The matrix is symmetric with a zero diagonal.
  • Every entry lies in [0, 1].

Examples:

>>> d = recurrence_distances(["the cat sat", "the cat sat", "a dog ran"])
>>> float(d[0, 1])
0.0
>>> bool(d[0, 2] > 0.9)
True
Source code in lexograph/layout/recurrence.py
def recurrence_distances(
    units: Sequence[str],
    *,
    shingle: int | None = None,
) -> FloatArray:
    """Return the pairwise Jaccard distance matrix between units.

    Args:
        units: The sentences (or any strings) to compare against each other.
        shingle: If ``None``, compare lowercased word-token sets. If a positive
            integer ``k``, compare sets of character ``k``-grams instead (robust
            to short or morphologically varied sentences).

    Returns:
        An ``(N, N)`` symmetric float array of Jaccard distances in ``[0, 1]``
        with a zero diagonal. Two units with no features in common are at
        distance 1; two empty units are at distance 0.

    Contract:
        - The matrix is symmetric with a zero diagonal.
        - Every entry lies in ``[0, 1]``.

    Examples:
        >>> d = recurrence_distances(["the cat sat", "the cat sat", "a dog ran"])
        >>> float(d[0, 1])
        0.0
        >>> bool(d[0, 2] > 0.9)
        True
    """
    n = len(units)
    features = _features(units, shingle)
    dist = np.zeros((n, n), dtype=float)
    for i in range(n):
        fi = features[i]
        for j in range(i + 1, n):
            fj = features[j]
            union = len(fi | fj)
            similarity = (len(fi & fj) / union) if union else 1.0
            dist[i, j] = dist[j, i] = 1.0 - similarity
    return dist

lexograph.layout.recurrence.recurrence_matrix(distances, *, threshold=0.6)

Threshold a distance matrix into a boolean recurrence (dotplot) grid.

Parameters:

Name Type Description Default
distances FloatArray

An (N, N) distance matrix (from :func:recurrence_distances, or an embedding distance matrix from the [graph] extra).

required
threshold float

Cells with distance <= threshold are lit (recurrent).

0.6

Returns:

Type Description
ndarray

An (N, N) boolean array; True marks a recurrent (similar) cell.

ndarray

The diagonal is always True (every sentence echoes itself).

Raises:

Type Description
ValueError

If distances is not a square 2-D matrix.

Examples:

>>> import numpy as np
>>> d = np.array([[0.0, 0.2, 0.9], [0.2, 0.0, 0.8], [0.9, 0.8, 0.0]])
>>> recurrence_matrix(d, threshold=0.5).tolist()
[[True, True, False], [True, True, False], [False, False, True]]
Source code in lexograph/layout/recurrence.py
def recurrence_matrix(
    distances: FloatArray,
    *,
    threshold: float = 0.6,
) -> np.ndarray:
    """Threshold a distance matrix into a boolean recurrence (dotplot) grid.

    Args:
        distances: An ``(N, N)`` distance matrix (from
            :func:`recurrence_distances`, or an embedding distance matrix from
            the ``[graph]`` extra).
        threshold: Cells with distance ``<= threshold`` are lit (recurrent).

    Returns:
        An ``(N, N)`` boolean array; ``True`` marks a recurrent (similar) cell.
        The diagonal is always ``True`` (every sentence echoes itself).

    Raises:
        ValueError: If ``distances`` is not a square 2-D matrix.

    Examples:
        >>> import numpy as np
        >>> d = np.array([[0.0, 0.2, 0.9], [0.2, 0.0, 0.8], [0.9, 0.8, 0.0]])
        >>> recurrence_matrix(d, threshold=0.5).tolist()
        [[True, True, False], [True, True, False], [False, False, True]]
    """
    array = np.asarray(distances, dtype=float)
    if array.ndim != 2 or array.shape[0] != array.shape[1]:
        msg = f"distances must be a square (N, N) matrix, got {array.shape}"
        raise ValueError(msg)
    return array <= threshold