<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged text-mining</title>
<link>https://coderlegion.com/tag/text-mining</link>
<description>Powered by Question2Answer</description>
<item>
<title>When Stylometry Meets Intellectual History: What &quot;The Bakhtin Circle&quot; Teaches Us About Authorship</title>
<link>https://coderlegion.com/25027/when-stylometry-meets-intellectual-history-what-bakhtin-circle-teaches-about-authorship</link>
<description>&lt;p&gt;Every developer who has worked with stylometry knows the drill: you feed a corpus of texts with known authors into a machine learning pipeline, extract a feature set (usually the most frequent words), apply some dimensionality reduction (PCA, anyone?), and watch as the unknown text confidently clusters with its true author. It's elegant, it's powerful, and for most practical cases—from disputed Federalist Papers to identifying anonymous blog posts—it works remarkably well.&lt;/p&gt;
&lt;p&gt;But what happens when you point this finely-tuned machinery at a &lt;em&gt;collective&lt;/em&gt; intellectual project? What if the &quot;author&quot; isn't an individual at all, but a circle of thinkers who read each other's drafts, argued over concepts, and published under each other's names? This is exactly the fascinating problem tackled by Orekhov and Vasilenko in their recent paper &quot;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nevmenandr.github.io/portfolio/assets/pdf/bakhtin_delta.pdf&quot;&gt;Quantitative Authorship Attribution and the Bakhtin Circle: The Problem of Authorship in Intellectual Communities&lt;/a&gt;&quot; (2026).&lt;/p&gt;
&lt;h3 id=&quot;the-core-problem-attribution-in-a-collaborative-context&quot;&gt;The Core Problem: Attribution in a Collaborative Context&lt;/h3&gt;
&lt;p&gt;The Bakhtin Circle—that legendary group of Soviet thinkers including Mikhail Bakhtin, Valentin Voloshinov, and Pavel Medvedev—presents a nightmare scenario for traditional authorship attribution. These scholars didn't just influence each other; they actively collaborated, shared ideas, and in some cases, published works that may have been jointly authored or even ghost-written. The question &quot;Who really wrote &lt;em&gt;Marxism and the Philosophy of Language&lt;/em&gt;?&quot; has haunted literary scholars for decades.&lt;/p&gt;
&lt;p&gt;The paper, which I'll cite properly in a moment, approaches this not as a simple &quot;who wrote what&quot; puzzle, but as a methodological challenge: &lt;strong&gt;how do we test authorship attribution methods when the ground truth itself is contested, and when the very concept of &quot;individual authorship&quot; breaks down?&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-technical-approach-stylo-and-delta&quot;&gt;The Technical Approach: Stylo and Delta&lt;/h3&gt;
&lt;p&gt;The researchers used the excellent R package &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/computationalstylistics/stylo&quot;&gt;&lt;code&gt;stylo&lt;/code&gt;&lt;/a&gt; (Eder et al., 2016)—a toolkit that has become the workhorse of computational stylistics. If you haven't used it, imagine &lt;code&gt;scikit-learn&lt;/code&gt; for text analysis but with a specialized focus on authorship problems. It implements the classic &quot;Delta&quot; method (Burrows' distance) along with various machine learning classifiers.&lt;/p&gt;
&lt;p&gt;Their pipeline is refreshingly straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Feature extraction&lt;/strong&gt;: They used the most frequent words (MFW) as features—a common and effective choice in stylometry&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distance calculation&lt;/strong&gt;: They computed the Delta distance between texts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visualization&lt;/strong&gt;: They used Principal Component Analysis (PCA) and cluster analysis to visualize textual relationships&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt;: They performed cross-validation to test the stability of their attributions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What makes this study particularly interesting for developers is &lt;strong&gt;the data strategy&lt;/strong&gt;. Instead of relying on contested attributions, they built their training set from texts with &lt;em&gt;uncontroversial&lt;/em&gt; authorship—works that everyone agrees were written by Bakhtin, Voloshinov, or Medvedev individually. The test set then included the disputed works.&lt;/p&gt;
&lt;p&gt;This approach is worth remembering. In real-world attribution problems, we rarely have perfect ground truth. We often have to construct it from a subset of cases where attribution is clear, then test our methods on the ambiguous ones.&lt;/p&gt;
&lt;h3 id=&quot;the-surprising-and-cautionary-result&quot;&gt;The Surprising (and Cautionary) Result&lt;/h3&gt;
&lt;p&gt;Here's where it gets interesting for practitioners. The stylometric analysis did not produce the clean, unambiguous clusters that we often see in textbook examples. The disputed texts didn't neatly align with a single author. Instead, they showed varying degrees of similarity to different circle members.&lt;/p&gt;
&lt;p&gt;Now, a less careful researcher might have concluded &quot;the method doesn't work.&quot; But Orekhov and Vasilenko make a much more sophisticated argument: &lt;strong&gt;the ambiguous results actually reflect the collaborative reality of the Bakhtin Circle&lt;/strong&gt;. When Voloshinov and Bakhtin worked closely together, exchanging ideas and editing each other's prose, the resulting texts naturally contain elements of multiple writing styles. The &quot;noise&quot; in our stylometric analysis isn't a bug—it's a feature that tells us about the social dynamics of intellectual production.&lt;/p&gt;
&lt;p&gt;This is a crucial insight for developers: &lt;strong&gt;our tools are never neutral&lt;/strong&gt;. They encode assumptions about authorship (e.g., that style is stable and unique to individuals) that may not hold in all contexts. When we see ambiguous results, we should ask: is this a methodological failure, or is the data telling us something about the nature of authorship in this particular case?&lt;/p&gt;
&lt;h3 id=&quot;practical-implementation-r-code-for-stylometric-analysis&quot;&gt;Practical Implementation: R Code for Stylometric Analysis&lt;/h3&gt;
&lt;p&gt;To make this concrete, here's how you can perform a similar analysis using the &lt;code&gt;stylo&lt;/code&gt; package in R. The following code demonstrates the key steps used in the paper—from loading the corpus to performing cluster analysis and cross-validation.&lt;/p&gt;
&lt;h4 id=&quot;setting-up-the-environment&quot;&gt;Setting Up the Environment&lt;/h4&gt;
&lt;p&gt;First, install and load the necessary packages:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# Install stylo from CRAN if you haven't already
# install.packages(&quot;stylo&quot;)

# Load the package
library(stylo)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;basic-stylometric-analysis&quot;&gt;Basic Stylometric Analysis&lt;/h4&gt;
&lt;p&gt;The main workhorse function is &lt;code&gt;stylo()&lt;/code&gt;. Here's how to run a basic analysis on your own corpus:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# --- Basic stylometric analysis ---
# Place your plain text files (.txt) in a folder, e.g., &quot;bakhtin_corpus&quot;
# Each file should be named with author information, e.g., &quot;Bakhtin_text1.txt&quot;

results &amp;lt;- stylo(
    gui = FALSE,                        # Set to TRUE for interactive mode
    corpus.dir = &quot;bakhtin_corpus&quot;,      # Path to your text files
    mfw.min = 100,                      # Minimum number of most frequent words
    mfw.max = 100,                      # Maximum number (fixed set here)
    mfw.incr = 1,                       # Increment step (ignored if min=max)
    analysis.type = &quot;CA&quot;,               # &quot;CA&quot; for Cluster Analysis
    distance.measure = &quot;delta&quot;,         # Burrows' Delta distance
    write.png = TRUE                    # Save plots as PNG files
)

# View the distance matrix (first 10 rows and columns)
print(&quot;Delta distance matrix (first 10 texts):&quot;)
print(results$distance.matrix[1:10, 1:10])
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;advanced-manual-feature-selection-and-cross-validation&quot;&gt;Advanced: Manual Feature Selection and Cross-Validation&lt;/h4&gt;
&lt;p&gt;For more control over the process—mirroring the approach in Orekhov and Vasilenko—you can manually extract features and perform cross-validation:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# --- Manual feature extraction ---
# Load all texts from the corpus directory
corpus &amp;lt;- load.corpus(files = &quot;all&quot;, corpus.dir = &quot;bakhtin_corpus&quot;)

# Create a table of frequencies for all words
all_freq &amp;lt;- make.frequencies(corpus)

# Select the top N most frequent words as features
n_features &amp;lt;- 150
mfw_list &amp;lt;- frequent.features(all_freq, cutoff = n_features)

# Create a table of relative frequencies for these features
# This is your feature matrix: rows = texts, columns = word frequencies
feature_table &amp;lt;- make.table.of.frequencies(corpus, mfw_list)

# --- Delta distance calculation ---
# Calculate Delta distance matrix manually
delta_matrix &amp;lt;- dist.delta(feature_table)

