Skip to content

Commit eae5d74

Browse files
openclaw-dvclaude
andauthored
feat: add GlobalOpinionQA dataset adapter (sb-gqa) (#9)
Add adapter for Anthropic/llm_global_opinions HuggingFace dataset (2,556 questions, 138 countries). Supports --country flag for country-specific ground truth distributions. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d1e970 commit eae5d74

4 files changed

Lines changed: 228 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ dependencies = [
2020
anthropic = ["anthropic>=0.18"]
2121
openai = ["openai>=1.0"]
2222
viz = ["matplotlib>=3.7"]
23-
all = ["anthropic>=0.18", "openai>=1.0", "matplotlib>=3.7"]
23+
hf = ["datasets>=2.14"]
24+
all = ["anthropic>=0.18", "openai>=1.0", "matplotlib>=3.7", "datasets>=2.14"]
2425
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
2526

2627
[project.scripts]

src/synthbench/cli.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ def main():
120120
is_flag=True,
121121
help="Run all 8 demographic attributes (AGE,CREGION,EDUCATION,INCOME,POLIDEOLOGY,POLPARTY,RACE,SEX).",
122122
)
123+
@click.option(
124+
"--country",
125+
default=None,
126+
help="Country for GlobalOpinionQA ground truth (e.g., France, Japan).",
127+
)
123128
def run(
124129
provider,
125130
model,
@@ -136,6 +141,7 @@ def run(
136141
baselines_dir,
137142
demographics,
138143
full_evaluation,
144+
country,
139145
):
140146
"""Run a benchmark evaluation.
141147
@@ -178,6 +184,7 @@ def run(
178184
topic,
179185
baselines_dir,
180186
demo_list,
187+
country,
181188
)
182189
)
183190

@@ -197,6 +204,7 @@ async def _run_async(
197204
topic,
198205
baselines_dir,
199206
demographics=None,
207+
country=None,
200208
):
201209
from synthbench.datasets import DATASETS
202210
from synthbench.providers import load_provider
@@ -216,6 +224,8 @@ async def _run_async(
216224
ds_kwargs = {}
217225
if data_dir:
218226
ds_kwargs["data_dir"] = data_dir
227+
if country:
228+
ds_kwargs["country"] = country
219229
ds = DATASETS[dataset_name](**ds_kwargs)
220230

221231
# Load provider
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
from synthbench.datasets.base import Dataset, Question
22
from synthbench.datasets.opinionsqa import OpinionsQADataset
3+
from synthbench.datasets.globalopinionqa import GlobalOpinionQADataset
34

45
DATASETS: dict[str, type[Dataset]] = {
56
"opinionsqa": OpinionsQADataset,
7+
"globalopinionqa": GlobalOpinionQADataset,
68
}
79

8-
__all__ = ["Dataset", "Question", "OpinionsQADataset", "DATASETS"]
10+
__all__ = [
11+
"Dataset",
12+
"Question",
13+
"OpinionsQADataset",
14+
"GlobalOpinionQADataset",
15+
"DATASETS",
16+
]
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""GlobalOpinionQA dataset loader.
2+
3+
Loads 2,556 survey questions (2,203 Pew Global Attitudes + 353 World Values
4+
Survey) across 138 countries from the Anthropic/llm_global_opinions HuggingFace
5+
dataset.
6+
7+
Source: https://huggingface.co/datasets/Anthropic/llm_global_opinions
8+
License: CC-BY-NC-SA-4.0
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import hashlib
14+
import json
15+
from pathlib import Path
16+
17+
from synthbench.datasets.base import Dataset, Question
18+
19+
20+
def _default_cache_dir() -> Path:
21+
return Path.home() / ".synthbench" / "data" / "globalopinionqa"
22+
23+
24+
def _question_key(text: str, index: int) -> str:
25+
"""Generate a stable key from question text."""
26+
h = hashlib.sha256(text.encode()).hexdigest()[:8]
27+
return f"GOQA_{index}_{h}"
28+
29+
30+
def _aggregate_distributions(
31+
selections: dict[str, list[float]],
32+
options: list[str],
33+
country: str | None = None,
34+
) -> dict[str, float]:
35+
"""Build a probability distribution from the selections dict.
36+
37+
If *country* is given, use that country's distribution as ground truth.
38+
Otherwise, average across all countries.
39+
"""
40+
if country is not None:
41+
probs = selections.get(country)
42+
if probs is None:
43+
raise ValueError(
44+
f"Country '{country}' not found in selections. "
45+
f"Available: {sorted(selections)[:10]}... ({len(selections)} total)"
46+
)
47+
dist = {opt: float(p) for opt, p in zip(options, probs)}
48+
else:
49+
# Average across all countries
50+
n_countries = len(selections)
51+
if n_countries == 0:
52+
return {}
53+
sums = [0.0] * len(options)
54+
for probs in selections.values():
55+
for i, p in enumerate(probs):
56+
sums[i] += float(p)
57+
dist = {opt: s / n_countries for opt, s in zip(options, sums)}
58+
59+
return dist
60+
61+
62+
class GlobalOpinionQADataset(Dataset):
63+
"""GlobalOpinionQA: 2,556 questions across 138 countries."""
64+
65+
def __init__(
66+
self,
67+
data_dir: Path | str | None = None,
68+
country: str | None = None,
69+
):
70+
self._data_dir = Path(data_dir) if data_dir else _default_cache_dir()
71+
self._country = country
72+
73+
@property
74+
def name(self) -> str:
75+
if self._country:
76+
return f"globalopinionqa ({self._country})"
77+
return "globalopinionqa"
78+
79+
def info(self) -> dict:
80+
return {
81+
"name": "GlobalOpinionQA",
82+
"source": "Anthropic/llm_global_opinions (HuggingFace)",
83+
"license": "CC-BY-NC-SA-4.0",
84+
"n_questions": 2556,
85+
"n_countries": 138,
86+
"country_filter": self._country,
87+
}
88+
89+
def load(self, n: int | None = None) -> list[Question]:
90+
cache_path = self._data_dir / "questions.json"
91+
92+
if cache_path.exists():
93+
questions = self._load_cached(cache_path)
94+
else:
95+
questions = self._download_and_process()
96+
97+
if n is not None:
98+
questions = questions[:n]
99+
return questions
100+
101+
def _load_cached(self, path: Path) -> list[Question]:
102+
with open(path) as f:
103+
data = json.load(f)
104+
105+
questions = []
106+
for q in data["questions"]:
107+
dist = _aggregate_distributions(
108+
q["selections"],
109+
q["options"],
110+
country=self._country,
111+
)
112+
if not dist:
113+
continue
114+
questions.append(
115+
Question(
116+
key=q["key"],
117+
text=q["text"],
118+
options=q["options"],
119+
human_distribution=dist,
120+
survey=q.get("survey", ""),
121+
)
122+
)
123+
return questions
124+
125+
def _save_cache(self, raw_rows: list[dict]) -> None:
126+
self._data_dir.mkdir(parents=True, exist_ok=True)
127+
data = {
128+
"dataset": "globalopinionqa",
129+
"version": "1.0",
130+
"n_questions": len(raw_rows),
131+
"questions": raw_rows,
132+
}
133+
cache_path = self._data_dir / "questions.json"
134+
with open(cache_path, "w") as f:
135+
json.dump(data, f, indent=2)
136+
137+
def _download_and_process(self) -> list[Question]:
138+
try:
139+
from datasets import load_dataset
140+
except ImportError:
141+
raise ImportError(
142+
"The 'datasets' package is required for GlobalOpinionQA.\n"
143+
"Install it with: pip install 'synthbench[hf]'\n"
144+
" or: pip install datasets"
145+
)
146+
147+
ds = load_dataset(
148+
"Anthropic/llm_global_opinions",
149+
cache_dir=str(self._data_dir / "hf_cache"),
150+
)
151+
152+
# The dataset has a single split (typically "train")
153+
split = ds["train"] if "train" in ds else ds[list(ds.keys())[0]]
154+
155+
raw_rows: list[dict] = []
156+
questions: list[Question] = []
157+
158+
for i, row in enumerate(split):
159+
text = row["question"]
160+
options = row["options"]
161+
selections = row["selections"]
162+
163+
key = _question_key(text, i)
164+
survey = ""
165+
if "source" in row:
166+
survey = row["source"]
167+
168+
raw_rows.append(
169+
{
170+
"key": key,
171+
"text": text,
172+
"options": options,
173+
"selections": selections,
174+
"survey": survey,
175+
}
176+
)
177+
178+
dist = _aggregate_distributions(selections, options, country=self._country)
179+
if not dist:
180+
continue
181+
182+
questions.append(
183+
Question(
184+
key=key,
185+
text=text,
186+
options=options,
187+
human_distribution=dist,
188+
survey=survey,
189+
)
190+
)
191+
192+
self._save_cache(raw_rows)
193+
return questions
194+
195+
def available_countries(self) -> list[str]:
196+
"""Return sorted list of available country names from the cached data."""
197+
cache_path = self._data_dir / "questions.json"
198+
if not cache_path.exists():
199+
return []
200+
201+
with open(cache_path) as f:
202+
data = json.load(f)
203+
204+
countries: set[str] = set()
205+
for q in data["questions"]:
206+
countries.update(q.get("selections", {}).keys())
207+
return sorted(countries)

0 commit comments

Comments
 (0)