toxpol-nlp 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.
- toxpol_nlp-0.1.0/PKG-INFO +62 -0
- toxpol_nlp-0.1.0/README.md +35 -0
- toxpol_nlp-0.1.0/pyproject.toml +40 -0
- toxpol_nlp-0.1.0/setup.cfg +4 -0
- toxpol_nlp-0.1.0/toxpol/__init__.py +3 -0
- toxpol_nlp-0.1.0/toxpol/datagen.py +373 -0
- toxpol_nlp-0.1.0/toxpol_nlp.egg-info/PKG-INFO +62 -0
- toxpol_nlp-0.1.0/toxpol_nlp.egg-info/SOURCES.txt +9 -0
- toxpol_nlp-0.1.0/toxpol_nlp.egg-info/dependency_links.txt +1 -0
- toxpol_nlp-0.1.0/toxpol_nlp.egg-info/requires.txt +10 -0
- toxpol_nlp-0.1.0/toxpol_nlp.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toxpol-nlp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NLP toolkit for toxicity and polarization research: synthetic datasets and detection algorithms
|
|
5
|
+
Author-email: SwkratisCS <swkratisgiannoutsos@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/SwkratisCS/polarizedtrees
|
|
8
|
+
Keywords: toxicity,polarization,annotation,synthetic data,nlp,disagreement,crowdsourcing,demographics
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Provides-Extra: ndfu
|
|
22
|
+
Requires-Dist: ndfu; extra == "ndfu"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# toxpol-nlp
|
|
29
|
+
|
|
30
|
+
NLP toolkit for **toxicity and polarization research**. Provides tools for synthetic dataset generation and polarization detection in human annotation studies.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install toxpol-nlp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Repository Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
data_gen/ synthetic annotation dataset generator
|
|
42
|
+
polarized_trees/ Polarized Trees detection algorithm
|
|
43
|
+
toxpol/ installable package (source code)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `data_gen/`
|
|
47
|
+
Tools for generating synthetic annotation datasets with **injected, known polarization**. Real annotation data cannot provide ground truth for which demographic dimensions drive disagreement — this module does. The generated datasets are the primary validation input for the Polarized Trees algorithm.
|
|
48
|
+
|
|
49
|
+
→ See [`data_gen/README.md`](data_gen/README.md) for the full API and usage.
|
|
50
|
+
|
|
51
|
+
### `polarized_trees/`
|
|
52
|
+
The Polarized Trees detection algorithm. Given an annotation dataset, it identifies which demographic dimensions split annotators into opposing rating poles and at what severity.
|
|
53
|
+
|
|
54
|
+
→ Coming soon.
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
| Module | Description | Status |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `toxpol.datagen` | Synthetic annotator pool with injected, ground-truth polarization | Stable |
|
|
61
|
+
| `toxpol.trees` | Polarized Trees detection algorithm | Coming soon |
|
|
62
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# toxpol-nlp
|
|
2
|
+
|
|
3
|
+
NLP toolkit for **toxicity and polarization research**. Provides tools for synthetic dataset generation and polarization detection in human annotation studies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install toxpol-nlp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Repository Structure
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
data_gen/ synthetic annotation dataset generator
|
|
15
|
+
polarized_trees/ Polarized Trees detection algorithm
|
|
16
|
+
toxpol/ installable package (source code)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `data_gen/`
|
|
20
|
+
Tools for generating synthetic annotation datasets with **injected, known polarization**. Real annotation data cannot provide ground truth for which demographic dimensions drive disagreement — this module does. The generated datasets are the primary validation input for the Polarized Trees algorithm.
|
|
21
|
+
|
|
22
|
+
→ See [`data_gen/README.md`](data_gen/README.md) for the full API and usage.
|
|
23
|
+
|
|
24
|
+
### `polarized_trees/`
|
|
25
|
+
The Polarized Trees detection algorithm. Given an annotation dataset, it identifies which demographic dimensions split annotators into opposing rating poles and at what severity.
|
|
26
|
+
|
|
27
|
+
→ Coming soon.
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
| Module | Description | Status |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `toxpol.datagen` | Synthetic annotator pool with injected, ground-truth polarization | Stable |
|
|
34
|
+
| `toxpol.trees` | Polarized Trees detection algorithm | Coming soon |
|
|
35
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "toxpol-nlp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "NLP toolkit for toxicity and polarization research: synthetic datasets and detection algorithms"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "SwkratisCS", email = "swkratisgiannoutsos@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["toxicity", "polarization", "annotation", "synthetic data", "nlp", "disagreement", "crowdsourcing", "demographics"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"numpy",
|
|
28
|
+
"pandas",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
ndfu = ["ndfu"]
|
|
33
|
+
dev = ["pytest", "build", "twine"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Repository = "https://github.com/SwkratisCS/polarizedtrees"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["toxpol*"]
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Synthetic annotation dataset generator for studying demographic polarization in human labeling.
|
|
3
|
+
|
|
4
|
+
Builds a pool of annotators with explicit demographic identities and generates structured
|
|
5
|
+
disagreement patterns where rating behavior is governed by per-dimension bias configurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import itertools
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Default demographic dimensions used in the paper
|
|
16
|
+
DEFAULT_DIMENSIONS = {
|
|
17
|
+
"gender": ["male", "female", "non-binary"],
|
|
18
|
+
"politics": ["left", "center", "right"],
|
|
19
|
+
"age": ["<25", "25-50", ">50"],
|
|
20
|
+
"education": ["low", "medium", "high"],
|
|
21
|
+
"orientation": ["heterosexual", "lgbtq+"],
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AnnotatorPool:
|
|
26
|
+
"""
|
|
27
|
+
Synthetic annotator pool for generating polarized rating datasets.
|
|
28
|
+
|
|
29
|
+
Builds a Cartesian-product pool of demographic identities, then generates
|
|
30
|
+
annotation datasets where each dimension is randomly assigned either a
|
|
31
|
+
"polarizing" role (splitting annotators into toxic/civil poles) or a
|
|
32
|
+
"unimodal" role (converging all annotators toward one rating range).
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
dimensions : dict[str, list[str]]
|
|
37
|
+
Mapping from dimension name to the list of possible values.
|
|
38
|
+
Example: {"politics": ["left", "center", "right"], "age": ["<25", ">25"]}
|
|
39
|
+
|
|
40
|
+
exclude : list[str] | None
|
|
41
|
+
Dimension names to drop before building identities. Useful for ablations.
|
|
42
|
+
|
|
43
|
+
annotators_per_identity : int
|
|
44
|
+
How many annotators share each unique demographic combination.
|
|
45
|
+
Pool size = product(len(v) for v in dimensions.values()) * annotators_per_identity.
|
|
46
|
+
|
|
47
|
+
scale : int
|
|
48
|
+
Maximum value on the rating scale (ratings are integers in [1, scale]).
|
|
49
|
+
|
|
50
|
+
toxic_range : tuple[int, int]
|
|
51
|
+
(low, high) inclusive range from which toxic-pole annotators draw ratings.
|
|
52
|
+
|
|
53
|
+
civil_range : tuple[int, int]
|
|
54
|
+
(low, high) inclusive range from which civil-pole annotators draw ratings.
|
|
55
|
+
|
|
56
|
+
neutral_range : tuple[int, int]
|
|
57
|
+
(low, high) inclusive range used when a unimodal dimension converges to "neutral".
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
>>> from toxpol.datagen import AnnotatorPool, DEFAULT_DIMENSIONS
|
|
62
|
+
>>> pool = AnnotatorPool(DEFAULT_DIMENSIONS)
|
|
63
|
+
>>> dataset, bias_config = pool.generate_dataset(n_texts=50, n_annotators_per_text=100)
|
|
64
|
+
>>> dataset.head()
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
dimensions,
|
|
70
|
+
exclude=None,
|
|
71
|
+
annotators_per_identity=10,
|
|
72
|
+
scale=5,
|
|
73
|
+
toxic_range=(4, 5),
|
|
74
|
+
civil_range=(1, 2),
|
|
75
|
+
neutral_range=(3, 3),
|
|
76
|
+
):
|
|
77
|
+
self.annotators_per_identity = annotators_per_identity
|
|
78
|
+
self.identities, self.active_dims = self._get_identities(dimensions, exclude)
|
|
79
|
+
self.pool = self._build_pool()
|
|
80
|
+
|
|
81
|
+
self.scale = scale
|
|
82
|
+
self.toxic_range = toxic_range
|
|
83
|
+
self.civil_range = civil_range
|
|
84
|
+
self.neutral_range = neutral_range
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Pool construction
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def _get_identities(self, dimensions, exclude=None):
|
|
91
|
+
active_dims = {k: v for k, v in dimensions.items() if k not in (exclude or [])}
|
|
92
|
+
identities = [
|
|
93
|
+
dict(zip(active_dims.keys(), combo))
|
|
94
|
+
for combo in itertools.product(*active_dims.values())
|
|
95
|
+
]
|
|
96
|
+
return identities, active_dims
|
|
97
|
+
|
|
98
|
+
def _build_pool(self):
|
|
99
|
+
pool = []
|
|
100
|
+
for identity in self.identities:
|
|
101
|
+
for _ in range(self.annotators_per_identity):
|
|
102
|
+
pool.append(identity.copy())
|
|
103
|
+
pool = pd.DataFrame(pool)
|
|
104
|
+
pool.index.name = "annotator_id"
|
|
105
|
+
return pool
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Bias configuration
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def _generate_bias_config(self, polarizing_prob=0.7):
|
|
112
|
+
"""
|
|
113
|
+
Randomly assign each active dimension a role for one dataset instance.
|
|
114
|
+
|
|
115
|
+
A "polarizing" dimension splits its values into a toxic pole and a civil
|
|
116
|
+
pole. An "unimodal" dimension converges all annotators toward one range.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
dict
|
|
121
|
+
Keys are dimension names. Each value is a dict with:
|
|
122
|
+
- role: "polarizing" | "unimodal"
|
|
123
|
+
- toxic_pole / civil_pole (if polarizing): lists of dimension values
|
|
124
|
+
- convergence (if unimodal): "toxic" | "civil" | "neutral"
|
|
125
|
+
"""
|
|
126
|
+
config = {}
|
|
127
|
+
for dim, values in self.active_dims.items():
|
|
128
|
+
role = random.choices(
|
|
129
|
+
["polarizing", "unimodal"],
|
|
130
|
+
weights=[polarizing_prob, 1 - polarizing_prob],
|
|
131
|
+
)[0]
|
|
132
|
+
if role == "polarizing":
|
|
133
|
+
shuffled = values.copy()
|
|
134
|
+
random.shuffle(shuffled)
|
|
135
|
+
split = random.randint(1, len(shuffled) - 1)
|
|
136
|
+
config[dim] = {
|
|
137
|
+
"role": "polarizing",
|
|
138
|
+
"toxic_pole": shuffled[:split],
|
|
139
|
+
"civil_pole": shuffled[split:],
|
|
140
|
+
}
|
|
141
|
+
else:
|
|
142
|
+
config[dim] = {
|
|
143
|
+
"role": "unimodal",
|
|
144
|
+
"convergence": random.choice(["toxic", "civil", "neutral"]),
|
|
145
|
+
}
|
|
146
|
+
return config
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------------
|
|
149
|
+
# Public API
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
def generate_dataset(
|
|
153
|
+
self,
|
|
154
|
+
n_texts=100,
|
|
155
|
+
n_annotators_per_text=100,
|
|
156
|
+
noise=0.1,
|
|
157
|
+
polarizing_prob=0.7,
|
|
158
|
+
):
|
|
159
|
+
"""
|
|
160
|
+
Generate a synthetic annotation dataset.
|
|
161
|
+
|
|
162
|
+
A single bias configuration is drawn for the entire dataset (all texts
|
|
163
|
+
share the same demographic polarization structure). Each text is then
|
|
164
|
+
annotated by a random subset of the pool.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
n_texts : int
|
|
169
|
+
Number of texts to annotate. Must be >= 1.
|
|
170
|
+
|
|
171
|
+
n_annotators_per_text : int
|
|
172
|
+
Annotators sampled per text (without replacement).
|
|
173
|
+
Must be <= pool size (annotators_per_identity * number_of_identities).
|
|
174
|
+
|
|
175
|
+
noise : float in [0, 1]
|
|
176
|
+
Probability that any annotator ignores the bias config and draws
|
|
177
|
+
a uniformly random rating instead.
|
|
178
|
+
|
|
179
|
+
polarizing_prob : float in [0, 1]
|
|
180
|
+
Prior probability that each dimension is assigned a "polarizing"
|
|
181
|
+
role in the bias config (vs. "unimodal").
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
dataset : pd.DataFrame
|
|
186
|
+
One row per (text_id, annotator_id) pair. Columns:
|
|
187
|
+
text_id, annotator_id, <all active dimension columns>, rating.
|
|
188
|
+
|
|
189
|
+
bias_config : dict
|
|
190
|
+
The bias configuration used for this dataset. See
|
|
191
|
+
`_generate_bias_config` for the structure.
|
|
192
|
+
"""
|
|
193
|
+
if n_annotators_per_text > len(self.pool):
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"n_annotators_per_text ({n_annotators_per_text}) exceeds pool size "
|
|
196
|
+
f"({len(self.pool)}). Reduce n_annotators_per_text or increase "
|
|
197
|
+
f"annotators_per_identity."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
bias_config = self._generate_bias_config(polarizing_prob)
|
|
201
|
+
|
|
202
|
+
# Precompute vote lookup for each polarizing dimension:
|
|
203
|
+
# maps annotator value → "toxic" | "civil"
|
|
204
|
+
vote_maps = {}
|
|
205
|
+
unimodal_fallback = None
|
|
206
|
+
for dim, config in bias_config.items():
|
|
207
|
+
if config["role"] == "polarizing":
|
|
208
|
+
vote_maps[dim] = (
|
|
209
|
+
{v: "toxic" for v in config["toxic_pole"]}
|
|
210
|
+
| {v: "civil" for v in config["civil_pole"]}
|
|
211
|
+
)
|
|
212
|
+
elif unimodal_fallback is None:
|
|
213
|
+
unimodal_fallback = config["convergence"]
|
|
214
|
+
|
|
215
|
+
frames = []
|
|
216
|
+
for text_id in range(n_texts):
|
|
217
|
+
sampled = self.pool.sample(n=n_annotators_per_text, replace=False)
|
|
218
|
+
|
|
219
|
+
if vote_maps:
|
|
220
|
+
# Vectorised majority vote across all polarizing dimensions
|
|
221
|
+
toxic = np.zeros(len(sampled), dtype=np.int32)
|
|
222
|
+
civil = np.zeros(len(sampled), dtype=np.int32)
|
|
223
|
+
for dim, vmap in vote_maps.items():
|
|
224
|
+
mapped = sampled[dim].map(vmap)
|
|
225
|
+
toxic += (mapped == "toxic").values
|
|
226
|
+
civil += (mapped == "civil").values
|
|
227
|
+
label = np.where(toxic > civil, 0,
|
|
228
|
+
np.where(civil > toxic, 1, 2)) # 0=toxic,1=civil,2=neutral
|
|
229
|
+
else:
|
|
230
|
+
fb = {"toxic": 0, "civil": 1, "neutral": 2}[unimodal_fallback or "neutral"]
|
|
231
|
+
label = np.full(len(sampled), fb, dtype=np.int32)
|
|
232
|
+
|
|
233
|
+
# Draw ratings from the appropriate range per label
|
|
234
|
+
ranges = [self.toxic_range, self.civil_range, self.neutral_range]
|
|
235
|
+
ratings = np.array([
|
|
236
|
+
np.random.randint(ranges[l][0], ranges[l][1] + 1) for l in label
|
|
237
|
+
])
|
|
238
|
+
|
|
239
|
+
# Inject noise
|
|
240
|
+
noise_mask = np.random.random(len(sampled)) < noise
|
|
241
|
+
ratings[noise_mask] = np.random.randint(1, self.scale + 1, noise_mask.sum())
|
|
242
|
+
|
|
243
|
+
frame = sampled.copy()
|
|
244
|
+
frame.insert(0, "text_id", text_id)
|
|
245
|
+
frame["rating"] = ratings
|
|
246
|
+
frames.append(frame)
|
|
247
|
+
|
|
248
|
+
dataset = pd.concat(frames)
|
|
249
|
+
dataset.index.name = "annotator_id"
|
|
250
|
+
return dataset, bias_config
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
# Convenience / diagnostics
|
|
254
|
+
# ------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def pool_size(self):
|
|
258
|
+
"""Total number of annotators in the pool."""
|
|
259
|
+
return len(self.pool)
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def n_identities(self):
|
|
263
|
+
"""Number of unique demographic identity combinations."""
|
|
264
|
+
return len(self.identities)
|
|
265
|
+
|
|
266
|
+
def summary(self):
|
|
267
|
+
"""Print a brief summary of the pool configuration."""
|
|
268
|
+
print(f"Active dimensions : {list(self.active_dims.keys())}")
|
|
269
|
+
print(f"Unique identities : {self.n_identities}")
|
|
270
|
+
print(f"Annotators/identity: {self.annotators_per_identity}")
|
|
271
|
+
print(f"Pool size : {self.pool_size}")
|
|
272
|
+
print(f"Rating scale : 1–{self.scale}")
|
|
273
|
+
print(f" toxic_range : {self.toxic_range}")
|
|
274
|
+
print(f" civil_range : {self.civil_range}")
|
|
275
|
+
print(f" neutral_range : {self.neutral_range}")
|
|
276
|
+
|
|
277
|
+
def describe_bias(self, bias_config):
|
|
278
|
+
"""Pretty-print the bias config as a readable table."""
|
|
279
|
+
col_w = max(len(d) for d in bias_config) + 2
|
|
280
|
+
print(f"{'dimension':<{col_w}} {'role':<12} details")
|
|
281
|
+
print("-" * 60)
|
|
282
|
+
for dim, config in bias_config.items():
|
|
283
|
+
if config["role"] == "polarizing":
|
|
284
|
+
details = (
|
|
285
|
+
f"toxic={config['toxic_pole']} civil={config['civil_pole']}"
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
details = f"convergence={config['convergence']}"
|
|
289
|
+
print(f"{dim:<{col_w}} {config['role']:<12} {details}")
|
|
290
|
+
|
|
291
|
+
def analyze(self, dataset, bias_config):
|
|
292
|
+
"""
|
|
293
|
+
Compute nDFU scores for every text, overall and per dimension value.
|
|
294
|
+
|
|
295
|
+
Requires the `ndfu` package (`pip install toxpol-nlp[ndfu]`).
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
dict
|
|
300
|
+
results[text_id]["overall"] -> float
|
|
301
|
+
results[text_id][dim][value] -> float
|
|
302
|
+
"""
|
|
303
|
+
try:
|
|
304
|
+
from ndfu import dfu
|
|
305
|
+
except ImportError:
|
|
306
|
+
raise ImportError(
|
|
307
|
+
"ndfu is required for analyze(). "
|
|
308
|
+
"Install it with: pip install toxpol-nlp[ndfu]"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _ndfu(ratings):
|
|
312
|
+
counts = np.bincount(ratings, minlength=self.scale + 1)[1:]
|
|
313
|
+
hist = counts / counts.sum()
|
|
314
|
+
return dfu(hist)
|
|
315
|
+
|
|
316
|
+
results = {}
|
|
317
|
+
for text_id, text_data in dataset.groupby("text_id"):
|
|
318
|
+
text_results = {"overall": _ndfu(text_data["rating"].values)}
|
|
319
|
+
for dim in self.active_dims:
|
|
320
|
+
text_results[dim] = {
|
|
321
|
+
value: _ndfu(group["rating"].values)
|
|
322
|
+
for value, group in text_data.groupby(dim)
|
|
323
|
+
}
|
|
324
|
+
results[text_id] = text_results
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
def summarize(self, dataset, bias_config, text_id=0):
|
|
328
|
+
"""
|
|
329
|
+
Print nDFU scores for one text, grouped by dimension, with bias roles shown.
|
|
330
|
+
|
|
331
|
+
Calls analyze() internally. Requires `ndfu`.
|
|
332
|
+
"""
|
|
333
|
+
results = self.analyze(dataset, bias_config)
|
|
334
|
+
text = results[text_id]
|
|
335
|
+
print(f"Text {text_id} — overall nDFU: {text['overall']:.3f}\n")
|
|
336
|
+
for dim, values in text.items():
|
|
337
|
+
if dim == "overall":
|
|
338
|
+
continue
|
|
339
|
+
role = bias_config[dim]["role"]
|
|
340
|
+
print(f"{dim} ({role}):")
|
|
341
|
+
for value, score in values.items():
|
|
342
|
+
print(f" {value}: {score:.3f}")
|
|
343
|
+
print()
|
|
344
|
+
|
|
345
|
+
def summarize_all(self, dataset, bias_config):
|
|
346
|
+
"""
|
|
347
|
+
Print mean nDFU per dimension value, aggregated across all texts.
|
|
348
|
+
|
|
349
|
+
Gives a compact cross-text view: instead of per-text scores, each
|
|
350
|
+
dimension value shows its average nDFU and the spread (min–max).
|
|
351
|
+
Useful for seeing which demographic groups consistently disagree
|
|
352
|
+
more across the whole dataset.
|
|
353
|
+
|
|
354
|
+
Calls analyze() internally. Requires `ndfu`.
|
|
355
|
+
"""
|
|
356
|
+
results = self.analyze(dataset, bias_config)
|
|
357
|
+
n_texts = len(results)
|
|
358
|
+
|
|
359
|
+
overall_scores = [results[t]["overall"] for t in results]
|
|
360
|
+
print(f"Overall nDFU — mean: {np.mean(overall_scores):.3f} "
|
|
361
|
+
f"min: {np.min(overall_scores):.3f} "
|
|
362
|
+
f"max: {np.max(overall_scores):.3f} "
|
|
363
|
+
f"(across {n_texts} texts)\n")
|
|
364
|
+
|
|
365
|
+
for dim in self.active_dims:
|
|
366
|
+
role = bias_config[dim]["role"]
|
|
367
|
+
print(f"{dim} ({role}):")
|
|
368
|
+
for value in self.active_dims[dim]:
|
|
369
|
+
scores = [results[t][dim][value] for t in results if value in results[t][dim]]
|
|
370
|
+
print(f" {value:<15} mean: {np.mean(scores):.3f} "
|
|
371
|
+
f"min: {np.min(scores):.3f} "
|
|
372
|
+
f"max: {np.max(scores):.3f}")
|
|
373
|
+
print()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: toxpol-nlp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: NLP toolkit for toxicity and polarization research: synthetic datasets and detection algorithms
|
|
5
|
+
Author-email: SwkratisCS <swkratisgiannoutsos@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/SwkratisCS/polarizedtrees
|
|
8
|
+
Keywords: toxicity,polarization,annotation,synthetic data,nlp,disagreement,crowdsourcing,demographics
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Provides-Extra: ndfu
|
|
22
|
+
Requires-Dist: ndfu; extra == "ndfu"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: build; extra == "dev"
|
|
26
|
+
Requires-Dist: twine; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# toxpol-nlp
|
|
29
|
+
|
|
30
|
+
NLP toolkit for **toxicity and polarization research**. Provides tools for synthetic dataset generation and polarization detection in human annotation studies.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install toxpol-nlp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Repository Structure
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
data_gen/ synthetic annotation dataset generator
|
|
42
|
+
polarized_trees/ Polarized Trees detection algorithm
|
|
43
|
+
toxpol/ installable package (source code)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `data_gen/`
|
|
47
|
+
Tools for generating synthetic annotation datasets with **injected, known polarization**. Real annotation data cannot provide ground truth for which demographic dimensions drive disagreement — this module does. The generated datasets are the primary validation input for the Polarized Trees algorithm.
|
|
48
|
+
|
|
49
|
+
→ See [`data_gen/README.md`](data_gen/README.md) for the full API and usage.
|
|
50
|
+
|
|
51
|
+
### `polarized_trees/`
|
|
52
|
+
The Polarized Trees detection algorithm. Given an annotation dataset, it identifies which demographic dimensions split annotators into opposing rating poles and at what severity.
|
|
53
|
+
|
|
54
|
+
→ Coming soon.
|
|
55
|
+
|
|
56
|
+
## Tools
|
|
57
|
+
|
|
58
|
+
| Module | Description | Status |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `toxpol.datagen` | Synthetic annotator pool with injected, ground-truth polarization | Stable |
|
|
61
|
+
| `toxpol.trees` | Polarized Trees detection algorithm | Coming soon |
|
|
62
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
toxpol
|