Ana içeriğe geç

Python API

Kaynaktan mkdocstrings aracılığıyla otomatik oluşturulmuştur. Aşağıda listelenen her sembol, bitig üst düzeyinde yeniden dışa aktarılır (aksi belirtilmedikçe).

Corpus

Corpus dataclass

An ordered collection of Documents that share a metadata schema.

Iteration yields Documents in the order provided. Equality and hashing ignore order — two Corpora with the same documents in different orders hash identically.

__getitem__

__getitem__(index: int | slice | Sequence[int] | ndarray) -> Document | Corpus

Index by int → Document; by slice or array-like → Corpus.

The array-like branch is needed for sklearn's cross-validation splitters, which slice X with an ndarray of fold indices.

filter

filter(**query: Any) -> Corpus

Return a new Corpus containing documents whose metadata matches every key in query.

Values may be scalars (exact match) or lists (membership).

groupby

groupby(field_name: str) -> dict[Any, Corpus]

Group documents by a metadata field value.

Raises KeyError if any document lacks the field.

metadata_column

metadata_column(field_name: str) -> list[Any]

Return the list of metadata values at field_name, in document order.

Missing values become None; use filter first if you want to exclude them.

hash

hash() -> str

Stable, order-independent hash binding each text to its id and metadata.

Scheme 2 (CORPUS_HASH_SCHEME) hashes sorted (id, text hash, metadata hash) triples. Scheme 1 hashed the text hashes and the (id, metadata) pairs as two independently sorted lists, so swapping two documents' texts left the hash unchanged (audit 2026-09-26 N-P1.16).

Document dataclass

A single text with optional metadata.

hash is derived lazily from text — identical texts hash identically regardless of id or metadata, which is intentional: the cache key for parsed documents is content-addressed.

Features

FeatureMatrix dataclass

concat

concat(other: FeatureMatrix) -> FeatureMatrix

Column-concatenate two FeatureMatrix objects with identical document_ids.

MFWExtractor

Bases: BaseFeatureExtractor

Most-Frequent-Words table.

Parameters:

Name Type Description Default
n int

Retain the top n words by corpus frequency.

1000
min_df int

Drop words appearing in fewer than min_df documents.

1
max_df float

Drop words appearing in more than max_df fraction of documents (1.0 disables).

1.0
scale ('none', 'zscore', 'l1', 'l2')

Per-feature scaling applied at transform-time. Burrows Delta requires "zscore", which z-scores the relative frequencies count / document token count — the classical Mosteller & Wallace / Burrows formulation, as in stylo. "l1" normalises rows to sum to 1 over the retained vocabulary; "l2" normalises rows to unit length.

"none"
lowercase bool

If True, case-fold before counting.

False

tokenise

tokenise(text: str) -> list[str]

Tokenise text exactly as the extractor does.

project_tokens

project_tokens(tokens: list[str]) -> np.ndarray

Z-scored relative-frequency vector for a token sequence (fitted, scale="zscore").

count / len(tokens) for each vocabulary word, standardised with the fitted column means and SDs. Used for documents and for text windows.

Methods

Delta

BurrowsDelta

Bases: _DeltaBase

Burrows Classic Delta.

Distance = mean(|x_i - c_i|) across features -- the L1 norm divided by the feature count. Assumes both X and centroid are z-scored in the same coordinate system.

Zeta

ZetaClassic

Bases: _ZetaBase

Clustering

HierarchicalCluster

scipy-based hierarchical clustering — returns both flat labels and the full linkage matrix (the latter is what the viz layer uses to render dendrograms).

Classification

bitig.methods.classify.build_classifier

build_classifier(name: str, **kwargs: Any) -> BaseEstimator

bitig.methods.classify.cross_validate_bitig

cross_validate_bitig(estimator: BaseEstimator, fm: FeatureMatrix | None, y: ndarray, *, cv_kind: str = 'stratified', groups_from: ndarray | None = None, folds: int = 5, seed: int = 42, extractor: BaseFeatureExtractor | None = None, corpus: Corpus | None = None) -> dict[str, Any]

