Mine recurrent surface text patterns in a corpus (built on generalized suffix trees)
Project description
patternminer: Mining Recurrent Text Patterns in a Corpus
patternminer makes it easy to mine recurrent surface text patterns in a corpus of texts: which token subsequences recur, where, how often, and how they nest inside each other.
It is built on pygstlib (generalized suffix trees — mining is linear-time) and generalizes the pattern-mining approach of dialign / pydialign beyond dyadic dialogue.
Features:
- cross-document mining: patterns present in ≥ k documents, with every
occurrence located as
(doc_id, unit_id, start)and both absolute (doc_freq,unit_freq) and relative (doc_support,unit_support) frequencies; - within-document mining: patterns a document repeats across its own units (sentences, lines, utterances…);
- hierarchy: regroup subpatterns under their parent patterns (e.g.
parent
hello world !groupshelloandworld !); - filtering: by size, frequency, arbitrary predicates, or down to the maximal patterns;
- text in, patterns out: built-in basic tokenizers and unit splitters — or bring your own tokens;
- pandas layer (optional): patterns and occurrences as DataFrames.
Installation
uv add patternminer # or: pip install patternminer
uv add 'patternminer[analysis]' # with the pandas layer
Requires Python ≥ 3.10. The only required runtime dependency is pygstlib.
Quickstart
A corpus is a list of documents; each document is a list of units (the granularity at which repetition is observed — by default, one unit per line); each unit is a tuple of string tokens.
from patternminer import Corpus, mine_corpus
corpus = Corpus.from_texts([
"hello world !\nhow are you today ?",
"hello world ! how are you doing ?",
])
patterns = mine_corpus(corpus) # patterns shared by >= 2 documents
for p in patterns:
print(f"{p.surface!r}: doc_freq={p.doc_freq}, occurrences={list(p.occurrences)}")
print()
print(patterns.hierarchy().render())
Output:
'hello world !': doc_freq=2, occurrences=[(0, 0, 0), (1, 0, 0)]
'how are you': doc_freq=2, occurrences=[(0, 1, 0), (1, 0, 3)]
'?': doc_freq=2, occurrences=[(0, 1, 4), (1, 0, 7)]
hello world ! [doc_freq=2, unit_freq=2]
how are you [doc_freq=2, unit_freq=2]
? [doc_freq=2, unit_freq=2]
Subpatterns that never occur independently (are you, world !, you,
! — always enclosed in hello world ! / how are you) are discarded;
they would appear if they occurred free somewhere in the corpus.
Tour of the API
from patternminer import Corpus, mine_corpus, mine_document, mine_within, nlp
# Corpus building — raw text with basic helpers…
corpus = Corpus.from_texts(texts, tokenizer=nlp.word_punct_tokenizer,
unit_splitter=nlp.split_sentences, lowercase=True)
corpus = Corpus.from_files(paths)
# …or pre-tokenized input:
corpus = Corpus.from_token_lists(list_of_token_lists) # 1 unit per document
corpus = Corpus.from_token_units(docs_units_tokens) # nested
# Mining — threshold by absolute freq or relative support (or both)
patterns = mine_corpus(corpus, min_doc_freq=2, is_valid=nlp.has_alphabetic)
patterns = mine_corpus(corpus, min_doc_support=0.5) # >= half the documents
repetitions = mine_document(corpus[0]) # within one document
per_doc = mine_within(corpus, min_unit_support=0.1) # doc_id -> PatternSet
# Pattern: frequencies, supports, free/constrained classification
p = patterns.get("world !")
p.doc_freq; p.unit_freq # absolute counts (documents / units)
p.doc_support # doc_freq / total documents in the corpus
p.unit_support # unit_freq / total units in the corpus
p.occurrences # every occurrence, (doc_id, unit_id, start)
p.free_occurrences # the subset not enclosed in a larger pattern
p.constrained_occurrences # the enclosed complement
# PatternSet
patterns.filter(min_size=2, min_unit_freq=3, min_doc_support=0.5, ...)
patterns.maximal() # patterns contained in no other
patterns.get("world !") # lookup by surface or token tuple; None if the
# key is absent (e.g. a pattern that never
# occurs free — see caveats below)
patterns.to_dataframe() # pandas ([analysis] extra)
# Hierarchy (containment DAG)
hier = patterns.hierarchy()
hier.roots; hier.children(p); hier.parents(p); hier.descendants(p)
print(hier.render())
➡ Tutorials: the quickstart notebook and a real corpus study on three chapters of David Copperfield.
Semantics & caveats
- Free patterns only. The inventory holds the right-maximal repeats
that occur free at least once; a pattern whose every occurrence is
enclosed in a free occurrence of a larger pattern is discarded (e.g.
are youis dropped when it only ever appears insidehow are you). Kept patterns still report all their occurrences, enclosed instances included;Pattern.free_occurrences/constrained_occurrencesexpose the per-occurrence split. Details in docs/architecture.md. - Repetition is counted across units. A pattern occurring twice inside
a single unit (and nowhere else) is not detected. Choose the unit
granularity accordingly (
nlp.split_sentences,nlp.split_lines, …). - Surface-level. patternminer sees exactly the tokens you give it —
normalize (case, tokenization) upstream, or use the
lowercase=Trueand tokenizer options. The built-in tokenizers are deliberately simple. - Determinism. Results come in a canonical order (size desc, then tokens) and are identical across processes.
- Concurrency. Like pygstlib, mining structures are not thread-safe;
the returned
Pattern/PatternSet/PatternHierarchyobjects are immutable and safe to share once built.
Development
uv venv && uv pip install -e '.[dev]' --group notebook
uv run pytest # unit, property, randomized naive-reference and notebook tests
uv run ruff check .
The project is managed BMAD-style: see docs/prd.md,
docs/architecture.md and the story backlog in
docs/stories/.
Contributors
- Guillaume Dubuisson Duplessis (2026)
Usage for Research Purposes
If you use this library for research purposes, please make reference to it by citing the following paper on recurrent surface text patterns:
- Dubuisson Duplessis, G.; Charras, F.; Letard, V.; Ligozat, A.-L.; Rosset, S., Utterance Retrieval based on Recurrent Surface Text Patterns, 39th European Conference on Information Retrieval (ECIR), 2017, pp. 199--211 [More DOI]
See also dialign and pydialign for the dialogue-specific measures built on the same mining approach.
License
MIT — see the LICENSE.txt file.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file patternminer-0.1.0.tar.gz.
File metadata
- Download URL: patternminer-0.1.0.tar.gz
- Upload date:
- Size: 31.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c10dde957de7ebe11b98a72ecfd3bab259d27e4d9eaa46c66349dd9bb57e5198
|
|
| MD5 |
cf7ab91dad3f421daec64b1762ef9410
|
|
| BLAKE2b-256 |
0acec92f7fc90f6fe2a57bd1421ffcf74889873f545061d7184e75ba129371f9
|
Provenance
The following attestation bundles were made for patternminer-0.1.0.tar.gz:
Publisher:
release.yml on GuillaumeDD/patternminer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
patternminer-0.1.0.tar.gz -
Subject digest:
c10dde957de7ebe11b98a72ecfd3bab259d27e4d9eaa46c66349dd9bb57e5198 - Sigstore transparency entry: 2148814532
- Sigstore integration time:
-
Permalink:
GuillaumeDD/patternminer@393717d20a090e26ce467d2984f6af5779adf1b7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GuillaumeDD
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@393717d20a090e26ce467d2984f6af5779adf1b7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file patternminer-0.1.0-py3-none-any.whl.
File metadata
- Download URL: patternminer-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3112906cccea6fb84116e98e69918099ebb43e7c7b33f5a7b43faa859bc9fe73
|
|
| MD5 |
81af88f45d045d6fa80faa71267345aa
|
|
| BLAKE2b-256 |
6dd5ebee50001a6692f31d9a908b02ed68ce7f65fbc0e595207693df843b42b8
|
Provenance
The following attestation bundles were made for patternminer-0.1.0-py3-none-any.whl:
Publisher:
release.yml on GuillaumeDD/patternminer
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
patternminer-0.1.0-py3-none-any.whl -
Subject digest:
3112906cccea6fb84116e98e69918099ebb43e7c7b33f5a7b43faa859bc9fe73 - Sigstore transparency entry: 2148814595
- Sigstore integration time:
-
Permalink:
GuillaumeDD/patternminer@393717d20a090e26ce467d2984f6af5779adf1b7 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/GuillaumeDD
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@393717d20a090e26ce467d2984f6af5779adf1b7 -
Trigger Event:
push
-
Statement type: