Analytical Methods
This document describes the computational analysis pipeline applied to the cleaned corpus: corpus preparation for topic modeling, model selection and training, topic labeling and categorization, and the preparation of Datawrapper chart data. The main scripts for this stage are in scripts/build_mallet/ and scripts/build_datawrapper_data/.
Topic Modeling
Topic modeling was conducted using MALLET, the Machine Learning for Language Toolkit, which implements Latent Dirichlet Allocation with efficient Gibbs sampling. Two parallel model runs were performed: a deduplicated MALLET corpus of 1,100 documents and a full corpus of 1,525 documents including reprints. Comparing results across both corpora reveals how the reprinting network amplified certain discourse patterns.
Corpus preparation
Corpus preparation proceeded in three scripted steps before any MALLET command was invoked.
Step 1: Document normalization (mallet_1.py)
Each document was assigned a unique identifier (DOC_000001 through DOC_001535) and two text representations were generated:
dedup_text: All text lowercased, punctuation and digits removed. Used only for similarity-based reprint detection in the next step.model_text: Lightly cleaned text preserving original wording. Used as MALLET input.
Step 2: Reprint detection and deduplication (mallet_2.py)
Nineteenth-century newspapers routinely copied articles verbatim or with minor edits. This step identifies reprint chains using word trigram Jaccard similarity. Two documents are flagged as probable reprints if they share at least 20% of their trigrams. For short or truncated texts, one text must be largely contained within the other. The Jaccard threshold is 0.20, and the containment threshold is 0.50.
For each reprint group:
- The longest article in each reprint group is included in the deduplicated MALLET corpus with its full text.
- Other group members are included only if they contribute at least 20 unique words after sentence-level deduplication. Sentences are treated as shared when their overlap score reaches 0.50. If a document meets this threshold, a deduplicated version of its text is used as the MALLET input for the deduplicated run.
The result is two MALLET-ready text files:
corpus_for_mallet.txt: 1,100 documents selected for the deduplicated run, including non-reprinted articles, the longest article from each reprint group, and group members with sufficient unique textcorpus_for_mallet_all.txt: 1,525 documents, full corpus including reprints
The two exports deliberately use different text fields. The deduplicated export uses mallet_ready_text, which may contain text with shared sentences removed for selected members of reprint groups. The full corpus export does not filter on use_for_mallet; it uses model_text so that each reprinted article remains in the corpus as a full document. This lets the full corpus model reflect actual circulation intensity rather than only textual uniqueness.
Step 3: Temporal and regional labeling (mallet_3.py)
Each document was enriched with metadata fields used later for time-series and regional analysis:
| Field | Description |
|---|---|
time_bin | Publication year (YYYY) |
month | Publication month (1–12) |
year_month | Publication year-month (YYYY-MM) |
region_bin | U.S. Census region: Northeast, Midwest, South, West |
The state-to-region mapping covers all 50 states, Washington D.C., and historical territories.
Model training
Both corpora were imported with MALLET's import-file command using --keep-sequence, which is required for LDA, and --remove-stopwords. In addition to MALLET's built-in English stopword list, a custom stopword file, scripts/build_mallet/custom_stopwords.txt, was compiled specifically for this corpus. It removes high-frequency terms that are uninformative for topical discrimination in this particular collection, including newspaper boilerplate such as said, would, and made, OCR artifacts, and corpus-specific proper nouns that would otherwise dominate topic keys without adding interpretive value.
The deduplicated corpus was imported first:
mallet import-file \
--input corpus_for_mallet.txt \
--output corpus.mallet \
--keep-sequence \
--remove-stopwords \
--extra-stopwords custom_stopwords.txtThe K value scan was then run across three topic counts and three random seeds:
for K in 20 25 30; do
for SEED in 1 2 3; do
mallet train-topics \
--input corpus.mallet \
--num-topics $K \
--num-iterations 10000 \
--optimize-interval 10 \
--output-state output/state_K${K}_S${SEED}.gz \
--output-topic-keys output/keys_K${K}_S${SEED}.txt \
--output-doc-topics output/doc-topics_K${K}_S${SEED}.txt \
--num-top-words 25 \
--diagnostics-file output/diag_K${K}_S${SEED}.xml \
--random-seed $SEED
done
done| Parameter | Value | Purpose |
|---|---|---|
--num-topics | 20, 25, 30 | Tests several possible topic counts |
--num-iterations | 10,000 | Gives the model enough time to settle |
--optimize-interval | 10 | Updates model settings during training |
Selecting the number of topics
After the candidate models were trained, evaluate_k.py evaluated the resulting topic keys files against the deduplicated corpus vocabulary. The script uses Gensim's CoherenceModel with the c_v metric to score each K and seed combination.
| K | Seed 1 | Seed 2 | Seed 3 | Mean |
|---|---|---|---|---|
| 20 | 0.4680 | 0.4788 | 0.4898 | 0.4789 |
| 25 | 0.4892 | 0.4988 | 0.5069 | 0.4983 |
| 30 | 0.4885 | 0.4950 | 0.4829 | 0.4888 |
K = 25 was selected based on coherence scores and interpretability. It produced internally consistent topics while maintaining sufficient granularity to distinguish sub-discourses within the largest categories, especially education and the Chinese Educational Mission.
INFO
K = 30 split the results too much, creating several topics that looked almost the same. K = 20 grouped different patterns together, such as missionary schools and public school admission.
Final model runs
After K = 25 was selected, the full corpus was imported with the same import-file settings, using corpus_for_mallet_all.txt as input and corpus_all.mallet as output. It was then trained at K = 25 across the same three seeds. The full corpus outputs used the all_ prefix, for example all_keys_K25_S1.txt, all_doc-topics_K25_S1.txt, and all_diag_K25_S1.xml, so they did not overwrite the deduplicated corpus results.
The final analysis uses the following saved model runs:
- Deduped model, K25_S2: trained on the deduplicated corpus of 1,100 documents
- Full corpus model, K25_S1: trained on the full corpus of 1,525 documents
INFO
The two models answer different questions. The deduped model reduces the effect of repeated boilerplate and copied passages, while the full corpus model shows what readers were more likely to see, including repeated reprints across many newspapers.
Topic labeling and categorization
Each of the 25 topics in each model was manually reviewed using the highest-probability words and the top documents. In the deduplicated run, five topics were excluded as noise; in the full-corpus run, three topics were excluded as noise. These excluded topics consisted primarily of OCR artifacts, empty or near-empty vocabulary, advertising boilerplate, symbols, church directories, or garbled tokens. This left 20 substantive topics in the deduplicated model and 22 substantive topics in the full-corpus model. Topics were labeled at two levels:
- Analytic label: a short descriptive title for the topic itself, based on its word list and representative documents (e.g., "Public School Admission").
- Thematic category: a broader interpretive grouping used to compare related topics across model runs and to structure the visualizations.
Because the deduplicated and full-corpus models sometimes split or merge the same discourse differently, categories were assigned at the interpretive level rather than by requiring one-to-one topic matches. Across both model runs, substantive topics were grouped into 10 thematic categories:
| Thematic category | Analytic labels |
|---|---|
| Chinese Educational Mission | Chinese Educational Mission; CEM: Government Policy & Institutional Recall; CEM: Student Lives & Personal Narratives; CEM: Students & Cross-Cultural Marriage; CEM: Political Controversy |
| Education & Schools | Public School Admission, Missionary & Church Schools, Classroom Instruction, Mission School Directories & Schedules |
| Children & Family | Children & Family Life, Confucian Family Ethics, Childhood Conditions & Moral Commentary |
| Daily Life & Urban Space | Domestic Employment, Chinatown Spatial Narratives, Routine Press Reporting |
| Law, Politics & Exclusion | Exclusion Legislation, Criminal Cases & Court Proceedings, Criminal Cases & Court Proceedings (Police), Criminal Cases & Court Proceedings (Trials) |
| Violence & War | Anti-Chinese Violence, Sino-French War |
| Commerce & Material Culture | East Asian Consumer Goods, Trade in Chinese Goods & Furnishings, Clothing & Physical Description, Social Dining & Interior Spaces |
| Land, Migration & Labor | Hawaii & Pacific Migration, Agriculture & Land Use |
| Culture, Perception & Acculturation | Physical Appearance & Curiosity Narratives, Public Gatherings & Lectures, Language Learning & Cultural Adjustment, Opium & Moral Degradation |
| Diplomacy | Diplomacy & Ceremonial Events |
Topic labels, category assignments, and color codes are stored in data/topic_labels.csv (deduped model), data/all_topic_labels.csv (full-corpus model), and data/merged_topic_labels.csv (cross-model comparison).
Datawrapper Visualizations
All charts embedded in the analysis pages were built using Python preprocessing scripts that output Datawrapper-ready CSV files. Each CSV was uploaded to Datawrapper and configured manually for color, annotation, and interactivity. The charts are embedded in the site via the <DatawrapperChart> Vue component.
Scatter plot: thematic landscape (Chart 1)
Script: scripts/build_datawrapper_data/chart1_build_scatter.py
Two scatter plot CSVs are generated from the MALLET output and dataset metadata.
The analysis scatter (scatter_analysis.csv) places each document on a grid with publication year on the horizontal axis and topic category on the vertical axis. Deterministic jitter (seed = 42) is applied to both axes to prevent overplotting. Documents are colored by topic category using the category_color field from the topic labels CSV.
The showcase scatter (scatter_showcase.csv) is used for homepage display. It uses exact publication date on the horizontal axis and dominant topic confidence (weight × 100) on the vertical axis. For each document, a KWIC (Key Word in Context) snippet is extracted from the model_text field using a tiered keyword pattern: Chinese-education collocations are prioritized, followed by general Chinese mentions, with a 120-character context window.
Education topics time series (Chart 2)
Script: scripts/build_datawrapper_data/chart2_edu_lines.py
The MALLET doc-topics file is parsed to extract per-document weights for four education-related topics: Chinese Educational Mission, Classroom Instruction, Public School Admission, and Missionary & Church Schools. Weights are aggregated by publication month (year_month) and converted to percentages. The output is a wide-format CSV with one row per month and one column per topic, used to draw the four-line time series chart.
Topic prominence comparison (Charts 4, 5, 5B)
Scripts: chart4_build_split.py, chart5_build_dotplot.py, chart5B_build_dotplot.py
These charts compare topic weights between the deduplicated and full corpora. Mean topic weights are computed separately for both model runs. The dumbbell and dot-plot charts show each topic as a paired point (deduplicated weight in blue, full-corpus weight in red) with a connecting bar indicating the magnitude and direction of the reprint effect. The bar chart (chart5B) ranks topics by the percentage-point difference between the two corpora.
Alluvial flow diagram (Chart 5C)
Script: scripts/build_datawrapper_data/chart5C_build_alluvial_flow.py
This chart maps the 1,100 documents present in both corpora to their dominant category in each model, plus the 425 reprint-only documents that appear only in the full-corpus model. For the shared documents, the dominant category is compared across both model runs; for reprint-only documents, the source node is labeled "New (reprints)."
The output is an edge-list CSV (source category, target category, count). The Sankey diagram itself was built using RAWGraphs, an open-source data visualization framework, and exported as an SVG for embedding.
Heatmap: keyword-to-topic loadings (Chart 7)
Script: scripts/build_datawrapper_data/chart7_build_heatmap.py
For each of the seven search keywords, the mean topic weight across all documents retrieved with that keyword is computed. The result is a keyword × topic matrix showing which topics are disproportionately associated with each search term. This reveals how the choice of vocabulary shaped what the corpus contains.
Entropy chart (Chart 8)
Script: scripts/build_datawrapper_data/chart8_build_entropy.py
Shannon entropy of the topic weight distribution is computed for each publication month. Higher entropy indicates that a month's coverage was spread across many topics; lower entropy indicates narrowing onto a few dominant themes. This chart tracks discourse concentration across the full 1880–1885 window.
Bubble chart: presence vs. propagation (Chart 9)
Script: scripts/build_datawrapper_data/chart9_spread.py
Each topic category is plotted as a bubble. The horizontal axis shows total article count (presence); the vertical axis shows average reprint count per article (propagation). Bubble size is proportional to total article count. This makes the distinction between topics that were written about frequently and topics that spread widely through reprinting visually explicit.