topiclayers
From posts to topical multilayer networks — hardened and social-scientist-friendly.
Quickstart
# Requires Python 3.12
pip install topiclayers
# copy runnable examples (configs + toy data) into ./examples/
topiclayers init
topiclayers run examples/generic.yml
That's it. You get a GML network file in ./out/generic_toy/networks/.
What does it do?
- Loads your posts (Twitter JSONL, CSV, Parquet).
- Cleans text (removes URLs, @mentions, newlines).
- Models topics using BERTopic with SentenceTransformer embeddings.
- Labels topics with an LLM (local via Ollama by default) and propagates topic labels through retweet chains.
- Builds networks: single-layer retweet, multilayer per-topic, and bipartite temporal-text networks.
Minimal config
Save this as my_config.yml:
input:
format: csv_posts
posts: my_data.csv
name: my_dataset
topic_model:
embedder: all-MiniLM-L6-v2
min_topic_size: 50
network:
type: multilayer_repost
output_dir: ./out/my_dataset
Run it: topiclayers run my_config.yml
Checking progress
Long runs write a live status file to <output_dir>/run_status.json. Check the current stage, elapsed time, and ETA from another terminal without touching the running job:
topiclayers status out/my_dataset
Example output:
Stage 3/7: embed
Status: running
Run: my_dataset
Progress: 412000/1200000 (34%)
Stage elapsed: 18m 12s
Stage ETA: ~35m 20s
Total elapsed: 21m 05s
Stages are load → data → embed → topic_model → label → network → manifest. Stages with a per-item counter (data, embed, network) report a live ETA. Opaque stages (BERTopic's UMAP+HDBSCAN) fall back to the median duration of past runs, stored in <output_dir>/stage_timings.json. While running, the terminal itself shows a live progress line (spinner, counter, ETA, elapsed time) that keeps ticking even in stages without a counter, plus a per-stage duration summary when the run finishes.
Topic labeling with an LLM (optional)
After clustering, the pipeline can ask a local LLM to write one short, readable
label per topic (instead of raw keyword lists like -1_proclamation_plante_trending).
Default is Ollama — free, local, nothing leaves your machine:
# one-time setup
pip install 'topiclayers[labeling]'
ollama pull qwen2.5:7b
# start the server in another terminal
ollama serve
Then just run the pipeline as usual (label_model is on by default). Any
LiteLLM model string works:
topic_model:
label_model: ollama/qwen2.5:7b # default
# label_model: gpt-4o-mini # OpenAI (needs OPENAI_API_KEY)
# label_model: anthropic/claude-3-haiku-20240307
# label_model: "" # disable labeling
Custom / self-hosted OpenAI-compatible endpoints
Any server speaking the OpenAI protocol (Unsloth, llama.cpp, vLLM, LM Studio...)
works via label_api_base:
topic_model:
label_model: openai/unsloth/Qwen3.8-27B-GGUF # note the openai/ prefix
label_api_base: http://localhost:8888/v1
with the token in .env (key resolution order: LABEL_API_KEY →
OPENAI_LIKE_API_KEY → OPENAI_API_KEY). You can also set LABEL_API_BASE
in .env instead of the YAML. Find the exact model name your server exposes
with curl http://localhost:8888/v1/models.
Labeling is non-fatal: if the model server is unreachable, topics keep their keyword labels and everything else proceeds normally.
Input formats
| Format | Extension | Description |
|---|---|---|
twitter_jsonl |
.json, .jsonl |
Twitter/X API v2 JSONL |
csv_posts |
.csv |
Generic CSV with post_id, user_id, text columns |
parquet_posts |
.parquet |
Same schema as CSV, Parquet format |
CSV optional columns: created_at, lang, interaction_type (original/repost/quote/reply), target_post_id, mentions (semicolon-separated), extra_* passthrough columns.
What if something fails?
| Symptom | Likely cause | Fix |
|---|---|---|
| "All topics are -1" / "no topics found" | min_topic_size too high or dataset too small |
Halve min_topic_size in config, or use a larger dataset (>100 posts) |
| "Module not found" | topiclayers not installed | pip install topiclayers |
topiclayers: command not found |
installed in a venv that isn't active | Activate the venv (source .venv/bin/activate) or use python -m topiclayers.cli; on install with --user add ~/.local/bin to PATH |
| "OpenAI API key not set" | Using OpenAI embedder without key | Switch embedder to all-MiniLM-L6-v2 (default, works offline) |
| "Qdrant connection refused" | Qdrant vector DB not running | Ignore — Qdrant is optional. Set QDRANT_URL in .env to enable |
| "Cannot create multilayer network" | All posts are outliers | Reduce min_topic_size or provide more data |
| "UMAP spectral layout failed" | Dataset too small (<10 posts) | Topic modeling needs more data; consider using topic labels from elsewhere |
Output files
For a dataset named <name> (e.g. my_dataset), output goes to <output_dir>/:
<output_dir>/
├── run_manifest.json # Reproducibility metadata
├── run_status.json # Live stage/ETA tracking (while running)
├── stage_timings.json # Per-stage durations for ETA prediction
└── networks/
├── <name>_retweet.gml # Single-layer retweet network
├── <name>_retweet_network_ml.gml # Multilayer (uunet format)
├── <name>_ttt.gml # Temporal-text bipartite network
└── projected/
└── <name>__prj_<topic>.gml # Per-topic projected networks
Reusable artifacts (processed data, embeddings, the BERTopic model) go to the
cache, not the output dir. By default the cache lives in a cache/ folder
next to the input data file, so it persists across runs regardless of
output_dir and can be shared between configs (files are prefixed with the
dataset name). Override the location with cache_dir: in the config:
<cache_dir>/
├── data/
│ ├── tweets_<name>.pkl/.csv # Full tweet table
│ ├── retweet_labeled_<name>.pkl/.csv # Retweets with topic labels
│ └── manifest_<name>.json # Cache validity key
└── tm/<embedder_name>/
├── embeddings_<name>.pkl # Document embeddings
├── topics_<name>.csv # Topic table (with LLM labels)
└── model_<name>/ # BERTopic safetensors dir
Requirements
- Python 3.12 (uunet, used for multilayer networks, does not ship wheels for 3.13+ yet)
- Optional: Ollama +
topiclayers[labeling]extra (LLM topic labeling), Docker (for Qdrant vector search), OpenAI API key (for OpenAI embeddings)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file topiclayers-0.3.0.tar.gz.
File metadata
- Download URL: topiclayers-0.3.0.tar.gz
- Upload date:
- Size: 33.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48f821a603e84497dd7edaf59a72d81d87fca10709af5ff52165631fd0815acf
|
|
| MD5 |
957444078ba93876abbb61378ec1d4a3
|
|
| BLAKE2b-256 |
c47730706e5f0785fa67039e9b811cd16a8721054c74e0064b6056aa70ee8554
|
Provenance
The following attestation bundles were made for topiclayers-0.3.0.tar.gz:
Publisher:
publish.yml on alessiogandelli/topiclayers
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
topiclayers-0.3.0.tar.gz -
Subject digest:
48f821a603e84497dd7edaf59a72d81d87fca10709af5ff52165631fd0815acf - Sigstore transparency entry: 2639975243
- Sigstore integration time:
-
Permalink:
alessiogandelli/topiclayers@98e2ee4c8e941f5a3ba135260d485df86579340c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/alessiogandelli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@98e2ee4c8e941f5a3ba135260d485df86579340c -
Trigger Event:
push
-
Statement type:
File details
Details for the file topiclayers-0.3.0-py3-none-any.whl.
File metadata
- Download URL: topiclayers-0.3.0-py3-none-any.whl
- Upload date:
- Size: 39.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
941c7e9abd4194da1230dcd296643cd843d813482116d870f9fdae50351c8ca1
|
|
| MD5 |
02551c1958fa3f5d31cb9c4cd5d0080b
|
|
| BLAKE2b-256 |
0c89cedf063084724de290fe65f7ff060dffba5c1b5629577fdcf811807b3d90
|
Provenance
The following attestation bundles were made for topiclayers-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on alessiogandelli/topiclayers
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
topiclayers-0.3.0-py3-none-any.whl -
Subject digest:
941c7e9abd4194da1230dcd296643cd843d813482116d870f9fdae50351c8ca1 - Sigstore transparency entry: 2639975380
- Sigstore integration time:
-
Permalink:
alessiogandelli/topiclayers@98e2ee4c8e941f5a3ba135260d485df86579340c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/alessiogandelli
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@98e2ee4c8e941f5a3ba135260d485df86579340c -
Trigger Event:
push
-
Statement type: