CatLLM R Package: Classify Survey Text with LLMs in R

6 minute read

Published:

CatLLM R package ecosystem banner

The CatLLM R package is now live. CatLLM started life as a Python package for classifying open-ended survey responses with LLMs, and it still is, but a lot of the researchers I want to reach work in R, not Python. Asking a sociologist or demographer to spin up a Python project just to categorize a column of text responses is a friction tax most won’t pay.

So I built the R layer. As of this week, eight R packages (cat.stack, cat.survey, cat.vader, cat.ademic, cat.cog, cat.pol, cat.web, and the cat.llm meta-package) are installable from r-universe. They wrap the Python CatLLM ecosystem via reticulate, expose the same classify(), extract(), explore(), and summarize() API, and return results bitwise-equivalent to the Python equivalents. There is no R-specific logic; it is a thin shim. But it means you can stay in your tidyverse pipeline.

Why a CatLLM R Package

A huge share of survey methodology, demography, sociology, political science, and public health work happens in R. The available options for those users have been (1) reticulate the Python package themselves, (2) round-trip data to a separate Python script, or (3) hand-code text by RA. The first is fiddly, the second is annoying, the third does not scale. A native R interface removes all three.

Installation

install.packages(
  "cat.llm",
  repos = c("https://chrissoria.r-universe.dev",
            "https://cloud.r-project.org")
)

library(cat.llm)
install_cat_stack()   # one-time Python backend setup

Not on CRAN yet. Install from r-universe for now. The packages are not on CRAN while the API stabilizes. R-universe is set up like an additional repo, so install.packages() works as usual.

Why the dot in cat.llm? R’s convention for multi-word package names uses a dot, like data.table or R.utils. The Python equivalents drop the dot (catllm, catstack, catsurvey, and so on). Use the dotted form in library() and install.packages().

install_cat_stack() runs once. Under the hood it calls pip install for the Python backend packages and configures reticulate to find them. After that you do not think about Python again.

A First Classification with a Local LLM

The simplest way to verify the install is to classify a handful of responses with a local Ollama model. No API key, no cloud calls, no per-token cost.

In a terminal:

ollama pull qwen2.5:14b   # ~9 GB on disk, ~10 GB RAM to run

Then in R:

library(cat.survey)

responses <- c(
  "The new wellness program is great, I've been using it daily.",
  "It's confusing and the app crashes constantly.",
  "I haven't tried it yet but it sounds promising."
)

# Verbose category descriptions classify several percentage points
# more accurately than short labels, especially for smaller local
# models like Qwen 14B below.
verbose_cats <- c(
  "Positive: The respondent expresses satisfaction, approval, or favorable sentiment.",
  "Negative: The respondent expresses dissatisfaction, frustration, or criticism.",
  "Neutral: The respondent is factual, ambivalent, or does not express clear sentiment.",
  "Other: The response does not fit any of the above categories."
)

results <- classify(
  input_data   = responses,
  categories   = verbose_cats,
  user_model   = "qwen2.5:14b",
  model_source = "ollama"
)

print(results)

You get back a data frame with one column per category and one row per response, a binary matrix ready to merge back into your main dataset. The Ollama server starts automatically the first time you call classify() with model_source = "ollama"; you do not have to manage ollama serve yourself.

To run the same example against a cloud model, swap model_source = "ollama" and user_model = "qwen2.5:14b" for user_model = "gpt-5" (or "claude-sonnet-4-6", "gemini-2.0-flash", etc.) and pass api_key = Sys.getenv("OPENAI_API_KEY"). Everything else stays the same.

The Eight R Packages

Each domain package depends on cat.stack (the shared classification engine) and adds parameters relevant to its data type.

PackageDomainWhat it adds
cat.stackGeneral-purpose classification baseCore classify(), extract(), explore(), summarize()
cat.surveyOpen-ended survey responsessurvey_question parameter for question context
cat.vaderSocial media postsThreads/Bluesky metadata, sentiment-tuned defaults
cat.ademicAcademic papers (OpenAlex)journal_name, topic_name, paper_limit fetchers
cat.cogCognitive assessment scoringcerad_drawn_score() for CERAD constructional praxis
cat.polPolicy documentsFetchers for city ordinances, federal laws, exec orders
cat.webWeb content (URL fetching)Auto-fetch URLs, inject domain + content-type metadata
cat.llmMeta-package (installs all 7)Single install for the full ecosystem

A few examples of what the domain-specific parameters look like.

cat.ademic: pull abstracts directly from OpenAlex and classify them by method:

library(cat.ademic)

results <- classify(
  categories   = c("Quantitative", "Qualitative", "Mixed Methods"),
  journal_name = "American Sociological Review",
  paper_limit  = 100L,
  polite_email = "you@university.edu",
  api_key      = Sys.getenv("OPENAI_API_KEY")
)

No data frame to assemble; journal_name and paper_limit do the fetching for you.

cat.web: point at URLs and let the package render the page content before classification:

library(cat.web)

results <- classify(
  input_data = c("https://example.com/post-1", "https://example.com/post-2"),
  categories = c("News", "Opinion", "Personal blog", "Documentation"),
  api_key    = Sys.getenv("OPENAI_API_KEY")
)

cat.cog: a single-purpose function for scoring CERAD constructional-praxis drawings (image input only, for clinical and cognitive-aging research):

library(cat.cog)

score <- cerad_drawn_score(
  image_path = "path/to/participant-drawing.png",
  api_key    = Sys.getenv("OPENAI_API_KEY")
)

The remaining packages (cat.survey, cat.vader, cat.pol, cat.stack) all share the same classify() / extract() / explore() / summarize() API; the vignettes show domain-tuned examples for each.

Verifying the R Package Install

After cloning the parent repo, this one command installs all eight R packages from local source, sets up the Python backends, and runs a minimal classification per package:

OPENAI_API_KEY=sk-... Rscript r-package/test-all-packages.R

Expected output: 8 / 8 passed (0 failed, 0 skipped). CI runs the same script on every push.

Caveats

  • Not on CRAN yet. R-universe only while the API stabilizes.
  • Python ≥3.9 required. install_cat_stack() runs pip install under the hood; if you do not have Python, reticulate will prompt you to install miniconda.
  • For Ollama workflows, the Ollama binary is a separate download from ollama.com. Local 7B–14B models trail frontier cloud models by a few percentage points on classification accuracy, which is fine for most survey work but worth validating against a labeled subsample for high-stakes coding.
  • cat.cog is single-function for now. CERAD scoring on drawn-shape images. More cognitive instruments to come.

If you run into trouble installing, hit a confusing error from reticulate, or want to suggest a domain wrapper that does not exist yet, reach out at ChrisSoria@Berkeley.edu.