# Perform PCA for visualization
pca_results &amp;lt;- prcomp(delta_matrix, scale. = TRUE)

# Create a PCA plot with labels
plot(pca_results$x[,1], pca_results$x[,2],
     type = &quot;n&quot;,
     xlab = &quot;PC1&quot;, ylab = &quot;PC2&quot;,
     main = &quot;PCA of Bakhtin Circle Corpus&quot;)
text(pca_results$x[,1], pca_results$x[,2],
     labels = rownames(pca_results$x),
     cex = 0.7)

# --- Cross-validation to test classification accuracy ---
# Use the classify() function to perform leave-one-out cross-validation
# This tests how well the model can predict authorship for unknown texts

# First, create a vector of known author labels
# These should match the filenames or be provided separately
# For example: author_labels &amp;lt;- c(&quot;Bakhtin&quot;, &quot;Bakhtin&quot;, &quot;Voloshinov&quot;, ...)

# Perform cross-validation using the Delta distance
cv_results &amp;lt;- classify(
    corpus.dir = &quot;bakhtin_corpus&quot;,      # Path to corpus
    mfw = n_features,                   # Number of features to use
    distance.measure = &quot;delta&quot;,         # Distance measure
    cv.folds = 10,                      # Number of cross-validation folds
    classification.method = &quot;knn&quot;       # k-nearest neighbors classification
)

# Print classification accuracy
print(&quot;Cross-validation accuracy:&quot;)
print(paste(&quot;Overall accuracy:&quot;, 
            round(cv_results$accuracy * 100, 2), &quot;%&quot;))

# View the confusion matrix
print(&quot;Confusion matrix:&quot;)
print(cv_results$confusion.matrix)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;processing-the-bakhtin-circle-dataset&quot;&gt;Processing the Bakhtin Circle Dataset&lt;/h4&gt;
&lt;p&gt;If you want to replicate the analysis from the paper, you would modify the code to handle multiple feature sets and visualize the ambiguous cases:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-r&quot;&gt;# --- Iterative feature selection to find optimal MFW ---
# Test different numbers of most frequent words
# to find the range that yields the best separation

mfw_range &amp;lt;- seq(from = 50, to = 500, by = 50)
accuracy_results &amp;lt;- data.frame(mfw = mfw_range, accuracy = NA)

for (i in seq_along(mfw_range)) {
    cat(&quot;Testing MFW =&quot;, mfw_range[i], &quot;...\n&quot;)
    cv_result &amp;lt;- classify(
        corpus.dir = &quot;bakhtin_corpus&quot;,
        mfw = mfw_range[i],
        distance.measure = &quot;delta&quot;,
        cv.folds = 10,
        classification.method = &quot;knn&quot;
    )
    accuracy_results$accuracy[i] &amp;lt;- cv_result$accuracy
}

# Plot accuracy by number of features
plot(accuracy_results$mfw, accuracy_results$accuracy,
     type = &quot;b&quot;,
     xlab = &quot;Number of Most Frequent Words&quot;,
     ylab = &quot;Cross-Validation Accuracy&quot;,
     main = &quot;Feature Selection for Bakhtin Circle Attribution&quot;)

# --- Visualizing the ambiguous cases ---
# In the paper, disputed texts showed mixed affiliations
# You can highlight these in your plots

# Identify disputed texts (e.g., those with uncertain authorship)
# For demonstration, let's assume you have a vector of disputed text names
# disputed_texts &amp;lt;- c(&quot;Marxism_and_Philosophy_of_Language.txt&quot;, ...)

# In PCA plot, color disputed texts differently
is_disputed &amp;lt;- rownames(pca_results$x) %in% disputed_texts
plot_colors &amp;lt;- ifelse(is_disputed, &quot;red&quot;, &quot;blue&quot;)

plot(pca_results$x[,1], pca_results$x[,2],
     col = plot_colors,
     pch = ifelse(is_disputed, 17, 16),
     xlab = &quot;PC1&quot;, ylab = &quot;PC2&quot;,
     main = &quot;PCA with Disputed Texts Highlighted&quot;)
text(pca_results$x[,1], pca_results$x[,2],
     labels = rownames(pca_results$x),
     pos = 3, cex = 0.6)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-open-dataset-a-resource-for-the-community&quot;&gt;The Open Dataset: A Resource for the Community&lt;/h3&gt;
