aegean.analysis¶
analysis ¶
Script-agnostic + Aegean-specific analysis over the core model.
Ported faithfully from the Linear A Research Workbench (src/lib/*.ts) and
checked against shared golden fixtures so the Python port can't silently
diverge. Methods over the undeciphered Linear A material are exploratory —
see each function's docstring.
BalanceCheck
dataclass
¶
BalanceCheck(stated_total: float, computed_sum: float, item_count: int, difference: float, balances: bool, marker: str, total_line_index: int)
One total line reconciled against the item lines feeding it: the stated total, the computed
sum, their signed difference (computed − stated), whether they balance, the total marker
(e.g. KU-RO), and the index of the total line.
AlignCell
dataclass
¶
One aligned position: a char from a (or "" for ins), a char from
b (or "" for del), and the operation that relates them.
ExclusivityRow
dataclass
¶
One (ideogram group, co-occurring word) pair: the in-group co-occurrence
count, the word's word_total across all groups, and exclusivity =
count / word_total (1.0 = the word is seen only with this group).
PhoneticComparison
dataclass
¶
PhoneticComparison(word_a: str, word_b: str, script_a: str, script_b: str, phonemes_a: str, phonemes_b: str, distance: float, similarity: float, alignment: tuple[AlignCell, ...])
One cross-script comparison: the two words, their script ids, their
romanized phoneme strings, the normalized distance (0 = identical, 1 = wholly
different), its similarity complement, and the per-segment alignment.
PhoneticClasses
dataclass
¶
Concrete vowel set + consonant-class tables for the distance metric.
PhoneticScheme
dataclass
¶
PhoneticScheme(interdentals: str = 'dental', pharyngeal_h: str = 'velar', voiced_postalveolar: str = 'sibilant', strip_notation: bool = True)
The four typologically ambiguous decisions, exposed for tuning.
PhoneticWeights
dataclass
¶
Tunable substitution / indel costs, kept in [0,1] so the normalized distance stays in [0,1].
EdgeBiasRow
dataclass
¶
One affix's edge-bias result: its edge-slot count, its interior-window count, and the signed G².
PositionalRow
dataclass
¶
PositionalRow(word: str, count: int, initial: int, medial: int, final: int, dominant: str, g2: float)
A word's positional profile: token count, its slot tallies
(initial / medial / final), its dominant slot, and the signed
positional-bias G².
Productivity
dataclass
¶
One affix's productivity: count (token sum of bearing words),
distinct word types, hapax types (attested once), and Baayen's
productivity P = hapax / count.
SuccessorRow
dataclass
¶
A prefix's successor variety: variety distinct following signs, the
parent_variety of the prefix one sign shorter, and ratio =
variety / mean branching at this sign-depth.
SuccessorVariety
dataclass
¶
SuccessorVariety(rows: list[SuccessorRow], total: int)
Successor-variety rows (passing the thresholds, strongest ratio first)
and total = how many passed.
ClusterMember
dataclass
¶
A word in a cluster, with the signs it appends beyond the cluster stem
("" for the stem itself; "≠" flags a member that doesn't actually
extend the stem).
MorphCluster
dataclass
¶
MorphCluster(stem: str, members: tuple[ClusterMember, ...], total_count: int, suffixes: tuple[str, ...])
A stem and its productive-suffix derivations.
CAPoint
dataclass
¶
A row or column in the CA plane: x/y principal coordinates and
mass (its marginal share of the table, which drives point size).
CAResult
dataclass
¶
CAResult(rows: list[CAPoint], cols: list[CAPoint], inertia: tuple[float, float], total_inertia: float)
Correspondence-analysis biplot: rows and cols in a shared plane,
the share of total inertia on axes 1 and 2, and total_inertia.
DendroMerge
dataclass
¶
One merge: the two child cluster ids, the height (cosine distance) it
joined at, the sorted member labels it creates, and bootstrap
support (0–1; 1 for the trivially-present root).
DendroResult
dataclass
¶
DendroResult(labels: list[str], merges: list[DendroMerge], order: list[str])
A dendrogram: the leaf labels, the merges (leaves are ids
0..n-1; merge k creates id n+k), and the left→right leaf order.
CompiledSignPattern
dataclass
¶
A parsed sign-pattern query: the normalized sign tokens (with * = one sign and ** =
zero-or-more wildcards) and whether the pattern contains a **.
FieldDef
dataclass
¶
A queryable field: its display label, scope (inscription/word), and value kind.
FilterRow
dataclass
¶
One query row. connector joins this row to the running result within
its scope (ignored on the first row); negate flips the row's own test.
QueryResults
dataclass
¶
QueryResults(inscriptions: list[Document], words: list[tuple[str, int]], provenance: Provenance | None = None, description: str = '')
A query's result set: the matching inscriptions and/or (word, count) pairs.
In words, count is the word's document frequency: the number of
distinct inscriptions the word occurs in (e.g. ("KU-RO", 34) means KU-RO
appears in 34 separate documents), not how many times it is written. A word
repeated within one inscription still counts that inscription once. This
differs from Corpus.word_frequencies, whose count is the token
frequency (every occurrence). The list is sorted by descending count, then
by word.
Corpus.query attaches the corpus's provenance and a description
of the filters, so cite can cite the exact result set used in a paper.
cite ¶
Cite this exact result set: the source plus the query that produced it.
style: "plain" (one line), "bibtex" (a @misc entry), or
"apa". Raises ValueError when the results carry no provenance
(results from eval_query directly rather than Corpus.query).
to_corpus ¶
Rebuild a reusable Corpus from this result set's matched inscriptions.
Carries source's sign inventory and script id, and stamps a subset: provenance
note so cite on the saved corpus names the query. A words-only result has no
inscriptions, so it yields an empty corpus — query with output="inscriptions".
WordEntry
dataclass
¶
Per-word index entry: the documents a (multi-sign) word appears in.
BootstrapCI
dataclass
¶
A percentile bootstrap interval: the statistic on the full corpus
(estimate) and the [low, high] band holding level of the
resampled values.
Chao1Result
dataclass
¶
Chao1 richness estimate: estimate total types, unseen =
estimate − S_obs, and a log-normal 95% CI [ci_low, ci_high]. It is a
lower bound — it only sees the rare-type tail.
Dispersion
dataclass
¶
How evenly one item spreads over the documents of a corpus.
dp is Gries' deviation of proportions: 0 = the item is distributed
exactly as the document sizes predict; values toward 1 = concentrated in
few documents. dp_norm rescales by the attainable maximum
(Lijffijt & Gries 2012) so corpora with different size profiles compare.
range is the count of documents attesting the item (out of parts
documents that have any items at all).
HeapsFit
dataclass
¶
Heaps' law fit V(N) = k·N^β: beta < 1 = sublinear vocabulary
growth (normal for language). r2 is in log–log space.
KeynessRow
dataclass
¶
KeynessRow(item: str, target_count: int, target_total: int, reference_count: int, reference_total: int, log_likelihood: float, log_ratio: float, p_value: float)
One item's keyness in a target (sub)corpus against a reference.
log_likelihood is Dunning's G² (significance: is the imbalance more
than chance?); p_value its χ²₁ tail. log_ratio is Hardie's log₂
ratio of relative frequencies (effect size: how big is the difference?)
— positive = overused in the target, negative = underused; each whole
point is a doubling. Zero counts are smoothed (default +0.5) for the
ratio only, never for G².
ZipfMandelbrotFit
dataclass
¶
Truncated Zipf–Mandelbrot MLE fit p(r) ∝ (r+β)^(−s) over the observed
ranks. ks is the one-sample Kolmogorov–Smirnov statistic over token-share
CDFs (smaller = closer; no p-value — the parameters are estimated from
the same data). r2_log is log-space R²; log_z the log normalizer.
DivergenceRow
dataclass
¶
DivergenceRow(value: str, labels: list[str], la_count: int, lb_count: int, la_per_1000: float, lb_per_1000: float, log_ratio: float)
One shared phonetic value: its Linear A / Linear B counts and per-mille
rates, and log_ratio = log₂ of the add-half-smoothed rate ratio
(positive = over-used in Linear A relative to Linear B).
LaValueCount
dataclass
¶
A Linear A phonetic value's token count and the AB sign labels behind it.
LaValueCounts
dataclass
¶
LaValueCounts(by_value: dict[str, LaValueCount], total_signs: int)
Linear A sign counts keyed by phonetic value, plus the total_signs
denominator (every sign of every lexical word, valued or not).
LbFrequencies
dataclass
¶
LbFrequencies(version: str, generated: str, cite: str, counts: dict[str, int], total_signs: int, word_tokens: int, doc_count: int)
Linear B syllabogram frequencies from a DAMOS payload: counts maps a
lowercase syllabogram value to its token count inside multi-sign words;
total_signs / word_tokens / doc_count are the denominators and
coverage, and version / generated / cite echo the dataset meta.
CommodityMetrology
dataclass
¶
CommodityMetrology(head: str, gloss: str, entries: int, fractional_pct: float, denominators: str, median: float, max: float)
A commodity's metrology over its counted lines: entries lines, the
fractional_pct that carry a fraction, the denominators seen
(space-joined), and the median / max line quantity.
Dossier
dataclass
¶
Dossier(word: str, entries: list[DossierEntry], entry_count: int, tablet_count: int, total_value: float, commodities: dict[str, int], sites: dict[str, int], co_listed: dict[str, int])
An account-head candidate and its evidence: the heading word, its
entries (counted lines), distinct tablet_count, summed
total_value (mixed units — a coarse proxy), and the commodities /
sites / co_listed heads it travels with (insertion-ordered counts).
DossierEntry
dataclass
¶
One ledger line an account head appears on: the source ins_id and
site, the full line_tokens, the summed value after the head, and
the line's commodity (or None).
DocumentTypeProfile
dataclass
¶
DocumentTypeProfile(type: str, count: int, share_pct: float, words_per_doc: float, numerals_pct: float, top_sites: list[str])
One physical-support type: document count, corpus share_pct, mean
multi-sign words per document (words_per_doc), the share of documents
carrying at least one numeral (numerals_pct — a proxy for accounting
function, NOT a token fraction), and the top_sites (up to 2).
FractionRow
dataclass
¶
FractionRow(value: float, display: str, count: int, commodities: dict[str, int], example_ids: list[str])
One attested metrological fraction: its value, a display form
(e.g. 3/4), corpus count, the commodities it co-occurs with on a
line, and up to 12 example_ids.
MetrologyProfile
dataclass
¶
MetrologyProfile(fraction_rows: list[FractionRow], commodity_profiles: list[CommodityMetrology], numeral_tokens: int, fraction_tokens: int, integer_tokens: int, distinct_fraction_values: int)
The corpus metrology: the fraction_rows census, per-commodity
commodity_profiles (commodities with ≥3 counted lines), and the
numeral / fraction / integer token totals.
HandProfile
dataclass
¶
HandProfile(hand: str, doc_count: int, token_count: int, word_count: int, sites: dict[str, int] = dict(), periods: dict[str, int] = dict(), top_words: list[tuple[str, int]] = list())
A summary of one scribal hand's output within a corpus.
StructureCategory
dataclass
¶
A heuristic tablet-structure category (e.g. accounting/libation/list): key, label, description.
SignBigramModel
dataclass
¶
SignBigramModel(bigram: dict[str, dict[str, int]], context_total: dict[str, int], cont_types: dict[str, int], next_count: dict[str, int], total: int, vocab: int)
A trained first-order sign-bigram model.
bigram maps a context to (next → token count); contexts include ^ and
nexts include $. context_total is the outgoing tokens per context,
cont_types the distinct continuations per context (Witten-Bell's T(a)),
next_count the next-symbol token counts (the backoff distribution),
total the transition-token total, and vocab the number of distinct
next symbols.
SurprisalStep
dataclass
¶
One transition's surprisal in bits, showing where a word is improbable.
WordSurprisal
dataclass
¶
WordSurprisal(mean: float, steps: list[SurprisalStep])
A scored word: mean bits per transition (boundaries included — the
headline number) and the per-transition steps.
MonteCarloResult
dataclass
¶
MonteCarloResult(observed: float, p_value: float, null_mean: float, null_low: float, null_high: float, level: float, n: int, null: str)
A Monte-Carlo permutation test of one structure statistic against a null.
observed is the statistic on the real word list. p_value is the
one-sided (upper-tail) permutation p-value (1 + #{null ≥ observed}) / (1 +
n) — the add-one form (Davison & Hinkley 1997, North et al. 2002), so it is
never exactly 0 and is valid for finite n. null_mean and the
[null_low, null_high] percentile band summarize the null distribution at
level confidence; n is the number of null replicates and null
names the generator. A small p_value means the observed value sits in the
upper tail of what the null produces (more "structured" than chance under
that null); a p_value near 0.5 means it is unremarkable.
McNemarResult
dataclass
¶
McNemar's paired test on two systems' binary correctness.
b = items system A got right and B wrong; c = B right, A wrong;
these discordant counts are all the test uses (n = b + c). method
is "exact" (two-sided binomial, used for small n) or
"chi2" (continuity-corrected chi-square, 1 dof). statistic is the
chi-square value for "chi2" and min(b, c) for "exact";
p_value is two-sided. A small p_value means the systems differ on
the items where they disagree.
PairedBootstrapResult
dataclass
¶
PairedBootstrapResult(mean_difference: float, low: float, high: float, level: float, n_resamples: int, frac_a: float, frac_b: float)
A paired percentile bootstrap on per-item score differences A − B.
mean_difference is the observed mean of scores_a[i] − scores_b[i]
(positive = A scores higher on average). [low, high] is the level
percentile interval over the resampled mean differences: a band entirely
above 0 favours A, entirely below favours B, straddling 0 is inconclusive.
frac_a / frac_b are the fractions of resamples whose mean difference
favoured A / B (resamples with a mean difference of exactly 0 count toward
neither, so the two need not sum to 1).
SignEmbeddings
dataclass
¶
SignEmbeddings(vocab: tuple[str, ...], vectors: tuple[tuple[float, ...], ...], dim: int, window: int)
Dense distributional vectors for the signs of a script (EXPLORATORY).
vocab is the row order of vectors (one dim-vector per sign, already
SVD-reduced and L2-normalized). window records the neighbour radius used.
The vectors capture distributional similarity only; see the module docstring
on why, on undeciphered or tiny corpora, neighbour lists are leads, not truth.
vector ¶
The (L2-normalized) embedding of sign. Raises KeyError if unseen.
neighbours ¶
The k nearest signs to sign by cosine similarity, closest first.
sign itself is excluded. Vectors are unit-normalized, so cosine is a dot
product; a returned score near 1 means near-identical context profiles, near
0 means unrelated. On a tiny corpus treat only the very top of the list as
meaningful (EXPLORATORY: distributional, not phonetic, similarity).
HandGroup
dataclass
¶
HandGroup(hand: str, doc_count: int, doc_ids: list[str] = list(), sites: dict[str, int] = dict(), series: dict[str, int] = dict(), periods: dict[str, int] = dict())
One hand attribution's tablets, broken down by find-site and archival series.
hand is one distinct editorial attribution string (a hand number, possibly
qualified). sites / series / periods are value -> tablet count
maps (most-common first); doc_ids lists the tablets in corpus order. The
series breakdown is populated only for a Linear B corpus (the designation
convention it parses); a tablet whose series does not parse is counted in
doc_count but not in series.
HandReport
dataclass
¶
HandReport(hand: str, doc_count: int, token_count: int, word_count: int, doc_ids: list[str] = list(), sites: dict[str, int] = dict(), series: dict[str, int] = dict(), periods: dict[str, int] = dict(), top_words: list[tuple[str, int]] = list())
One hand attribution's full descriptive profile.
hand is one editorial attribution string (a hand number, possibly
qualified). Tablet / token / lexical-word totals, its tablets (doc_ids),
its sites / series / periods breakdowns (value -> count,
most-common first), and its top_words (the attribution's most frequent
lexical words, (word, count)) computed with the standard corpus frequency
machinery over the attribution's slice. The series breakdown is populated
only for a Linear B corpus.
SeriesDossier
dataclass
¶
SeriesDossier(site: str, series: str, doc_count: int, doc_ids: list[str] = list(), hands: dict[str, int] = dict(), periods: dict[str, int] = dict(), token_count: int = 0, word_count: int = 0)
One archival grouping: the tablets sharing a find-site and a parsed series prefix.
A common Mycenological working unit, e.g. the Knossos Da sheep-tablets or the
Pylos Aa personnel tablets. The grouping follows the tablet-designation
convention: a well-established prefix names a recognised set, while a residual or
unconventional prefix (a single-capital X fragment class, say) is grouped as
parsed and is not asserted to be an attested archival set. hands / periods
are value -> tablet count maps (most-common first) over the grouping's
tablets; doc_ids lists them in corpus order; token_count / word_count
sum the writing they carry.
Segmentation
dataclass
¶
One word's Harris segmentation.
word is the input string; units its unit sequence (signs for a
hyphen-joined word, characters otherwise); cuts the inferred boundary
offsets into units (each c means a boundary before units[c], so
cuts are in 1 .. len(units)-1); and pieces the resulting unit-group
morphs, each joined the same way the word was (- for signs, else
concatenated). A single-unit or empty word has no cuts and one piece.
ClusterReport
dataclass
¶
ClusterReport(n_classes: int, n_signs: int, corpus_signs: int, corpus_bigrams: int, mutual_information: float, perplexity: float, mi_loss: float)
Quality of an induced sign-classing, for honest reporting.
n_classes is how many classes remain; n_signs the signs that were
clustered. corpus_signs is the number of sign tokens the bigram model saw
and corpus_bigrams the number of distinct adjacent pairs: both small on
Aegean material, which is why the classes overfit (read them as leads).
mutual_information is the average MI (bits) of the class-bigram model that
the merges maximised, perplexity its 2^H branching factor over the
class-bigram distribution. mi_loss is the total MI given up to reach this
classing from one-sign-per-class (0 when no merge happened).
SignClasses
dataclass
¶
SignClasses(_class_of: dict[str, int], _members: list[list[str]], report: ClusterReport)
An induced distributional classing of a script's signs (EXPLORATORY).
class_of maps a sign label to its integer class id (-1 for a sign the
corpus never attested). classes lists the members of each class. The
report carries the quality numbers and the corpus size they rest on.
The classes reflect distribution, not phonetic value: two signs share a class because they occur in the same contexts, which is a candidate grammatical or phonotactic role, never a reading.
Correspondence
dataclass
¶
One ranked cross-script sign-correspondence hypothesis (EXPLORATORY).
source_sign (in the source script) is aligned nearest to target_sign (in the
target script) with cosine score in the rotated space, at 1-based rank among
all target signs for this source sign. A high score means similar distributional
position, not a reading or a decipherment; corroborate before use.
IdentityCheck
dataclass
¶
How well aligning a space to itself recovers the identity (calibration).
top1_recovery / top5_recovery are the fractions of the evaluated signs whose
own sign is the rank-1 / within-rank-5 correspondence after learning the rotation from
an anchor subset. anchor_fraction records the split; n is the number of signs
evaluated. With anchor_fraction = 1.0 (all signs anchored) recovery is near-perfect
and is the sanity floor: on a clean, well-separated space it is 1.0, and on a real
corpus it falls short of 1.0 only where distinct signs share an identical context
vector (distributional twins that resolve to a tied sibling), for example 0.901 on the
bundled Linear A. A lower fraction measures how well a partial seed generalizes. A
floor down near chance would instead signal broken machinery, not a real null.
ProcrustesAlignment
dataclass
¶
ProcrustesAlignment(source_script: str, target_script: str, dim: int, n_anchors: int, anchor_fit: float, rotation: tuple[tuple[float, ...], ...], source_vocab: tuple[str, ...], source_vectors: tuple[tuple[float, ...], ...], target_vocab: tuple[str, ...], target_vectors: tuple[tuple[float, ...], ...])
A learned source→target embedding alignment and its hypothesis generator (EXPLORATORY).
rotation is the orthogonal dim × dim matrix mapping the (truncated,
re-normalized) source space onto the target space; anchor_fit is the mean cosine
of the anchor pairs after rotation (1 = perfect fit on the seeds). Use
:meth:correspondences for one source sign or :meth:hypotheses for a ranked global
list. These are geometry statistics offered as leads for a specialist, never readings.
correspondences ¶
correspondences(source_sign: str, k: int = 5) -> list[Correspondence]
The k best target-sign hypotheses for one source sign, strongest first.
Ranks every target sign by cosine similarity to the rotated source vector.
Raises KeyError if source_sign is not in the source vocabulary
(EXPLORATORY: distributional geometry, not a reading).
hypotheses ¶
hypotheses(*, k: int = 1, top: int | None = None) -> list[Correspondence]
A ranked global list of correspondence hypotheses across all source signs.
Takes each source sign's top k target matches and returns them sorted by score
(strongest first). top truncates the returned list. Every entry is exploratory
and the score is an alignment statistic, not a probability of correspondence.
RankReport
dataclass
¶
RankReport(n: int, n_targets: int, ranks: tuple[int, ...], top1: float, top5: float, median_rank: float, mean_rank: float, leave_one_out: bool)
Where known correspondence pairs land in the ranked hypotheses (calibration).
ranks is the 1-based rank of each known pair's true target sign among all
n_targets target signs (lower is better; rank 1 = recovered). top1 / top5
are the fractions at rank ≤ 1 / ≤ 5; median_rank / mean_rank summarize the
distribution. leave_one_out records whether each pair was held out of the anchors
when it was scored (the honest generalization measure). Whatever this shows, weak or
strong, is the honest strength of the cross-script signal (EXPLORATORY).
chance_median
property
¶
The median rank pure chance would give ((n_targets + 1) / 2), for comparison.
account_lines ¶
account_lines(document: Document) -> list[list[str]]
The document's physical lines as token-text lists.
balance_check ¶
balance_check(document: Document) -> list[BalanceCheck]
Verify every total line on a document against its summed item lines.
Uses the script's total markers (Linear A's KU-RO/KU-RA and PO-TO-KU-RO, Linear B's TO-SO/TO-SA/TO-SO-DE).
checkable_accounts ¶
checkable_accounts(corpus: Any, *, tolerance: float = 0.1) -> list[Document]
The intact, balancing accounts of a corpus (see :func:is_checkable_account).
is_checkable_account ¶
is_checkable_account(document: Document, *, tolerance: float = 0.1) -> bool
Whether a document is an intact, balancing account — a clean drill / teaching candidate, and a useful "trust the arithmetic" corpus filter.
Intact = every token is securely read (ReadingStatus.CERTAIN, no bracketed
restoration in the text), so no lacuna or damage muddies the sum. Balancing =
at least one stated total (e.g. KU-RO) sits within max(1, tolerance ×
stated) of its summed items — the workbench's Scribe-School cutoff, lenient
by default (10%) because Aegean metrology is imperfectly understood.
add_sequence ¶
Add one word sequence to a growing alignment via Needleman–Wunsch at the
word level (exact-token match rewarded, substitution columns allowed, gaps
penalized). prior_n is how many sequences are already in the alignment.
align_phonetic ¶
align_phonetic(a: str, b: str, w: PhoneticWeights = DEFAULT_WEIGHTS, cl: PhoneticClasses = DEFAULT_PHONETIC_CLASSES) -> list[AlignCell]
Run the weighted Levenshtein, then backtrace to emit a per-position alignment classifying each substitution as vowel / same-class / far.
align_sequences ¶
Progressive multiple alignment of word sequences (e.g. several inscriptions). Returns aligned positions, one column per input sequence.
chi_squared_2x2 ¶
Yates-corrected chi-squared test statistic for the 2×2 table.
The continuity correction subtracts N/2 from |ad − bc| and clamps the
corrected deviation at 0 (so near-independent pairs score ~0, not a small
spurious positive). Returns 0 for degenerate tables.
chi_squared_p_value ¶
p-value for chi-squared with 1 degree of freedom: P(X² ≥ x). In [0,1],
and non-increasing in x.
fishers_exact ¶
Fisher's exact test, two-sided, for the 2×2 table: the summed hypergeometric probability of all tables with the same marginals whose probability is ≤ the observed table's. More accurate than χ² for small expected counts but O(N) per pair. Returns 1 for a degenerate margin or an impossible table (one whose implied cell counts are negative).
log_likelihood_ratio_2x2 ¶
Log-likelihood ratio (G²) for the 2×2 table — Dunning (1993), the
corpus-linguistics standard. G² = 2 · Σ O·ln(O/E) over the four cells;
more robust than χ² for the sparse, low-count pairs of a small corpus.
Returns 0 for degenerate tables; larger = stronger association.
pmi_interval ¶
Propagate a Wilson interval on the joint probability into a pointwise mutual information confidence interval (log₂ space), holding the marginals fixed. A zero lower joint clamps PMI low to a finite floor (−20).
sign_bigram_pmi ¶
Pointwise mutual information (bits) of an adjacent ordered sign pair a→b.
joint is the directed adjacency count of a→b; left_total the total
outgoing adjacencies from a (a as the left/previous sign); right_total
the total incoming adjacencies to b (b as the right/next sign);
grand_total all adjacency-pair tokens. Returns
log₂(joint·grand / (left·right)) — positive = the pair occurs more often
than the two signs' slot frequencies predict, negative = less. Returns
None (PMI undefined) when any input is zero, e.g. a never-attested pair.
Directed: PMI(a→b) ≠ PMI(b→a) in general. Unsmoothed, so rare pairs read high.
sign_bigram_pmis ¶
Directed sign-bigram PMI (bits) for every adjacent sign pair attested in a multi-sign word vocabulary.
words is an iterable of (word, count) pairs (hyphen-joined signs, a
token frequency); adjacencies are token-weighted and subscript sign labels
are folded (RA₂ → RA2). No boundary markers — interior adjacencies
only. Returns {(a, b): pmi} over attested pairs (a never-attested pair is
simply absent, its PMI being undefined).
wilson_interval ¶
Wilson score interval for a binomial proportion p̂ = k/n. Stays inside
[0,1] with good coverage even at small/extreme p̂. z = 1.96 ≈ 95%.
ideogram_group_exclusivity ¶
ideogram_group_exclusivity(lines: Iterable[Sequence[str]]) -> list[ExclusivityRow]
How exclusively each hyphenated word co-occurs (per line) with one ideogram group.
For every line, each ideogram occurrence is paired with each hyphenated word
on the same line (multiplicity kept, not deduplicated). exclusivity of a
word in a group is its in-group co-occurrence count divided by its total
ideogram co-occurrences across all groups — so values for a fixed word sum to
1 across groups. A high exclusivity with a count ≥ 2 is the workbench's
"strong domain evidence" signal. This is a descriptive share, not an
association test (no smoothing, no expected-value model).
line_cooccurrence_pmi ¶
line_cooccurrence_pmi(lines: Iterable[Sequence[str]], head: str, *, min_joint: int = 2) -> list[tuple[str, float]]
Line-level PMI of each transaction term against a commodity head,
strongest first.
The event space is the set of physical lines: a term and the commodity are
each counted once per line they appear on (deduplicated). For a term t with
joint lines holding both, commodity_lines holding the commodity, and
term_lines holding the term, out of total_lines lines::
PMI(t) = log₂( joint · total_lines / (commodity_lines · term_lines) )
A term is any token containing a hyphen; a commodity is any token that
resolves to head via the catalog (ligatures / sex markers folded) or, for
*NNN, the raw logogram. Only terms sharing at least min_joint lines
with the commodity are returned (a hard cutoff, not smoothing); PMI may be
negative. head may be a catalog head ("GRA") or an undeciphered
"*NNN".
nearest ¶
nearest(word: str, script: str, candidates: Iterable[str], candidate_script: str, *, top: int = 5, weights: PhoneticWeights = DEFAULT_WEIGHTS, classes: PhoneticClasses = DEFAULT_PHONETIC_CLASSES, fold_aspiration: bool = False) -> list[tuple[str, float]]
Rank candidates (in candidate_script) by phonetic distance to
word (in script), nearest first; returns (candidate, distance)
for the top closest (top=0 = all).
The intended use is decipherment-adjacent triage — e.g. which alphabetic Greek words sound closest to a Linear B form — where the ordering is the result and the absolute distances are secondary (see the module caution). Candidates that cannot be romanized are skipped.
phonetic_compare ¶
phonetic_compare(word_a: str, script_a: str, word_b: str, script_b: str, *, weights: PhoneticWeights = DEFAULT_WEIGHTS, classes: PhoneticClasses = DEFAULT_PHONETIC_CLASSES, fold_aspiration: bool = False, overrides_a: dict[str, str] | None = None, overrides_b: dict[str, str] | None = None) -> PhoneticComparison
Compare two words across scripts by sound: romanize each, then run the weighted phonetic distance and the per-segment alignment.
The classic bridge is phonetic_compare("po-me", "linearb", "ποιμήν",
"greek") — Linear B po-me against Greek poimēn 'shepherd'. Tune the
metric with weights/classes (see distance) and meet defective
syllabic spelling with fold_aspiration.
romanize_greek ¶
Romanize alphabetic Greek to the Latin phoneme alphabet.
Strips accents, breathings, iota subscript, and diaeresis (NFD, then drop
combining marks), lowercases, and maps each letter: θ→th, φ→ph, χ→kh,
ξ→ks, ψ→ps, η→ē, ω→ō, and γ→n before a velar (γγ/γκ/γχ/γξ). Rough breathing
(the /h/) is dropped with the other diacritics — the syllabaries don't write
it either. fold_aspiration further maps θ/φ/χ → t/p/k for a fairer match
against aspiration-blind syllabic spelling. Non-Greek letters pass through.
to_phonemes ¶
to_phonemes(word: str, script: str, *, fold_aspiration: bool = False, overrides: dict[str, str] | None = None) -> str
Reduce word (in script) to a Latin phoneme string.
greek romanizes alphabetic text; lineara / linearb / cypriot
map a hyphenated transliteration through their sign→sound tables (overrides
tests alternative sign values). Raises ValueError for an unsupported
script (e.g. undeciphered Cypro-Minoan).
build_phonetic_classes ¶
build_phonetic_classes(scheme: PhoneticScheme = DEFAULT_PHONETIC_SCHEME) -> PhoneticClasses
Assemble concrete class tables from a scheme.
describe_phonetic_scheme ¶
describe_phonetic_scheme(s: PhoneticScheme) -> str
One-line scheme description, for stamping into saved findings/reports so a match ranking stays reproducible.
extract_root ¶
The consonant skeleton of a word's phonetic form (vowels stripped),
e.g. KU-RO → kr. Exploratory root-cognate heuristic.
is_numeral_token ¶
True for digit / superscript / subscript / approx numeral tokens.
phonetic_distance ¶
phonetic_distance(a: str, b: str, w: PhoneticWeights = DEFAULT_WEIGHTS, cl: PhoneticClasses = DEFAULT_PHONETIC_CLASSES) -> float
Weighted Levenshtein over phonetic strings, normalized to [0,1] by the
longer length. Vowel↔vowel swaps cost 0.3, same-class consonants 0.5,
everything else 1 (see PhoneticWeights).
reference_key ¶
Bare comparison key for a reference word: drop hyphens (so syllables
concatenate like the Linear A side) and lowercase. With strip_notation,
also remove pure-notation marks (reconstruction *, PIE laryngeal
subscripts ₁₂₃, the labialization/aspiration modifiers ʰ ʷ, and the
combining syllabic ring U+0325). So PIE *ǵʰésr̥ → ǵésr.
sequence_distance ¶
Standard Levenshtein over arbitrary token sequences — compares whole inscriptions as ordered bags of words.
sequence_similarity ¶
Sequence distance normalized to a 0–1 similarity (1 = identical).
affix_edge_bias ¶
affix_edge_bias(words: Iterable[tuple[str, int]], *, affix_len: int = 1, mode: str = 'suffix') -> list[EdgeBiasRow]
Edge-bias G² for every length-affix_len affix over a multi-sign word
vocabulary, strongest (most edge-biased) first.
words is an iterable of (word, count) pairs — hyphen-joined signs and
a token frequency; everything is token-weighted. mode is "suffix"
(final slot) or "prefix" (initial slot). Words with no remaining stem
(parts <= affix_len signs) are skipped. Interior occurrences are window
matches of the affix that don't fall in the edge slot.
baayen_productivity ¶
baayen_productivity(words: Iterable[tuple[str, int]], *, affix_len: int = 1, mode: str = 'suffix') -> list[Productivity]
Baayen's category-conditioned productivity P for every length-affix_len
affix, most productive first.
P = (number of hapax-legomena types bearing the affix) / (total token
frequency of all words bearing it). words is an iterable of
(word, count) pairs; words with no remaining stem are skipped.
Caveat. On a corpus that is overwhelmingly hapax (most Linear A vocabulary) P runs high for nearly every affix, because almost every bearing type is itself a hapax. Read P as a relative ranking among affixes, never as an absolute productivity figure.
edge_bias_g2 ¶
Signed Dunning G² of an affix's edge-slot rate vs its interior-window rate.
edge_count of edge_total edge slots hold the affix; interior_count
of interior_total interior windows do. Positive = over-represented at the
word edge (affix-like); negative = interior-leaning; an exact rate tie is
positive. Returns 0 for an empty edge or interior population.
positional_bias ¶
positional_bias(inscriptions: Iterable[Sequence[str]], *, min_count: int = 2) -> list[PositionalRow]
Positional-bias G² for every word attested min_count times, over the
word-position events of a corpus.
inscriptions is an iterable of per-inscription word lists (any tokens
without a hyphen are dropped, as in the workbench). Within an inscription a
word's position is initial / medial / final among its hyphenated words; a
lone hyphenated word counts in both edge slots (a deliberate workbench
quirk that inflates the edge baselines). Baselines are summed over the whole
vocabulary (hapaxes included); only words with count >= min_count are
scored. Strongest positive bias first.
positional_bias_g2 ¶
Signed Dunning G² of a word's share in its dominant slot vs the rest of the corpus's share in that slot.
in_pos of the word's slots positional events fall in its dominant
slot; slot_total of grand_total corpus-wide events fall in that same
slot. Positive = the word concentrates in its dominant slot more than the
rest of the corpus does (a tie is positive); negative = it leans there least
of all despite it being its commonest slot. Returns 0 when degenerate.
successor_variety ¶
successor_variety(words: Iterable[str], *, min_prefix_signs: int = 2, min_variety: int = 3) -> SuccessorVariety
Harris (1955) successor variety over a multi-sign word vocabulary, scored as a ratio against the per-depth mean branching factor.
words is an iterable of distinct word types (token frequency is
ignored — this is type-based). For each prefix of k signs, variety is
the number of distinct signs that immediately follow it across the
vocabulary; ratio divides that by the mean variety of all prefixes at
depth k. Only prefixes of at least min_prefix_signs signs with variety
at least min_variety are reported. A high ratio marks a candidate
morpheme boundary — a heuristic, not a significance test.
find_morphological_clusters ¶
find_morphological_clusters(words: Iterable[Mapping[str, object] | tuple[str, int]], min_suffix_productivity: int = 5, min_cluster_size: int = 2, max_suffix_len: int = 2) -> list[MorphCluster]
Cluster stems with their productive-suffix derivations.
words is an iterable of {"word": str, "count": int} mappings or
(word, count) pairs (e.g. straight from Corpus.word_frequencies).
A suffix is productive when it ends at least min_suffix_productivity
distinct words; clusters smaller than min_cluster_size are dropped;
suffixes are considered up to max_suffix_len signs long.
correspondence_analysis ¶
correspondence_analysis(row_labels: Sequence[str], col_labels: Sequence[str], counts: Sequence[Sequence[float]]) -> CAResult | None
Correspondence analysis of a rows × columns contingency table.
Keeps the top two axes. Rows and columns land in the same plane: a row sits
in the direction of the columns it over-uses; distance from the origin is
deviation from the average profile. Returns None when there are fewer
than 3 rows or columns, any zero-margin row/column (filter those first), or
the table is independent (no inertia to plot).
label_propagation ¶
label_propagation(nodes: Sequence[str], edges: Iterable[tuple[str, str, float]], *, seed: int = 7, max_iters: int = 50) -> dict[str, int]
Weighted label-propagation community detection (Raghavan et al. 2007).
edges is an iterable of (a, b, weight) tuples (undirected;
self-loops and edges to unknown nodes are ignored). Deterministic via the
seeded visit-order shuffle and label < current tie-break. Returns
{node: community_id} with communities renumbered by descending size.
Good for coloring a few hundred nodes — not a modularity method for large
graphs.
upgma_with_bootstrap ¶
upgma_with_bootstrap(items: Sequence[tuple[str, Mapping[str, float]]], *, iters: int = 100, seed: int = 42) -> DendroResult | None
Average-linkage UPGMA over labeled count vectors (cosine distance) with feature-resampled bootstrap support.
items is a sequence of (label, counts) pairs. Support for each node
is the share of bootstrap replicates (features resampled with replacement,
the standard move when features carry the signal) whose tree contains exactly
the same member set. Returns None below 3 items or 2 features. Read
support below ~0.5 as weak.
compile_sign_pattern ¶
compile_sign_pattern(raw: str) -> CompiledSignPattern | None
Parse a wildcard sign pattern (KU-*-RO) into a CompiledSignPattern, or None if empty.
match_sign_pattern ¶
match_sign_pattern(signs: list[str], pattern: CompiledSignPattern) -> bool
Match a word's sign sequence against a compiled pattern.
normalize_sign_label ¶
Fold subscript digits to ASCII (RA₂ → RA2).
word_matches_sign_pattern ¶
Compile and match in one call. False for single-sign words / empty patterns.
build_cooccurrence_map ¶
build_cooccurrence_map(documents: Iterable[Document]) -> dict[str, set[str]]
Map each multi-sign word to the set of multi-sign words it shares a document with.
build_word_index ¶
Index every word token to the documents it appears in.
A word is any TokenKind.WORD token: a hyphen-joined multi-sign Aegean word
(KU-RO), a single-sign Aegean word, or an alphabetic-Greek word (λόγος). Numerals,
logograms, and punctuation are not words and are skipped.
eval_query ¶
eval_query(filters: list[FilterRow], output: Output, documents: list[Document], word_index: dict[str, WordEntry], annotated_ids: set[str], cooccur_map: dict[str, set[str]]) -> QueryResults
Run a query (filters + output mode) over pre-built indices and return the result set in canonical shape.
For output="words" each (word, count) pair's count is the word's
document frequency (the number of matching inscriptions it occurs in), not
its token frequency; a word repeated within one inscription counts that
inscription once.
inscription_matches ¶
True if a document satisfies the inscription-scope filter rows (AND/OR/NOT-combined).
run_query ¶
run_query(corpus: Any, filters: list[FilterRow], output: Output = 'inscriptions', annotated_ids: set[str] | None = None) -> QueryResults
Build the indices from a Corpus and evaluate filters.
Convenience over eval_query for the common whole-corpus case. The result
carries the corpus's provenance and a filter summary, so it is citable
via QueryResults.cite.
Raises TypeError if filters is not a sequence of FilterRow (e.g. a
bare "field=value" string), and ValueError naming FIELDS if a row's
field is not a known query field. This is the one shared guard behind
Corpus.query; the CLI aegean query and MCP query_corpus wrappers
validate the same way before reaching here.
summarize_filters ¶
summarize_filters(filters: list[FilterRow]) -> str
One-line, human-readable label for a filter set.
word_matches ¶
word_matches(word: str, filters: Iterable[FilterRow], cooccur_map: dict[str, set[str]]) -> bool
True if a word satisfies the word-scope filter rows (AND/OR/NOT-combined).
bootstrap_ci ¶
bootstrap_ci(corpus: Any, statistic: Callable[[Sequence[Document]], float], *, n_resamples: int = 999, level: float = 0.95, seed: int = 0) -> BootstrapCI
Percentile bootstrap CI for statistic(documents).
Documents are the resampling unit (drawn with replacement, original size),
the right grain for corpus questions where tokens within a document are
not independent (Efron & Tibshirani 1993). The seed makes the interval
reproducible by default — vary it to see Monte-Carlo wobble. The band
quantifies sampling variability given these documents; it cannot speak
to what was never excavated.
mean_doc_words = lambda docs: sum( ... len([t for t in d.tokens if t.kind is TokenKind.WORD]) for d in docs ... ) / len(docs) bootstrap_ci(corpus, mean_doc_words) # doctest: +SKIP BootstrapCI(estimate=7.1, low=6.4, high=7.9, level=0.95, n_resamples=999)
bootstrap_counts_ci ¶
bootstrap_counts_ci(counts: Sequence[float], stat: Callable[[list[int]], float], *, iters: int = 200, seed: int = 1, alpha: float = 0.05) -> tuple[float, float]
Percentile bootstrap CI for any stat of a count vector.
Resamples N tokens from the empirical distribution (multinomial with
p̂ᵢ = cᵢ/N) and hands the resampled count vector — same length, same
category order — to stat; returns the [α/2, 1−α/2] percentile
interval. Deterministic via seed (the same mulberry32 stream as the
workbench). The resample treats tokens as independent draws, which corpus
tokens are not (words repeat whole), so the interval is a lower bound on
the real uncertainty.
chao1 ¶
chao1(s_obs: float, f1: float, f2: float) -> Chao1Result
Chao1 lower-bound vocabulary size from observed types and the hapax / dis legomena counts.
Ŝ = S_obs + F₁²/(2F₂) when F₂ > 0; the bias-corrected
Ŝ = S_obs + F₁(F₁−1)/2 when F₂ = 0. Uses Chao's (1987) variance and
the standard log-normal CI on the unseen-type count.
dispersion ¶
dispersion(corpus: Any, item: str, *, kind: str = 'words') -> Dispersion
Gries' DP for one item over the documents of corpus.
DP = ½ · Σᵢ |vᵢ − sᵢ| where sᵢ is document i's share of the
corpus (in items of this kind) and vᵢ the share of the item's
occurrences falling in document i (Gries 2008). dp_norm divides by
the attainable maximum 1 − min(sᵢ) (Lijffijt & Gries 2012). Raises
ValueError if the item never occurs.
dispersions ¶
dispersions(corpus: Any, *, kind: str = 'words', min_frequency: int = 2, top: int = 0) -> list[Dispersion]
DP for every item with frequency ≥ min_frequency, most evenly
dispersed first (ties: higher frequency first). top truncates (0 = all).
Reading the ranking: a frequent item with low dp_norm is corpus-wide
vocabulary; a frequent item with high dp_norm lives in few documents
(a formulaic or genre/site-bound term) — on Aegean material often the more
interesting case.
fit_heaps ¶
fit_heaps(points: Sequence[tuple[float, float]]) -> HeapsFit | None
Fit Heaps' law V(N) = k·N^β by least squares in log–log space over a
vocabulary-growth curve of (tokens, types) points. Needs at least five
points with tokens ≥ 1 and types ≥ 1; returns None otherwise.
fit_zipf_mandelbrot_mle ¶
fit_zipf_mandelbrot_mle(freqs: Sequence[float]) -> ZipfMandelbrotFit | None
Fit a truncated Zipf–Mandelbrot rank–frequency model by maximizing the
multinomial log-likelihood over a coarse (s, β) grid refined twice
around the optimum. freqs are rank-ordered frequencies (rank 1 first);
returns None for fewer than five ranks.
keyness ¶
keyness(target: Any, reference: Any, *, kind: str = 'words', min_target: int = 2, smoothing: float = 0.5) -> list[KeynessRow]
Key items of target against reference, strongest first.
For each item the 2×2 table is (count in target, rest of target, count in
reference, rest of reference); G² follows Rayson & Garside (2000) and the
log-ratio Hardie (2014). Items need target_count ≥ min_target or to
be similarly frequent in the reference (so marked under-use surfaces
too). Sorted by G² descending — filter log_ratio > 0 for the target's
own vocabulary, < 0 for what it conspicuously lacks.
The two corpora must be distinct texts (a subset vs its complement is the
classic design: keyness(c.filter(site="Pylos"), rest)).
mattr ¶
MATTR — moving-average type-token ratio (Covington & McFall 2010).
The mean TTR over every sliding window of window tokens. Unlike raw TTR
it does not shrink mechanically as the stream grows, so differently sized
slices compare. Returns None when the stream is shorter than one
window.
miller_madow_entropy ¶
Miller–Madow bias-corrected entropy in bits.
The plug-in estimator systematically underestimates entropy in small
samples (unseen categories contribute nothing); the first-order correction
adds (K − 1) / (2·N·ln 2) bits, K = observed categories,
N = sample size. Still an underestimate when many categories are unseen
— the honest situation for sign bigrams in a few-thousand-token corpus.
mulberry32 ¶
A tiny, fast, seeded 32-bit PRNG (mulberry32).
Returns a zero-argument callable yielding floats in [0, 1). Every
resample / permutation here runs from an explicit seed so a cited number is
reproducible on re-run; the 32-bit arithmetic reproduces the workbench's
JavaScript implementation bit-for-bit, so both tools agree given one seed.
shannon_entropy ¶
Shannon entropy in bits of a count vector (maximum-likelihood plug-in).
Zero counts contribute nothing; returns 0 for an empty or single-category vector.
spearman_rho ¶
Spearman rank correlation, with average ranks for ties.
One number for "do two paired series rank their items the same way?" — e.g. do two scripts use a shared signary in the same proportions? Returns 0 for fewer than 3 points, mismatched lengths, or a constant series.
build_lb_divergence ¶
build_lb_divergence(la: LaValueCounts, lb: LbFrequencies) -> list[DivergenceRow]
Join the two sign-value frequency tables on their shared attested values, most divergent (largest |log ratio|) first.
Rates are smoothed add-half before the log₂ ratio. Only values attested in both scripts are returned. Empty if either side has no signs. The join itself assumes the conventional sign values — that is the hypothesis, not a result.
linear_a_sign_value_counts ¶
linear_a_sign_value_counts(words: Sequence[tuple[str, int]]) -> LaValueCounts
Token-weighted Linear A sign counts over the lexical multi-sign vocabulary, keyed by each sign's conventional phonetic value (lowercase — the Linear B join key).
words is an iterable of (word, count) pairs; non-lexical words (see
:func:is_lexical_word) are skipped. Signs without a conventional value
still count toward total_signs so the two corpora share a denominator
definition.
parse_damos_frequencies ¶
parse_damos_frequencies(payload: dict[str, object]) -> LbFrequencies
Count Linear B syllabogram frequencies in a damos-corpus.json payload
({_meta, documents: [{content}]}).
Counting basis (mirrored on the Linear A side): signs inside multi-sign word tokens only. Logograms, numerals, single-sign words, and pieces that don't parse as syllabogram chains are skipped. Damaged-sign dots and editorial brackets are stripped rather than excluded — DAMOS marks uncertainty densely, and dropping every dotted sign would bias against worn tablets.
account_dossiers ¶
account_dossiers(corpus: Any) -> list[Dossier]
Gather, per account-head candidate, the counted ledger lines it heads.
A line's head is its first lexical (syllabic) word; the script's total /
grand-total / deficit markers (KU-RO, PO-TO-KU-RO, KI-RO; Linear B's
to-so / to-sa / to-so-de, o-pe-ro; matched case-insensitively) are
excluded — they are accounting operators, not holders. A line counts only if a numeral follows
the head; its value sums the tokens after the head and its commodity is
the first commodity logogram after it. Sorted by entry count desc, head asc.
"Account holder" is a working hypothesis — a head may be a person, place, institution, or transaction term; this just assembles the evidence.
document_type_profile ¶
document_type_profile(corpus: Any) -> list[DocumentTypeProfile]
Profile a corpus by physical document type, most common type first.
words_per_doc counts multi-sign (hyphenated) word tokens, the writing
an object carries. numerals_pct is the percentage of documents with at
least one numeral (the separator dot is never a numeral).
metrology_profile ¶
metrology_profile(corpus: Any, *, min_entries: int = 3) -> MetrologyProfile
Build the corpus metrology profile (fraction census + per-commodity counted-vs-measured), iterating physical lines.
Each line is credited to its first commodity logogram; the line's numerals
sum to one entry for that commodity. A commodity needs min_entries (3)
counted lines to get a profile. Linear A metrology is contested, so read the
denominators and fractional shares as exploratory description.
hand_keyness ¶
hand_keyness(corpus: Any, hand: str, *, kind: str = 'words', min_target: int = 2) -> list[KeynessRow]
Words (or signs) characteristic of one scribal hand versus all other hands.
Splits corpus into the hand's documents (target) and every other document
(reference) and runs the standard log-likelihood keyness. Raises ValueError if no
document is attributed to hand.
scribal_hands ¶
scribal_hands(corpus: Any, *, top_n: int = 8, min_docs: int = 1) -> list[HandProfile]
Profile every scribal hand in corpus (documents grouped by meta.scribe).
Returns one HandProfile per hand with at least min_docs tablets — tablet count,
token and lexical-word totals, the sites and chronologies the hand is attested at, and
the hand's top_n most frequent words — sorted by tablet count (then hand id).
Documents with no recorded hand are skipped.
classify_corpus ¶
Classify every document in a corpus, returning {category_key:
[doc_id, ...]} with every category present (empty lists included) and
documents in corpus order.
classify_structure ¶
classify_structure(document: Document) -> str
The heuristic category key for one inscription, from its content shape.
Mirrors the workbench precedence exactly: a KU-RO total marker (or numerals with several multi-sign words) ⇒ accounting; otherwise a libation formula ⇒ libation; otherwise many separators and no numerals ⇒ list; otherwise an extended hyphenated text with no numerals ⇒ text; else other.
train_sign_bigram_model ¶
train_sign_bigram_model(words: Iterable[tuple[str, int]]) -> SignBigramModel
Train the bigram model on a multi-sign vocabulary, token-weighted.
words is an iterable of (word, count) pairs; single-sign words (no
-) are skipped. A transition in a 20× word counts 20 times — the model
describes what scribes actually wrote, not the type list.
word_surprisal ¶
word_surprisal(model: SignBigramModel, word: str, self_count: int = 0) -> WordSurprisal
Score word against the model.
self_count is the word's own corpus token count: its contribution is
subtracted from every count before computing probabilities (leave-one-out),
per occurrence — a word like a-b-a-b carries a→b twice, so its two
self-occurrences are both removed there. Pass 0 to score a hypothetical word
not in the corpus. Probabilities use add-one-smoothed Witten-Bell backoff, so
unseen symbols keep nonzero mass and bits stay finite and non-negative.
length_reshuffle_null ¶
One length-stratified whole-word reshuffle replicate of words.
Groups the words by sign-length and shuffles each length stratum among its
own members, then reassembles in the original length-slot order.
Preserves every word intact (its exact internal sign sequence) and the
per-length word counts; destroys the position of a word within its
length stratum. rand is a zero-argument [0, 1) source.
monte_carlo_p ¶
monte_carlo_p(observed: float, statistic: Statistic, words: Sequence[str], *, null: str = 'within_word', n: int = 999, seed: int = 0, level: float = 0.95) -> MonteCarloResult
One-sided permutation p-value of a structure statistic against a null.
Generates n null word lists with the chosen null generator (each
seeded from mulberry32(seed)), scores statistic on each, and compares
the observed value to that null distribution.
The p-value is the add-one upper-tail estimate (1 + #{null ≥ observed}) /
(1 + n): it is one-sided, treats larger statistic values as more
"structured", and is bounded away from 0 so a finite n never reports an
impossibly exact result. Pass observed = statistic(words) for the usual
case; it is a separate argument only so a precomputed or differently
measured observed value can be tested against the same null.
null is "within_word" (within-word sign permutation: holds word
lengths and the unigram-sign counts, breaks sign order) or "reshuffle"
(length-stratified whole-word reshuffle: holds each word intact, breaks word
position). See the module docstring for exactly what each preserves.
Exploratory. A small p-value means the statistic exceeds what this null
produces; it is evidence of structure relative to the null, not a
decipherment or a linguistic claim. Always report the null name, seed,
and n with the number.
within_word_null ¶
One within-word sign-permutation null replicate of words.
Pools every sign across the whole corpus, shuffles the pool, and redeals it
into words of the original sign-lengths (in order). Preserves the
per-word length sequence and the corpus-wide unigram-sign multiset (each
sign's total count); destroys sign order and within-word co-occurrence.
rand is a zero-argument [0, 1) source (e.g. from
:func:aegean.analysis.stats.mulberry32).
mcnemar ¶
mcnemar(correct_a: Sequence[bool], correct_b: Sequence[bool], *, exact_threshold: int = 25) -> McNemarResult
McNemar's test for whether two systems' per-item correctness differs.
correct_a / correct_b are equal-length, gold-aligned booleans (item
i right or wrong for each system). Only discordant items contribute:
b = A-right/B-wrong, c = A-wrong/B-right. When b + c ≤
exact_threshold the exact two-sided binomial is used (reliable for the
small discordant counts typical of a single fold); otherwise the
continuity-corrected chi-square (|b−c|−1)² / (b+c) with its 1-dof
tail. No discordant items (b + c = 0) is an undefined test and returns
p_value = 1.0. Raises ValueError on a length mismatch.
r = mcnemar([True, True, False], [True, False, False]) r.b, r.c, r.method (1, 0, 'exact')
paired_bootstrap ¶
paired_bootstrap(scores_a: Sequence[float], scores_b: Sequence[float], *, n_resamples: int = 999, level: float = 0.95, seed: int = 0) -> PairedBootstrapResult
Paired bootstrap CI on the mean per-item score difference A − B.
scores_a / scores_b are equal-length, gold-aligned per-item scores
(0/1 accuracy, a per-sentence metric, any number). The per-item differences
are resampled with replacement n_resamples times; the returned interval
is the level percentile band on the mean difference, with the fraction
of resamples favouring each system. Reproducible by seed (the shared
mulberry32 stream); vary it to gauge Monte-Carlo wobble. The band
quantifies sampling variability given these items only. Raises
ValueError on a length mismatch, fewer than two items, or a level
outside (0, 1).
r = paired_bootstrap([1, 1, 1, 1], [0, 0, 0, 0], seed=1) r.mean_difference, r.low > 0 (1.0, True)
sign_embeddings ¶
sign_embeddings(corpus: Any, *, dim: int = 50, window: int = 1) -> SignEmbeddings
Learn a distributional vector per sign from a script corpus (EXPLORATORY).
Builds a (sign × context) co-occurrence table from sign adjacency within words
(left/right neighbours up to window positions, plus word-initial and
word-final slot columns), reweights it with PPMI, and reduces each row to at most
dim dimensions with a deterministic truncated SVD; the returned vectors are
L2-normalized so :meth:SignEmbeddings.neighbours reads as cosine similarity.
Parameters¶
corpus:
A Corpus, QueryResults, or iterable of Document. Only multi-sign
WORD tokens contribute (single-sign words have no internal adjacency).
dim:
The maximum embedding dimensionality. The effective dimension is capped at
the context-vocabulary size and at the number of non-degenerate singular
values, so small corpora yield shorter vectors.
window:
Neighbour radius in signs (1 = immediate neighbours only).
Returns a :class:SignEmbeddings. Raises ValueError if the corpus has no
multi-sign words or dim/window is not positive.
Caveat (EXPLORATORY). The vectors encode distributional context, not phonetic or semantic value; on undeciphered scripts they are a structure-surfacing aid, not a decipherment, and on the small Aegean corpora the geometry is noisy. Trust only the strongest neighbours and corroborate before reading anything into them.
by_hand ¶
by_hand(corpus: Any, *, min_docs: int = 1) -> list[HandGroup]
Group a corpus's documents by hand attribution, with a site / series breakdown.
Returns one HandGroup per distinct attribution string (meta.scribe)
attested on at least min_docs documents, sorted by tablet count desc, then
attribution string. A group key is one attribution string (a hand number,
possibly qualified with a certainty mark or sub-hand tag), so the number of
groups counts distinct attribution strings, not necessarily distinct scribes.
Documents with no recorded hand are skipped.
The series breakdown is parsed from each document's designation (see
:func:series_of) and is filled only for a Linear B corpus, where that
convention applies; on other scripts it is left empty. The attribution is the
edition's, passed through unchanged; this just counts the tablets, sites, and
series each recorded attribution carries.
dossiers ¶
dossiers(corpus: Any, *, min_docs: int = 1) -> list[SeriesDossier]
Group Linear B documents into archival dossiers by shared find-site and series.
A dossier here is a (site, series) grouping: the tablets found at one site
and sharing one series prefix (see :func:series_of). This is the conservative
reading of the metadata that exists, the find-site (meta.site) and the series
parsed from the designation. It does not attempt joins, hand-based sets, or
physical-fit reconstructions the recorded fields cannot support.
The series parse is a Linear B designation convention, so this raises
ValueError on a non-Linear-B corpus (corpus.script_id != "linearb")
rather than read a spurious prefix out of an unrelated id scheme. The grouping
follows the designation convention: a residual or unconventional prefix is
grouped as parsed, not asserted to be an attested archival set.
Returns one SeriesDossier per grouping with at least min_docs documents,
sorted by tablet count desc, then site, then series. Documents whose series
does not parse are left out (they belong to no series).
hand_profile ¶
hand_profile(corpus: Any, hand: str, *, top_n: int = 15) -> HandReport
Profile one hand attribution hand: its tablets, sites, series, and vocabulary.
hand is an editorial attribution string (a hand number, possibly qualified).
Builds the sub-corpus of the tablets carrying that attribution and reports its
counts, its site / series / chronology breakdowns, and its top_n most
frequent lexical words (via Corpus.word_frequencies, the standard machinery).
The series breakdown is filled only for a Linear B corpus. Raises ValueError
if no document carries the attribution.
The vocabulary is descriptive (the words this attribution happened to write
most), not a claim about a scribe's remit; for what is distinctive of one
attribution versus the others use :func:aegean.analysis.hand_keyness.
series_of ¶
The archival series of a Linear B tablet, parsed from its designation, or None.
Defined for Linear B tablet designations (the Bennett/Olivier sigla). Accepts a
Document (uses doc.id) or a plain id string. The series is the alphabetic
run of the second whitespace-delimited field, the classification prefix after
the site code: series_of("KN Fp(1) 1 (138)") == "Fp",
series_of("PY Ta 641") == "Ta". A designation with no such field (only a
site code and a number, e.g. "SID 1 (-)") returns None.
This is a pure parser: it reads whatever second field an id carries and does not
check the script, so a caller that passes an unrelated id scheme gets that
scheme's second field back. It classifies nothing anew, and a residual or
unconventional prefix is returned as parsed, not asserted to be an attested
series. For the script-guarded grouping use :func:dossiers.
brainerd_robinson ¶
The Brainerd-Robinson similarity matrix of an abundance table.
Each row of matrix (an assemblage's type counts) is first rescaled to sum to
100, then the similarity of two rows p and q is
BR = 200 - Σ_k |p_k - q_k|: 200 for identical proportional profiles, 0
for no shared types. The result is a symmetric n × n matrix with 200 on the
diagonal.
Raises ValueError on an empty or ragged matrix or negative counts. This is a
proportional-abundance similarity; on undeciphered material the "types" are signs
or word forms, so read it as compositional similarity, not chronology (EXPLORATORY).
chronology ¶
Parse every document's meta.period into a numeric year span (EXPLORATORY).
Reuses :func:aegean.viz.parse_period (BCE negative, CE positive) on each
document's free-text date, returning a :class:Chronology that pairs the parsed
spans with an honest count of what could not be read.
Parameters¶
corpus:
A Corpus, QueryResults, or iterable of Document.
Caveat (EXPLORATORY). parse_period is a best-effort reader of origDate-style
strings, not a dating authority; a returned span is a coarse century-level bin, and
the unparsed fraction is reported precisely because a corpus's dates are often
imprecise or unreadable. This is input for a chronological hypothesis, not a date.
seriate ¶
seriate(matrix_or_corpus: Any, *, labels: Sequence[str] | None = None, max_iter: int = 200) -> SeriationResult
Seriate an abundance table (or a corpus) by Brainerd-Robinson similarity (EXPLORATORY).
Builds the Brainerd-Robinson similarity matrix (see :func:brainerd_robinson) and
orders the rows so that compositionally similar assemblages sit next to each other,
using a deterministic spectral ordering (the Fiedler vector of the similarity's
Laplacian). The ordering does not depend on the order the rows were supplied in.
Parameters¶
matrix_or_corpus:
Either a 2-D abundance table (rows = assemblages, columns = type counts) or a
Corpus / Document iterable, in which case a document × sign-type count
matrix is built automatically (rows are the sign-bearing documents, columns the
signs that occur).
labels:
Optional row labels for a matrix input (must match the row count). Ignored for a
corpus input, where document ids are used.
max_iter:
Cap on iterations for the large-matrix power-iteration fallback (the dense
eigensolver used for ordinary tables ignores it). Must be positive.
Returns a :class:SeriationResult. Raises ValueError on an empty/ragged matrix,
a labels-length mismatch, or a corpus with no sign-bearing documents.
Caveat (EXPLORATORY). The ordering is a hypothesis about relative sequence from compositional similarity, with no direction and no calendar anchor; on undeciphered scripts a "type" is a sign, so the axis may track scribal or graphotactic drift, not time. Corroborate against external evidence before reading chronology into it.
variant_groups ¶
Group a script's signs into catalogued variant sets, from the inventory's own data.
Reads get_script(script_id).sign_inventory and groups signs sharing a base value
by the label conventions the inventory uses: numbered homophone series (Linear A /
Linear B) and Cypro-Minoan catalogue-letter suffixes. Only groups with 2+ members are
returned; ligature/compound labels are listed under composite_signs, not grouped.
Returns an :class:AllographReport. Raises KeyError for an unregistered script id.
Caveat (EXPLORATORY). These groupings come entirely from the transliteration and catalogue naming (a shared base romanization or catalogue number), which is not the same as palaeographic allography, the same sign drawn differently by different hands. That letter-form variation is not present in the bundled inventories and is not claimed. Use the report as a catalogue-structure starting point for a specialist, not as a palaeographic result.
candidate_morphs ¶
Recurring word-final candidate morphs, by the count of distinct words bearing them.
Each word is segmented (see :func:segment); a word's final piece, i.e.
the unit-group after its last cut, is taken as its candidate suffix. The count
for a morph is the number of distinct word types ending in it (so a single
word repeated cannot inflate a morph), and only morphs reaching min_count
distinct words and at least one cut (the word actually had a stem before the
morph) are returned. Sorted by count descending, then by the morph string.
EXPLORATORY: a high count means the vocabulary reuses that final string after a variety-peak boundary, a candidate productive suffix, never a confirmed morpheme or a meaning. Read the ranking as leads to inspect.
segment ¶
segment(words: Iterable[str]) -> list[Segmentation]
Harris bidirectional segmentation for every word, input order preserved.
Forward and backward unit tries are built once over the de-duplicated word
list; each word is then cut at the local peaks of its successor- and
predecessor-variety curves (the two boundary sets are unioned). Duplicate
input words yield identical :class:Segmentation records. Single-unit and
empty words return uncut, with one piece (or none).
EXPLORATORY: the cuts are distributional hypotheses. See the module docstring; on hapax-heavy undeciphered corpora the variety signal is thin.
induce_classes ¶
induce_classes(corpus: Any, *, n_classes: int) -> SignClasses
Induce n_classes distributional sign classes by Brown clustering.
EXPLORATORY. Builds the adjacent sign-bigram model of corpus (a Corpus,
QueryResults, or document list; numerals/separators/punctuation skipped, with
each token bounded so bigrams never cross a token edge), seeds one class per
sign, then greedily merges the class pair whose merge gives up the least
average mutual information of the class-bigram model, until n_classes
classes remain (Brown et al. 1992). Ties in MI loss are broken by the lower
pre-sorted class index, jittered with the shared mulberry32 PRNG, so a run
is reproducible.
The result is a :class:SignClasses: signs in one class occur in the same
contexts, which is a candidate shared role (a vowel/consonant or open/closed
split, a positional pattern), not a phonetic reading. On the small Aegean
corpora (especially undeciphered Cypro-Minoan and Linear A) most sign bigrams
are seen once or never, so the classing overfits: the attached report
carries the corpus size so the classes are read as leads, not facts.
Raises ValueError for n_classes < 1 or a corpus with no multi-sign
sequences to learn from.
align_embeddings ¶
align_embeddings(source: SignEmbeddings, target: SignEmbeddings, anchors: Sequence[tuple[str, str]], *, source_script: str = 'source', target_script: str = 'target') -> ProcrustesAlignment
Align two sign-embedding spaces by orthogonal Procrustes on anchor pairs (EXPLORATORY).
Learns the orthogonal rotation mapping source onto target from the anchors
(a seed dictionary of (source_sign, target_sign) correspondences), truncating both
spaces to their common leading dimensionality. The returned :class:ProcrustesAlignment
generates ranked correspondence hypotheses.
Raises ValueError if either space is empty or no anchor pair is present in both
vocabularies.
Caveat (EXPLORATORY). Alignment between an undeciphered and a deciphered script is
not decipherment; a score reflects distributional position in tiny corpora. Calibrate
with :func:recover_identity and :func:rank_known_pairs before trusting any lead.
align_scripts ¶
align_scripts(source_corpus: Any, target_corpus: Any, *, source_script: str = 'source', target_script: str = 'target', dim: int = 50, window: int = 1, anchors: Sequence[tuple[str, str]] | None = None) -> ProcrustesAlignment
Build sign embeddings for two corpora and align them by Procrustes (EXPLORATORY).
Convenience wrapper: learns :func:aegean.analysis.embeddings.sign_embeddings for each
corpus, then :func:align_embeddings. When anchors is None the shared-value
signs (:func:shared_label_anchors) are used as the seed dictionary.
Caveat (EXPLORATORY). See :func:align_embeddings. Cross-script alignment produces
hypotheses for a human expert, not readings.
rank_known_pairs ¶
rank_known_pairs(source: SignEmbeddings, target: SignEmbeddings, pairs: Sequence[tuple[str, str]], *, leave_one_out: bool = True) -> RankReport
Report where known correspondence pairs rank in the aligned hypotheses (calibration).
For each known (source_sign, target_sign) pair, learns the alignment and finds the
rank of target_sign in the source sign's ranked correspondence list. With
leave_one_out=True (default) the pair being scored is excluded from the anchors, so
the rank measures genuine generalization rather than memorized anchors. With
leave_one_out=False a single alignment is learned from all pairs and reused.
Only pairs whose both signs are in the two vocabularies are scored. Raises ValueError
if none qualify. Whatever rank distribution results is the honest measure of how much
the geometry recovers the known mapping, weak or strong (EXPLORATORY).
recover_identity ¶
recover_identity(emb: SignEmbeddings, *, anchor_fraction: float = 0.5, seed: int = 1) -> IdentityCheck
Align an embedding to itself from an anchor subset and measure identity recovery.
Splits the vocabulary into an anchor set (anchor_fraction) and an evaluation set
(the remainder, or the whole vocabulary when anchor_fraction = 1.0) using a seeded
deterministic shuffle, learns the source→source rotation from the anchors, and reports
how often an evaluated sign's top-ranked correspondence is itself. Aligning a space to
itself should recover almost every sign (it falls short only on distributional twins,
distinct signs with an identical context vector), so a high but sub-perfect score is
the expected calibration sanity floor; a floor near chance would instead signal broken
machinery rather than a genuine null.
Raises ValueError for an out-of-range anchor_fraction or an empty vocabulary.
shared_label_anchors ¶
shared_label_anchors(source: SignEmbeddings, target: SignEmbeddings) -> list[tuple[str, str]]
Anchor pairs where a source and target sign share a transliteration value.
Matches signs by folded label (subscripts to ASCII, upper-cased), so a source RA₂
pairs with a target RA2. Returns (source_label, target_label) pairs using each
vocabulary's own spelling, in source-label order. For Linear A vs Linear B these are
the AB chart-shared signs, a partial ground truth for calibration.