Notebook 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:

  1. spaCy — what kinds of words are these (part of speech, lemma)?
  2. 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.
  3. (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)

# %pip install spacy networkx sentence-transformers pandas matplotlib
# python -m spacy download en_core_web_sm

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.