Run cross-validation with a stylometry-aware CV strategy.

Pass extractor and corpus (and fm=None) to fit the feature extractor inside each training fold, so held-out documents never shape the vocabulary or the z-score statistics. A precomputed fm is used as-is: if it was fit on all documents, the CV estimate is optimistically biased (audit 2026-09-26 P2).

cv_kind: - "stratified": StratifiedKFold(folds) — uses seed for the shuffle - "loao": LeaveOneGroupOut (requires groups_from; deterministic). Refused when the groups merely relabel the target classes. - "leave_one_text_out": LeaveOneOut (deterministic)

Results

Result dataclass

to_json

to_json(path: str | Path) -> None

Persist params + values + provenance to a single JSON file.

ndarray values are encoded as {"__ndarray__": list, "shape": ..., "dtype": ...} so they round-trip exactly.

save

save(directory: str | Path) -> Path

Persist everything to directory/: result.json, tables as parquet, figures deferred to viz layer.

Provenance dataclass

has_forensic_metadata property

has_forensic_metadata: bool

True if any chain-of-custody field has been populated.

Forensic

GeneralImpostors, Unmasking ve CalibratedScorer, bitig üst düzeyinde yeniden dışa aktarılır; compute_pan_report ve PANReport ise bitig.forensic modülünden içe aktarılır.

GeneralImpostors

Authorship verification via the General Impostors method.

This is the document-level variant: Q is compared with the candidate's known documents and with individual impostor documents in precomputed feature matrices. :class:bitig.methods.imposters.GeneralImposters (used by bitig run and Forensic Lab Cases) is the corpus-level variant: it fits its own MFW space and compares Q with author centroids. Both sample ceil(sqrt(pool)) impostors per iteration by default and return a win fraction in [0, 1]; neither score is a likelihood ratio. A parity test (tests/forensic/test_gi_parity.py) checks they agree on clear cases.

Parameters:

Name Type Description Default
n_iterations int

Number of random feature-subspace + impostor-subsample iterations (Koppel & Winter 2014 used 100; PAN baselines typically use 50-200).

100
feature_subsample_rate float

Fraction of feature columns sampled per iteration, in (0, 1]. 0.5 is the classical default; smaller values increase variance but decorrelate impostor rankings.

0.5
impostor_sample_size int or None

Number of impostors to sample per iteration. If None, uses ceil(sqrt(n_impostors)) (a defensible default that scales sub-linearly with the pool size, so a large pool does not make every iteration trivially win).

None
similarity ('cosine', 'minmax')

Similarity function used to compare the questioned document to the candidate and to each impostor.

  • cosine: dot(u, v) / (||u|| * ||v||); works for any real-valued features.
  • minmax: sum(min(u, v)) / sum(max(u, v)); Koppel et al.'s MinMax similarity, which requires non-negative features (e.g., raw relative frequencies). Raises if any feature in Q/known/impostors is negative.
"cosine"
aggregate ('centroid', 'nearest')

How to build a single comparison point from the candidate's known documents:

  • centroid: compare Q to the mean vector of the known samples (simple, standard).
  • nearest: compare Q to the nearest known sample in the projected subspace. This gives the candidate its best of k chances per iteration, so it biases the score toward same-author (anti-conservative); calibrate under the same setting.
"centroid"
seed int

Seed for the numpy random generator used to sample features and impostors.

42

Examples:

>>> from bitig.forensic import GeneralImpostors
>>> gi = GeneralImpostors(n_iterations=100, seed=42)
>>> result = gi.verify(questioned=q_fm, known=known_fm, impostors=pool_fm)
>>> result.values["score"]    # fraction of iterations where known beat all impostors
0.87

verify

verify(*, questioned: FeatureMatrix, known: FeatureMatrix, impostors: FeatureMatrix) -> Result