&lt;p&gt;One of the most valuable contributions of this paper is the release of their data on &lt;a rel=&quot;nofollow&quot; href=&quot;https://osf.io/r9wbz/&quot;&gt;OSF&lt;/a&gt;. The dataset (Orekhov &amp;amp; Vasilenko, 2025) contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The full corpus of texts used in the analysis&lt;/li&gt;
&lt;li&gt;Metadata about authorship attributions (both certain and disputed)&lt;/li&gt;
&lt;li&gt;The feature tables generated during analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is a significant resource for developers working in stylometry. It provides a test case for authorship attribution in a &quot;difficult&quot; context—one where the usual assumptions about individual style don't straightforwardly apply. If your algorithm can handle the Bakhtin Circle, it can probably handle most real-world attribution problems.&lt;/p&gt;
&lt;h3 id=&quot;key-takeaways-for-developers&quot;&gt;Key Takeaways for Developers&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Always validate your assumptions.&lt;/strong&gt; Stylometry works best when authorship is individual and style is consistent. Before applying it, ask: does the text corpus meet these assumptions? If not, how might that affect the results?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Embrace ambiguity.&lt;/strong&gt; Not every classification problem yields a clear answer. Sometimes the &quot;noise&quot; contains important information about the data generation process. In the Bakhtin Circle case, the ambiguous classifications revealed patterns of intellectual collaboration that traditional scholarship had debated for decades.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Use the right tools for the right problem.&lt;/strong&gt; &lt;code&gt;stylo&lt;/code&gt; is a mature, well-documented package that handles the core tasks of authorship attribution elegantly. Its GUI interface makes it accessible to non-programmers, while its R backend allows for deep customization. The paper demonstrates its effectiveness even on challenging datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Share your data.&lt;/strong&gt; The authors' decision to release their dataset on OSF is a model for reproducible research. If you're working on stylometric problems, consider doing the same—it allows others to validate your findings and build upon your work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Tune your parameters carefully.&lt;/strong&gt; The choice of the number of most frequent words (MFW) can significantly impact results, as shown in the feature selection plot above. Always test a range of values and consider the stability of your classifications across different feature sets.&lt;/p&gt;
&lt;h3 id=&quot;the-full-citation&quot;&gt;The Full Citation&lt;/h3&gt;
&lt;p&gt;For those who want to dig deeper, here's the proper reference (paper in Russian):&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Orekhov, B.V., &amp;amp; Vasilenko, A.G. (2026). Quantitative Authorship Attribution and the Bakhtin Circle: The Problem of Authorship in Intellectual Communities. &lt;em&gt;New Philological Bulletin&lt;/em&gt;, (1), 40-55. DOI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.54770/20729316-2026-1-40&quot;&gt;10.54770/20729316-2026-1-40&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And the dataset:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Orekhov, B.V., &amp;amp; Vasilenko, A.G. (2025). Quantitative Authorship Attribution and the Bakhtin Circle: the Problem of Authorship in Intellectual Communities (stylometry data). OSF. DOI: &lt;a rel=&quot;nofollow&quot; href=&quot;https://doi.org/10.17605/OSF.IO/R9WBZ&quot;&gt;10.17605/OSF.IO/R9WBZ&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25027/when-stylometry-meets-intellectual-history-what-bakhtin-circle-teaches-about-authorship</guid>
<pubDate>Fri, 25 Sep 2026 03:31:00 +0000</pubDate>
</item>
<item>
<title>Finding Accidental Palindromes in 130,000 Russian Texts</title>
<link>https://coderlegion.com/24650/finding-accidental-palindromes-in-130-000-russian-texts</link>
<description>&lt;h3 id=&quot;or-why-i-didn-t-use-manacher-s-algorithm-and-still-got-217-new-palindromes&quot;&gt;Or: why I didn't use Manacher's algorithm and still got 217 new palindromes&lt;/h3&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-idea&quot;&gt;The Idea&lt;/h3&gt;
&lt;p&gt;Everyone knows crafted palindromes like &lt;em&gt;&quot;Never odd or even&quot;&lt;/em&gt; or &lt;em&gt;&quot;A man, a plan, a canal – Panama&quot;&lt;/em&gt;. They're beautiful, but they're &lt;strong&gt;constructed&lt;/strong&gt; — someone sat down and deliberately created them.&lt;/p&gt;
&lt;p&gt;What about &lt;strong&gt;accidental&lt;/strong&gt; palindromes? Phrases that appear naturally in everyday speech, journalism, and prose, without anyone trying?&lt;/p&gt;
&lt;p&gt;There are accidental meters in prose. There must be accidental palindromes in texts, beyond the reach of word artists.&lt;/p&gt;
&lt;p&gt;This is an attempt to find them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-data&quot;&gt;The Data&lt;/h3&gt;
&lt;p&gt;I used the &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/&quot;&gt;Russian National Corpus&lt;/a&gt;&lt;/strong&gt; — a massive collection of Russian texts spanning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fiction (19th–21st centuries)&lt;/li&gt;
&lt;li&gt;Journalism (newspapers, magazines)&lt;/li&gt;
&lt;li&gt;Memoirs, letters, diaries&lt;/li&gt;
&lt;li&gt;Scientific and popular science texts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Two separate corpuses were scanned:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Corpus &lt;/th&gt;&lt;th&gt;Files&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Main corpus &lt;/td&gt;&lt;td&gt;131,391&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Newspaper corpus &lt;/td&gt;&lt;td&gt;--&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-algorithm&quot;&gt;The Algorithm&lt;/h3&gt;
&lt;h4 id=&quot;search-strategy&quot;&gt;Search strategy&lt;/h4&gt;
&lt;p&gt;I don't search for palindromes at the &lt;strong&gt;character level&lt;/strong&gt; (like Manacher's algorithm does). Instead, I search at the &lt;strong&gt;word level&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Extract all Russian words (Cyrillic only) using regex&lt;/li&gt;
&lt;li&gt;For each position, test sequences of 2–10 consecutive words&lt;/li&gt;
&lt;li&gt;Normalize: remove punctuation, lowercase, replace &lt;code&gt;ё&lt;/code&gt; with &lt;code&gt;е&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Check if the normalized string equals its reverse&lt;/li&gt;
&lt;li&gt;Deduplicate against already found palindromes&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;why-not-manacher-s-algorithm-&quot;&gt;Why not Manacher's algorithm?&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Aspect &lt;/th&gt;&lt;th&gt;My approach &lt;/th&gt;&lt;th&gt;Manacher's algorithm&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Unit&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Words &lt;/td&gt;&lt;td&gt;Characters&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Search space&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;≤ 9 sequences per word &lt;/td&gt;&lt;td&gt;Every possible substring&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Time complexity&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;O(n × 10) &lt;/td&gt;&lt;td&gt;O(L)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Word boundaries respected&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ Yes &lt;/td&gt;&lt;td&gt;❌ No&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Semantic value&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;High (phrases) &lt;/td&gt;&lt;td&gt;Low (arbitrary substrings)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Manacher's algorithm is elegant and linear, but it solves a &lt;strong&gt;different problem&lt;/strong&gt;. I don't need every palindromic substring — I need &lt;strong&gt;phrases that make sense&lt;/strong&gt;. Restricting to 2–10 words makes the naive &lt;code&gt;s == s[::-1]&lt;/code&gt; check not just sufficient, but optimal.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;
&lt;p&gt;Processing 131,391 files on a modern machine took:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Search completed: 2026-08-14 07:51:49.609437
Files processed: 131,391
New palindromes found: 217
Total time: 6:48:27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;~6 hours 48 minutes for the entire main corpus. No parallelization, no optimization tricks — just straightforward file-by-file processing. For a one-time research task, this is perfectly acceptable.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;key-functions&quot;&gt;Key functions&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def normalize_text(text):
    &quot;&quot;&quot;Remove punctuation, lowercase, convert ё→е&quot;&quot;&quot;
    cleaned = re.sub(r'[^а-яёА-ЯЁ]', '', text)
    cleaned = cleaned.replace('ё', 'е')
    return cleaned.lower()

