Social Representation Analysis via Semantic Network Mapping
themescopeR implements the ThemeScope
framework for detecting and visualising social representations
in large-scale digital text corpora. From raw documents it builds
sentence-level word co-occurrence networks, detects
thematic communities, and locates them on a
two-dimensional strategic diagram using two indicators
grounded in Social Representation Theory (SRT):
The whole analysis runs from the R console; an optional Shiny GUI offers the same pipeline point-and-click and is backed by the very same exported functions.
Using themescopeR or the ThemeScope method requires
citing the paper that introduces it. Publishing results without
attribution violates the terms of use.
Misuraca, M., Spano, M., and D’Aniello, L. (2026). ThemeScope: A quantitative thematic analysis for depicting social representations in digital arenas. Journal of Information Science. doi:10.1177/01655515261454276
@article{misuraca2026themescope,
author = {Misuraca, Michelangelo and Spano, Maria and D'Aniello, Luca},
title = {ThemeScope: A quantitative thematic analysis for depicting social representations in digital arenas},
journal = {Journal of Information Science},
year = {2026},
doi = {10.1177/01655515261454276},
url = {https://doi.org/10.1177/01655515261454276}
}In R, citation("themescopeR") prints the same
reference.
# install.packages("remotes")
remotes::install_github("lucadaniello/themescopeR")The optional GUI additionally needs: shiny,
bslib, DT, plotly,
visNetwork, shinycssloaders.
writexl enables the Excel exports.
library(themescopeR)
# 1. Read a collection of raw documents (csv / tsv / txt / xlsx / RData / zip)
coll <- read_collection(
system.file("extdata", "sample_collection.csv", package = "themescopeR"),
sequential_ids = TRUE # documents numbered doc_1, doc_2, ...
)
# 2. Annotate with udpipe (the language model is downloaded once and cached)
tokens <- preprocess_texts(coll, model = "english")
# 3. Run the full ThemeScope pipeline
result <- themescope(tokens, vocab_size = 1500, seed = 1)
# 4. Inspect and visualise
print(result)
as.data.frame(result) # community-level table: PSI, CS, z-scores, quadrant
plot(result, type = "map") # the ThemeScope strategic diagram (ggplot2)
plot(result, type = "network")
top_terms(result, n = 10) # most representative terms per community
# 5. Save the whole study in one file, and reopen it later
save_themescope(result, "climate_study.themescope", words = tokens, collection = coll)
archive <- read_themescope("climate_study.themescope")
summary(archive)themescope() can also start directly from raw
texts, annotating internally:
result <- themescope(coll, model = "english", vocab_size = 1500, seed = 1)To skip the annotation step entirely while trying the package out, the bundled demo corpus ships already tokenised, lemmatised and POS tagged (English GUM treebank):
tokens <- readRDS(system.file("extdata", "demo_annotated.rds", package = "themescopeR"))
result <- themescope(tokens, seed = 1)Preprocessing uses the updated Universal Dependencies 2.15
udpipe models maintained for TALL.
ts_list_models() # available languages / treebanks
ts_download_model("italian") # download + cache (no-op if already cached)
themescope_cache_dir() # where models would be stored (creates nothing)
ts_clear_cache() # delete everything that was cachedThe cache lives in
tools::R_user_dir("themescopeR", "cache"); override it with
options(themescopeR.model_dir = "/your/path"), or pass
model_dir to a single call
(ts_download_model("italian", model_dir = tempdir())) to
keep a download inside the session.
The directory is created only when a model is actually downloaded,
that is when you call ts_download_model() or hand a
language name to preprocess_texts(). Asking where the cache
is creates nothing, and ts_clear_cache() removes the models
and the directory again. Nothing else in the package writes outside the
session temporary directory.
themescope_app()The interface follows the pipeline step by step, and every step calls the exported functions, so the GUI and the console give identical results.
.themescope archive,
or the bundled demo corpus. Tabular files let you pick which column
holds the text. Documents are numbered doc_1,
doc_2, … with the original identifier kept as
source_id. A file bigger than the current upload limit
opens a window offering to raise it, then imports automatically.The results are shown as an interactive map and network (ThemeScope tab), a filterable community table, and top terms as both charts and collapsible per-cluster lists. The Reproduce tab prints the console code equivalent to what the GUI just did, and Export .themescope saves the annotated corpus, the parameters and the term scores in a single file.
The package mirrors the ThemeScope pipeline as a chain of small,
pure, command-line functions. themescope() runs them all in
order, but each stage is exported so you can inspect or customise any
step. They are presented below in the order the pipeline calls them.
read_collection()read_collection(path, text_col = NULL, id_col = NULL, sequential_ids = FALSE)Reads a document collection into a tidy data frame with a
doc_id and a text column. It auto-detects the
format from the file extension and supports a single
.csv/.tsv, .txt,
.xlsx/.xls,
.RData/.rda, or a
.zip containing many such files (unzipped to a temporary
directory, read, and row-bound). Text and id columns are guessed but can
be forced with text_col / id_col. With
sequential_ids = TRUE documents are numbered
doc_1, doc_2, … and the identifier that came
with the file is kept as source_id, which is handy when the
source ids are long hashes. Malformed input raises a clear, actionable
error.
preprocess_texts()preprocess_texts(collection, model = "english", text_col = "text",
doc_id_col = "doc_id", verbose = TRUE)Annotates the raw texts with udpipe:
tokenisation, lemmatisation, sentence segmentation and part-of-speech
(UPOS) tagging. model accepts a language name, a
.udpipe file path, or a loaded model object; language
models come from the updated Universal Dependencies 2.15 treebanks and
are downloaded once and cached (see ts_list_models(),
ts_download_model(), ts_model_path(),
themescope_cache_dir()). It returns the
full udpipe annotation (one row per token, with
doc_id, sentence_id, token,
lemma, upos and the rest), which is the input
every downstream function expects.
build_vocab()build_vocab(words_df, unit = "lemma", vocab_size = 1500,
pos_filter = c("NOUN", "ADJ", "PROPN"))Filters the annotation to content words (by default nouns, adjectives
and proper nouns), counts them on the chosen unit
("lemma" or "token"), and keeps the
vocab_size most frequent terms. Returns a data frame of
term, freq and upos, the node set
of the network.
build_cooccurrence_matrix()build_cooccurrence_matrix(words_df, vocab, unit = "lemma",
window = "sentence", normalization = "association")Counts how often each pair of vocabulary terms appears together
within the same window ("sentence" or
"document") and rescales the raw co-occurrence counts into
a similarity matrix via normalization. The default
"association" (Association Strength,
AS(t,t') = a_tt' / (a_t * a_t')) is the measure used in the
paper; "jaccard", "salton",
"inclusion", "equivalence" and
"frequency" are also available
(normalize_cooccurrence(),
compute_association_strength() expose them individually).
It returns both the normalised matrix and each term’s
presence a_t (sentence/document count),
which PSI needs later.
build_cooccurrence_network()build_cooccurrence_network(as_matrix, threshold_percentile = 0.98)Turns the similarity matrix into a weighted, undirected
igraph network, keeping only the strongest edges (those
above the threshold_percentile) and dropping isolated
nodes. This sparse backbone is what gets partitioned into themes.
detect_communities()detect_communities(graph, algorithm = "walktrap", min_size = 10, seed = NULL)Partitions the network into thematic communities
(candidate social representations) with "walktrap"
(default), "louvain" or "leiden". Communities
smaller than min_size are dropped; pass a seed
for reproducible results. get_community_subgraphs() splits
the graph into one subgraph per community.
compute_psi() and
compute_cs()compute_psi(graph, communities, presence)
compute_cs(graph, communities, concreteness_lexicon = brysbaert)compute_psi(), the
Prototypical Salience Index (anchoring): how
structurally salient and frequent a community’s terms are within the
wider discourse, normalised by community density.compute_cs(), the Concreteness
Score (objectification): the association-weighted mean
concreteness of a community’s terms, using the bundled
brysbaert lexicon (lexicon_coverage() /
match_concreteness() report and apply the matching).Both return one value per community; themescope()
z-scores them and places each community in a quadrant
(assign_quadrant(), zscore()).
term_relevance() and
top_terms()term_relevance(graph, membership, presence)
top_terms(x, n = 10, by = "relevance") # or by = "frequency" / "degree"Rank the terms inside each community so each theme can be labelled by
its most characteristic words. term_relevance() implements
the case-study relevance measure
\[R_t(g_i) = \log(1 + a_t)\cdot\frac{s_t^{\text{in}}(g_i)}{s_t^{\text{in}}(g_i) + s_t^{\text{out}}(g_i)},\]
where a_t is the term presence, and s_t^in
/ s_t^out are the total association strength linking
t to terms inside / outside its own community.
top_terms() ranks by "relevance" (the default
R_t, which downweights generic terms shared across
clusters), by "frequency" (term presence a_t
alone), or by "degree". Relevance is the labelling method
used throughout ThemeScope; frequency is kept as a simpler
alternative.
plot_themescope() and
plot_network()plot_themescope(psi, cs, ...) # the strategic diagram (ggplot2)
plot_network(graph, ...) # the coloured co-occurrence networkplot_themescope() draws the z-scored PSI by CS
strategic diagram with the four labelled quadrants;
plot_network() draws the community-coloured network. Both
share themescope_colours(), so a community keeps the same
colour on the map and on the network. On a themescope
object simply call plot(result, type = "map" | "network");
plot(result, label = "terms") labels each community with
its top terms (ranked by relevance by default; pass
label_by = "frequency" to label by term frequency
instead).
save_themescope() and
read_themescope()save_themescope(result, "study.themescope", words = tokens, collection = coll,
meta = list(language = "english", lexicon = "Brysbaert"))
archive <- read_themescope("study.themescope")
summary(archive) # corpus, parameters, vocabulary, network, lexicon
plot(archive$result, type = "map", label = "terms")A .themescope file is a compressed archive holding the
themescope object, the annotated corpus it was computed
from, the raw texts, the term-level table with the scores behind the
maps, the community table and the parameters of the run. Reopening it
restores maps, communities and top terms with nothing recomputed, in the
console or in the GUI.
themescope() and
themescope_app()themescope() runs stages 3 to 9 (and stages 1 to 2 if
you hand it a raw collection plus a model) and returns a
structured themescope object with print(),
summary(), plot() and
as.data.frame() methods. themescope_app()
launches the Shiny GUI, which calls these same exported functions.
| Function | Purpose |
|---|---|
read_collection() |
Import raw documents (single file or zip of many) into a tidy data frame |
preprocess_texts() |
udpipe annotation; returns the full udpipe data frame |
ts_download_model(), ts_list_models(),
ts_model_path(), ts_clear_cache() |
Manage cached language models |
build_vocab() |
Vocabulary as a data frame (term, freq,
upos) |
build_cooccurrence_matrix() |
Sentence/document co-occurrence + chosen
normalization |
normalize_cooccurrence(),
compute_association_strength() |
Similarity measures (association, jaccard, salton, inclusion, equivalence) |
build_cooccurrence_network() |
Thresholded weighted network |
detect_communities(),
get_community_subgraphs() |
Walktrap / Louvain / Leiden community detection |
compute_psi(), compute_cs() |
The two SRT indicators |
term_relevance(), top_terms() |
Representative terms per community |
plot_themescope(), plot_network(),
themescope_colours() |
Map, igraph network, shared palette |
themescope() |
End-to-end pipeline (words or raw text) into a
themescope object |
save_themescope(), read_themescope() |
Write and reopen a .themescope archive |
themescope_app() |
Launch the Shiny GUI |
Key options of themescope() /
build_cooccurrence_matrix(): unit
("lemma"/"token"), window
("sentence"/"document"),
normalization ("association",
"jaccard", "salton", "inclusion",
"equivalence", "frequency"), and
community_algorithm ("walktrap",
"louvain", "leiden").
For communities in the Association-Strength network
(AS(t,t') = a_tt' / (a_t * a_t'), with a_t the
sentence presence of term t):
| Indicator | Definition | SRT construct |
|---|---|---|
| PSI | Psi(g_i) = ( sum_{t in g_i} a_t * s_t ) / max_j delta(g_j) |
Anchoring |
| CS | Cw(g_i) = ( sum_{(t,t') in E_i} ((c(t)+c(t'))/2) * AS(t,t') ) / ( sum AS(t,t') ) |
Objectification |
where s_t is the mean Association Strength of
t’s in-community neighbours,
delta(g_i) = 2 W_i / (|V_i|(|V_i|-1)) the weighted
community density, and c(.) the Brysbaert concreteness
rating. Communities are placed on the z-scored PSI by CS plane:
| High CS | Low CS | |
|---|---|---|
| High PSI | Stable Core | Ideological Core |
| Low PSI | Emerging Practices | Latent Representations |
brysbaert: 39,954
English concreteness norms (Brysbaert, Warriner and Kuperman, 2014,
Behavior Research Methods, doi:10.3758/s13428-013-0403-5).inst/extdata/sample_collection.csv: a 1000-document random
sample of the publicly available Public
Opinion on Climate Change Reddit dataset on Kaggle, plus
inst/extdata/demo_annotated.rds, the same 1000 documents
already annotated with udpipe.Michelangelo Misuraca, Maria Spano, Luca D’Aniello (maintainer).
MIT, 2026. See LICENSE.