Run the GI algorithm for one questioned document against one candidate's known set.

Parameters:

Name Type Description Default
questioned FeatureMatrix

Exactly one row (the Q document).

required
known FeatureMatrix

The candidate author's known documents (>= 1 row).

required
impostors FeatureMatrix

Pool of documents from other authors (>= 2 rows so each iteration can sample distinct impostors even with the smallest default impostor_sample_size).

required

Returns:

Type Description
Result

With values["score"] in [0, 1] (higher = more likely same author), values["wins"] raw iteration-win count, and sampling counts in params.

Notes

All three FeatureMatrix inputs must share the same feature space — i.e., identical feature_names in the same order. Callers that build features independently should use a single fit_transform on the pooled corpus and then slice by document id.

Unmasking

Koppel & Schler (2004) Unmasking for authorship verification.

Parameters:

Name Type Description Default
chunk_size int

Words per chunk. 500 is the common default in the literature.

500
n_rounds int

Number of iteration rounds (each round eliminates n_eliminate features and retrains). 10 is the standard setting.

10
n_eliminate int

Per-class top-N features to remove each round, following Koppel & Schler 2004: the N most Q-discriminating (top positive coefficients) and the N most K-discriminating (top negative coefficients), so up to 2 * N features drop per round. Default 3 (6 features per round), matching the classical literature setup.

3
n_folds int

CV folds for accuracy estimation per round. Must be >= 2 and <= min(#Q chunks,

K chunks).

10
min_chunks_per_class int

Minimum chunks required on each side before Unmasking is meaningful. Raises ValueError if either side falls below this threshold.

3
seed int

Seed for the CV split's random state.

42

Examples:

>>> from bitig.features import MFWExtractor
>>> from bitig.forensic import Unmasking
>>> unmasking = Unmasking(chunk_size=500, n_rounds=10, seed=42)
>>> result = unmasking.verify(
...     questioned=questioned_text,
...     known=known_text,
...     extractor=MFWExtractor(n=200, scale="zscore", lowercase=True),
... )
>>> result.values["accuracy_curve"]
[0.82, 0.79, 0.70, 0.58, 0.55, ...]
>>> result.values["accuracy_drop"]
0.27  # large drop = same author; small drop = different author

verify

verify(*, questioned: Corpus | Document | str, known: Corpus | Document | str, extractor: BaseFeatureExtractor) -> Result

Run Unmasking and return the accuracy-degradation curve plus summary stats.

Parameters:

Name Type Description Default
questioned Corpus, Document, or str

The questioned text.

required
known Corpus, Document, or str

The candidate author's known text.

required
extractor BaseFeatureExtractor

Any bitig feature extractor. Fit on the combined chunks, so the feature space is the union of terms seen in Q and K.

required

Returns:

Type Description
Result

values["accuracy_curve"] (list of CV accuracies per round, length n_rounds), values["accuracy_drop"] (accuracy at round 0 minus accuracy at final round), values["n_q_chunks"], values["n_k_chunks"].

CalibratedScorer

Fit a monotone calibrator mapping raw scores to calibrated posteriors.

Parameters:

Name Type Description Default
method ('platt', 'isotonic')
  • platt: one-dimensional LogisticRegression (Platt scaling). Parametric; assumes the score-to-probability mapping is sigmoidal. Robust on small calibration sets.
  • isotonic: IsotonicRegression. Non-parametric, monotone. More flexible but requires more calibration data: at least ISOTONIC_MIN_PER_CLASS (20) trials per class are enforced; >= 100 per class is advisable.
"platt"

Attributes:

Name Type Description
method str
fitted bool
log_lr_cap_ float | None

Cap on |log10 LR| (isotonic only; None for Platt).

prior_target_ float