def is_palindrome(word_sequence):
    &quot;&quot;&quot;Check if a sequence of words forms a palindrome&quot;&quot;&quot;
    if len(word_sequence) &amp;lt; 2:
        return False
    # Exclude sequences where all words are identical
    if all(w == word_sequence[0] for w in word_sequence):
        return False
    combined = ''.join(word_sequence)
    normalized = normalize_text(combined)
    return len(normalized) &amp;gt;= 10 and normalized == normalized[::-1]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;automated-pipeline&quot;&gt;Automated pipeline&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;palindromes.py&lt;/code&gt; automatically launches &lt;code&gt;palindromes_paper.py&lt;/code&gt; after completion — no manual intervention needed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;results&quot;&gt;Results&lt;/h3&gt;
&lt;h4 id=&quot;accidental-palindromes-with-corpus-context-&quot;&gt;Accidental palindromes (with corpus context)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;English translation &lt;/th&gt;&lt;th&gt;Original &lt;/th&gt;&lt;th&gt;Context&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;more and more and more&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;еще и еще и еще&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/P1qA4&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;to look for a taxi&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;искать такси&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/Q1rB9&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;he is right here, but&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;он тут как тут но&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/MjnxG&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;water from ships&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;воду с судов&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/Nxoyv&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;in Odessa before&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;одессе до&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/RgvDE&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;and mannequins&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;и манекенами&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/s/O7pzr&quot;&gt;link&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;newspaper-corpus-finds&quot;&gt;Newspaper corpus finds&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;English translation &lt;/th&gt;&lt;th&gt;Original &lt;/th&gt;&lt;th&gt;Note&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;Ani Lorak, Carolina&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Ани Лорак Каролина&lt;/code&gt; &lt;/td&gt;&lt;td&gt;The singer's real name&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Sonic in cinema with&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;соник в кино с&lt;/code&gt; &lt;/td&gt;&lt;td&gt;From a 2019 film review&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;or infected&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;или заразили&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Random coincidence in a news text&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;beautiful-crafted-palindromes-found-accidentally-in-citations-&quot;&gt;Beautiful crafted palindromes (found accidentally in citations)&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;English translation &lt;/th&gt;&lt;th&gt;Original&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&quot;The cat is learned, but how insensitive he is!&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Кот учен, но как он нечуток!&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;And in the window, Kirichenkova was chirping&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;А в окне чирикала Кириченкова&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Lenin ate noodles by the sleepers&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;У шпал Ленин ел лапшу&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Muse, wounded by experience's awl, you will pray to reason&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Муза, ранясь шилом опыта, ты помолишься на разум&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;You and I are gods and the yoke of existence&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Я и ты — боги и иго бытия&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;Dante's hell is revealed in deed&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Да вот на деле дантов Ад&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&quot;The end of marks&quot; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Конец оценок&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;common-natural-patterns&quot;&gt;Common natural patterns&lt;/h4&gt;
&lt;p&gt;We found entire &lt;strong&gt;classes&lt;/strong&gt; of palindromes following the same template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X and X and X
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;еще и еще и еще&lt;/code&gt; (more and more and more)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;летел и летел&lt;/code&gt; (flew and flew)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;лил и лил и лил&lt;/code&gt; (poured and poured and poured)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Repetitive structures with conjunctions naturally produce palindromic effects.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;post-processing-cleaning-the-noise&quot;&gt;Post-processing: Cleaning the Noise&lt;/h3&gt;
&lt;p&gt;The algorithm finds &lt;strong&gt;formal&lt;/strong&gt; palindromes that are semantically meaningless:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;е о а а а са а а о е          → random letters
м ммм м м м м м м             → just 'm' variations
жжж жжж жжж ууу ууу ууу жжж → garbage
э эээ эээээээээ               → meaningless
с ссссс сссс ссссс            → noise
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These were &lt;strong&gt;manually removed&lt;/strong&gt; from the results to keep only meaningful or at least amusing phrases.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accidental palindromes exist&lt;/strong&gt; — they appear naturally in texts without anyone trying.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Most are short phrases&lt;/strong&gt; built from repetitive or common words.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context is everything&lt;/strong&gt; — some look constructed, but the corpus context proves they're accidental.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Crafted palindromes sneak in anyway&lt;/strong&gt; — they're quoted in articles, and you can't automatically distinguish them without manual verification.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You don't need Manacher&lt;/strong&gt; — if you're searching for meaningful phrases, the naive approach with word boundaries works better than character-level algorithms.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Manual cleaning is essential&lt;/strong&gt; — algorithms find too much noise; semantic filtering requires human judgment.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://ruscorpora.ru/&quot;&gt;Russian National Corpus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Longest_palindromic_substring&quot;&gt;Manacher's algorithm (Wikipedia)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://nevmenandr.github.io/portfolio/assets/pdf/rhythm_prose.pdf&quot;&gt;Random meters in prose (paper)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://t.me/schonenrede/882&quot;&gt;Telegram discussion (Russian)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24650/finding-accidental-palindromes-in-130-000-russian-texts</guid>
<pubDate>Sat, 05 Sep 2026 17:36:00 +0000</pubDate>
</item>
</channel>
</rss>