# %pip install spacy networkx sentence-transformers pandas matplotlib
# python -m spacy download en_core_web_smNotebook 0 — Preview the corpus’s language (no audio, no ASR)
MDIG2026 Summer School · NLP track (0 / 5) — the 2-minute preview
Before transcribing anything, you can already see what kind of language lives in this dataset, because the game words are in metadata.csv: 40 target words, each with 5 taboo words. This notebook gives a fast, zero-setup look:
- spaCy — what kinds of words are these (part of speech, lemma)?
- A semantic network graph — embed every word and connect the similar ones, so the dataset’s semantic structure (clusters, hub concepts) becomes a picture. Nice for a poster or a first slide.
- (optional) once you’ve run Notebook 1, the same idea on the spoken words.
No GPU; nothing here needs the audio.
0 · Install (run once)
1 · Load the game vocabulary (targets + taboos)
import os, certifi
if not os.path.exists(os.environ.get("SSL_CERT_FILE", "")): # fix broken Anaconda cert path
os.environ["SSL_CERT_FILE"] = certifi.where(); os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
def find_corpus(start=None):
d = os.path.abspath(start or os.getcwd())
for _ in range(8):
if os.path.isfile(os.path.join(d, "metadata.csv")) and os.path.isdir(os.path.join(d, "audios")):
return d
parent = os.path.dirname(d)
if parent == d: break
d = parent
raise FileNotFoundError("Could not find the BalanceCorpus root (metadata.csv + audios/).")
CORPUS = find_corpus()
TRANSCRIPTS = os.path.join(CORPUS, "scripts", "nlp_taboo", "transcripts")
print("Corpus root:", CORPUS)import pandas as pd
meta = pd.read_csv(os.path.join(CORPUS, "metadata.csv"), encoding="utf-8-sig")
# 40 unique target words, each with its five taboo words
vocab = meta.drop_duplicates("target_word")[
["target_word", "taboo_1", "taboo_2", "taboo_3", "taboo_4", "taboo_5"]].reset_index(drop=True)
print(f"{len(vocab)} target words, each with 5 taboos "
f"({vocab.shape[0]*5} taboo slots).")
vocab.head(6)2 · spaCy — what kind of words are these?
We tag every word with its part of speech and lemma. Targets in a Taboo set are concrete nouns; the taboos are the words you’d most naturally reach for — a mix of related nouns, properties (adjectives), and actions (verbs). Let’s confirm.
import spacy
try:
nlp = spacy.load("en_core_web_sm")
except OSError:
raise OSError("Run: python -m spacy download en_core_web_sm")
def pos_of(word):
doc = nlp(str(word))
return doc[0].pos_ if len(doc) else "X"
targets = vocab.target_word.tolist()
taboos = sorted({str(vocab.loc[i, f"taboo_{k}"]) for i in vocab.index for k in range(1, 6)})
import collections
tgt_pos = collections.Counter(pos_of(w) for w in targets)
taboo_pos = collections.Counter(pos_of(w) for w in taboos)
import matplotlib.pyplot as plt
labels = sorted(set(tgt_pos) | set(taboo_pos), key=lambda p: -(tgt_pos[p] + taboo_pos[p]))
x = range(len(labels)); w = 0.4
fig, ax = plt.subplots(figsize=(7, 3.2))
ax.bar([i - w/2 for i in x], [tgt_pos[p] for p in labels], w, label="targets", color="#c44")
ax.bar([i + w/2 for i in x], [taboo_pos[p] for p in labels], w, label="taboos", color="#48a")
ax.set_xticks(list(x)); ax.set_xticklabels(labels); ax.set_ylabel("count"); ax.legend()
ax.set_title("Part-of-speech mix: targets vs taboo words")
plt.tight_layout(); plt.show()
print("targets:", dict(tgt_pos)); print("taboos :", dict(taboo_pos))3 · The semantic network of the whole game
Now the fun part. We embed every word (targets + taboos) and draw a graph: each word is a node, and we connect two words when they are semantically similar (cosine above a threshold). Communities in that graph are the dataset’s semantic neighbourhoods — the themes the players had to talk around.
try:
from sentence_transformers import SentenceTransformer
except ModuleNotFoundError:
raise ModuleNotFoundError("sentence-transformers is not installed — run the install cell "
"in Section 0, then re-run this cell.")
import numpy as np
EMB = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2")
words = list(dict.fromkeys(targets + taboos)) # de-duplicated, order-preserving
is_target = {w: (w in set(targets)) for w in words}
V = EMB.encode(words, normalize_embeddings=True, show_progress_bar=False)
S = V @ V.T # cosine similarity matrix
print(f"{len(words)} unique words embedded -> {S.shape} similarity matrix")import networkx as nx
THRESHOLD = 0.45 # connect words more similar than this (raise -> sparser graph)
G = nx.Graph()
G.add_nodes_from(words)
for i in range(len(words)):
for j in range(i + 1, len(words)):
if S[i, j] >= THRESHOLD:
G.add_edge(words[i], words[j], weight=float(S[i, j]))
# drop isolated nodes so the picture is about the connected structure
G.remove_nodes_from(list(nx.isolates(G)))
# communities = semantic neighbourhoods
comms = list(nx.algorithms.community.greedy_modularity_communities(G))
color_of = {w: k for k, cset in enumerate(comms) for w in cset}
print(f"{G.number_of_nodes()} connected words, {G.number_of_edges()} edges, "
f"{len(comms)} semantic communities")pos = nx.spring_layout(G, seed=1, k=0.5)
cmap = plt.get_cmap("tab20", max(len(comms), 1)) # plt.get_cmap works on all matplotlib versions
fig, ax = plt.subplots(figsize=(11, 8))
nx.draw_networkx_edges(G, pos, alpha=.15, ax=ax)
for node in G.nodes():
nx.draw_networkx_nodes(
G, pos, nodelist=[node], ax=ax,
node_color=[cmap(color_of.get(node, 0))],
node_size=320 if is_target[node] else 90,
edgecolors="black" if is_target[node] else "none", linewidths=1.2)
nx.draw_networkx_labels(
G, pos, ax=ax,
font_size={True: 9, False: 7}.get(True), # base size; targets stand out via ring+size
font_color="black")
ax.set_title(f"Semantic network of Taboo words (cos ≥ {THRESHOLD})\n"
"large ringed = target words · colour = semantic community")
ax.axis("off"); plt.tight_layout(); plt.show()# Read the structure out in words: hub concepts and the biggest neighbourhoods
deg = dict(G.degree())
hubs = sorted(deg, key=deg.get, reverse=True)[:10]
print("Most-connected 'hub' words:", ", ".join(f"{w}({deg[w]})" for w in hubs))
print()
for k, cset in enumerate(sorted(comms, key=len, reverse=True)[:6]):
tgts = [w for w in cset if is_target[w]]
print(f"community {k+1} ({len(cset)} words) — targets: {', '.join(tgts) or '—'}")
print(" ", ", ".join(sorted(cset))[:110], "...")4 · Zoom in: one target and its taboo trap
For a single target, here is the designed structure — the target linked to its five taboos — laid out by real semantic similarity. This is exactly the “boundary” a clue-giver must talk around in Notebook 2.
def taboo_star(target):
row = vocab[vocab.target_word == target].iloc[0]
tb = [str(row[f"taboo_{k}"]) for k in range(1, 6)]
sub = [target] + tb
vs = EMB.encode(sub, normalize_embeddings=True, show_progress_bar=False)
g = nx.Graph()
for i in range(len(sub)):
for j in range(i + 1, len(sub)):
g.add_edge(sub[i], sub[j], weight=float(vs[i] @ vs[j]))
p = nx.spring_layout(g, seed=2)
fig, ax = plt.subplots(figsize=(5.5, 4))
nx.draw_networkx_edges(g, p, ax=ax,
width=[3*g[u][v]["weight"] for u, v in g.edges()], alpha=.4)
nx.draw_networkx_nodes(g, p, ax=ax, nodelist=[target], node_color="#c44", node_size=1400)
nx.draw_networkx_nodes(g, p, ax=ax, nodelist=tb, node_color="#48a", node_size=900)
nx.draw_networkx_labels(g, p, ax=ax, font_size=10, font_color="white")
ax.set_title(f"'{target}' and its taboo words (edge width = similarity)")
ax.axis("off"); plt.tight_layout(); plt.show()
taboo_star(vocab.target_word.iloc[0]) # first target in the data (robust to any dataset)5 · Optional — the same idea on spoken words
If you’ve already run Notebook 1, we can preview the actual speech: build a co-occurrence network of the content words the clue-givers used (words that appear together in a trial). This skips itself if no transcripts exist yet.
import re, itertools
tx_path = os.path.join(TRANSCRIPTS, "transcripts_all.csv")
if not os.path.exists(tx_path):
print("No transcripts yet — run Notebook 1 first to enable this preview.")
else:
tx = pd.read_csv(tx_path, encoding="utf-8")
STOP = nlp.Defaults.stop_words
def content(text):
return {t.lemma_.lower() for t in nlp(str(text))
if t.is_alpha and t.lemma_.lower() not in STOP and len(t) > 2}
co_g = nx.Graph()
for tid, d in tx[tx.role == "clue_giver"].groupby("trial_id"):
w = sorted(set().union(*[content(t) for t in d.text]) if len(d) else set())
for a, b in itertools.combinations(w, 2):
if co_g.has_edge(a, b):
co_g[a][b]["weight"] += 1 # seen this pair before -> bump the count
else:
co_g.add_edge(a, b, weight=1) # first time -> create the edge
# keep the busier words for a readable picture
keep = [n for n, dg in co_g.degree() if dg >= 2]
H = co_g.subgraph(keep)
print(f"spoken-word network: {H.number_of_nodes()} words, {H.number_of_edges()} co-occurrences")
if H.number_of_nodes():
p = nx.spring_layout(H, seed=3, k=0.6)
fig, ax = plt.subplots(figsize=(10, 7))
nx.draw_networkx_edges(H, p, alpha=.15, ax=ax)
nx.draw_networkx_nodes(H, p, node_size=[40*H.degree(n) for n in H], node_color="#2a7", ax=ax)
nx.draw_networkx_labels(H, p, font_size=8, ax=ax)
ax.set_title("Clue-givers' content words that co-occur within trials"); ax.axis("off")
plt.tight_layout(); plt.show()Recap
- spaCy confirmed the game’s shape: concrete-noun targets, taboos spanning nouns / adjectives / verbs.
- The semantic network turned 40×6 words into a map of neighbourhoods and hub concepts — a fast, honest preview of what the dataset is about, with no audio touched.
- These are the same embeddings Notebook 2 uses to measure a clue-giver homing in on the target.
Next → Notebook 1 to create the transcripts, then Notebooks 2–4 for the analyses.