Share of target trials the calibrator was fitted on. The calibrated posteriors carry this prior, so predict_log_lr divides out its odds: the LR then does not depend on how many trials of each class were used (audit 2026-09-26 P2; a 1:9 uninformative set gave log10 LR ~ -0.9).

Notes

Isotonic safeguards (audit 2026-09-26 N-P1.11). Plain isotonic regression outputs exactly 0 or 1 wherever the extreme calibration bins are pure, which the log-LR clip then turned into log10 LR = +/-12 ("extremely strong support") from as few as six trials. Here one pseudo-trial per class is added at the opposite extreme (a target at the lowest score, a non-target at the highest), so calibrated probabilities stay strictly inside (0, 1); and |log10 LR| is capped at log10(n) for a calibration set of n trials, a simple empirical bound in the spirit of Vergeer et al. (2016)'s ELUB. predict_log_lr warns when any output hits the cap; log_lr_cap_ holds it.

predict_proba

predict_proba(scores: ndarray) -> np.ndarray

Return calibrated p(H1 | score) for each input score.

predict_log_lr

predict_log_lr(scores: ndarray, *, base: float = 10.0) -> np.ndarray

Calibrated posteriors → log-LR, with the calibration set's prior odds divided out.

For isotonic calibration the result is capped at ±log_lr_cap_ (expressed in base); a UserWarning names how many outputs hit it.

bitig.forensic.metrics.compute_pan_report

compute_pan_report(probs: ndarray, y: ndarray, *, log_lrs: ndarray | None = None, ece_bins: int = 10, c_at_1_margin: float = 0.0, f05u_margin: float = 0.0) -> PANReport

Run the full PAN evaluation suite on one set of trials.

Parameters:

Name Type Description Default
probs np.ndarray of shape (n,)

Calibrated probabilities of the target hypothesis.

required
y np.ndarray of shape (n,)

Binary labels.

required
log_lrs ndarray

log10-LRs for each trial. If provided, cllr_bits is included in the report.

None
ece_bins int
10
c_at_1_margin float

Abstention half-widths around 0.5; the default 0.0 (exactly 0.5 is a non-answer) matches the PAN evaluator.

0.0
f05u_margin float

Abstention half-widths around 0.5; the default 0.0 (exactly 0.5 is a non-answer) matches the PAN evaluator.

0.0

PANReport dataclass

Bundled PAN-style evaluation summary for a verification system.

All metrics are computed over binary (same-author / different-author) trials. cllr_bits requires log-LR inputs and so is optional — set when available. Fields match the metric menu reported in PAN verification-task overviews (Stamatatos et al. 2014 onward).

Cases

Adli Laboratuvar vaka API'si bitig üst düzeyinden değil, bitig.cases modülünden içe aktarılır.

Case

Live handle on a Case directory.

Always construct via :meth:Case.create (new) or :meth:Case.load (existing). Mutating methods write to disk on completion; save() forces a flush of case.json when you mutate the record directly.

create classmethod

create(root: Path, *, id: str, title: str, examiner: str, recipe: str, overrides: dict[str, Any] | None = None) -> Case

Lay out a new Case directory and return a handle.

root is the parent directory (e.g. ~/.bitig/cases/); the case dir itself is root / id. Raises :class:CaseError if the case directory already exists, to prevent silent overwrites.

save

save() -> None

Recompute derived hashes and flush case.json.

study_hash is deliberately NOT refreshed from disk here: it is set only by :meth:regenerate_study_yaml, so a hand edit of study.yaml is detected (:meth:study_yaml_intact) instead of being silently re-blessed by the next save (audit 2026-09-26 N-P1.1). The Custom editor goes through :meth:change_recipe, which regenerates.

add_evidence

add_evidence(src: Path, *, role: EvidenceRole, author: str | None = None, year: int | None = None, dest_name: str | None = None) -> EvidenceEntry

Copy src into the role dir, hash it, register it, save.

Refuses to overwrite an existing file with the same destination name — analysts can pass dest_name to disambiguate, or rename the source. Raises :class:CaseError if the Case is signed.

