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__ ¶
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 ¶
Return a new Corpus containing documents whose metadata matches every key in query.
Values may be scalars (exact match) or lists (membership).
groupby ¶
Group documents by a metadata field value.
Raises KeyError if any document lacks the field.
metadata_column ¶
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 ¶
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 ¶
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 |
1000
|
min_df
|
int
|
Drop words appearing in fewer than |
1
|
max_df
|
float
|
Drop words appearing in more than |
1.0
|
scale
|
('none', 'zscore', 'l1', 'l2')
|
Per-feature scaling applied at transform-time. Burrows Delta requires "zscore", which
z-scores the relative frequencies |
"none"
|
lowercase
|
bool
|
If True, case-fold before counting. |
False
|
project_tokens ¶
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 ¶
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
¶
Provenance
dataclass
¶
has_forensic_metadata
property
¶
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
|
None
|
similarity
|
('cosine', 'minmax')
|
Similarity function used to compare the questioned document to the candidate and to each impostor.
|
"cosine"
|
aggregate
|
('centroid', 'nearest')
|
How to build a single comparison point from the candidate's known documents:
|
"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 ¶
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 |
required |
Returns:
| Type | Description |
|---|---|
Result
|
With |
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 |
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
|
|
CalibratedScorer ¶
Fit a monotone calibrator mapping raw scores to calibrated posteriors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
('platt', 'isotonic')
|
|
"platt"
|
Attributes:
| Name | Type | Description |
|---|---|---|
method |
str
|
|
fitted |
bool
|
|
log_lr_cap_ |
float | None
|
Cap on |log10 LR| (isotonic only; |
prior_target_ |
float
|
Share of target trials the calibrator was fitted on. The calibrated
posteriors carry this prior, so |
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.
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, |
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 ¶
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 the impostor pool reference (forensic-mode Cases).
unregistered_evidence_files ¶
Files under evidence/ that are not registered (never analysed or sealed).
reacknowledge_evidence ¶
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 ¶
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 ¶
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 ¶
True iff study.yaml still hashes to the value recorded when it was written.
resolved_study_dict ¶
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 ¶
Write the resolved study to study.yaml and refresh study_hash.
set_param ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
¶
True only for a verified seal with a cryptographic signature.
status
property
¶
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.,
|
None
|