| Title: | Qualitative Coding and Measurement with Local Language Models |
|---|---|
| Description: | Qualitatively code a corpus by asking a local language model a set of questions about each document and reading the answer from the model's next-token probability distribution rather than parsing its text. Extraction never fails to parse, returns a probability per label, and reports the probability mass the model placed outside the label set. Includes the auditing steps that separate usable annotation from plausible-looking output. These inspect the documents that fired on an item and draw blinded samples for human coding. Requires a running Ollama server. |
| Authors: | Jose J. Alcocer [aut, cre] (ORCID: <https://orcid.org/0009-0005-0469-3689>), Ewon Baik [ctb] (ORCID: <https://orcid.org/0000-0002-4811-9401>), Karen Borglund [ctb], Alain S. Muñoz [ctb], Sandra S. Muñoz [ctb] |
| Maintainer: | Jose J. Alcocer <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-30 09:17:49 UTC |
| Source: | https://github.com/alcocer-jj/fathom |
Pivots results into one column per item, holding the probability of the target label.
as_wide( results, ..., target = NULL, value = c("prob_renorm", "prob", "expected_score"), prefix = "", dichotomize = FALSE, keep = character(0), drop_incomplete = TRUE )as_wide( results, ..., target = NULL, value = c("prob_renorm", "prob", "expected_score"), prefix = "", dichotomize = FALSE, keep = character(0), drop_incomplete = TRUE )
results |
A |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
target |
A string naming the label whose probability fills each cell.
Defaults to the first label for a multi-item instrument, and to |
value |
A string naming the column that supplies the value. Defaults to
|
prefix |
A string prepended to column names. Default is |
dichotomize |
A logical value indicating whether to threshold at 0.5 and
return integers. Default is |
keep |
A character vector of row-level columns to carry alongside. Each
must hold one value per document, since this returns one row per document.
A column that varies within a document is refused rather than allowed to
widen the pivot. Default is |
drop_incomplete |
A logical value indicating whether to drop documents
missing any item. Default is |
A tibble, one row per document, carrying id, then any keep
columns, then the pivoted values. Column names take prefix if given.
What the value columns are depends on the arguments, and there are three shapes.
Naming target gives one column per item, holding that label's
probability. The usual shape for an instrument of independent binary items,
and the one a measurement model wants.
Leaving target as NULL gives one column per label on a single-item
instrument, or one per item and label combination on an instrument of
several, named item_label. Use it when every label matters rather than
one.
value = "expected_score" gives one column per item, since an expected
score is already one number per document and item. Requires an instrument
built with scores, and cannot be combined with dichotomize.
Values are probabilities, or integers when dichotomize = TRUE. A failed,
truncated, or all-absent record becomes NA, and drop_incomplete decides
whether its document leaves the frame. Either way the count is reported
rather than left to be noticed.
There is no argument to normalize across items. K independent binary items do not form a distribution, as each probability comes from its own softmax over its own two-token distribution. Dividing them by their sum produces a number that looks like a proportion and is not one.
An exclusive instrument is the exception. Its K labels share one softmax at
one position, so its rows do sum to one and are comparable across categories.
This detects that from the results and defaults target to NULL, giving
one column per category.
instrument(..., exclusive = TRUE) produces a real distribution. Row sums
are meaningful, minus whatever leaked outside the label set. On an exclusive
instrument scored on discovered first tokens, that remainder is mostly the
gap left by the approximation, since the probability of a first token is an
upper bound on the probability of the label it opens.
measure() for the results it pivots and item_report() for a
summary per item.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six # An exclusive instrument gives one column per category. head(as_wide(res, prefix = "p_")) head(as_wide(res, prefix = "p_", dichotomize = TRUE))# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six # An exclusive instrument gives one column per category. head(as_wide(res, prefix = "p_")) head(as_wide(res, prefix = "p_", dichotomize = TRUE))
Three hundred requests filed through Mexico's INFOMEX transparency system,
drawn from a corpus Erlich et al. hand-coded with six Mexico City research
assistants over ten weeks. Their codings are not shipped here. What ships is
the request text and the four fields that trace one back to its source,
which is what a run with no training data needs. The coded variables are in
the replication materials named under @source, but the codebook was never
published and the supplemental material names them without saying how they
were defined, so what arrives is column names. The requests are in Spanish
and run from a single word to 588, median 42.
atiati
A tibble with 300 rows and 5 columns.
id is the row identifier from the source data.
folio is the INFOMEX request number, which traces back to the source.
date is the date of the request.
dependency is the agency the request was filed with.
text is the request as filed, in Spanish.
text is what the requester typed. Commas were stripped when the source was
written to CSV, which is not recoverable and shows as double spaces.
Erlich, A., S. G. Dantas, B. E. Bagozzi, D. Berliner, and B. Palmer-Rubin. 2022. Multi-label Prediction for Political Text-as-Data. Political Analysis 30(4), 463–480. doi:10.1017/pan.2021.15
Replication data at doi:10.7910/DVN/SOVPA4, released under CC0 1.0. Sampled with seed 444. The underlying requests are published by Mexico's federal transparency system.
data(ati)data(ati)
Takes the completed sheets and returns one sheet, so
validate_sample(coder_reference(ana, luis, bea), res) composes without
changing a signature. Read $ceiling from validate_sample() given the
same sheets first. It says whether collapsing is honest, since a high alpha
means the coders were interchangeable and a low one means the reference is a
consensus that never existed.
coder_reference( ..., method = c("majority", "unanimous", "soft"), target = NULL )coder_reference( ..., method = c("majority", "unanimous", "soft"), target = NULL )
... |
Two or more completed coding sheets, as data frames or paths. Name them to name the coders. |
method |
A string naming how to collapse a row. |
target |
A string naming the label |
A tibble, one row per document-item pair that any coder saw. It
carries id, item, human_label, and n_coders and n_agree beside
them.
Under "soft" the label column is replaced by human_prop, with
human_target naming the label the proportion is of and n_target in place
of n_agree. validate_sample() branches on which of the two arrived.
"majority" writes the label most coders chose. Where two or more labels tie
for the most it writes NA and names those rows, rather than breaking the
tie. A rule that broke it would invent a coding no coder produced.
"unanimous" writes the label only where every coder who saw the row chose
it, and NA otherwise. It warns, since the rows it discards are the hard
ones, so what survives is easier than the draw was.
A row only one coder saw has a majority of one and is trivially unanimous.
n_coders on the returned sheet is what separates those from a real
agreement.
"soft" writes no label. It writes human_prop, the share of coders who
chose target, smoothed as (n_target + 1) / (n_coders + 2) so two coders
who both said yes give 0.75 rather than 1. That is the honest reading of two
observations, and it keeps the target off the boundaries, which matters
because prob_renorm reaches exactly zero and a log score would then be
infinite. validate_sample() scores it with Brier for the same reason.
It needs exactly two labels. Above two the honest soft target is a vector of proportions rather than one number, and reporting one label against the rest would leave a Brier over two categories where a label sheet is scored over all of them. Refused rather than silently rescaled.
validate_sample(), whose $ceiling says whether collapsing is
honest and which reads the result.
ana <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "NO", "YES") ) luis <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "YES", "NO") ) bea <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "NO", "NO") ) coder_reference(ana = ana, luis = luis, bea = bea)ana <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "NO", "YES") ) luis <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "YES", "NO") ) bea <- data.frame( id = c("1", "2", "3"), item = "q", human_label = c("YES", "NO", "NO") ) coder_reference(ana = ana, luis = luis, bea = bea)
Draws a sheet of documents and questions with a blank column for a coder,
and the model's answer withheld, and writes it as a CSV when file is
given. A coder shown the model's label agrees with it more often, so
withholding it is what makes the resulting agreement figure interpretable.
coding_sample( results, docs = NULL, ..., text = NULL, id = NULL, image = NULL, n = 200L, items = NULL, labels_allowed = NULL, copy_media = TRUE, file = NULL, seed = 1234L )coding_sample( results, docs = NULL, ..., text = NULL, id = NULL, image = NULL, n = 200L, items = NULL, labels_allowed = NULL, copy_media = TRUE, file = NULL, seed = 1234L )
results |
A |
docs |
A character vector, or the data frame passed to |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
text, id
|
Strings naming the text and identifier columns, if |
image |
A string naming a column in |
n |
An integer giving the total document-item pairs to draw, across all questions. Default is 200. |
items |
A character vector restricting the draw to specific items, or
|
labels_allowed |
A character vector of permitted values for the
|
copy_media |
A logical value indicating whether to copy the files
|
file |
A string giving the path to write the CSV to, or |
seed |
An integer giving the seed for the draw, so the same call draws
the same rows. The session's random stream is put back afterwards, so this
seed reaches the draw and nothing after it. |
A tibble with one row per document-item pair drawn, in random order so the order leaks nothing about the draw. The same document appears once per item it was drawn on.
The columns are for the coder, the document last so the others stay on screen beside it.
id is the document identifier, matching the results.
item names which question this row is about.
question is the question text as the model received it, so a coder
answers the same question rather than a paraphrase of it. NA when the
results carry no instrument.
allowed holds the permitted answers, pipe-separated, so the sheet states
its own answer scheme.
human_label is blank, for the coder to fill with one value from
allowed. The model's answer is deliberately absent.
note is blank, for the coder. Nothing reads it, and it exists so a coder
has somewhere to record a case they were unsure about.
text is the document, in full.
image is the file image named, or NA when none was. Several files, a
video window's frames, are joined in order with |, the way allowed
joins the answers. Rewritten relative to the CSV when copy_media acted.
Every document-item pair has the same chance of being drawn, and the budget is split across questions in proportion to how many pairs each one has, so a figure computed on the sheet is already a figure about the corpus.
Item is a stratification variable rather than a filter, so the draw runs
within each question and n stays the total across them. The draw is
systematic, so the sample is exactly n rows on every call. An n at or
above the number of pairs is a census.
validate_sample(), which reads the coded sheet, and
coder_reference() for collapsing several coders into one.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) sheet <- coding_sample(baked$res_six, parlasent, text = "sentence", id = "id", n = 20, seed = 1234) head(sheet)# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) sheet <- coding_sample(baked$res_six, parlasent, text = "sentence", id = "id", n = 20, seed = 1234) head(sheet)
Pulls the documents that endorsed an item, with their full text, for checking whether the model was right.
inspect_cases( results, item, docs = NULL, ..., text = NULL, id = NULL, which = c("endorsed", "uncertain"), target = NULL, n = 6L, chars = NULL, file = NULL, seed = NULL )inspect_cases( results, item, docs = NULL, ..., text = NULL, id = NULL, which = c("endorsed", "uncertain"), target = NULL, n = 6L, chars = NULL, file = NULL, seed = NULL )
results |
A |
item |
A string naming the item to inspect. |
docs |
A character vector of documents, or the data frame passed to
|
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
text, id
|
Strings naming the text and identifier columns, if |
which |
A string choosing the documents, |
target |
A string naming the label counted as endorsement. Defaults to the first. |
n |
An integer giving the number of documents to show. Default is 6. |
chars |
An integer giving the characters each document is truncated to,
or |
file |
A string giving a path to write markdown to instead of printing,
or |
seed |
An integer giving a seed for the sample, or |
Invisibly, a tibble of the selected cases, one row per document, in the order they are printed. Printing is the point of this function and the returned object is a convenience, so it carries the columns the printout drew on rather than a curated set.
id is the document identifier.
item is the item inspected, the same value on every row.
label is target, the same value on every row, since the rows for other
labels are dropped before selection.
prob_renorm is the probability on target for that document, rescaled
over the labels. This is what which filtered on and what the printout
shows.
prob is the raw probability on target, before rescaling.
prob_other is the probability the model placed on tokens that were none
of the labels, for that document.
text is the document, joined from docs. Truncated if chars was set.
Named text even when the source column was called something else.
Any other columns the results carry travel through untouched, including
anything passed to keep in measure() and the remaining diagnostic
columns. A run that found no qualifying cases returns a tibble with no rows.
Three numbers, in this order. How many cases are printed, how many qualified
before sampling down to n, and how large the corpus was. The middle one is
the one to read, since it says whether the printed cases are all of them or a
sample. Reporting the corpus size alone would read as though every document
not shown had failed the filter, which is the opposite of what it means.
On two labels it is the 0.5 cut on prob_renorm, which is what
item_report() counts as endorse_renorm. Above two the labels compete for
one pool, a winner at 0.3 clears no cut, and endorsement is instead the label
that took the most mass, which is what item_report() counts as n_won.
"uncertain" is a rank rather than a threshold in both cases, so it commits
to no cut point. On two labels it sorts by distance from 0.5. Above two it
sorts by margin, the gap between the winning label and the runner-up, so
the first rows are the closest calls.
Full text is the default. The passage that triggered an item is often outside
a keyword-centered window, so an excerpt can read as a false negative when
the item was working. Setting chars truncates and warns.
item_report() for the summary these documents sit behind.
coding_sample() for drawing cases to hand to a coder rather than reading
a few.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six # measure() was given a data frame, so the documents are passed again. inspect_cases(res, "category", parlasent, text = "sentence", id = "id", target = "1", n = 2, seed = 1)# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six # measure() was given a data frame, so the documents are passed again. inspect_cases(res, "category", parlasent, text = "sentence", id = "id", target = "1", n = 2, seed = 1)
Bundles the questions to be answered, the labels the model may answer with, and the instruction that elicits them.
instrument( items, labels = c("YES", "NO"), ..., definitions = NULL, scores = NULL, exclusive = FALSE, exact = FALSE, probe_texts = NULL, cue = NULL, lang = c("en", "es"), medium = NULL, text_label = NULL, answer_label = NULL, name = NULL, backend = NULL )instrument( items, labels = c("YES", "NO"), ..., definitions = NULL, scores = NULL, exclusive = FALSE, exact = FALSE, probe_texts = NULL, cue = NULL, lang = c("en", "es"), medium = NULL, text_label = NULL, answer_label = NULL, name = NULL, backend = NULL )
items |
A named list or named character vector of question strings. An
unnamed single string is accepted and named A question is a free string and it is yours. It may run to a paragraph, reproduce a codebook definition verbatim, carry an exclusion clause, or include worked examples. Nothing about the scoring changes, since the question sits in the prompt and the answer is read from one token. |
labels |
A character vector of answer words, at least two. Each element
is its own label. Matching is case folded and reads by prefix, so a token
counts when it opens a label and a tokenizer splitting One word each, and whitespace is refused. |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
definitions |
A named character vector explaining what each label
means, or |
scores |
A named numeric vector giving each label a value, or |
exclusive |
A logical value indicating whether to build an
exclusive-choice instrument. One question lists all K categories, the model
picks one, and all K share a softmax at position 1. The result is a
distribution, unlike an instrument of independent binary items. The
category names become answer tokens here, so they must differ at their
first character. Default is |
exact |
A logical value indicating whether to score by exact match
against discovered tokens rather than by prefix. Requires This is the one case where a label carries more than one string. Prefix
matching needs the labels to differ at their first character, because a
split label often emits only that character and Nobody types those entries. Use |
probe_texts |
A named character vector of example documents, one per
label, used in place of the generated probe document, or |
cue |
A string giving the instruction appended after each question.
Generated from |
lang |
A string giving the language preset for the prompt scaffolding,
This is the scaffolding, not the question. A question written in Spanish
under the default |
medium |
A string naming the stem set a preset name is looked up in, one
of |
text_label, answer_label
|
Strings giving the prompt scaffolding. Default
to the |
name |
A string giving a label for the instrument, carried into
provenance, or |
backend |
An |
An object of class fathom_instrument, a list. Its print method
shows the parts usually wanted, and preview() shows what the model will
actually receive.
items is the named list of questions, one per item, in the order
declared. That order is the order item_report() and the results use, so
an instrument reads as the questionnaire it was written as.
labels is the named list of answer labels.
normalized holds what each label is actually matched against. Under
prefix matching this is the label itself, and under exact = TRUE it is
the variants probing discovered. This is the field to read to find out why
a token did or did not count.
definitions is the category text shown to the model beneath the
question, or NULL. Where long category wording goes, since it has no
first-character constraint on it.
scores is the numeric value per label, or NULL. Sits outside the prompt
hash on purpose, so rescaling re-extracts nothing.
cue is the assembled instruction naming the permitted answers.
text_label and answer_label are the words introducing the document and
the answer slot.
lang names which scaffolding preset supplied the three above.
exclusive says whether the labels share one distribution. FALSE gives
independent binary items, so a document may match several.
exact_match says whether scoring compares against probed tokens rather
than by prefix. The escape for a label set that has to collide.
k and n_items are the counts of labels and of items.
name is the short label-set name used in printing.
probe_texts and firsts are implementation. They hold what probing was run
against and the token each label emitted when probed, and neither is a stable
interface.
Scoring reads one token and counts it as a label when it opens that label, so
BU counts as BUDGET. Nothing has to be discovered first, because a model
writing BUDGET must emit a prefix of it, and that follows from every
tokenizer in use detokenizing by concatenation rather than from any one
model's vocabulary.
The price is that labels must differ at their first character. Under prefix matching this is checked at construction from the strings alone, with no backend. A split label frequently emits only its first character, and two labels sharing one cannot then be told apart. With distinct first characters no two labels share any prefix, so every match resolves to one label.
A label carrying whitespace is refused at construction, because scoring reads
one token. One token cannot span a space, so a two-word label is matched by
its opening word and the rest is never read, which scores the opening word
rather than scoring zero. Join the words if the answer really is compound,
since NOT_SURE and NOT-SURE are both accepted.
exact = TRUE is the way out when they have to collide. Each label is probed
once, the emitted token is recorded, and scoring consults that record instead
of reading a prefix. Passing a backend alone runs the probe without
changing how scoring works, which is worth doing once on a new model to see
the token map.
Labels and definitions are different things. labels names the answer words
and the surface forms they may appear in. definitions holds the text a
reader or a model needs to know what a label means. Keeping them apart
matters because scoring matches the token at position 1 against labels, so
a definition sitting in labels registers a token the model never emits and
that label scores zero on every document with no error raised.
An exclusive instrument lists the definitions under its question, which is what lets a category be a long phrase while the answer stays one word. Other instruments store them without putting them in the prompt, since their question is yours to write.
scores attaches a numeric value to each label. The values are metadata.
They never appear in the prompt and the model never types them, so the labels
stay semantic words.
Scoring then returns an expected_score column, the sum of each label's
value weighted by its renormalized probability. The labels of any one call
share a softmax, since they are read at a single position from a single
distribution, so this works on any instrument rather than only exclusive
ones.
Nothing checks that the labels are ordered. scores asserts an order the
package cannot verify, and reversing the values reverses every score without
complaint.
Three strings the prompt is built from, plus the word used to join the labels inside the cue.
| part | en | es |
text_label |
Text | Texto |
answer_label |
Answer | Respuesta |
| label joiner | or | o |
| cue | Answer with exactly one word: ... | Responde con exactamente una palabra: ... |
So a two-label instrument under lang = "es" closes with
Responde con exactamente una palabra: sí o no. and
labels its parts Texto and Respuesta.
Two stems ship for exclusive = TRUE, in both languages, and unlike the item
stems in template_items() neither takes a {label}, since the categories
arrive through definitions instead.
| stem | en | es |
"primary" |
What is the PRIMARY subject of this text? | ¿Cuál es el tema PRINCIPAL de este texto? |
"best" |
Which category best describes this text? | ¿Qué categoría describe mejor este texto? |
They are not interchangeable. "primary" asks what the text is about, which
assumes the categories are subjects. "best" asks which category describes
it, which assumes only that they are a scheme worth sorting into. Any other
string is used as the question verbatim, in whatever language it is written.
A generated token is folded to upper case before it is compared to a label, so sí, Sí, and SÍ all count as one answer.
preview() and check_backend() fold the same way, which is what makes the
preview table a reliable picture of what scoring will do.
The first-character rule is therefore about characters and not bytes. Two
labels differing only in an accent still collide once folded, and this
function refuses them. Where a coding scheme forces that collision, exact = TRUE is the escape.
template_items() for items built from one stem, preview() for
the prompt it assembles, and measure() for running it.
b <- instrument( items = list( econ = "Does the text discuss economic conditions?", immig = "Does the text discuss immigration?" ), labels = c("YES", "NO") ) bb <- instrument( items = list( econ = "Does the text discuss economic conditions?", immig = "Does the text discuss immigration?" ), labels = c("YES", "NO") ) b
Produces a diagnostic data frame where each row is one item, or each row is one item and label, summarizing how each item behaved across the corpus. Each column answers a different question about whether an item is usable.
It is purely descriptive, meaning no column here reports whether an item is
correct. A confident, low-leakage item can still be measuring something
other than the construct. inspect_cases() is what answers that, by showing
the documents.
item_report(results, target = NULL, order = c("declared", "size"))item_report(results, target = NULL, order = c("declared", "size"))
results |
A |
target |
A string naming the label counted as endorsement. Defaults to the first (e.g., "yes" in an instrument where "yes" was written first before "no"). Used by the endorsement shape only. |
order |
A string setting how labels are ordered within a question.
|
A tibble in one of three shapes, chosen as described below. Items come back in the order the instrument declares them rather than alphabetically, and an item present in the results but absent from the instrument sorts after the declared ones.
Exactly two labels and no scores. One row per item.
item is the question name.
n counts the documents scored on this item. Failed calls are excluded, so
this can sit below the corpus size.
endorse_renorm is the share of those documents where target reached 0.5
on prob_renorm, the probability after the labels have been rescaled to
sum to 1. Reaching it means the label beat the other one.
n_endorse_renorm is the count behind endorse_renorm. Its denominator is
n minus the documents where no label appeared, which have no renormalized
probability. That count is pct_both_absent times n, and is 0 whenever
pct_both_absent is.
endorse is the same cut on prob, the probability against everything the
model could have written. Reaching it means the model committed to the
label.
n_endorse is the count behind endorse. Its denominator is n.
mean_prob is the mean prob on target, so it belongs to endorse
rather than to endorse_renorm. It separates from endorse only when the
probabilities cluster near the 0.5 cut. When they all sit near 0 or 1 the
two agree almost exactly, so the gap between them says how far the
endorsements were from being coin flips.
pct_extreme_renorm is the share of renormalized probabilities at or
beyond 0.01 and 0.99, so it asks how the on-label mass was shaped rather
than how much of it there was. This is the substantive one, for the same
reason endorse_renorm is. It is not unconditionally the safer one. A
document that answered almost entirely outside the label set can leave this
denominator, read as graded, or read as certain, depending on how its
residue fell.
pct_extreme is the share of raw probabilities at or beyond 0.01 and 0.99.
Reads prob, so it cannot separate a decisive answer from one that never
entered the label set, since both put every label at the floor or the
ceiling. Read it against pct_extreme_renorm and max_prob_other rather
than alone. Near 1 means almost every answer came back at one end or the
other, so these are labels in numeric clothing. Reporting them as degrees
of confidence would claim more than they carry.
pct_both_absent is the share of documents where no label appeared at
position 1. That is a template or prompt failure rather than a low score. A
broken chat template registers in n_off_label rather than here, because
the labels stay in the distribution at trace mass underneath whatever the
template wrote first, which leaves pct_both_absent at 0 while every
answer is unusable.
max_prob_other is the most probability the model ever placed on tokens
that were none of your labels, anywhere in the corpus. Leaked mass, in the
term used elsewhere in these docs.
min_top1 is the least confident single answer in the corpus. For each
document, take the probability of whichever token the model was most likely
to write, label or not. This is the smallest of those. High values do not
mean the item is correct.
n_off_label counts the documents where the emitted token matched no
label. It is judged by the same rule scoring uses, so a token that opens a
label counts as that label and a split label is not reported as off-label.
n_failed counts the documents whose call failed. Divided by the label
count, so it counts documents rather than rows.
other_vs_median is max_prob_other divided by the middle value of that
column across the instrument, so 2 means this item placed twice as much
probability outside the labels as the typical item did. Present only for an
instrument of three or more items, since a middle value across one or two
carries nothing. This is the one column the item count decides. Absolute
leaked mass varies by model, by language, and by how close to 0 and 1 the
instrument's probabilities sit, so values are not comparable across
instruments.
mean_expected is the average expected score across documents, which on
two labels is the mean of prob_renorm on target rescaled onto the
scores. Present only when the instrument carries scores. Note that
mean_prob is the raw probability, so the two differ by whatever mass fell
outside the labels.
More than two labels and no scores. One row per item and label.
item and label are the question and category.
n_won counts the documents where this label took the largest probability.
A document whose distribution held none of the labels wins nothing.
share_won is n_won over the documents scored on that item, so the
shares of an item fall short of 1 by the share that answered outside the
labels.
Whenever the instrument carries scores and there are more than two labels.
One row per item and label, the same winner distribution plus the scale.
item and label are the question and category.
score is the numeric value the instrument assigns that label.
n_won and share_won are as in the choice shape.
mean_expected is the average expected score across documents, per
question. For a single document the expected score multiplies each label's
value by its probability and adds them up, after those probabilities have
been rescaled to sum to 1 across the labels. Because it uses every label
rather than only the winner, a document the model split between two
neighboring levels lands between them.
sd_expected is the standard deviation of the same, across documents.
endorse and endorse_renorm count the same documents at the same cut of
0.5, and differ only in which probability they cut. Do not read the gap
between them as measurement error or as disagreement between two estimates of
one quantity. It is two quantities.
Neither cut is configurable. 0.5 on prob_renorm is the argmax on a
two-label instrument, and threshold_sensitivity() exists to show how little
the cut point matters on prob. Both are reported so the choice does not
have to be made in advance.
Three shapes come back, and which one is decided by the labels rather than by the items. The number of items changes how many rows come back, not which columns. The test runs in this order.
Exactly two labels. Endorsement shape.
Otherwise, the instrument carries scores. Scored shape.
Otherwise, more than two labels. Choice shape.
The label count is tested first because on two labels the scored shape
reports one number, the average expected score, and drops a dozen
diagnostics to do it. There is no reason to choose between them. A scored
instrument of two labels reports as endorsement and gains mean_expected as
a column, so the number arrives without the diagnostics leaving.
exclusive = TRUE is not itself the test. An exclusive instrument lands in
the choice or scored shape because it usually has more than two labels or a
scale, and an exclusive instrument of two labels reports as endorsement like
any other. Equally, K separate questions sharing one scale reach the scored
shape without being exclusive at all.
Nothing here decides whether your labels are ordered, and nothing can. What
scores says is that an average of those numbers is defined, which is why
the shape is named for the argument rather than for a measurement level.
validate_sample() is where ordering is declared, by its level.
The shapes differ in row granularity as well as in columns. Endorsement gives one row per item. Choice and scored give one row per item and label, because a shared distribution has no single number per question to report. Choice and scored report a winner distribution, and leaked mass reaches them as a message rather than as a column.
target applies to the endorsement shape only, and is ignored by the other
two. There is no per-label endorsement rate for a forced choice, since the
labels all compete for one pool of probability, and a rate for whichever
label sorted first is not a meaningful quantity.
inspect_cases() for the documents behind an item, and
threshold_sensitivity() for how the rates move with the cut point.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) item_report(baked$res_indep)# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) item_report(baked$res_indep)
Runs every item in the instrument against every document and returns the
probabilities in $probs, one row per document, item and label, beside
$calls, one row per document and item.
measure(x, ...) ## S3 method for class 'character' measure(x, backend, instrument, ...) ## S3 method for class 'data.frame' measure( x, backend, instrument, ..., text = NULL, id = NULL, image = NULL, keep = character(0), num_ctx = "auto", items = NULL, checkpoint = NULL, verbosity = c("normal", "quiet", "verbose") )measure(x, ...) ## S3 method for class 'character' measure(x, backend, instrument, ...) ## S3 method for class 'data.frame' measure( x, backend, instrument, ..., text = NULL, id = NULL, image = NULL, keep = character(0), num_ctx = "auto", items = NULL, checkpoint = NULL, verbosity = c("normal", "quiet", "verbose") )
x |
A character vector of documents, or a data frame. |
... |
Passed to methods by the generic. On a method it is not used, and is refused if anything reaches it, since every argument after it is name-only and a misspelling would otherwise be swallowed. |
backend |
An |
instrument |
An |
text |
A string naming the column holding document text. Optional when
|
id |
A string naming an identifier column. Defaults to the integer row index. |
image |
A string naming a column holding media sent alongside the prompt
rather than inside it. A character column is one picture per document. A
list column is several, one element per document holding its paths in
order, which is how a video window arrives. Nothing is shared between
calls, so a run costs documents times items at close to full price, and a
window of K frames costs K encodes on every call. |
keep |
A character vector of additional columns to carry into the
results. Names must be unique, and a column whose name the results already
use is refused, since both copies would come back suffixed and the name
would then read as |
num_ctx |
An integer giving the context window ceiling, or So |
items |
A character vector of item names to restrict the run to, or
|
checkpoint |
A string giving the directory for per-document |
verbosity |
A string setting how much to report. Each level is the one before it plus more.
Every check runs at every level. Only the reporting changes. |
A fathom_results object. A list of two frames, with the run's
provenance on its attributes. $probs holds one row per document, item and
label. $calls holds one row per document and item, carrying everything that
describes the answer rather than any one label, so it is stored once instead
of repeating down the label rows.
id is the document identifier, from the id column or from the names of
a character vector.
item names which instrument question produced the row.
$probs, differing down the labels of one calllabel is the label this row scores, in the order the instrument declares
them.
prob_renorm is prob rescaled so the labels sum to 1. The measure. NA
when no label appeared at all, since there is nothing to condition on.
prob is the raw probability, against every token the model could have
written. Does not sum to 1 down the labels.
present says whether any token counting as this label appeared in the
returned distribution. A prob of zero means the label was measured at
zero when this is TRUE, and fell below top_logprobs when it is FALSE.
call_ok is FALSE when the call failed. One fact per call, kept here
rather than only in $calls because it guards almost every filter over
these rows.
$calls, one row per document and itemAnything named in keep rides here too, since a carried column describes the
document rather than any one label.
error is the message when the call failed, otherwise NA. Non-NA
exactly when call_ok is FALSE.
truncated is TRUE when the prompt came within 64 tokens of num_ctx,
so the document may have lost its opening.
top1_token is the token the model actually generated, whether or not it
counts as a label.
top1_prob is the probability of that token. When it is not a label, this
is the only record of how much mass the winning non-label held.
prob_other is the mass on tokens counting as no label. The diagnostic
that says whether the question landed.
all_absent is TRUE when no label appeared anywhere in the returned
distribution.
margin is the gap between the top two prob_renorm values. What
inspect_cases() sorts on to find the closest calls above two labels.
expected_score is prob_renorm weighted by the instrument's scores.
NA for an instrument built without them.
prompt_hash is the hash of the assembled prompt. What checkpoint
invalidation matches on, and not for reading.
instrument and backend as given, model, num_ctx as resolved,
exclusive, and run_at.
The document goes before the question, and items for a document run contiguously, so Ollama's prefix cache prefills the document once per document rather than once per item.
Checkpointing is off by default. Supply checkpoint to enable it. One .rds
per document holds the full record plus provenance, and a document whose
items are all current is skipped without a model call. There is no separate
resume function. Calling this twice against the same directory does only what
is outstanding.
Invalidation is per item, keyed on a hash of the question, cue, labels, scaffolding, and model. Rewording one item re-extracts that item across the corpus at question-only cost, since the first call still warms the document cache. This is not proportionally cheap. A partial rerun still pays the full document prefill for every row.
scores are the exception, and they are outside the hash on purpose.
Rescaling an ordinal instrument changes no token the model sees, so
re-extracting would be waste. The expected score is instead recomputed from
the stored probabilities against whatever instrument is in hand, on this path
and in read_checkpoints(), so a rescaled resume returns the new arithmetic
without spending a call.
There is no schema tag. A change to scoring code that leaves prompts unchanged will not invalidate anything. Delete the directory by hand.
num_ctx = "auto"
One extra call per measure() invocation, not per document. Running several
instruments over the same corpus measures once each, because a different
instrument means a different prompt and therefore a different token count.
Passing an integer once the corpus is known skips it.
A failed item is stored with call_ok = FALSE and is therefore stale on the
next pass, so re-running retries exactly those. There is no separate retry
sweep, which would run against a cold cache.
A character vector stores the documents on the results in a docs
attribute, so the audit functions find them without a docs argument. Ids
are the vector's names when it has them, and its positions otherwise.
A data frame does not, and measure() never writes the document into a
column. It reads the one you name and keeps a hash of the assembled prompt.
Carrying the text instead would put a copy of the corpus in $calls, once
per item, and the results are the object that gets saved.
Naming that column in keep carries it deliberately, and then it is a kept
column like any other rather than something the schema promises.
ollama_backend() and instrument() for its two inputs,
preview() for the prompt it sends, read_checkpoints() for a run cut
short, and item_report() and inspect_cases() for reading what comes
back.
## Not run: # Needs a running Ollama server, so it is not run here. oll <- ollama_backend("gemma4:31b") b <- instrument( items = "Where does this sentence fall on the six-point sentiment scale?", labels = c("1", "2", "3", "4", "5", "6"), exclusive = TRUE ) res <- measure(parlasent, instrument = b, backend = oll, text = "sentence", id = "id") res$probs ## End(Not run)## Not run: # Needs a running Ollama server, so it is not run here. oll <- ollama_backend("gemma4:31b") b <- instrument( items = "Where does this sentence fall on the six-point sentiment scale?", labels = c("1", "2", "3", "4", "5", "6"), exclusive = TRUE ) res <- measure(parlasent, instrument = b, backend = oll, text = "sentence", id = "id") res$probs ## End(Not run)
Bundles connection and sampling settings into one object, passed down whole so parameters cannot drift between call sites. Drift can create context size differences between calls, defeating the prefix cache and forcing Ollama to reload the model.
Scoring reads log probabilities, which need an Ollama server at version
0.12.11 or later. measure() checks for them before a run and stops when
none come back.
ollama_backend( model = NULL, ..., keep_alive = "24h", timeout = 600, top_logprobs = 20L, host = NULL, modalities = c("text", "image"), validate = TRUE )ollama_backend( model = NULL, ..., keep_alive = "24h", timeout = 600, top_logprobs = 20L, host = NULL, modalities = c("text", "image"), validate = TRUE )
model |
A string giving the model name as Ollama knows it, e.g.
|
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
keep_alive |
A string setting how long Ollama holds the model after a
call. The default, |
timeout |
A number giving the request timeout in seconds for generation calls. A cold prefill on a very long document can run minutes. Discovery and validation use their own shorter timeout. Default is 600. |
top_logprobs |
An integer giving the number of alternatives to return at each position. Ollama caps this at 20. Lower it only if every label reliably appears in the top-k. Default is 20. |
host |
A string giving the server URL. Defaults to |
modalities |
A character vector of what the server accepts, which is
|
validate |
A logical value indicating whether to contact the server and
confirm |
An object of class fathom_backend, a list of eight settings. It
holds no connection, so it is cheap to build and safe to keep.
model is the tag calls are sent to. Recorded on the results as the
model attribute, so a corpus says which model produced it.
host is the server address, normalized to include a scheme.
modalities holds what this server accepts. measure() refuses a column
naming something absent here, before any call, because Ollama answers HTTP
200 to a field it does not recognize and an undeclared column would
otherwise be discarded in silence.
top_logprobs says how many alternatives are requested at the answer
position. Ollama caps this at 20. A label whose token falls outside the
returned set is scored as absent, so prob is a lower bound and
prob_other absorbs the difference.
think is always FALSE, and not an argument. Reasoning is disabled on
every call, because scoring reads generated position 1 and a model that
reasons first puts something else there.
keep_alive says how long the server holds the model in memory between
calls. Long enough that a corpus run does not pay reload cost.
timeout is the number of seconds before a single call is abandoned. A
call that times out returns a failed row rather than ending the run.
validated says whether the constructor confirmed the model is installed.
FALSE when built with validate = FALSE, which skips the check rather
than failing it.
Called with no model name it prints what is installed and stops, which doubles as a way to confirm the server is running.
model
The call contacts the server, prints the models installed there, and errors.
It does not return NULL or a partial object, so instrument() and
measure() never receive a backend with no model attached.
Neither is exposed. Temperature is fixed at 0, where the returned probabilities are the model's unmodified posterior. Scoring reads the distribution at generated position 1 and discards the token that would have been drawn from it, so temperature rescales the logits being measured without changing what is read.
At temperature 0 there is no sampling, and identical calls return byte-identical token sequences. A seed has nothing to control.
measure(), which takes the backend, and instrument(), which can
probe labels against it.
ollama_backend("gemma3:27b", validate = FALSE)ollama_backend("gemma3:27b", validate = FALSE)
A hundred sentences from ParlaSent 1.0 (Mochtak et al. 2023), the English training file, drawn from UK parliamentary proceedings and annotated by two coders with a reconciliation pass. It is the package's exclusive-choice corpus and the only one whose labels collide.
parlasentparlasent
A tibble with 100 rows and 19 columns.
id is the document identifier, uk_001 to uk_100.
sentence is the document, 11 to 32 words, median 22.
label is the three-level reconciled sentiment. Factor, Negative,
Neutral, Positive.
reconciliation is the six-level reconciled code. Factor, ordered from
Negative through Positive.
annotator1 and annotator2 are the two independent six-level codings.
name is the member speaking. 93 distinct across the hundred.
party is the party affiliation. Eight parties.
gender is the recorded gender of the member.
ruling says whether the member sat with the government or the opposition.
date is the date of the sitting, 2015-01-08 to 2021-02-23.
term is the parliamentary term.
document_id and sentence_id are ParlaSent internal identifiers.
split is the authors' own train, dev, and test partition. This sample
crosses all three, since nothing here is trained.
stratum_N is the size of the six-level stratum in the source file.
stratum_n is the number drawn from it.
weight is stratum_N / stratum_n, for weighting back at the six-level
stratum.
weight3 is the same pooled to the three-level class.
Normalized for scoring, Negative and Neutral both begin with N. Prefix
matching cannot tell them apart, since a split label often emits only its
first character, so instrument() refuses to build this label set that way
and it can only be scored with exact = TRUE and a live backend. The
collision is a property of a published coding scheme rather than something
constructed to demonstrate one.
The annotators worked six levels and the three-level label is their
collapse. Both mixed levels fold to their own polarity, so Neutral means no
sentiment content and nothing else. Ambiguity lives in M_Positive and
M_Negative, which are inside the positive and negative classes rather than
beside them.
| level | meaning |
Positive |
entirely or predominantly positive |
M_Positive |
ambiguous or mixed, leaning positive |
P_Neutral |
no sentiment content, leaning positive |
N_Neutral |
no sentiment content, leaning negative |
M_Negative |
ambiguous or mixed, leaning negative |
Negative |
entirely or predominantly negative |
Stratified on the six levels rather than on the three, seed 444. Seventeen
each of Positive, Negative, P_Neutral, and N_Neutral, and sixteen
each of the two mixed levels, giving 33 negative, 34 neutral, and 33
positive.
Sampling on the collapse would have let the draw decide the mix inside each
class, and nothing would record which mix arrived. The neutral levels matter
most. P_Neutral outnumbers N_Neutral 413 to 267 in the source, so a
proportional draw would give a neutral class that leans positive, and a model
answering that lean would look biased when the sample was.
The mixed levels are kept rather than dropped. They are the only sentences
that give margin and prob_other anything to separate. A corpus of
unambiguous cases returns saturated on every row and every uncertainty
diagnostic has nothing to show.
The source file is 48.8 percent negative, 26.2 percent neutral, and 25.0
percent positive, against 33, 34, and 33 here. Recall, specificity, and AUC
are computed within a class and carry over. Accuracy, precision, F1, and
kappa move with the class mix and do not. weight3 reweights to the source
proportions, and weight does the same at the six-level stratum, though at a
higher cost in precision. The effective sample size is about 90 using
weight3 and about 69 using weight.
The sample is also deliberately harder than the file it came from. Mixed levels are 32 percent here against 14.6 percent there.
annotator1 and annotator2 are the two independent codings before
reconciliation, so the corpus carries its own benchmark. On these hundred
sentences the two coders agree with each other on 0.720, which is 0.765 on
the four unambiguous levels, 0.625 on the two mixed ones, and 0.570 on the
raw six. Across the full 2,600-sentence file they agree on 0.783, and on the
raw six levels 0.660.
A model is worth reading against 0.720 rather than against 1. The gap between the pure and mixed figures is what the uncertainty diagnostics should track.
Mochtak, M., Rupnik, P., Meden, K., and Ljubesic, N. (2023). The multilingual sentiment dataset of parliamentary debates ParlaSent 1.0. Slovenian language resource repository CLARIN.SI. http://hdl.handle.net/11356/1868
Distributed under CC BY-SA 4.0, unlike the rest of this package. See
LICENSE.note. Drawn from the English training file with seed 444.
Kuzman Pungersek, T., Ljubesic, N., Erjavec, T., et al. (2025). Linguistically annotated multilingual comparable corpora of parliamentary debates ParlaMint-en.ana 5.0. CLARIN.SI. http://hdl.handle.net/11356/2006
Mochtak, M., Rupnik, P., and Ljubesic, N. (2024). The ParlaSent multilingual training dataset for sentiment identification in parliamentary proceedings. Proceedings of LREC-COLING 2024, 16024-16036.
data(parlasent) table(parlasent$reconciliation, parlasent$label) # The two coders before reconciliation, on the raw six levels. mean(parlasent$annotator1 == parlasent$annotator2)data(parlasent) table(parlasent$reconciliation, parlasent$label) # The two coders before reconciliation, on the raw six levels. mean(parlasent$annotator1 == parlasent$annotator2)
Transcribes a recording and writes its pauses and speakers back into the
text, so measure(text = ) reads a transcript that carries them. A plain
transcript records what was said and discards how. Pauses, hesitation and who
was speaking leave with it, and for a construct that lives in delivery rather
than in wording that is the absence of the thing being measured.
prepare_audio( x, ..., diarize_names = FALSE, annotate_timing = FALSE, tool = c("whisperx", "whispermlx"), asr_model = "large-v3", min_speakers = NULL, max_speakers = NULL, language = NULL, hf_token = NULL, extra = NULL, min_pause = 0.2, speakers = NULL, id = NULL, cache_dir = NULL, force = FALSE, verbosity = c("normal", "quiet", "verbose") )prepare_audio( x, ..., diarize_names = FALSE, annotate_timing = FALSE, tool = c("whisperx", "whispermlx"), asr_model = "large-v3", min_speakers = NULL, max_speakers = NULL, language = NULL, hf_token = NULL, extra = NULL, min_pause = 0.2, speakers = NULL, id = NULL, cache_dir = NULL, force = FALSE, verbosity = c("normal", "quiet", "verbose") )
x |
A character vector of paths to audio files, one row each. A path
that does not resolve comes back with |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
diarize_names |
A logical value indicating whether to label who is
speaking. This is a transcription setting rather than a rendering one. It
runs the diarizer, needs |
annotate_timing |
A logical value indicating whether silences are
written into the text, as |
tool |
A string naming the recognizer to run, |
asr_model |
A string naming the Whisper checkpoint, which decides the
words. Both tools take the same short names. The command-line default is
|
min_speakers, max_speakers
|
Integers passed to the diarizer when known,
or |
language |
A string giving the spoken language, detected per file when
|
hf_token |
A string giving a HuggingFace read token, required when
|
extra |
A character vector of further flags for the recognizer, passed
through verbatim and recorded on the result. Each element is its own
argument, so |
min_pause |
A number giving the seconds below which a silence is not marked or counted. Jefferson leaves anything under 0.2 as an untimed micropause and gives a figure above it, which is where the default comes from. |
speakers |
A named character vector renaming the speakers, such as
|
id |
A character vector of identifiers, one per file. Defaults to the file names without their extensions, and must be unique. |
cache_dir |
A string giving the directory where transcripts are written and looked for. Defaults to beside each recording. |
force |
A logical value indicating whether to transcribe again even
where a usable transcript is cached. Default is |
verbosity |
A string setting how much to report.
|
A tibble of one row per file.
id, audio and exists identify the recording.
text is the transcript, with turns where diarize_names was on and
silences where annotate_timing was.
duration_s and n_words hold what the recording holds.
n_turns and n_speakers are NA when the timings carry no speaker.
n_pauses counts the silences inside a turn at or above min_pause.
n_gaps counts the silences at a turn boundary, NA when not diarized.
median_gap_s is the middle silence between two words.
speech_rate and articulation_rate are words per second over the whole
recording, and over voiced time only. The first counts the silences and the
second does not, so the ratio between them is how much of the elapsed time
was pausing rather than talking.
median_turn_s is the middle turn, NA without diarization.
median_score is the middle alignment confidence, NA where the
transcript carries none. Every silence figure rests on the word boundaries
this describes, so it is what says how far to trust them.
diarized says whether the transcript carried speakers, which is also
whether the text has turns in it.
annotate_timing says whether the silences were written in.
transcribed says whether this call produced the transcript.
tool, asr_model and extra hold what made the words, or NA where a
transcript was found with no record beside it. transcribed and these
together say whether a row was made now, read from a verified transcript,
or read from one of unknown origin.
language and min_pause are read off the transcript or applied by this
call, so both hold however the transcript was obtained.
A transcript already on disk is read instead of transcribing again, so an
interrupted run resumes and a hand-corrected transcript survives. force = TRUE returns to the recognizer.
What counts as already done is the question measure() asks of its
checkpoints, which is whether the work was done under the settings now being
requested rather than merely done. tool, asr_model, diarize_names,
language and extra are recorded in a .fathom.json beside each
transcript, and a run whose settings differ transcribes again. Without that,
a call with diarize_names off in a directory that already holds a diarized
transcript would read the diarized one and quietly ignore the argument.
annotate_timing, min_pause and speakers are not in that record, since
none of them reaches the recognizer. Changing a rendering costs nothing and
re-reads the same transcript.
A transcript with no record beside it is used rather than discarded, because
that is what a hand-corrected one looks like. Its tool, asr_model and
extra come back NA, since naming settings that did not produce those
words would be worse than saying nothing.
The row a model is asked about is the whole recording, and the structure
inside it is written into text rather than spread across rows.
Inside a row, a line is a turn, meaning a maximal run of words by one
speaker. Nothing splits a turn further. A silence inside one person's talk is
written where it happened, as [pause 0.4s], which says how long it was
rather than only that it passed a threshold.
Silence at a turn boundary is written [gap 0.8s] and belongs to neither
speaker, so it goes on a line of its own. Whether a silence is a hesitation
or a turn transition is a fact about the recording, so the two markers are
told apart by whether the timings carry a speaker, not by whether labels were
asked for.
Lines are newline separated with the speaker in brackets ahead of them.
Speakers reach the output only from the recognizer. Absent, n_speakers and
n_turns are NA and diarized is FALSE, so a transcript with no speaker
in it is never read as a transcript with one speaker in it.
speakers renames what the recognizer emitted, since SPEAKER_00 carries
nothing a model can use. It is also the fastest way to put the answer in the
prompt. A speaker renamed Angry Protester has been coded before the model
saw the text, and nothing in the results would show it.
n_pauses and n_gaps count what min_pause found in the timings, so they
hold with annotate_timing off, where the text shows nothing. Only text
and annotate_timing change with the rendering.
None of them reaches the model, since measure() reads id and text and
nothing else.
n_pauses also moves with diarization on the same words, because without
turns a turn transition is just another silence.
Jefferson, G. (1989). Preliminary notes on a possible metric which provides for a standard maximum silence of approximately one second in conversation. In D. Roger and P. Bull (eds), Conversation. An Interdisciplinary Perspective, 166–196. Multilingual Matters.
Stivers, T., Enfield, N. J., Brown, P., Englert, C., Hayashi, M., Heinemann, T., Hoymann, G., Rossano, F., de Ruiter, J. P., Yoon, K.-E., and Levinson, S. C. (2009). Universals and cultural variation in turn-taking in conversation. Proceedings of the National Academy of Sciences 106(26), 10587–10592. doi:10.1073/pnas.0903616106
prepare_video() for recordings with pictures, and measure(),
which reads the text column. vignette("quick_start", package = "fathom") for installing the recognizer and the diarization token.
## Not run: # Needs ffmpeg and a recognizer, so it is not run here. # Everything off, which is a plain transcript and needs no token and no # optional package. plain <- prepare_audio("hearing_04.wav") # Two speakers known in advance, labeled turns with the silences marked. docs <- prepare_audio( c("hearing_04.wav", "hearing_11.wav"), diarize_names = TRUE, annotate_timing = TRUE, min_speakers = 2, max_speakers = 2, hf_token = Sys.getenv("HF_TOKEN") ) # Anything else the recognizer takes. tuned <- prepare_audio( "hearing_04.wav", extra = c("--no_speech_threshold", "0.4", "--beam_size", "10") ) ## End(Not run)## Not run: # Needs ffmpeg and a recognizer, so it is not run here. # Everything off, which is a plain transcript and needs no token and no # optional package. plain <- prepare_audio("hearing_04.wav") # Two speakers known in advance, labeled turns with the silences marked. docs <- prepare_audio( c("hearing_04.wav", "hearing_11.wav"), diarize_names = TRUE, annotate_timing = TRUE, min_speakers = 2, max_speakers = 2, hf_token = Sys.getenv("HF_TOKEN") ) # Anything else the recognizer takes. tuned <- prepare_audio( "hearing_04.wav", extra = c("--no_speech_threshold", "0.4", "--beam_size", "10") ) ## End(Not run)
Builds a tibble of one row per file, with the columns the rest of the package
expects. id keys the results and the audit joins, image is the path
measure() reads, and bytes is a size covariate.
Nothing is decoded. bytes comes from file.size(), which needs no image
reader and no system library.
prepare_image(paths, id = NULL)prepare_image(paths, id = NULL)
paths |
A character vector of file paths. |
id |
A character vector of identifiers, one per path. Defaults to the file name without its extension, which is what a corpus of scanned pages usually keys on. |
A tibble with id, image, bytes, exists and ext.
A path that does not resolve comes back with exists = FALSE and bytes as
NA rather than being silently removed, because a corpus that quietly lost
four documents is the failure this exists to prevent. Filter on exists
after reading the count.
measure(), which reads the image column.
f <- tempfile(fileext = ".png") grDevices::png(f, width = 8, height = 8) graphics::par(mar = c(0, 0, 0, 0)) graphics::plot.new() grDevices::dev.off() prepare_image(f)f <- tempfile(fileext = ".png") grDevices::png(f, width = 8, height = 8) graphics::par(mar = c(0, 0, 0, 0)) graphics::plot.new() grDevices::dev.off() prepare_image(f)
Turns each recording into one row, with frames in a list column and a
transcript beside them. measure(text = , image = ) then reads both on the
same call, since they are two columns of one row rather than two objects to
align.
The audio half is prepare_audio() unchanged, running on a wav ffmpeg
extracts.
prepare_video( x, ..., frames = 8, select = c("uniform", "change"), include_first = FALSE, min_gap = NULL, quality = 3, diarize_names = FALSE, annotate_timing = FALSE, tool = c("whisperx", "whispermlx"), asr_model = "large-v3", min_speakers = NULL, max_speakers = NULL, language = NULL, hf_token = NULL, extra = NULL, min_pause = 0.2, speakers = NULL, id = NULL, cache_dir = NULL, force = FALSE, verbosity = c("normal", "quiet", "verbose") )prepare_video( x, ..., frames = 8, select = c("uniform", "change"), include_first = FALSE, min_gap = NULL, quality = 3, diarize_names = FALSE, annotate_timing = FALSE, tool = c("whisperx", "whispermlx"), asr_model = "large-v3", min_speakers = NULL, max_speakers = NULL, language = NULL, hf_token = NULL, extra = NULL, min_pause = 0.2, speakers = NULL, id = NULL, cache_dir = NULL, force = FALSE, verbosity = c("normal", "quiet", "verbose") )
x |
A character vector of paths to video files, one row each. A vector
rather than a directory, so |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
frames |
An integer giving the number of frames per recording. Fixed across the corpus. Default is 8. |
select |
A string naming the rule that chooses the frames. |
include_first |
A logical value indicating whether the opening frame is
always taken. It spends one of |
min_gap |
A number giving the seconds below which two chosen frames may
not sit. |
quality |
An integer giving the JPEG quality passed to |
diarize_names |
A logical value indicating whether to label who is
speaking. This is a transcription setting rather than a rendering one. It
runs the diarizer, needs |
annotate_timing |
A logical value indicating whether silences are
written into the text, as |
tool |
A string naming the recognizer to run, |
asr_model |
A string naming the Whisper checkpoint, which decides the
words. Both tools take the same short names. The command-line default is
|
min_speakers, max_speakers
|
Passed to |
language |
A string giving the spoken language, detected per file when
|
hf_token |
A string giving a HuggingFace read token, required when
|
extra |
A character vector of further flags for the recognizer, passed
through verbatim and recorded on the result. Each element is its own
argument, so |
min_pause |
A number giving the seconds below which a silence is not marked or counted. Jefferson leaves anything under 0.2 as an untimed micropause and gives a figure above it, which is where the default comes from. |
speakers |
A named character vector renaming the speakers, such as
|
id |
A character vector of identifiers, one per file. Defaults to the file names without their extensions, and must be unique. |
cache_dir |
A string giving the directory where frames, the wav and the transcript are written and looked for. Defaults to beside each recording. |
force |
A logical value indicating whether to extract and transcribe
again even where usable output is cached. Default is |
verbosity |
A string setting how much to report.
|
A tibble of one row per file, carrying every column prepare_audio()
returns plus these.
video is the recording, and has_audio says whether it had a sound track
at all.
image is a list column of frame paths in time order, which is the shape
measure(image = ) takes for several pictures per document.
n_frames and frame_times say how many were taken and when.
select and include_first name which rule chose them.
scene_median and scene_max are the middle and largest scene score
across every frame, which say whether the recording had cuts in it or
whether the spacing did the work. Read before trusting "change".
select = "uniform" divides the recording into frames equal segments and
takes the middle of each. Midpoints rather than endpoints, since a frame at
exactly zero is often a title card and one at exactly the end is often black.
select = "change" takes the frames highest scene scores instead, which
ffmpeg computes for every frame as how much this picture differs from the
one before it. It finds cuts that even spacing misses, and it concentrates.
min_gap is what stops that, and it exists only for this mode.
ffprobe is asked whether an audio stream exists before anything is
extracted. Where there is none, the row comes back with its frames and with
has_audio = FALSE, text and every audio count NA, and the count is
reported. A batch of campaign advertisements does not stop because three of
them are silent.
exists and has_audio answer different questions and both are needed.
| case | exists |
has_audio |
text |
| file not found | FALSE |
NA |
NA |
| present, silent | TRUE |
FALSE |
NA |
| present, with audio | TRUE |
TRUE |
the transcript |
NA on has_audio is doing work. The file was never opened, so FALSE
would assert something that was not checked.
n_words is NA for a silent recording rather than 0, because nothing
was transcribed. A recording whose audio track is silence is a different
thing, gets transcribed, and comes back with n_words = 0.
Every other audio column is NA there too, diarized and tool and
asr_model included. Nothing ran, so there is nothing to report and naming
a model would assert settings that produced no words.
On disk beside the recording, in <stem>_frames/, because measure(image = ) reads paths.
The wav lands beside it, which is what lets prepare_audio()'s cache work
unchanged. The frames keep a record of their own, <stem>_frames.fathom.json
in the frames directory, holding the times they were cut at and the JPEG
quality. Changing frames, select, include_first, min_gap or quality
changes that record, so every frame is cut again and the old pictures are
removed, while the transcript is read back from its own cache.
prepare_audio(), which transcribes the sound track, and
measure(), which reads both columns.
## Not run: # Needs ffmpeg, a recognizer and an Ollama server, so it is not run here. ads <- list.files("campaign_ads", "[.]mp4$", full.names = TRUE) ins <- instrument(c(attack = "Does this advertisement attack an opponent?")) # Frames spread evenly, and whatever was said alongside them. d <- prepare_video(ads, frames = 8) measure( d, backend = ollama_backend("gemma3:27b"), instrument = ins, text = "text", image = "image" ) # More frames, chosen where the picture moved, with the speakers labeled. d <- prepare_video( ads, frames = 16, select = "change", diarize_names = TRUE, annotate_timing = TRUE, hf_token = Sys.getenv("HF_TOKEN") ) ## End(Not run)## Not run: # Needs ffmpeg, a recognizer and an Ollama server, so it is not run here. ads <- list.files("campaign_ads", "[.]mp4$", full.names = TRUE) ins <- instrument(c(attack = "Does this advertisement attack an opponent?")) # Frames spread evenly, and whatever was said alongside them. d <- prepare_video(ads, frames = 8) measure( d, backend = ollama_backend("gemma3:27b"), instrument = ins, text = "text", image = "image" ) # More frames, chosen where the picture moved, with the speakers labeled. d <- prepare_video( ads, frames = 16, select = "change", diarize_names = TRUE, annotate_timing = TRUE, hf_token = Sys.getenv("HF_TOKEN") ) ## End(Not run)
Prints the exact string the model will receive. Makes no model calls. Prompt
failures do not appear in the output of measure().
preview( instrument, text = "[document text goes here]", item = NULL, image = NULL, truncate = 300L )preview( instrument, text = "[document text goes here]", item = NULL, image = NULL, truncate = 300L )
instrument |
An |
text |
An example document. A short placeholder by default. |
item |
A string naming the item to show. Defaults to the first. |
image |
A character vector of paths, one or several, to the media the
run would send, or |
truncate |
An integer giving the number of characters of document text
to show. |
Invisibly, the assembled prompt string for the item shown, exactly as the model would receive it. A single character scalar.
The printed output is larger than the return value and the difference matters. Printing shows the prompt and, beneath it, the table of what each label will be matched against. That table is where most extraction failures are visible, and it is not in the returned string.
So capture the value to diff two prompts or to paste one into a paper, and read the printed output to check the instrument. No call is made either way.
The table underneath has two columns. On the left the label, on the right every token that will be counted as that label. They read the same when a label is its own only variant, which is the usual arrangement. They diverge once accented forms are supplied, and again after probing registers whatever token a split label actually emits.
Under prefix matching the right column is not the whole story, since a token
also counts when it merely opens one of those forms. BU counts as BUDGET
without appearing there. The instrument header says which rule is in force.
instrument() for the prompt's parts and measure() for sending
it.
b <- instrument( template_items("topic", c(econ = "the economy", immig = "immigration"), medium = "text"), labels = c("YES", "NO") ) preview(b)b <- instrument( template_items("topic", c(econ = "the economy", immig = "immigration"), medium = "text"), labels = c("YES", "NO") ) preview(b)
Reconstructs the return value of measure() from disk without making any
model calls. Use when a run completed but the return value was lost, or to
inspect how far a killed run progressed.
read_checkpoints(dir)read_checkpoints(dir)
dir |
A string giving the checkpoint directory. |
A fathom_results object, the two frames measure() returns and
documents there. Rows arrive in whatever order the directory listing gives,
so sort if order matters.
The attributes are read back off the stored payloads rather than stamped from the current session, so a corpus reports the run that produced it.
instrument is the instrument as it was when the run happened.
num_ctx is the ceiling that run used, not the one a run would get now. A
resumed run keeps this, which is why mixing ceilings inside one corpus
cannot happen silently.
exclusive says whether the instrument shared one distribution.
run_at says when the run happened, not when it was read.
A file that cannot be read is treated as absent rather than as an error, since a killed run can leave one half-written and re-extracting that document is the only recoverable reading. Errors when the directory holds no readable checkpoints at all.
The context ceiling, the exclusivity flag and the run timestamp are read from disk rather than inferred from the current session. A run written under one ceiling reports that ceiling even if the current session would resolve a different one.
measure(), whose checkpoint argument writes what this reads.
## Not run: # Needs a running Ollama server, so it is not run here. oll <- ollama_backend("gemma4:31b") b <- instrument(c(tone = "Is this sentence positive?"), labels = c("YES", "NO")) res <- measure(parlasent, instrument = b, backend = oll, text = "sentence", id = "id", checkpoint = "ckpt") # A run cut short leaves its finished documents in the directory. res <- read_checkpoints("ckpt") ## End(Not run)## Not run: # Needs a running Ollama server, so it is not run here. oll <- ollama_backend("gemma4:31b") b <- instrument(c(tone = "Is this sentence positive?"), labels = c("YES", "NO")) res <- measure(parlasent, instrument = b, backend = oll, text = "sentence", id = "id", checkpoint = "ckpt") # A run cut short leaves its finished documents in the directory. res <- read_checkpoints("ckpt") ## End(Not run)
Turns a list of categories into an instrument of independent binary items. The category goes into the question, not the answer, so categories may be any length and the tokenization constraint never applies.
template_items(stem, categories, lang = c("en", "es"), medium = NULL)template_items(stem, categories, lang = c("en", "es"), medium = NULL)
stem |
A named stem, or a custom string containing |
categories |
A character vector of categories. Names, if present, become item names. |
lang |
A string giving the language for the named stems, |
medium |
A string naming the stem set a named stem is looked up in, one
of |
A named list of questions, one per category, ready to pass to
instrument() as its first argument.
Names are the names of categories, so they become the item names in the
results and in item_report(). Pass a named vector to control them. An
unnamed vector uses the values themselves, which rarely suits a category
that runs to a phrase.
Values are the stem with {label} replaced by each category's text. Order
is preserved, and an instrument reads in the order its items were declared
rather than alphabetically.
The result is an ordinary list, so an item can be edited before it reaches
instrument() when one category needs wording the stem cannot give it.
Four sets, one per medium, each in English and in Spanish. medium picks
the set and lang picks the language.
| medium | stems |
"text" |
topic, mention, express, about, frame |
"audio" |
topic, mention, express, about, frame |
"image" |
show, depict, express, about, frame |
"video" |
show, depict, discuss, express, about, frame |
The sets are not translations of one another, which is why medium has no
default.
The text set in full. The other three take the same shape with their own
noun and their own verbs.
| stem | en | es |
"topic" |
Does this text discuss {label}? | ¿Este texto trata sobre {label}? |
"mention" |
Does this text mention {label}? | ¿Este texto menciona {label}? |
"express" |
Does this text express {label}? | ¿Este texto expresa {label}? |
"about" |
Is this text primarily about {label}? | ¿Este texto trata principalmente de {label}? |
"frame" |
Does this text frame the issue in terms of {label}? | ¿Este texto enmarca el tema en términos de {label}? |
These differ in what they assert, not in topic. "mention" fires on a
passing reference. "about" requires the text to be centrally concerned with
the category. The choice is a measurement decision, and it is the same
decision in either language, since the two halves of a row are translations
of one another rather than separate instruments.
A custom string ignores lang entirely, so a Spanish question written by
hand is passed through as typed. The scaffolding around it is a separate
setting, on instrument(), and the two can disagree. See its lang
argument.
instrument(), which takes the items.
template_items("topic", c(econ = "the economy", immig = "immigration"), medium = "text") # The same stem name asks a different question of a different document. template_items("about", c(econ = "the economy"), medium = "image") # A custom string carries its own wording, so it needs no medium. template_items("Does the author appear to endorse {label}?", c("austerity"))template_items("topic", c(econ = "the economy", immig = "immigration"), medium = "text") # The same stem name asks a different question of a different document. template_items("about", c(econ = "the economy"), medium = "image") # A custom string carries its own wording, so it needs no medium. template_items("Does the author appear to endorse {label}?", c("austerity"))
Computes the endorsement rate per item at each cut point, which shows how much of an item's rate is a property of the documents and how much is a property of where the cut falls.
threshold_sensitivity( results, target = NULL, thresholds = seq(0.1, 0.9, by = 0.1), value = c("prob_renorm", "prob") )threshold_sensitivity( results, target = NULL, thresholds = seq(0.1, 0.9, by = 0.1), value = c("prob_renorm", "prob") )
results |
A |
target |
A string naming the label counted as endorsement. Defaults to the first. |
thresholds |
A numeric vector of cut points to evaluate. Default is
|
value |
A string naming the probability column to threshold. Defaults to
|
A tibble with one row per item and threshold, items in instrument order and thresholds ascending within an item. The message printed alongside is a ranking rather than a table and stays widest swing first.
item is the question name.
threshold is the cut point, one row per value in thresholds.
n counts the documents scored on this item. The same on every row of an
item, since the cut point changes which documents count as endorsing and
not how many were asked.
n_endorse counts the documents whose value for target reached the
cut.
endorse is n_endorse over the documents with a value to cut. A
document where no label appeared has no prob_renorm, so under the default
it leaves this denominator, and endorse is n_endorse over n only when
no such document exists. That is the split item_report() makes between
endorse_renorm and endorse. This is the column to read down, and the
spread from its smallest to largest value within an item is what the
message ranks on.
This assumes a two-label instrument. Thresholding one label's probability asks whether that label is present, which is a question only a two-label instrument answers. On an instrument of three or more labels the labels compete for one pool of probability, so a label can win at 0.3 and lose at 0.6, and its rate at a series of cut points describes the arithmetic rather than the corpus. On a scored instrument the labels are levels of a scale, and a cut point on one level is not a measurement of anything.
The function does not refuse, because a cut point is the whole subject here and there is no other quantity for it to report. It runs on whatever you pass and the figures will be arithmetically correct.
item_report() for the winner distribution, and for mean_expected
on a scored instrument, which needs no cut.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) threshold_sensitivity(baked$res_indep, target = "YES")# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) threshold_sensitivity(baked$res_indep, target = "YES")
Takes the sheet coding_sample() wrote, with human_label filled in, and
reports how well the model agreed with the coder.
The sheet carries no probabilities, which is what makes it blind. This joins
back to the results on id and item to recover them, so pass the same
results object the sample came from.
vignette("complete_run") works a run end to end.
validate_sample( sheet, results, ..., level = c("nominal", "ordinal"), labels = NULL, instrument = NULL, calibration = FALSE, coded = FALSE, ci = c("none", "bootstrap"), conf_level = 0.95, bootstrap_n = 2000L, seed = 1234L )validate_sample( sheet, results, ..., level = c("nominal", "ordinal"), labels = NULL, instrument = NULL, calibration = FALSE, coded = FALSE, ci = c("none", "bootstrap"), conf_level = 0.95, bootstrap_n = 2000L, seed = 1234L )
sheet |
A data frame holding the completed coding sheet, or a string giving a path to the CSV. A named list of sheets scores the model against every coder in it and adds the ceiling and the gap. See the section below. |
results |
A |
... |
Not used, and refused if anything reaches it. Every argument after it is name-only, so a misspelling lands here rather than being matched by position. |
level |
A string, |
labels |
A character vector giving, under |
instrument |
A |
calibration |
A logical value indicating whether to return
|
coded |
A logical value indicating whether to return |
ci |
A string choosing whether to report intervals. |
conf_level |
A number giving the coverage of every interval reported,
0.95 by default. It is stamped on the result as the |
bootstrap_n |
An integer giving the replicates for each interval. The default is a round two thousand. Below a hundred a percentile interval is reading a tail made of a handful of draws, and is refused. |
seed |
An integer giving the seed for the bootstrap, so the intervals
come out the same on every run with the same inputs. The session's random
stream is put back on exit, so nothing drawn before this call reaches the
intervals and this seed reaches nothing after it. |
An object of class fathom_validation, a list of tables. The model tables
carry item and, on the several-sheet path, coder. Under
ci = "bootstrap" each figure gains a bracketed _ci column beside it.
| component | holds | when |
$discrimination |
how often the model chose the coder's label | a label sheet |
$categories |
precision, recall and AUC for each label | a label sheet |
$calibration |
how honest the probabilities were | calibration = TRUE, or a soft sheet |
$ceiling |
how well the coders agreed with each other | several sheets |
$gap |
the model against that ceiling | several sheets |
$coded |
the sheet joined to the model's answer | coded = TRUE
|
One row per question, plus a pooled (all items) row where the instrument
has more than one.
n counts the coded rows.
agreement is the share of rows where the model chose the coder's label.
alpha is Krippendorff's alpha (Hayes and Krippendorff 2007) between the
model and the coder, under the level asked for.
One row per label per question, with label and n, the rows the coder
gave that label.
precision is the share of the rows the model gave the label where the
coder chose it too.
recall is the share of the rows the coder gave the label where the model
chose it too.
auc is the chance that a row the coder gave the label gets a higher
probability for it than a row the coder did not.
All three count exact matches, so under "ordinal" a model one level off is
charged as a full miss. alpha and rps are the figures that respect the
order. On two labels the two auc rows are equal, since one label's
probability is one minus the other's.
One row per question, plus the pooled row, with n and one score column
named for the rule level selects.
brier is squared error summed across categories, so on two labels a
confident correct answer costs 0, a confident wrong one costs 2, and never
committing costs 0.5.
rps is the same sum on the cumulative probabilities and runs from 0 to
k - 1. Lower is better for both. A soft reference stays on brier.
$ceiling$alpha is one row per question with n_units, the rows two or
more coders reached, and alpha over all coders at once. $ceiling$pairs
is one row per pair of coders per question, with coder_a, coder_b, n,
the rows both coded, agreement and alpha. This is what the coders
managed on these rows, and not a target.
One row per question.
n counts the rows the model and the coders share.
model_alpha is the model's average alpha with the coders.
coder_alpha is the coders' average alpha with each other on the same
rows.
gap is the first minus the second, positive where the model agreed with
the coders more closely than they agreed with each other, and gap_ci is
its interval.
The sheet joined to the model's label and its probabilities, one row per coded document-item, or per document-item per coder on the several-sheet path. It is returned because a design-based correction takes it as input (Egami et al. 2023, Angelopoulos et al. 2023).
The attributes carry level, conf_level, the number of coded rows as
n_coded, and on the several-sheet path the coder names as coders.
sheet takes a named list as well as one sheet, so
validate_sample(list(ana = ana, luis = luis), res) scores the model against
each coder in one call and adds $ceiling and $gap. Each coder is scored
through the one-sheet path, so no figure is computed twice or computed
differently, and the model tables gain a coder column after item.
Coders need not have worked every row. $ceiling$alpha uses every row two
or more coders reached, and each pair in $ceiling$pairs falls back to the
rows both of them saw, so the two tables can rest on different counts.
$gap resamples whatever ci is set to, since the gap is read through its
interval. Every replicate redraws the documents once and recomputes both
sides on that draw, because the two share the documents they rest on.
A list of one is refused. A sheet coder_reference() consolidated warns when
it is placed in a list, since agreeing with a consensus is easier than
agreeing with one person.
Ordered labels need level = "ordinal". The default charges every
disagreement the same, which is right for unordered labels and wrong for a
scale, where two coders one step apart have agreed more than two coders at
opposite ends. Pass labels in scale order, least to greatest, or an
instrument carrying scores. Nothing is inferred, because no order can be
recovered from the sheets.
A sheet carrying human_prop instead of human_label is scored as a
measurement and not as a classification. There is no second categorical
variable, so $discrimination and $categories are not returned. What comes
back is $calibration, with brier against the proportion, whatever
calibration is set to.
Angelopoulos, A. N., Bates, S., Fannjiang, C., Jordan, M. I., and Zrnic, T. (2023). Prediction-powered inference. Science 382(6671), 669–674. doi:10.1126/science.adi6000
Brier, G. W. (1950). Verification of forecasts expressed in terms of probability. Monthly Weather Review 78(1), 1–3. doi:10.1175/1520-0493(1950)078<0001:VOFEIT>2.0.CO;2
Egami, N., Hinck, M., Stewart, B., and Wei, H. (2023). Using imperfect surrogates for downstream inference. Design-based supervised learning for social science applications of large language models. Advances in Neural Information Processing Systems 36, 68589–68601. doi:10.52202/075280-3000
Hayes, A. F., and Krippendorff, K. (2007). Answering the call for a standard reliability measure for coding data. Communication Methods and Measures 1(1), 77–89. doi:10.1080/19312450709336664
coding_sample() for drawing the sheet, and coder_reference() for
consolidating several coders into one sheet.
# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six sheet <- coding_sample(res, parlasent, text = "sentence", id = "id", n = 20, seed = 1234) # The parlasent coders, standing in for someone filling the sheet in. six <- c("Negative", "M_Negative", "N_Neutral", "P_Neutral", "M_Positive", "Positive") coded <- function(who) { out <- sheet out$human_label <- as.character( match(parlasent[[who]][match(sheet$id, parlasent$id)], six) ) out } a1 <- coded("annotator1") v <- validate_sample(a1, res, level = "ordinal", labels = as.character(1:6)) v$discrimination v$categories # A second coder adds the ceiling the two set between themselves, and the # model against it. a2 <- coded("annotator2") v2 <- validate_sample(list(annotator1 = a1, annotator2 = a2), res, level = "ordinal", labels = as.character(1:6), bootstrap_n = 200) v2$ceiling$pairs v2$gap# Results from a real run ship with the package, so this needs no server. p <- system.file("extdata", "vignette_results.rds", package = "fathom") baked <- readRDS(p) res <- baked$res_six sheet <- coding_sample(res, parlasent, text = "sentence", id = "id", n = 20, seed = 1234) # The parlasent coders, standing in for someone filling the sheet in. six <- c("Negative", "M_Negative", "N_Neutral", "P_Neutral", "M_Positive", "Positive") coded <- function(who) { out <- sheet out$human_label <- as.character( match(parlasent[[who]][match(sheet$id, parlasent$id)], six) ) out } a1 <- coded("annotator1") v <- validate_sample(a1, res, level = "ordinal", labels = as.character(1:6)) v$discrimination v$categories # A second coder adds the ceiling the two set between themselves, and the # model against it. a2 <- coded("annotator2") v2 <- validate_sample(list(annotator1 = a1, annotator2 = a2), res, level = "ordinal", labels = as.character(1:6), bootstrap_n = 200) v2$ceiling$pairs v2$gap