set_control_corpus

set_control_corpus(corpus_id: str, n_docs: int) -> ControlCorpusRef

Set the impostor pool reference (forensic-mode Cases).

unregistered_evidence_files

unregistered_evidence_files() -> list[str]

Files under evidence/ that are not registered (never analysed or sealed).

reacknowledge_evidence

reacknowledge_evidence(path: str, *, reason: str, by: str | None = None) -> dict[str, Any]

Accept the current bytes of a changed evidence file, on the record.

For a registered file whose hash no longer matches (e.g. a re-export with different line endings), the analyst states why the change is legitimate. The old and new hashes, the reason, who and when are appended to record.custody_log. That log is part of the canonical case state, so it is sealed and printed in the report's chain of custody. It also changes the case state, so any earlier run must be re-run before the case can be signed.

Missing files cannot be re-acknowledged: fork the case instead. Raises :class:CaseError on a signed case, an empty reason, an unregistered path, a missing file, or a file that has not changed.

verify_custody

verify_custody() -> list[EvidenceEntry]

Return registered entries whose on-disk SHA-256 no longer matches.

Empty list means the chain of custody is intact (spec §5.1: the Evidence step uses this to flip cards red and block step 4+). Missing files also count as a mismatch — the entry is returned.

build_corpus

build_corpus(*, language: str = 'en') -> Corpus

Load the run corpus from the registered evidence only (audit 2026-09-26 N-P0.1).

Every document is read from its registered path and its bytes are re-hashed at read time, so the texts analysed are exactly the texts in the chain of custody: unregistered files under evidence/ are never loaded, and a file altered since registration aborts the load. Each document carries role and, when registered, author / year metadata. The document id is the file stem.

study_yaml_intact

study_yaml_intact() -> bool

True iff study.yaml still hashes to the value recorded when it was written.

resolved_study_dict

resolved_study_dict(*, fill_targets: bool = True) -> dict[str, Any]

The study.yaml-shaped dict for this Case's recipe + overrides.

corpus.path is the case-relative evidence dir, recorded for reference only: a Case run loads its corpus from the registered evidence (:meth:build_corpus), never by globbing a directory, so a copied or moved Case analyses its own evidence (audit N-P1.1).

verify methods without explicit target_ids target every registered questioned document (unless fill_targets is false), so a questioned document added later is targeted too.

regenerate_study_yaml

regenerate_study_yaml() -> Path

Write the resolved study to study.yaml and refresh study_hash.

set_param

set_param(target: str, value: Any) -> None

Apply a ParamField override (spec §5.2 drawer save).

target follows :func:bitig.recipes.apply_param_target syntax (e.g. "features[mfw].top_n"). The override lives in record.overrides so it survives recipe-mode re-derivation, and study.yaml is regenerated against the new resolved study.

Raises :class:CaseError if the Case is signed.

change_recipe

change_recipe(new_recipe: str, overrides: dict[str, Any] | None = None) -> None

Switch recipes (spec §3 confirmation dialog lives in the GUI).

Resets overrides, re-derives mode, regenerates study.yaml. Raises if the Case is signed.

register_run

register_run(run_id: str, *, case_state_hash: str | None = None) -> Path

Record a completed run. The runner creates runs/<run_id>/ and writes its artefacts; this method just updates case.json and refreshes the runs/latest symlink.

case_state_hash is the case state the run was computed on (taken before the run started). Signing refuses a run whose state differs from the current one (audit 2026-09-26 N-P1.2); a run registered without it can never be signed.

sign_blockers

sign_blockers() -> list[str]

Why this case cannot be signed right now; empty when it can.

Enforced by :meth:mark_signed itself, so every entry point (CLI, GUI, API) gets the same guarantees (audit 2026-09-26 N-P1.2, N-P1.7).

mark_signed

mark_signed(*, signed_by: str | None = None, signature_plugin: SignaturePlugin | None = None) -> dict[str, Any]

