ConvoKit

Overview

ConvoKit is a Python toolkit for computational analysis of conversations, built by Cornell’s Conversational Analysis Toolkit team. This tutorial loads a ChatbotLab conversation export into ConvoKit and applies the Fighting Words module. Fighting Words finds the words that most distinguish two groups of text. Here, we use it to compare participants with higher and lower PHQ-9 depression scores.

The example runs on the synthetic corpus in conversation_data/convokit/. This corpus contains 19 synthetic person-to-AI conversations. Each “person” speaker is tagged with age, gender, persona, and phq9. Swap in an export from your own study to reproduce the same workflow. A flattened version of this same corpus is used in the text (R) tutorial.

Setup

pip install convokit

Loading the Data

ChatbotLab exports conversations in ConvoKit’s native corpus format: index.json, utterances.jsonl, speakers.json, and conversations.json. No conversion is needed. Point Corpus at the export directory:

from convokit import Corpus, FightingWords

corpus = Corpus(filename="conversation_data/convokit")
corpus.print_summary_stats()
Number of Speakers: 20
Number of Utterances: 1543
Number of Conversations: 19

Every “person” speaker carries the survey metadata collected alongside the conversation:

speaker = corpus.get_speaker("user_00001")
print(speaker.id, speaker.meta)
user_00001 {'role': 'person', 'age': 28, 'gender': 'man',
            'persona': 'small business owner, the business is struggling',
            'phq9': 7}

Grouping Speakers by PHQ-9

We split the “person” speakers into two groups using a median split on PHQ-9. We then tag every utterance with its speaker’s group:

import statistics

person_speakers = [s for s in corpus.iter_speakers() if s.meta.get("role") == "person"]
median = statistics.median(s.meta["phq9"] for s in person_speakers)

def group(speaker):
    if speaker.meta.get("role") != "person":
        return None
    return "higher_symptom" if speaker.meta["phq9"] >= median else "lower_symptom"

for utt in corpus.iter_utterances():
    utt.meta["symptom_group"] = group(corpus.get_speaker(utt.speaker.id))

The median PHQ-9 in this corpus is 12. This gives 10 higher-symptom speakers (PHQ-9 at or above 12) and 9 lower-symptom speakers (PHQ-9 below 12), covering 762 “person” utterances. We exclude assistant turns, since the comparison is about how participants talk, not the bot.

Running Fighting Words

Fighting Words uses a Dirichlet-multinomial model to find the n-grams that most distinguish two groups of text. It corrects for the noise that plain frequency counts produce on small samples:

fw = FightingWords(ngram_range=(1, 2))
fw.fit(
    corpus,
    class1_func=lambda u: u.meta.get("symptom_group") == "higher_symptom",
    class2_func=lambda u: u.meta.get("symptom_group") == "lower_symptom",
)

result = fw.summarize(
    corpus, plot=True, class1_name="higher_symptom", class2_name="lower_symptom"
)

result is a DataFrame of every n-gram with a z-score. Positive values are characteristic of higher_symptom speech. Negative values are characteristic of lower_symptom speech. Setting plot=True also renders ConvoKit’s built-in scatter plot. The plot shows the weighted log-odds ratio (z-score) on the y-axis against how often each n-gram occurs, on a log-scaled x-axis. Marker size scales with the size of the z-score. Color marks the class. The most significant n-grams on each side are labeled.

Scatter plot of weighted log-odds ratio vs. word frequency, showing n-grams distinguishing higher- and lower-PHQ-9 speakers

Results

Top n-grams toward the higher-symptom group:

n-gram

z-score

bye

2.20

up

1.97

or

1.83

nothing

1.70

feels

1.57

feel

1.57

just

1.45

thing

1.41

don

1.41

he

1.39

Top n-grams toward the lower-symptom group:

n-gram

z-score

out

-2.88

there

-1.80

back

-1.80

alright

-1.77

one

-1.72

the

-1.69

ha

-1.65

man

-1.53

ok

-1.49

good

-1.47

Higher-PHQ-9 speakers use more affect language, such as feel, feels, and nothing. They also use more hedging language, such as just and don (from “don’t”). Lower-PHQ-9 speakers use more upbeat, closing language, such as good, alright, ok, and ha.

With only 19 speakers, the z-scores are modest. All magnitudes are under 3. Treat this as a demonstration of the workflow, not a finding. Re-run the same code on a full study export for a properly powered comparison.

Generalizing

This method is not specific to PHQ-9. Split speakers or utterances on any field in speaker.meta or conversation.meta, such as persona, age, or a REDCap or Qualtrics field passed through at Survey Integration. Then hand the two groups to a ConvoKit transformer. This works for any other feature module ConvoKit ships, such as politeness strategies, linguistic coordination, or prompt types, not just Fighting Words.