commonground-score 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: commonground-score
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure-Python scoring utilities for Common Ground deliberation evaluations.
|
|
5
|
+
Author: Common Ground contributors
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.12, <3.13
|
|
14
|
+
Project-URL: Repository, https://github.com/<OWNER>/commonground-envs
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# commonground-score
|
|
18
|
+
|
|
19
|
+
Pure-Python scoring utilities for Common Ground deliberation evaluations.
|
|
20
|
+
|
|
21
|
+
## API
|
|
22
|
+
|
|
23
|
+
`commonground_score` exposes:
|
|
24
|
+
|
|
25
|
+
- `prop_test(successes, trials)`
|
|
26
|
+
- `two_prop_test(s_in, s_out, p_in, p_out)`
|
|
27
|
+
- `comment_stats(votes)`
|
|
28
|
+
- `vote_entropy(votes)`
|
|
29
|
+
- `cluster_separation(votes)`
|
|
30
|
+
- `rating_to_vote(value)`
|
|
31
|
+
- `vote_accuracy(predictions, held_out)`
|
|
32
|
+
- `brier_score(predictions, held_out)`
|
|
33
|
+
|
|
34
|
+
`vote_entropy` normalizes agree/disagree/pass entropy to `[0, 1]`.
|
|
35
|
+
`cluster_separation` returns the fraction of faction-vote pairs taking different
|
|
36
|
+
stances. Together they provide the deterministic panel-disagreement math used
|
|
37
|
+
by `commonground-elicit`; missing votes are excluded from both calculations.
|
|
38
|
+
|
|
39
|
+
`brier_score` accepts bare point predictions (`1`, `-1`, or `0`) and
|
|
40
|
+
probability mappings keyed by `agree`, `disagree`, and `pass` or their numeric
|
|
41
|
+
equivalents. Valid non-negative finite mappings are normalized before scoring.
|
|
42
|
+
Invalid or non-normalizable mappings score as the uniform distribution
|
|
43
|
+
(`1/3`, `1/3`, `1/3`) to represent no information.
|
|
44
|
+
|
|
45
|
+
`rating_to_vote(value)` implements the canonical 0-10 rating conversion used for
|
|
46
|
+
dataset parity:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
signed = (2 * (value - 5)) / 10
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Values outside the inclusive `0`-`10` range, plus non-finite values, map to
|
|
53
|
+
neutral/pass (`0`). In-range values below `5` return a negative score, values
|
|
54
|
+
above `5` return a positive score, and exactly `5` returns `0`.
|
|
55
|
+
|
|
56
|
+
## Parity Fixtures
|
|
57
|
+
|
|
58
|
+
The test suite includes a parity fixture loader for
|
|
59
|
+
`tests/fixtures/parity_*.json`. The harness requires fixtures for
|
|
60
|
+
`prop_test`, `two_prop_test`, `comment_stats`, and `rating_to_vote`. Fixture
|
|
61
|
+
files should use:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{"function":"prop_test","cases":[{"args":[2,4],"expected":0.4472135954999579}]}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
JSON cannot encode NaN, so NaN arguments are represented as the exact string
|
|
68
|
+
`"NaN"` and decoded by the harness before invocation.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# commonground-score
|
|
2
|
+
|
|
3
|
+
Pure-Python scoring utilities for Common Ground deliberation evaluations.
|
|
4
|
+
|
|
5
|
+
## API
|
|
6
|
+
|
|
7
|
+
`commonground_score` exposes:
|
|
8
|
+
|
|
9
|
+
- `prop_test(successes, trials)`
|
|
10
|
+
- `two_prop_test(s_in, s_out, p_in, p_out)`
|
|
11
|
+
- `comment_stats(votes)`
|
|
12
|
+
- `vote_entropy(votes)`
|
|
13
|
+
- `cluster_separation(votes)`
|
|
14
|
+
- `rating_to_vote(value)`
|
|
15
|
+
- `vote_accuracy(predictions, held_out)`
|
|
16
|
+
- `brier_score(predictions, held_out)`
|
|
17
|
+
|
|
18
|
+
`vote_entropy` normalizes agree/disagree/pass entropy to `[0, 1]`.
|
|
19
|
+
`cluster_separation` returns the fraction of faction-vote pairs taking different
|
|
20
|
+
stances. Together they provide the deterministic panel-disagreement math used
|
|
21
|
+
by `commonground-elicit`; missing votes are excluded from both calculations.
|
|
22
|
+
|
|
23
|
+
`brier_score` accepts bare point predictions (`1`, `-1`, or `0`) and
|
|
24
|
+
probability mappings keyed by `agree`, `disagree`, and `pass` or their numeric
|
|
25
|
+
equivalents. Valid non-negative finite mappings are normalized before scoring.
|
|
26
|
+
Invalid or non-normalizable mappings score as the uniform distribution
|
|
27
|
+
(`1/3`, `1/3`, `1/3`) to represent no information.
|
|
28
|
+
|
|
29
|
+
`rating_to_vote(value)` implements the canonical 0-10 rating conversion used for
|
|
30
|
+
dataset parity:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
signed = (2 * (value - 5)) / 10
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Values outside the inclusive `0`-`10` range, plus non-finite values, map to
|
|
37
|
+
neutral/pass (`0`). In-range values below `5` return a negative score, values
|
|
38
|
+
above `5` return a positive score, and exactly `5` returns `0`.
|
|
39
|
+
|
|
40
|
+
## Parity Fixtures
|
|
41
|
+
|
|
42
|
+
The test suite includes a parity fixture loader for
|
|
43
|
+
`tests/fixtures/parity_*.json`. The harness requires fixtures for
|
|
44
|
+
`prop_test`, `two_prop_test`, `comment_stats`, and `rating_to_vote`. Fixture
|
|
45
|
+
files should use:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"function":"prop_test","cases":[{"args":[2,4],"expected":0.4472135954999579}]}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
JSON cannot encode NaN, so NaN arguments are represented as the exact string
|
|
52
|
+
`"NaN"` and decoded by the harness before invocation.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "commonground-score"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Pure-Python scoring utilities for Common Ground deliberation evaluations."
|
|
5
|
+
authors = [{ name = "Common Ground contributors" }]
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.12,<3.13"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Intended Audience :: Science/Research",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
16
|
+
]
|
|
17
|
+
dependencies = []
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Repository = "https://github.com/<OWNER>/commonground-envs"
|
|
21
|
+
|
|
22
|
+
[build-system]
|
|
23
|
+
requires = ["uv_build>=0.8.0,<0.9.0"]
|
|
24
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Scoring utilities for Common Ground deliberation evaluations."""
|
|
2
|
+
|
|
3
|
+
from commonground_score.scoring import (
|
|
4
|
+
brier_score,
|
|
5
|
+
cluster_separation,
|
|
6
|
+
comment_stats,
|
|
7
|
+
prop_test,
|
|
8
|
+
rating_to_vote,
|
|
9
|
+
two_prop_test,
|
|
10
|
+
vote_accuracy,
|
|
11
|
+
vote_entropy,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"brier_score",
|
|
18
|
+
"cluster_separation",
|
|
19
|
+
"comment_stats",
|
|
20
|
+
"prop_test",
|
|
21
|
+
"rating_to_vote",
|
|
22
|
+
"two_prop_test",
|
|
23
|
+
"vote_accuracy",
|
|
24
|
+
"vote_entropy",
|
|
25
|
+
]
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Polis-compatible vote statistics and reward helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from math import isfinite, log, sqrt
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
Vote = int | None
|
|
10
|
+
PointPrediction = int
|
|
11
|
+
ProbPrediction = Mapping[object, object]
|
|
12
|
+
Prediction = PointPrediction | ProbPrediction
|
|
13
|
+
|
|
14
|
+
_LABEL_TO_VOTE = {"agree": 1, "disagree": -1, "pass": 0}
|
|
15
|
+
_VOTE_TO_LABEL = {vote: label for label, vote in _LABEL_TO_VOTE.items()}
|
|
16
|
+
_LABELS = ("agree", "disagree", "pass")
|
|
17
|
+
RATING_MIN = 0
|
|
18
|
+
RATING_MAX = 10
|
|
19
|
+
VALID_VOTES = (-1, 0, 1)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def prop_test(successes: int, trials: int) -> float:
|
|
23
|
+
"""Return the one-proportion test statistic used by Polis math."""
|
|
24
|
+
|
|
25
|
+
return 2 * sqrt(trials + 1) * ((successes + 1) / (trials + 1) - 0.5)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def two_prop_test(s_in: int, s_out: int, p_in: int, p_out: int) -> float:
|
|
29
|
+
"""Return the smoothed two-proportion test statistic used by Polis math."""
|
|
30
|
+
|
|
31
|
+
s_in += 1
|
|
32
|
+
s_out += 1
|
|
33
|
+
p_in += 1
|
|
34
|
+
p_out += 1
|
|
35
|
+
pi1 = s_in / p_in
|
|
36
|
+
pi2 = s_out / p_out
|
|
37
|
+
pi_hat = (s_in + s_out) / (p_in + p_out)
|
|
38
|
+
if pi_hat == 1:
|
|
39
|
+
return 0
|
|
40
|
+
return (pi1 - pi2) / sqrt(pi_hat * (1 - pi_hat) * ((1 / p_in) + (1 / p_out)))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def comment_stats(votes: list[Vote]) -> dict[str, float | int]:
|
|
44
|
+
"""Summarize agree/disagree/pass counts and smoothed proportions for a statement."""
|
|
45
|
+
|
|
46
|
+
agree = sum(vote == 1 for vote in votes)
|
|
47
|
+
disagree = sum(vote == -1 for vote in votes)
|
|
48
|
+
passed = sum(vote == 0 for vote in votes)
|
|
49
|
+
seen = sum(vote is not None for vote in votes)
|
|
50
|
+
return {
|
|
51
|
+
"agree": agree,
|
|
52
|
+
"disagree": disagree,
|
|
53
|
+
"pass": passed,
|
|
54
|
+
"seen": seen,
|
|
55
|
+
"pa": (agree + 1) / (seen + 2),
|
|
56
|
+
"pd": (disagree + 1) / (seen + 2),
|
|
57
|
+
"pat": prop_test(agree, seen),
|
|
58
|
+
"pdt": prop_test(disagree, seen),
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def vote_entropy(votes: list[Vote]) -> float:
|
|
63
|
+
"""Return normalized agree/disagree/pass entropy over seen valid votes."""
|
|
64
|
+
|
|
65
|
+
seen = [vote for vote in votes if vote in VALID_VOTES]
|
|
66
|
+
if not seen:
|
|
67
|
+
return 0.0
|
|
68
|
+
entropy = 0.0
|
|
69
|
+
for vote in VALID_VOTES:
|
|
70
|
+
count = seen.count(vote)
|
|
71
|
+
if count:
|
|
72
|
+
probability = count / len(seen)
|
|
73
|
+
entropy -= probability * log(probability)
|
|
74
|
+
return entropy / log(len(VALID_VOTES))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cluster_separation(votes: list[Vote]) -> float:
|
|
78
|
+
"""Return the fraction of seen faction pairs taking different stances."""
|
|
79
|
+
|
|
80
|
+
seen = [vote for vote in votes if vote in VALID_VOTES]
|
|
81
|
+
pair_count = len(seen) * (len(seen) - 1) // 2
|
|
82
|
+
if pair_count == 0:
|
|
83
|
+
return 0.0
|
|
84
|
+
separated_pairs = sum(
|
|
85
|
+
left_vote != right_vote
|
|
86
|
+
for left_index, left_vote in enumerate(seen)
|
|
87
|
+
for right_vote in seen[left_index + 1 :]
|
|
88
|
+
)
|
|
89
|
+
return separated_pairs / pair_count
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def rating_to_vote(value: float) -> float:
|
|
93
|
+
"""Map a 0-10 rating to the canonical signed vote score.
|
|
94
|
+
|
|
95
|
+
Non-finite values and values outside ``[0, 10]`` return ``0`` (no stance).
|
|
96
|
+
In-range values return ``(2 * (value - 5)) / 10``: positive values lean
|
|
97
|
+
agree, negative values lean disagree, and exactly ``5`` is neutral/pass.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
if not isfinite(value) or value < RATING_MIN or value > RATING_MAX:
|
|
101
|
+
return 0
|
|
102
|
+
return (2 * (value - 5)) / 10
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def vote_accuracy(predictions: Mapping[str, int], held_out: Mapping[str, int]) -> float:
|
|
106
|
+
"""Return the exact-match fraction over held-out cells.
|
|
107
|
+
|
|
108
|
+
Missing predictions are counted as wrong.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if not held_out:
|
|
112
|
+
return 0.0
|
|
113
|
+
correct = sum(
|
|
114
|
+
predictions.get(cell_id) == vote for cell_id, vote in held_out.items()
|
|
115
|
+
)
|
|
116
|
+
return correct / len(held_out)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def brier_score(
|
|
120
|
+
predictions: Mapping[str, Prediction],
|
|
121
|
+
held_out: Mapping[str, int],
|
|
122
|
+
) -> float:
|
|
123
|
+
"""Return mean multiclass Brier score for agree/disagree/pass predictions.
|
|
124
|
+
|
|
125
|
+
Probabilistic predictions are dictionaries keyed by ``agree``, ``disagree``,
|
|
126
|
+
and ``pass``. Valid non-negative finite mappings are normalized to sum to
|
|
127
|
+
one. Invalid or non-normalizable mappings score as the uniform
|
|
128
|
+
distribution, representing no information. Point predictions using ``1``,
|
|
129
|
+
``-1``, or ``0`` are converted to one-hot probabilities. Missing
|
|
130
|
+
predictions use an all-zero vector.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
if not held_out:
|
|
134
|
+
return 0.0
|
|
135
|
+
total = 0.0
|
|
136
|
+
for cell_id, actual_vote in held_out.items():
|
|
137
|
+
pred_probs = _prediction_probs(predictions.get(cell_id))
|
|
138
|
+
actual_label = _VOTE_TO_LABEL[actual_vote]
|
|
139
|
+
total += sum(
|
|
140
|
+
(pred_probs[label] - float(label == actual_label)) ** 2 for label in _LABELS
|
|
141
|
+
)
|
|
142
|
+
return total / len(held_out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _prediction_probs(prediction: Prediction | None) -> dict[str, float]:
|
|
146
|
+
if isinstance(prediction, Mapping):
|
|
147
|
+
return _mapping_prediction_probs(prediction)
|
|
148
|
+
if prediction in _VOTE_TO_LABEL:
|
|
149
|
+
label = _VOTE_TO_LABEL[prediction]
|
|
150
|
+
return {candidate: float(candidate == label) for candidate in _LABELS}
|
|
151
|
+
return {label: 0.0 for label in _LABELS}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _mapping_prediction_probs(prediction: Mapping[object, object]) -> dict[str, float]:
|
|
155
|
+
scores: dict[str, float] = {}
|
|
156
|
+
invalid = False
|
|
157
|
+
for key, value in prediction.items():
|
|
158
|
+
label = _coerce_class_label(key)
|
|
159
|
+
if label is None:
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
score = float(cast(Any, value))
|
|
163
|
+
except (TypeError, ValueError):
|
|
164
|
+
invalid = True
|
|
165
|
+
continue
|
|
166
|
+
if not isfinite(score):
|
|
167
|
+
invalid = True
|
|
168
|
+
continue
|
|
169
|
+
scores[label] = score
|
|
170
|
+
if score < 0:
|
|
171
|
+
invalid = True
|
|
172
|
+
|
|
173
|
+
total = sum(scores.get(label, 0.0) for label in _LABELS)
|
|
174
|
+
if scores and not invalid and total > 0:
|
|
175
|
+
return {label: scores.get(label, 0.0) / total for label in _LABELS}
|
|
176
|
+
return _uniform_probs()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _uniform_probs() -> dict[str, float]:
|
|
180
|
+
return {label: 1 / len(_LABELS) for label in _LABELS}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _coerce_class_label(key: object) -> str | None:
|
|
184
|
+
if isinstance(key, bool):
|
|
185
|
+
return None
|
|
186
|
+
if isinstance(key, int) and key in _VOTE_TO_LABEL:
|
|
187
|
+
return _VOTE_TO_LABEL[key]
|
|
188
|
+
if isinstance(key, str):
|
|
189
|
+
if key in _LABEL_TO_VOTE:
|
|
190
|
+
return key
|
|
191
|
+
if key in {"-1", "0", "1"}:
|
|
192
|
+
return _VOTE_TO_LABEL[int(key)]
|
|
193
|
+
return None
|