Freeze the Case and write report/signed.json (spec §6).

signature_plugin is an optional :class:~bitig.signatures .SignaturePlugin that wraps the base chain-of-custody payload with an additional cryptographic binding (e.g. an HMAC or an HSM-backed signature). The default (None) keeps the chain-of-custody-only behaviour.

Refuses (:class:CaseError) unless :meth:sign_blockers is empty. The seal binds the canonical case state, the frozen signed.html and a hash manifest of every file in the latest run. signed.html and signed.json are written to temporary names and moved into place only once everything has succeeded; any failure (including an interrupt) removes them and leaves the case unsigned.

Returns the (possibly plugin-augmented) signed.json payload as a dict.

verify_seal

verify_seal(*, signature_key: bytes | str | None = None) -> SealVerification

Independently verify a signed case's chain-of-custody seal (audit P1.1).

Recomputes every sealed quantity from the current on-disk state and compares it to report/signed.json: the canonical state hash, the report hash, evidence custody, and — when a non-Null signature plugin was used — the cryptographic signature (HMAC needs signature_key or $BITIG_SIGNATURE_KEY). Returns a structured result; the overall .ok is True only if every check passes. Pure read-only.

SealVerification dataclass

Result of :meth:Case.verify_seal.

signed is False for an unsigned case (nothing to verify). ok is True only when the case is signed and every check passed.

tamper_evident property

tamper_evident: bool

True only for a verified seal with a cryptographic signature.

status property

status: SealStatus

One-word outcome.

broken if any check failed; unverifiable if every hash check passed but the signature could not be checked (no key); unsigned for an intact Null seal (hashes only, not tamper-evident); verified for an intact seal with a valid signature.

Runner

bitig.runner.run_study

run_study(config_path: str | Path, *, output_dir: str | Path | None = None, run_name: str | None = None, corpus: Corpus | None = None, overwrite: bool = False) -> Path

Execute a full study from a study.yaml file and save all results.

overwrite lets a run reuse a folder that holds a previous run, removing only that run's own outputs first; otherwise such a folder is refused, so stale method outputs never mix with new ones (audit 2026-09-26 P2).

corpus supplies the documents directly instead of loading cfg.corpus.path (a Forensic Lab Case passes its registered, hash-checked evidence this way). cfg.corpus.filter still applies.

Returns the path to the run directory (e.g., results/2026-04-17T10-15-30/).

Reporting

build_report, bitig üst düzeyinde yeniden dışa aktarılır; build_forensic_report ise bitig.report modülünden içe aktarılır.

bitig.report.render.build_report

build_report(result_dir: str | Path, *, output: str | Path, format: Format = 'html', title: str = 'bitig study', corpus_summary: dict[str, Any] | None = None) -> Path

Assemble result.json + figures in result_dir into a single HTML/MD file at output.

result_dir may be either a single Result directory (one method) or a parent directory containing per-method subdirectories (produced by bitig run).

bitig.report.render.build_forensic_report

build_forensic_report(result_dir: str | Path, *, output: str | Path, title: str = 'bitig forensic report', lr_summaries: dict[str, dict[str, str]] | None = None) -> Path

Render a forensic-styled report with known/questioned framing, LR output, and a chain-of-custody block.

The forensic fields (hypothesis_pair, questioned_description, known_description, acquisition_notes, custody_notes, source_hashes) are read from the provenance of the first Result in result_dir. Populate them when calling Provenance.current from your verification pipeline.

Parameters:

Name Type Description Default
result_dir path

A Result directory or a parent directory of per-method Result subdirectories.

required
output path

Output HTML path.

required
title str
'bitig forensic report'
lr_summaries dict

Per-method LR summaries keyed by method name, e.g., {"general_impostors": {"log_lr": "1.34", "lr": "21.9"}}. These render under the relevant method section. If None, no LR block is drawn per method.

None