Turkish stylometry walkthrough¶
A runnable Turkish example: set up a Turkish project, then explore a corpus of public-domain Ömer Seyfettin short stories from Turkish Wikisource with MFW, PCA and Ward clustering.
Setup¶
uv pip install 'bitig[turkish]'
python -c "import stanza; stanza.download('tr')"
bitig init seyfettin --language tr
cd seyfettin
This scaffolds a project directory with study.yaml pre-configured for Turkish
(preprocess.language: tr). Confirm with:
The language row shows tr.
Stanza (through spacy-stanza) is only needed for bitig ingest and for spaCy-based
features in the Python API. The features that bitig run builds (MFW, n-grams, function
words, punctuation, lexical diversity, readability) work from the raw text and never load
Stanza.
Corpus¶
Place your Turkish texts in corpus/ as UTF-8 .txt files. A good public-domain
source is Ömer Seyfettin on Turkish
Wikisource — dozens of early-20th-
century short stories are already transcribed there.
Add a corpus/metadata.tsv and uncomment the metadata: line in study.yaml:
filename author year
bomba.txt Omer_Seyfettin 1910
kesik_biyik.txt Omer_Seyfettin 1911
forsa.txt Omer_Seyfettin 1913
pembe_incili_kaftan.txt Omer_Seyfettin 1917
The scaffolded study runs Burrows Delta grouped by author, which needs documents from
at least two author values. With a single author the Delta method fails and
bitig run exits with status 1. Either add texts by a second author, or replace the Delta
method with reduce / cluster methods as in the worked example below.
Running the study¶
bitig ingest corpus/ --language tr --metadata corpus/metadata.tsv # optional
bitig run study.yaml --name first-run
bitig ingest runs Stanza through spacy-stanza and caches the parses as DocBins.
bitig run does not read that cache, so the ingest step is optional for this study.
What you get¶
The scaffolded Turkish study builds MFW (top 1 000 words, z-scored) and runs Burrows
Delta. bitig run writes one folder per method plus the run configuration:
results/first-run/
├── resolved_config.json # the fully resolved study configuration
└── burrows/
├── result.json # predictions, resubstitution accuracy, provenance
└── confusion_matrix.png
result.jsonholds the method's values and a provenance block (bitig version, corpus hash, feature hash, seed, resolved config). There is no separateprovenance.json.- The Delta values are in-sample:
resubstitution_accuracyis a separability check, andattributionslists documents that have noauthorvalue. - Other methods write their own default figure:
scatter.pngandpca_biplot.pngforreduce,dendrogram.pngforcluster,zeta.pngforzeta. Only methods that return tables (Zeta, rolling Delta, verification) also writetable_N.parquetfiles. - A failed method leaves an
error.txtin its folder instead ofresult.json, and the run exits with status 1.
Worked example: 28 short stories by Ömer Seyfettin¶
The examples/turkish_seyfettin/ directory in the repository ships a complete,
end-to-end run against 28 short stories by Ömer Seyfettin (1884-1920; in the
Turkish public domain since 1991), scraped from
tr.wikisource.org by fetch_corpus.py. The corpus is
committed alongside the study, so you do not need to fetch it again:
bitig run examples/turkish_seyfettin/study.yaml --name seyfettin
# optional: re-download the stories (~30s, polite to Wikisource)
python examples/turkish_seyfettin/fetch_corpus.py --n 30
Corpus. 28 stories survive the 200-token cutoff; story lengths range from
326 to 4 455 tokens (median ≈ 1 560). Wikisource transcriptions are CC BY-SA 4.0;
attribution and source URLs live in examples/turkish_seyfettin/manifest.json.
Study. Five hundred most frequent words (z-scored, min_df = 2) → PCA + Ward
hierarchical clustering. The study also builds a Turkish function-word feature
(tr_fwords) that no method uses by default; point a method's features: at it to try it.
A single-author corpus admits no between-author attribution or verification (Delta,
Imposters, classify), so this is exploratory within-author stylometry, not authorship
attribution.
The run writes:
examples/turkish_seyfettin/results/seyfettin/
├── resolved_config.json
├── pca/
│ ├── result.json # coordinates, loadings, explained variance
│ ├── scatter.png
│ └── pca_biplot.png
└── ward/
├── result.json # cluster labels and linkage matrix
└── dendrogram.png
PCA on the MFW-500 lexical space¶
PC1 explains 8.0 % of the variance, PC2 explains 6.9 %. No single component dominates: the lexical variance inside one author is spread across many small axes.
Largest loadings — PC1: o, akşam, gül, bana, karşı, bakıyordu, nihayet, ki.
Largest loadings — PC2: durdu, idi, başını, hafif, değildi, üzerine, şeyler, tarafa.
pca/pca_biplot.png overlays the top-15 loading vectors onto the same 2-D projection.
Ward hierarchical clustering (k = 4)¶
Cutting the dendrogram (ward/dendrogram.png) at four flat clusters yields:
| Cluster | n | Members | Story length (tokens) |
|---|---|---|---|
| 0 | 17 | and, antiseptik, elma, kasag, … |
median 1 152 (326–3 115) |
| 1 | 9 | aleko, bomba, ferman, forsa, … |
median 2 418 (1 096–4 455) |
| 2 | 1 | keramet |
508 |
| 3 | 1 | hediye |
454 |
Story length tracks the clusters: the two singletons are the second- and third-shortest stories, and cluster 1 collects the longer ones. z-scored MFW counts get noisy in short texts, so short stories drift away from the main cloud regardless of topic. This is not a bug in the method — it is the natural consequence of MFW estimation variance at small N:
If you are running stylometry on Turkish short prose, raise the per-document token floor to at least 1 000 — or move to character n-grams, which are far more length-tolerant — before drawing topic / period conclusions from the sub-cluster structure.
Caveats and what is not shown here¶
A genuine authorship-attribution demonstration needs at least one additional PD-old Turkish prose author with comparable Wikisource coverage, which Wikisource:tr does not currently have for the early-republican period (Refik Halit Karay's transcriptions are present but the underlying texts will not enter the Turkish public domain until 2036). For attribution work we recommend pairing Seyfettin with a different-genre control corpus (e.g. parliamentary speeches, Türkçe Wikipedia featured articles by topic, or your own institutional corpus) rather than a contemporaneous literary author.
Customising¶
Edit study.yaml to swap features or methods. bitig run builds the feature types mfw,
word_ngram, char_ngram, function_word, punctuation, lexical_diversity and
readability, and rejects any other type when it loads the study. For example, character
n-grams are more robust on short texts:
Contextual embeddings are not a bitig run feature type. Use them from Python with
bitig.features.ContextualEmbeddingExtractor; with language="tr" the model resolves to
dbmdz/bert-base-turkish-cased, and model= accepts any HuggingFace checkpoint (e.g.
stefan-it/bert5urk). This requires the bitig[embeddings] extra.
Notes on Turkish specifics¶
- Morphology. Turkish is agglutinative; a token like
evlerinizdenpacksev+ler+iniz+deninto one form. Stanza's Turkish BOUN model lemmatises and tags these correctly, which matters for POS-ngram and dependency-based features. - Syllable counting. Both Ateşman and Bezirci-Yılmaz count syllables using a
vowel-counter specialised for Turkish orthography (including
ı,ğ,ş,ç,ü,ö). - Function words. The bundled list leans on Turkish's closed-class postpositions,
conjunctions, and discourse particles (e.g.
ile,ancak,fakat,çünkü,ki,ise).
Troubleshooting¶
ModuleNotFoundError: No module named 'spacy_stanza'— runuv pip install 'bitig[turkish]'.FileNotFoundError: ... stanza_resources/tr/default.zip— runpython -c "import stanza; stanza.download('tr')". The model is about 600 MB.- Very slow first ingest on MPS. Stanza's Turkish model does not yet support Apple Silicon MPS. Expect CPU parse rates on first run; subsequent runs are cache hits.