polvader 1.0.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.
- polvader-1.0.0/LICENSE +21 -0
- polvader-1.0.0/PKG-INFO +114 -0
- polvader-1.0.0/README.md +82 -0
- polvader-1.0.0/pyproject.toml +50 -0
- polvader-1.0.0/src/polvader/__init__.py +34 -0
- polvader-1.0.0/src/polvader/lexicons/__init__.py +0 -0
- polvader-1.0.0/src/polvader/lexicons/combined_big_imbir_valence_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/combined_imbir_valence_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/combined_nencki_bigimbir_valence_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/derivative_grammatical_forms_of_terms_in_synonyms_dobry_slownik.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/derivative_grammatical_forms_of_terms_in_synonyms_kazojc.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/derivative_grammatical_forms_of_terms_in_the_final_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/expanded_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/fem_big_imbir_and_nencki_lex_with_kazojc_adjectives.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/feminine_forms_included_combined_nencki_bigimbir_valence_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/imbir_valence_lexicon.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/synonyms_dobry_slownik.dat +0 -0
- polvader-1.0.0/src/polvader/lexicons/synonyms_kazojc.dat +0 -0
- polvader-1.0.0/src/polvader/models.py +866 -0
- polvader-1.0.0/src/polvader/utils.py +15 -0
polvader-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Borys Jastrzębski
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
polvader-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: polvader
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Lexicon-and-rule-based sentiment analysis for Polish text, in the style of VADER.
|
|
5
|
+
Author: Borys Jastrzębski
|
|
6
|
+
Author-email: Borys Jastrzębski <borys@stanford.edu>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Natural Language :: Polish
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Requires-Dist: accelerate>=1.14.0
|
|
17
|
+
Requires-Dist: deep-translator>=1.11.4
|
|
18
|
+
Requires-Dist: numpy>=2.2.0
|
|
19
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
20
|
+
Requires-Dist: pandas>=2.0.0
|
|
21
|
+
Requires-Dist: pip>=26.1.2
|
|
22
|
+
Requires-Dist: scikit-learn>=1.5.0
|
|
23
|
+
Requires-Dist: scipy>=1.17.1
|
|
24
|
+
Requires-Dist: spacy>=3.8.10
|
|
25
|
+
Requires-Dist: textual>=8.2.7
|
|
26
|
+
Requires-Dist: tqdm>=3.67.3
|
|
27
|
+
Requires-Dist: transformers>=5.13.0
|
|
28
|
+
Requires-Dist: vadersentiment>=3.3.2
|
|
29
|
+
Requires-Dist: xlrd>=2.0.2
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# polVADER
|
|
34
|
+
|
|
35
|
+
Lexicon-and-rule-based sentiment analysis for Polish text, in the style of
|
|
36
|
+
[VADER](https://github.com/cjhutto/vaderSentiment). No training required at
|
|
37
|
+
inference time — polVADER scores text directly from a Polish sentiment
|
|
38
|
+
lexicon plus a rule layer for negation, intensifiers, capitalization,
|
|
39
|
+
punctuation emphasis, and contrastive conjunctions (`ale`, `jednak`, ...).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install polvader
|
|
45
|
+
python -m spacy download pl_core_news_lg
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
polVADER uses spaCy's `pl_core_news_lg` model for Polish tokenization and
|
|
49
|
+
lemmatization; it is not bundled with the package and must be downloaded
|
|
50
|
+
once after install.
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from polvader import Lexicon
|
|
56
|
+
|
|
57
|
+
lex = Lexicon()
|
|
58
|
+
scores = lex.polarity_scores("To był wspaniały dzień pełen szczęścia.")
|
|
59
|
+
print(scores)
|
|
60
|
+
# {'neg': 0.0, 'neu': 0.xxx, 'pos': 0.xxx, 'compound': 0.879}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`polarity_scores()` returns the same `{neg, neu, pos, compound}` contract as
|
|
64
|
+
the original English VADER. `compound` is a single normalized score in
|
|
65
|
+
`[-1, +1]`; `neg`/`neu`/`pos` are proportions of the text's sentiment-bearing
|
|
66
|
+
content.
|
|
67
|
+
|
|
68
|
+
Batch scoring (uses spaCy's `nlp.pipe` internally, much faster than a loop):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
results = lex.score_batch([
|
|
72
|
+
"Świetny produkt, polecam!",
|
|
73
|
+
"Nigdy więcej tu nie wrócę.",
|
|
74
|
+
], batch_size=256)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
For social-media text (tweets, comments — hashtags, @mentions, emoji), run
|
|
78
|
+
`preprocess_social()` first:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from polvader import preprocess_social
|
|
82
|
+
|
|
83
|
+
text = preprocess_social(raw_tweet)
|
|
84
|
+
scores = lex.polarity_scores(text)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Lexicon
|
|
88
|
+
|
|
89
|
+
By default `Lexicon()` loads the coverage-expanded, weight-tuned lexicon
|
|
90
|
+
(~29.5k words): the original ~8.5k-word hand-built Polish valence lexicon,
|
|
91
|
+
expanded via K-NN over PLLuM-8B's static input-embedding table (not a
|
|
92
|
+
contextual/forward-pass embedding — benchmarked as equal-or-better and far
|
|
93
|
+
cheaper to compute) across a multi-domain Polish corpus (tweets,
|
|
94
|
+
product/hotel/service reviews, general sentiment text), then weight-tuned
|
|
95
|
+
end-to-end against those same domains. Pass `expanded=False` for the
|
|
96
|
+
smaller, untuned ~8.5k-word base lexicon instead:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
lex = Lexicon(expanded=False)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Modifier system
|
|
103
|
+
|
|
104
|
+
`polarity_scores()` applies, on top of the raw lexicon lookup:
|
|
105
|
+
negation (`nie`, multi-word negators), booster/dampener adverbs, ALL-CAPS
|
|
106
|
+
emphasis, exclamation/question-mark emphasis, sentence-aware scoring for
|
|
107
|
+
multi-sentence text, and contrastive-conjunction reweighting (text after
|
|
108
|
+
"ale"/"jednak" counts more than text before it). Each category can be
|
|
109
|
+
disabled independently via keyword flags on `polarity_scores()` for
|
|
110
|
+
ablation/diagnostic purposes — see its docstring for the full flag list.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
polvader-1.0.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# polVADER
|
|
2
|
+
|
|
3
|
+
Lexicon-and-rule-based sentiment analysis for Polish text, in the style of
|
|
4
|
+
[VADER](https://github.com/cjhutto/vaderSentiment). No training required at
|
|
5
|
+
inference time — polVADER scores text directly from a Polish sentiment
|
|
6
|
+
lexicon plus a rule layer for negation, intensifiers, capitalization,
|
|
7
|
+
punctuation emphasis, and contrastive conjunctions (`ale`, `jednak`, ...).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install polvader
|
|
13
|
+
python -m spacy download pl_core_news_lg
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
polVADER uses spaCy's `pl_core_news_lg` model for Polish tokenization and
|
|
17
|
+
lemmatization; it is not bundled with the package and must be downloaded
|
|
18
|
+
once after install.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from polvader import Lexicon
|
|
24
|
+
|
|
25
|
+
lex = Lexicon()
|
|
26
|
+
scores = lex.polarity_scores("To był wspaniały dzień pełen szczęścia.")
|
|
27
|
+
print(scores)
|
|
28
|
+
# {'neg': 0.0, 'neu': 0.xxx, 'pos': 0.xxx, 'compound': 0.879}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`polarity_scores()` returns the same `{neg, neu, pos, compound}` contract as
|
|
32
|
+
the original English VADER. `compound` is a single normalized score in
|
|
33
|
+
`[-1, +1]`; `neg`/`neu`/`pos` are proportions of the text's sentiment-bearing
|
|
34
|
+
content.
|
|
35
|
+
|
|
36
|
+
Batch scoring (uses spaCy's `nlp.pipe` internally, much faster than a loop):
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
results = lex.score_batch([
|
|
40
|
+
"Świetny produkt, polecam!",
|
|
41
|
+
"Nigdy więcej tu nie wrócę.",
|
|
42
|
+
], batch_size=256)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For social-media text (tweets, comments — hashtags, @mentions, emoji), run
|
|
46
|
+
`preprocess_social()` first:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from polvader import preprocess_social
|
|
50
|
+
|
|
51
|
+
text = preprocess_social(raw_tweet)
|
|
52
|
+
scores = lex.polarity_scores(text)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Lexicon
|
|
56
|
+
|
|
57
|
+
By default `Lexicon()` loads the coverage-expanded, weight-tuned lexicon
|
|
58
|
+
(~29.5k words): the original ~8.5k-word hand-built Polish valence lexicon,
|
|
59
|
+
expanded via K-NN over PLLuM-8B's static input-embedding table (not a
|
|
60
|
+
contextual/forward-pass embedding — benchmarked as equal-or-better and far
|
|
61
|
+
cheaper to compute) across a multi-domain Polish corpus (tweets,
|
|
62
|
+
product/hotel/service reviews, general sentiment text), then weight-tuned
|
|
63
|
+
end-to-end against those same domains. Pass `expanded=False` for the
|
|
64
|
+
smaller, untuned ~8.5k-word base lexicon instead:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
lex = Lexicon(expanded=False)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Modifier system
|
|
71
|
+
|
|
72
|
+
`polarity_scores()` applies, on top of the raw lexicon lookup:
|
|
73
|
+
negation (`nie`, multi-word negators), booster/dampener adverbs, ALL-CAPS
|
|
74
|
+
emphasis, exclamation/question-mark emphasis, sentence-aware scoring for
|
|
75
|
+
multi-sentence text, and contrastive-conjunction reweighting (text after
|
|
76
|
+
"ale"/"jednak" counts more than text before it). Each category can be
|
|
77
|
+
disabled independently via keyword flags on `polarity_scores()` for
|
|
78
|
+
ablation/diagnostic purposes — see its docstring for the full flag list.
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "polvader"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Lexicon-and-rule-based sentiment analysis for Polish text, in the style of VADER."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"accelerate>=1.14.0",
|
|
9
|
+
"deep-translator>=1.11.4",
|
|
10
|
+
"numpy>=2.2.0",
|
|
11
|
+
"openpyxl>=3.1.5",
|
|
12
|
+
"pandas>=2.0.0",
|
|
13
|
+
"pip>=26.1.2",
|
|
14
|
+
"scikit-learn>=1.5.0",
|
|
15
|
+
"scipy>=1.17.1",
|
|
16
|
+
"spacy>=3.8.10",
|
|
17
|
+
"textual>=8.2.7",
|
|
18
|
+
"tqdm>=3.67.3",
|
|
19
|
+
"transformers>=5.13.0",
|
|
20
|
+
"vadersentiment>=3.3.2",
|
|
21
|
+
"xlrd>=2.0.2",
|
|
22
|
+
]
|
|
23
|
+
authors = [
|
|
24
|
+
{ name="Borys Jastrzębski", email="borys@stanford.edu" },
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Intended Audience :: Science/Research",
|
|
30
|
+
"Topic :: Text Processing :: Linguistic",
|
|
31
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
32
|
+
"Natural Language :: Polish",
|
|
33
|
+
"License :: OSI Approved :: MIT License",
|
|
34
|
+
]
|
|
35
|
+
license = "MIT"
|
|
36
|
+
license-files = ["LICENSE"]
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"ipykernel>=7.2.0",
|
|
41
|
+
"twine>=6.2.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build >= 0.10.7, <0.11.0"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
# [project.urls]
|
|
49
|
+
# Homepage = "https://github.com/pypa/sampleproject"
|
|
50
|
+
# Issues = "https://github.com/pypa/sampleproject/issues"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
polVADER - Polish VADER Sentiment Analysis
|
|
3
|
+
|
|
4
|
+
A Python package for Polish sentiment analysis based on VADER methodology.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
__author__ = "Borys Jastrzębski <borys@stanford.edu>"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .models import (
|
|
12
|
+
Lexicon,
|
|
13
|
+
string_preprocessing,
|
|
14
|
+
preprocess_social,
|
|
15
|
+
_BOOSTER_DICT,
|
|
16
|
+
_NEGATION_WORDS,
|
|
17
|
+
_MULT_NEGATION_WORDS,
|
|
18
|
+
_BUT_WORDS,
|
|
19
|
+
_OPINION_MARKERS,
|
|
20
|
+
_SOCIAL_MEDIA_SUPPLEMENT,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Lexicon",
|
|
25
|
+
"string_preprocessing",
|
|
26
|
+
"preprocess_social",
|
|
27
|
+
"_BOOSTER_DICT",
|
|
28
|
+
"_NEGATION_WORDS",
|
|
29
|
+
"_MULT_NEGATION_WORDS",
|
|
30
|
+
"_BUT_WORDS",
|
|
31
|
+
"_OPINION_MARKERS",
|
|
32
|
+
"_SOCIAL_MEDIA_SUPPLEMENT",
|
|
33
|
+
"__version__",
|
|
34
|
+
]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
polvader-1.0.0/src/polvader/lexicons/derivative_grammatical_forms_of_terms_in_synonyms_kazojc.dat
ADDED
|
Binary file
|
polvader-1.0.0/src/polvader/lexicons/derivative_grammatical_forms_of_terms_in_the_final_lexicon.dat
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core model classes for polVADER sentiment analysis.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import importlib.resources as data
|
|
6
|
+
import math
|
|
7
|
+
import pickle
|
|
8
|
+
import re
|
|
9
|
+
import string
|
|
10
|
+
|
|
11
|
+
import spacy
|
|
12
|
+
|
|
13
|
+
# ── VADER-style modifier constants ────────────────────────────────────────────
|
|
14
|
+
# [CHANGE 2] B_INCR/B_DECR tuned down from VADER's ±0.293.
|
|
15
|
+
# English VADER was calibrated on a ~7,500-word lexicon; the Polish lexicon is
|
|
16
|
+
# smaller, so each booster fires at higher relative frequency and the original
|
|
17
|
+
# constant over-shifts ~97 % of texts by a mean of 0.30 units. Halving to
|
|
18
|
+
# 0.15 reduces the mean shift while preserving the ordinal ranking of boosted
|
|
19
|
+
# vs. unboosted scores.
|
|
20
|
+
_B_INCR = 0.35 # amplifier boost (additive) — calibrated on PolEmo fit split; was 0.15
|
|
21
|
+
_B_DECR = -0.35 # diminisher boost — was -0.15
|
|
22
|
+
# Reverted from −1.15 to −0.74 (original VADER value). −1.15 amplifies negated
|
|
23
|
+
# words beyond their original magnitude, causing systematic positive-skew for
|
|
24
|
+
# texts with negated negative words ("nie zły" → strongly positive). −0.74
|
|
25
|
+
# dampens instead, keeping "not bad" < "good" in magnitude.
|
|
26
|
+
_N_SCALAR = -0.74 # negation scalar
|
|
27
|
+
_C_INCR = 0.733 # ALL-CAPS emphasis per word
|
|
28
|
+
_ALPHA = 20 # normalisation constant — calibrated; was 15
|
|
29
|
+
|
|
30
|
+
# [CHANGE 3] Balance detector constants.
|
|
31
|
+
# When a text has both positive and negative sentiment signals in similar
|
|
32
|
+
# proportions the document-level sentiment is ambiguous even if individual
|
|
33
|
+
# word scores are strong. We dampen the compound toward zero proportionally
|
|
34
|
+
# to how balanced the opposing signals are.
|
|
35
|
+
_BALANCE_THRESHOLD = 0.20 # min(pos,|neg|)/max(pos,|neg|) ratio to trigger
|
|
36
|
+
_BALANCE_DAMP = 0.60 # compound is multiplied by (1 − ratio×DAMP)
|
|
37
|
+
|
|
38
|
+
# [CHANGE 6] Subjectivity pre-filter constants.
|
|
39
|
+
# Texts that lack subjective/opinion markers and have a moderate compound
|
|
40
|
+
# are likely factual rather than opinionated, so their compound is dampened.
|
|
41
|
+
_SUBJ_THRESHOLD = 0.75 # only fires when |compound| < this value
|
|
42
|
+
_SUBJ_DAMP = 0.65 # multiplier when no opinion markers are present
|
|
43
|
+
|
|
44
|
+
# Polish opinion markers: first-person pronouns, opinion verbs, evaluative
|
|
45
|
+
# stance words, and common review feedback words (all lemma forms).
|
|
46
|
+
_OPINION_MARKERS: frozenset[str] = frozenset({
|
|
47
|
+
# first-person pronouns
|
|
48
|
+
"ja", "mi", "mnie", "mój", "moja", "moje", "moim", "moją",
|
|
49
|
+
"my", "nam", "nas", "nasz", "nasza", "nasze",
|
|
50
|
+
# opinion / evaluative verbs
|
|
51
|
+
"uważać", "myśleć", "sądzić", "wydawać", "czuć", "wierzyć",
|
|
52
|
+
"polecać", "odradzać", "radzić", "lubić", "kochać", "nienawidzić",
|
|
53
|
+
"preferować", "zachwycać", "rozczarować",
|
|
54
|
+
# evaluative stance markers
|
|
55
|
+
"niestety", "żal", "szkoda",
|
|
56
|
+
# common direct-feedback words in reviews
|
|
57
|
+
"polecam", "odradzam", "wracam", "wrócimy", "wróciłem", "wróciłam",
|
|
58
|
+
"żałować", "polecić",
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
# Polish amplifiers and diminishers — additive boost applied to a neighbouring
|
|
62
|
+
# sentiment word. Sign-aware application means amplifiers always increase
|
|
63
|
+
# magnitude and diminishers always reduce it, regardless of direction.
|
|
64
|
+
_BOOSTER_DICT: dict[str, float] = {
|
|
65
|
+
# ── Standard intensifiers ────────────────────────────────────────────────
|
|
66
|
+
"bardzo": _B_INCR, # very
|
|
67
|
+
"niezmiernie": _B_INCR, # immensely
|
|
68
|
+
"niezwykle": _B_INCR, # remarkably
|
|
69
|
+
"wyjątkowo": _B_INCR, # exceptionally
|
|
70
|
+
"szczególnie": _B_INCR, # particularly
|
|
71
|
+
"ekstremalnie": _B_INCR, # extremely
|
|
72
|
+
"absolutnie": _B_INCR, # absolutely
|
|
73
|
+
"całkowicie": _B_INCR, # completely
|
|
74
|
+
"zupełnie": _B_INCR, # totally
|
|
75
|
+
"kompletnie": _B_INCR, # completely (colloquial)
|
|
76
|
+
"ogromnie": _B_INCR, # enormously
|
|
77
|
+
"mocno": _B_INCR, # strongly
|
|
78
|
+
"niesamowicie": _B_INCR, # incredibly
|
|
79
|
+
"fenomenalnie": _B_INCR, # phenomenally
|
|
80
|
+
"zdecydowanie": _B_INCR, # decidedly
|
|
81
|
+
"wyraźnie": _B_INCR, # clearly
|
|
82
|
+
"naprawdę": _B_INCR, # truly / really
|
|
83
|
+
"totalnie": _B_INCR, # totally (colloquial)
|
|
84
|
+
"nadzwyczajnie": _B_INCR, # extraordinarily
|
|
85
|
+
"niewiarygodnie": _B_INCR, # unbelievably
|
|
86
|
+
"głęboko": _B_INCR, # deeply
|
|
87
|
+
"niesłychanie": _B_INCR, # incredibly
|
|
88
|
+
"niebywale": _B_INCR, # extraordinarily
|
|
89
|
+
"wręcz": _B_INCR, # outright
|
|
90
|
+
"doprawdy": _B_INCR, # indeed / truly
|
|
91
|
+
"wielce": _B_INCR, # greatly (formal)
|
|
92
|
+
"istotnie": _B_INCR, # indeed / significantly
|
|
93
|
+
"niewyobrażalnie": _B_INCR, # unimaginably
|
|
94
|
+
"nieprawdopodobnie": _B_INCR, # improbably (as intensifier)
|
|
95
|
+
"zadziwiająco": _B_INCR, # surprisingly
|
|
96
|
+
"niezaprzeczalnie": _B_INCR, # undeniably
|
|
97
|
+
"bezsprzecznie": _B_INCR, # indisputably
|
|
98
|
+
"ewidentnie": _B_INCR, # evidently
|
|
99
|
+
"definitywnie": _B_INCR, # definitively
|
|
100
|
+
"jednoznacznie": _B_INCR, # unequivocally
|
|
101
|
+
# ── Colloquial / social-media intensifiers ───────────────────────────────
|
|
102
|
+
"mega": _B_INCR, # mega (online)
|
|
103
|
+
"super": _B_INCR, # super (very common Polish intensifier)
|
|
104
|
+
"hiper": _B_INCR, # hyper-
|
|
105
|
+
"ultra": _B_INCR, # ultra-
|
|
106
|
+
"ekstra": _B_INCR, # extra / great (colloquial)
|
|
107
|
+
"strasznie": _B_INCR, # terribly (as intensifier)
|
|
108
|
+
"cholernie": _B_INCR, # damn / bloody (intensifier)
|
|
109
|
+
"diabelnie": _B_INCR, # devilishly (intensifier)
|
|
110
|
+
"szalenie": _B_INCR, # madly (intensifier)
|
|
111
|
+
"potwornie": _B_INCR, # monstrously (intensifier)
|
|
112
|
+
"okrutnie": _B_INCR, # cruelly (colloquial intensifier)
|
|
113
|
+
"kurewsko": _B_INCR, # intensifier (vulgar, very common on Twitter)
|
|
114
|
+
"zajebicie": _B_INCR, # intensifier (vulgar, common on Twitter)
|
|
115
|
+
"aż": _B_INCR, # "so/as" emphasiser ("aż tak dobry" = "that good")
|
|
116
|
+
"tak": _B_INCR, # "so" emphasiser before adjective ("tak piękny")
|
|
117
|
+
# ── Formal / news / political amplifiers ─────────────────────────────────
|
|
118
|
+
"rażąco": _B_INCR, # glaringly
|
|
119
|
+
"surowo": _B_INCR, # severely
|
|
120
|
+
"drastycznie": _B_INCR, # drastically
|
|
121
|
+
"dramatycznie": _B_INCR, # dramatically
|
|
122
|
+
"gwałtownie": _B_INCR, # sharply
|
|
123
|
+
"dotkliwie": _B_INCR, # painfully
|
|
124
|
+
"poważnie": _B_INCR, # seriously
|
|
125
|
+
"znacznie": _B_INCR, # considerably
|
|
126
|
+
"znacząco": _B_INCR, # significantly
|
|
127
|
+
"ostro": _B_INCR, # harshly
|
|
128
|
+
"bezwzględnie": _B_INCR, # ruthlessly
|
|
129
|
+
"przerażająco": _B_INCR, # frighteningly
|
|
130
|
+
"zatrważająco": _B_INCR, # alarmingly
|
|
131
|
+
"imponująco": _B_INCR, # impressively
|
|
132
|
+
"skandalicznie": _B_INCR, # scandalously
|
|
133
|
+
"katastrofalnie": _B_INCR, # catastrophically
|
|
134
|
+
"tragicznie": _B_INCR, # tragically
|
|
135
|
+
"fatalnie": _B_INCR, # terribly
|
|
136
|
+
"okropnie": _B_INCR, # horribly
|
|
137
|
+
# ── Warm / positive formal amplifiers ────────────────────────────────────
|
|
138
|
+
"serdecznie": _B_INCR, # warmly
|
|
139
|
+
"gorąco": _B_INCR, # fervently
|
|
140
|
+
"szczerze": _B_INCR, # sincerely
|
|
141
|
+
"cudownie": _B_INCR, # wonderfully
|
|
142
|
+
"genialnie": _B_INCR, # brilliantly
|
|
143
|
+
"wybitnie": _B_INCR, # outstandingly
|
|
144
|
+
"rewelacyjnie": _B_INCR, # awesomely
|
|
145
|
+
"doskonale": _B_INCR, # excellently
|
|
146
|
+
"świetnie": _B_INCR, # great
|
|
147
|
+
# ── Diminishers ──────────────────────────────────────────────────────────
|
|
148
|
+
"trochę": _B_DECR, # a bit
|
|
149
|
+
"nieco": _B_DECR, # somewhat
|
|
150
|
+
"odrobinę": _B_DECR, # a little
|
|
151
|
+
"troszkę": _B_DECR, # a tiny bit
|
|
152
|
+
"lekko": _B_DECR, # slightly
|
|
153
|
+
"delikatnie": _B_DECR, # gently
|
|
154
|
+
"prawie": _B_DECR, # almost
|
|
155
|
+
"ledwo": _B_DECR, # barely
|
|
156
|
+
"ledwie": _B_DECR, # barely
|
|
157
|
+
"zaledwie": _B_DECR, # merely
|
|
158
|
+
"raczej": _B_DECR, # rather (hedging)
|
|
159
|
+
"dość": _B_DECR, # fairly
|
|
160
|
+
"dosyć": _B_DECR, # fairly
|
|
161
|
+
"mniej": _B_DECR, # less
|
|
162
|
+
"mało": _B_DECR, # little
|
|
163
|
+
"niezbyt": _B_DECR, # not very
|
|
164
|
+
"niejako": _B_DECR, # somewhat
|
|
165
|
+
"poniekąd": _B_DECR, # in a way
|
|
166
|
+
"częściowo": _B_DECR, # partially
|
|
167
|
+
"umiarkowanie": _B_DECR, # moderately
|
|
168
|
+
"względnie": _B_DECR, # relatively
|
|
169
|
+
"jakby": _B_DECR, # kind of
|
|
170
|
+
"niby": _B_DECR, # supposedly
|
|
171
|
+
"stosunkowo": _B_DECR, # relatively
|
|
172
|
+
"marginalnie": _B_DECR, # marginally
|
|
173
|
+
"minimalnie": _B_DECR, # minimally
|
|
174
|
+
"nieznacznie": _B_DECR, # slightly
|
|
175
|
+
"powierzchownie": _B_DECR, # superficially
|
|
176
|
+
"pobieżnie": _B_DECR, # cursorily
|
|
177
|
+
"przeciętnie": _B_DECR, # averagely
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Fixed negators: apply N_SCALAR regardless of their own lexical value.
|
|
181
|
+
_NEGATION_WORDS: frozenset[str] = frozenset({"nie", "bez", "ani", "brak"})
|
|
182
|
+
|
|
183
|
+
# Multiplicative negators: scale the target word's valence by their own
|
|
184
|
+
# lexicon value (falls back to N_SCALAR when OOV).
|
|
185
|
+
_MULT_NEGATION_WORDS: frozenset[str] = frozenset({
|
|
186
|
+
"nigdy", "nigdzie", "nikt", "nic", "wcale", "bynajmniej",
|
|
187
|
+
"żaden", "żadna", "żadne", "żadnego", "żadnej", "żadnemu",
|
|
188
|
+
"żadnym", "żadnych", "żadnymi",
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
# Contrastive conjunctions: words after these carry more weight.
|
|
192
|
+
_BUT_WORDS: frozenset[str] = frozenset({
|
|
193
|
+
"ale", "jednak", "natomiast", "lecz", "jednakże",
|
|
194
|
+
"tymczasem", "niemniej", "pomimo", "chociaż", "mimo",
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
# [CHANGE 5] Informal / social-media vocabulary supplement.
|
|
198
|
+
# These words are common on Twitter and informal Polish but under-represented
|
|
199
|
+
# in the formal sentiment lexicon. Added via setdefault so they don't
|
|
200
|
+
# overwrite existing entries.
|
|
201
|
+
_SOCIAL_MEDIA_SUPPLEMENT: dict[str, float] = {
|
|
202
|
+
# informal positives
|
|
203
|
+
"spoko": 1.5, # cool / ok
|
|
204
|
+
"spoczko": 1.5, # cool (variant)
|
|
205
|
+
"git": 2.0, # great
|
|
206
|
+
"kapitalny": 2.2, # capital / great
|
|
207
|
+
"bomba": 2.2, # great (slang)
|
|
208
|
+
"odlotowy": 2.3, # awesome
|
|
209
|
+
"zajefajny": 2.5, # super cool (colloquial intensified)
|
|
210
|
+
"zarąbisty": 2.5, # awesome (colloquial)
|
|
211
|
+
"elegancko": 1.5, # cool (slang)
|
|
212
|
+
"cudo": 2.2, # wonder / great thing
|
|
213
|
+
"petarda": 2.2, # firecracker / awesome
|
|
214
|
+
"miodzio": 2.0, # sweet / great (colloquial)
|
|
215
|
+
"odpad": 2.0, # great (ironic slang — context-dependent; lexicon wins)
|
|
216
|
+
# informal negatives
|
|
217
|
+
"słabo": -1.8, # weak / bad
|
|
218
|
+
"żenada": -2.5, # cringe / embarrassing
|
|
219
|
+
"żenujący": -2.2, # embarrassing
|
|
220
|
+
"masakra": -2.0, # disaster (slang for awful)
|
|
221
|
+
"kupa": -2.2, # crap (vulgar)
|
|
222
|
+
"beznadziejnie": -2.0, # hopelessly
|
|
223
|
+
"dno": -2.5, # rock-bottom / the worst
|
|
224
|
+
"szajs": -2.2, # junk / shit (Polonized)
|
|
225
|
+
"żałosny": -2.2, # pathetic
|
|
226
|
+
"obrzydliwy": -2.5, # disgusting
|
|
227
|
+
"kiepski": -1.5, # poor / bad
|
|
228
|
+
"paskudny": -2.0, # ugly / disgusting
|
|
229
|
+
"wstyd": -1.5, # shame
|
|
230
|
+
"odpad": -2.0, # drop-out / rubbish (overloaded; lexicon wins if present)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
# ── Pre-compiled social-media preprocessing regexes ──────────────────────────
|
|
234
|
+
_MENTION_RE = re.compile(r"@\S+")
|
|
235
|
+
_URL_RE = re.compile(r"https?://\S+|www\.\S+")
|
|
236
|
+
_HASHTAG_RE = re.compile(r"#(\w+)")
|
|
237
|
+
_SPACE_RE = re.compile(r"\s+")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def preprocess_social(text: str) -> str:
|
|
241
|
+
"""[CHANGE 4] Strip @mentions and URLs; convert #hashtag → hashtag."""
|
|
242
|
+
text = _MENTION_RE.sub(" ", text)
|
|
243
|
+
text = _URL_RE.sub(" ", text)
|
|
244
|
+
text = _HASHTAG_RE.sub(r"\1", text)
|
|
245
|
+
return _SPACE_RE.sub(" ", text).strip()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def string_preprocessing(piece):
|
|
249
|
+
return (
|
|
250
|
+
piece.translate(str.maketrans("", "", string.punctuation))
|
|
251
|
+
.strip()
|
|
252
|
+
.replace(" ", " ")
|
|
253
|
+
.lower()
|
|
254
|
+
.split(" ")
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class Lexicon:
|
|
259
|
+
def _merge(self, dict1, dict2):
|
|
260
|
+
return {**dict1, **dict2}
|
|
261
|
+
|
|
262
|
+
def __init__(self, synonym_weight=0.75, derivative_weight=1,
|
|
263
|
+
valence_floor: float = 0.0,
|
|
264
|
+
valence_clamp: tuple[float, float] | None = None,
|
|
265
|
+
b_incr: float | None = None,
|
|
266
|
+
n_scalar: float | None = None,
|
|
267
|
+
alpha: float | None = None,
|
|
268
|
+
expanded: bool = True,
|
|
269
|
+
pos_scale: float = 1.0,
|
|
270
|
+
neg_scale: float = 1.0):
|
|
271
|
+
# [CHANGE 1] Enable sentencizer for sentence-aware scoring.
|
|
272
|
+
# The full parser is disabled to keep inference fast; the sentencizer
|
|
273
|
+
# provides sentence boundaries via punctuation heuristics.
|
|
274
|
+
self.nlp = spacy.load("pl_core_news_lg", disable=["parser", "ner"])
|
|
275
|
+
if "sentencizer" not in self.nlp.pipe_names:
|
|
276
|
+
self.nlp.add_pipe("sentencizer")
|
|
277
|
+
|
|
278
|
+
self._synonym_weight = synonym_weight
|
|
279
|
+
self._derivative_weight = derivative_weight
|
|
280
|
+
|
|
281
|
+
# Parameterised rule constants — default to module-level values.
|
|
282
|
+
# Overriding these allows calibration without re-running spaCy.
|
|
283
|
+
self._b_incr = b_incr if b_incr is not None else _B_INCR
|
|
284
|
+
self._n_scalar = n_scalar if n_scalar is not None else _N_SCALAR
|
|
285
|
+
self._alpha = alpha if alpha is not None else _ALPHA
|
|
286
|
+
|
|
287
|
+
# Scale booster dict proportionally when b_incr differs from default.
|
|
288
|
+
scale = self._b_incr / _B_INCR if _B_INCR != 0 else 1.0
|
|
289
|
+
self._booster_dict = (
|
|
290
|
+
_BOOSTER_DICT if scale == 1.0
|
|
291
|
+
else {k: v * scale for k, v in _BOOSTER_DICT.items()}
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
self._load_data(expanded=expanded)
|
|
295
|
+
self.lexicon = self._final_lex
|
|
296
|
+
|
|
297
|
+
# [OPT 1] Valence floor: drop low-signal lexicon entries.
|
|
298
|
+
# Neutral texts accumulate many weak-valence hits that partially cancel
|
|
299
|
+
# but leave a residual compound that pushes them out of the neutral band.
|
|
300
|
+
# Pruning entries below the floor removes noise hits without losing the
|
|
301
|
+
# decisive words that carry clear sentiment.
|
|
302
|
+
if valence_floor > 0.0:
|
|
303
|
+
self.lexicon = {
|
|
304
|
+
k: v for k, v in self.lexicon.items() if abs(v) >= valence_floor
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
# [OPT 2] Valence clamp: keep all entries but constrain magnitude to
|
|
308
|
+
# [clamp_min, clamp_max]. Unlike the floor (which prunes entries),
|
|
309
|
+
# clamping preserves lexicon coverage (OOV rate unchanged) while
|
|
310
|
+
# equalising weak signals up and/or strong outliers down.
|
|
311
|
+
if valence_clamp is not None:
|
|
312
|
+
clamp_lo, clamp_hi = valence_clamp
|
|
313
|
+
self.lexicon = {
|
|
314
|
+
k: math.copysign(min(max(abs(v), clamp_lo), clamp_hi), v)
|
|
315
|
+
for k, v in self.lexicon.items()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# Per-sign valence scaling: calibrate positive/negative signal strength
|
|
319
|
+
# independently, e.g. to correct corpus-level pos/neg asymmetry.
|
|
320
|
+
if pos_scale != 1.0 or neg_scale != 1.0:
|
|
321
|
+
self.lexicon = {
|
|
322
|
+
k: v * (pos_scale if v > 0 else neg_scale)
|
|
323
|
+
for k, v in self.lexicon.items()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# [CHANGE 5] Merge social-media supplement: setdefault preserves
|
|
327
|
+
# existing entries so the core lexicon is not overwritten.
|
|
328
|
+
for word, score in _SOCIAL_MEDIA_SUPPLEMENT.items():
|
|
329
|
+
apply = True
|
|
330
|
+
if valence_floor > 0.0 and abs(score) < valence_floor:
|
|
331
|
+
apply = False
|
|
332
|
+
if apply:
|
|
333
|
+
if valence_clamp is not None:
|
|
334
|
+
clamp_lo, clamp_hi = valence_clamp
|
|
335
|
+
score = math.copysign(min(max(abs(score), clamp_lo), clamp_hi), score)
|
|
336
|
+
if pos_scale != 1.0 or neg_scale != 1.0:
|
|
337
|
+
score = score * (pos_scale if score > 0 else neg_scale)
|
|
338
|
+
self.lexicon.setdefault(word, score)
|
|
339
|
+
|
|
340
|
+
def _lemmatize(self, piece):
|
|
341
|
+
return [self.nlp(word)[0].lemma_ for word in piece]
|
|
342
|
+
|
|
343
|
+
def _load_data(self, expanded: bool = False):
|
|
344
|
+
with open(
|
|
345
|
+
data.files("polvader.lexicons") / "synonyms_dobry_slownik.dat", "rb"
|
|
346
|
+
) as f:
|
|
347
|
+
self._synonyms_dobry_slownik = pickle.load(f)
|
|
348
|
+
|
|
349
|
+
with open(
|
|
350
|
+
data.files("polvader.lexicons") / "synonyms_kazojc.dat", "rb"
|
|
351
|
+
) as f:
|
|
352
|
+
self._synonyms_kazojc = pickle.load(f)
|
|
353
|
+
|
|
354
|
+
# expanded_lexicon.dat: coverage-expanded (PLLuM K-NN, stages/coverage.py)
|
|
355
|
+
# + Adam-tuned (stages/adam.py, trained on all 5 corpus sources) —
|
|
356
|
+
# this is the default and best-benchmarked lexicon (see
|
|
357
|
+
# annotation-data/benchmark_translate_vader_results.json). Pass
|
|
358
|
+
# expanded=False for the smaller, untuned hand-built base lexicon.
|
|
359
|
+
expanded_path = (
|
|
360
|
+
data.files("polvader.lexicons") / "expanded_lexicon.dat"
|
|
361
|
+
)
|
|
362
|
+
base_path = (
|
|
363
|
+
data.files("polvader.lexicons")
|
|
364
|
+
/ "fem_big_imbir_and_nencki_lex_with_kazojc_adjectives.dat"
|
|
365
|
+
)
|
|
366
|
+
lex_path = expanded_path if (
|
|
367
|
+
expanded and expanded_path.is_file()
|
|
368
|
+
) else base_path
|
|
369
|
+
with open(lex_path, "rb") as f:
|
|
370
|
+
self._final_lex = pickle.load(f)
|
|
371
|
+
|
|
372
|
+
def _modify_synonym_weights(self, new_weight):
|
|
373
|
+
self._weighted_synonyms = {
|
|
374
|
+
k: v * new_weight for k, v in self.synonyms.items()
|
|
375
|
+
}
|
|
376
|
+
self.lexicon = self._merge(self.lexicon, self._weighted_synonyms)
|
|
377
|
+
self.lexicon = {k: round(v, 2) for k, v in self.lexicon.items()}
|
|
378
|
+
|
|
379
|
+
def _integrate_derivative_forms(self, weight):
|
|
380
|
+
self._weighted_derivatives = {
|
|
381
|
+
k: v * weight for k, v in self.derivatives.items()
|
|
382
|
+
}
|
|
383
|
+
self.lexicon = self._merge(self.lexicon, self._weighted_derivatives)
|
|
384
|
+
self.lexicon = {k: round(v, 2) for k, v in self.lexicon.items()}
|
|
385
|
+
|
|
386
|
+
def _round_neutrals(self):
|
|
387
|
+
new_lex = {}
|
|
388
|
+
for k, v in self.lexicon.items():
|
|
389
|
+
if 1 > v >= 0.5:
|
|
390
|
+
new_lex[k] = 1
|
|
391
|
+
elif -0.5 >= v > -1:
|
|
392
|
+
new_lex[k] = -1
|
|
393
|
+
else:
|
|
394
|
+
new_lex[k] = v
|
|
395
|
+
self.lexicon = new_lex
|
|
396
|
+
|
|
397
|
+
def score(self, text, preprocess=True):
|
|
398
|
+
"""Simple lexicon lookup (no modifiers). Returns per-word scores."""
|
|
399
|
+
words = string_preprocessing(text) if preprocess else text
|
|
400
|
+
inner = [self.lexicon.get(w, False) for w in words]
|
|
401
|
+
n = len(words)
|
|
402
|
+
return inner, {
|
|
403
|
+
"neg": sum(1 for x in inner if x and x < 0) / n,
|
|
404
|
+
"unk": sum(1 for x in inner if x is False) / n,
|
|
405
|
+
"pos": sum(1 for x in inner if x and x > 0) / n,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# ── Internal per-token scoring ────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
def _score_token_lists(
|
|
411
|
+
self,
|
|
412
|
+
surfaces: list[str],
|
|
413
|
+
lemmas: list[str],
|
|
414
|
+
is_word: list[bool],
|
|
415
|
+
ep_count: int = 0,
|
|
416
|
+
qm_count: int = 0,
|
|
417
|
+
return_sentiments: bool = False,
|
|
418
|
+
*,
|
|
419
|
+
use_negation: bool = True,
|
|
420
|
+
use_boosters: bool = True,
|
|
421
|
+
use_caps: bool = True,
|
|
422
|
+
use_balance: bool = True,
|
|
423
|
+
use_subjectivity: bool = True,
|
|
424
|
+
use_nie_prefix: bool = True,
|
|
425
|
+
use_punct: bool = True,
|
|
426
|
+
use_contrastive: bool = True,
|
|
427
|
+
) -> dict:
|
|
428
|
+
"""
|
|
429
|
+
Core VADER-style scoring on pre-tokenised token lists.
|
|
430
|
+
|
|
431
|
+
Modifier rules (in order):
|
|
432
|
+
1. ALL-CAPS emphasis (±C_INCR per capped word in mixed-case text).
|
|
433
|
+
2. Boosters / diminishers in the 3-word window before each sentiment
|
|
434
|
+
word (distance damping: ×0.95 at −2, ×0.90 at −3).
|
|
435
|
+
3. Negation in the same window (N_SCALAR flip or multiplicative scale).
|
|
436
|
+
4. nie-prefix productive splitting ("niedobry" → −lex["dobry"]).
|
|
437
|
+
5. Balance detector: dampen compound when pos ≈ neg signals.
|
|
438
|
+
6. Contrastive conjunction (ale/jednak/…): last occurrence wins;
|
|
439
|
+
words before get ×0.5, words after get ×1.5.
|
|
440
|
+
7. Punctuation amplification (! and ?).
|
|
441
|
+
8. Subjectivity pre-filter: moderate compounds without opinion
|
|
442
|
+
markers are dampened toward neutral.
|
|
443
|
+
|
|
444
|
+
Ablation flags (keyword-only):
|
|
445
|
+
use_negation — fixed "nie/bez/ani/brak" and mult negation
|
|
446
|
+
use_boosters — intensifier/diminisher window
|
|
447
|
+
use_caps — ALL-CAPS emphasis
|
|
448
|
+
use_balance — balance detector
|
|
449
|
+
use_subjectivity — subjectivity pre-filter
|
|
450
|
+
use_nie_prefix — fused nie-prefix forms ("niedobry")
|
|
451
|
+
use_punct — !/? punctuation amplification
|
|
452
|
+
use_contrastive — ale/jednak contrastive conjunction
|
|
453
|
+
"""
|
|
454
|
+
word_surfaces = [s for s, w in zip(surfaces, is_word) if w]
|
|
455
|
+
has_mixed_case = bool(word_surfaces) and (
|
|
456
|
+
any(s.isupper() for s in word_surfaces)
|
|
457
|
+
and not all(s.isupper() for s in word_surfaces)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
sentiments: list[float] = []
|
|
461
|
+
sentiment_positions: list[int] = []
|
|
462
|
+
|
|
463
|
+
for i, lemma in enumerate(lemmas):
|
|
464
|
+
if not is_word[i]:
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
if lemma in _NEGATION_WORDS or lemma in _MULT_NEGATION_WORDS or lemma in _BOOSTER_DICT:
|
|
468
|
+
continue
|
|
469
|
+
|
|
470
|
+
# Find the nearest preceding word token (may be a modifier)
|
|
471
|
+
prev_lemma = None
|
|
472
|
+
for j in range(i - 1, -1, -1):
|
|
473
|
+
if is_word[j]:
|
|
474
|
+
prev_lemma = lemmas[j]
|
|
475
|
+
break
|
|
476
|
+
|
|
477
|
+
# Bigram lookup: "nie_dobry", "bardzo_dobry" etc.
|
|
478
|
+
# If a bigram entry exists it overrides the unigram + context path.
|
|
479
|
+
bigram_key = f"{prev_lemma}_{lemma}" if prev_lemma else None
|
|
480
|
+
if bigram_key and bigram_key in self.lexicon:
|
|
481
|
+
valence = self.lexicon[bigram_key]
|
|
482
|
+
if use_caps and surfaces[i].isupper() and has_mixed_case:
|
|
483
|
+
valence += _C_INCR if valence > 0 else -_C_INCR
|
|
484
|
+
sentiments.append(valence)
|
|
485
|
+
sentiment_positions.append(i)
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
# Determine valence: plain lexicon or nie-prefix fused form
|
|
489
|
+
is_nie_prefix = False
|
|
490
|
+
if lemma in self.lexicon:
|
|
491
|
+
valence: float = self.lexicon[lemma]
|
|
492
|
+
elif use_nie_prefix and lemma.startswith("nie") and len(lemma) > 5:
|
|
493
|
+
base = lemma[3:]
|
|
494
|
+
if base not in self.lexicon:
|
|
495
|
+
continue
|
|
496
|
+
valence = self.lexicon[base] * self._n_scalar
|
|
497
|
+
is_nie_prefix = True
|
|
498
|
+
else:
|
|
499
|
+
continue
|
|
500
|
+
|
|
501
|
+
if use_caps and surfaces[i].isupper() and has_mixed_case:
|
|
502
|
+
valence += _C_INCR if valence > 0 else -_C_INCR
|
|
503
|
+
|
|
504
|
+
n_fixed_neg = 0
|
|
505
|
+
had_mult_neg = False
|
|
506
|
+
words_seen = 0
|
|
507
|
+
for j in range(i - 1, -1, -1):
|
|
508
|
+
if not is_word[j]:
|
|
509
|
+
continue
|
|
510
|
+
words_seen += 1
|
|
511
|
+
if words_seen > 3:
|
|
512
|
+
break
|
|
513
|
+
ctx = lemmas[j]
|
|
514
|
+
|
|
515
|
+
if use_negation and ctx in _NEGATION_WORDS:
|
|
516
|
+
n_fixed_neg += 1
|
|
517
|
+
continue
|
|
518
|
+
|
|
519
|
+
if use_negation and ctx in _MULT_NEGATION_WORDS:
|
|
520
|
+
valence *= self.lexicon.get(ctx, self._n_scalar)
|
|
521
|
+
had_mult_neg = True
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
if use_boosters:
|
|
525
|
+
b = self._booster_dict.get(ctx, 0.0)
|
|
526
|
+
if b:
|
|
527
|
+
if words_seen == 2:
|
|
528
|
+
b *= 0.95
|
|
529
|
+
elif words_seen == 3:
|
|
530
|
+
b *= 0.90
|
|
531
|
+
valence += b if valence >= 0 else -b
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
if ctx in self.lexicon or (
|
|
535
|
+
ctx.startswith("nie") and len(ctx) > 5 and ctx[3:] in self.lexicon
|
|
536
|
+
):
|
|
537
|
+
break
|
|
538
|
+
|
|
539
|
+
if is_nie_prefix:
|
|
540
|
+
# Word already carries its own negation via the nie- prefix.
|
|
541
|
+
# A context "nie" produces a weak double-negative (mildly
|
|
542
|
+
# positive) rather than a full re-flip with amplification.
|
|
543
|
+
# "nie niedobry" ≈ "not un-good" → weakly positive, dampened.
|
|
544
|
+
if use_negation and n_fixed_neg > 0:
|
|
545
|
+
valence = abs(valence) * 0.5
|
|
546
|
+
if use_negation and had_mult_neg:
|
|
547
|
+
valence = -abs(valence)
|
|
548
|
+
else:
|
|
549
|
+
if n_fixed_neg > 0:
|
|
550
|
+
valence *= self._n_scalar
|
|
551
|
+
if n_fixed_neg > 1:
|
|
552
|
+
# Polish grammatical double-negation stays negative.
|
|
553
|
+
valence = -abs(valence)
|
|
554
|
+
if had_mult_neg and n_fixed_neg > 0:
|
|
555
|
+
valence = -abs(valence)
|
|
556
|
+
|
|
557
|
+
sentiments.append(valence)
|
|
558
|
+
sentiment_positions.append(i)
|
|
559
|
+
|
|
560
|
+
# Balance detector ────────────────────────────────────────────────────
|
|
561
|
+
if use_balance and sentiments:
|
|
562
|
+
pos_raw = sum(s for s in sentiments if s > 0)
|
|
563
|
+
neg_raw = abs(sum(s for s in sentiments if s < 0))
|
|
564
|
+
if pos_raw > 0 and neg_raw > 0:
|
|
565
|
+
balance = min(pos_raw, neg_raw) / max(pos_raw, neg_raw)
|
|
566
|
+
if balance > _BALANCE_THRESHOLD:
|
|
567
|
+
damp = 1.0 - balance * _BALANCE_DAMP
|
|
568
|
+
sentiments = [s * damp for s in sentiments]
|
|
569
|
+
|
|
570
|
+
# Contrastive conjunction (last occurrence wins) ───────────────────────
|
|
571
|
+
if use_contrastive:
|
|
572
|
+
but_idx = None
|
|
573
|
+
for i, w in enumerate(lemmas):
|
|
574
|
+
if w in _BUT_WORDS:
|
|
575
|
+
but_idx = i
|
|
576
|
+
if but_idx is not None and sentiments:
|
|
577
|
+
sentiments = [
|
|
578
|
+
s * 0.5 if pos < but_idx else s * 1.5
|
|
579
|
+
for s, pos in zip(sentiments, sentiment_positions)
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
# Aggregate with punctuation amplifier ────────────────────────────────
|
|
583
|
+
sum_s = sum(sentiments) if sentiments else 0.0
|
|
584
|
+
|
|
585
|
+
if use_punct:
|
|
586
|
+
punct_amp = ep_count * 0.292
|
|
587
|
+
if qm_count > 1:
|
|
588
|
+
punct_amp += (min(qm_count, 3) * 0.18) if qm_count <= 3 else 0.96
|
|
589
|
+
if sum_s > 0:
|
|
590
|
+
sum_s += punct_amp
|
|
591
|
+
elif sum_s < 0:
|
|
592
|
+
sum_s -= punct_amp
|
|
593
|
+
else:
|
|
594
|
+
punct_amp = 0.0
|
|
595
|
+
|
|
596
|
+
compound = round(
|
|
597
|
+
max(-1.0, min(1.0, sum_s / math.sqrt(sum_s ** 2 + self._alpha))), 4
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Subjectivity pre-filter ─────────────────────────────────────────────
|
|
601
|
+
if use_subjectivity and 0 < abs(compound) < _SUBJ_THRESHOLD:
|
|
602
|
+
has_opinion = any(
|
|
603
|
+
lemmas[i] in _OPINION_MARKERS for i in range(len(lemmas)) if is_word[i]
|
|
604
|
+
)
|
|
605
|
+
if not has_opinion:
|
|
606
|
+
compound = round(compound * _SUBJ_DAMP, 4)
|
|
607
|
+
|
|
608
|
+
# Pos / neg proportions ───────────────────────────────────────────────
|
|
609
|
+
pos_sum = sum(s for s in sentiments if s > 0)
|
|
610
|
+
neg_sum = sum(s for s in sentiments if s < 0)
|
|
611
|
+
|
|
612
|
+
if use_punct:
|
|
613
|
+
if pos_sum > abs(neg_sum):
|
|
614
|
+
pos_sum += punct_amp
|
|
615
|
+
elif pos_sum < abs(neg_sum):
|
|
616
|
+
neg_sum -= punct_amp
|
|
617
|
+
|
|
618
|
+
sent_total = pos_sum + abs(neg_sum)
|
|
619
|
+
|
|
620
|
+
# [OPT 3] neu = OOV rate: fraction of alpha words NOT in the lexicon.
|
|
621
|
+
# Previously neu counted zero-valence lexicon hits (~0.01 universally).
|
|
622
|
+
# OOV rate is informative: neutral texts contain many non-lexicon words
|
|
623
|
+
# (factual content, proper nouns, domain terms) while opinionated texts
|
|
624
|
+
# hit the lexicon more selectively. This makes neu a real signal.
|
|
625
|
+
n_words = sum(is_word)
|
|
626
|
+
n_oov = sum(
|
|
627
|
+
1 for i in range(len(lemmas))
|
|
628
|
+
if is_word[i] and lemmas[i] not in self.lexicon
|
|
629
|
+
)
|
|
630
|
+
neu_oov = round(n_oov / n_words, 3) if n_words > 0 else 0.0
|
|
631
|
+
|
|
632
|
+
if sent_total == 0:
|
|
633
|
+
return {"neg": 0.0, "neu": neu_oov, "pos": 0.0, "compound": compound}
|
|
634
|
+
|
|
635
|
+
out = {
|
|
636
|
+
"neg": round(abs(neg_sum) / sent_total, 3),
|
|
637
|
+
"neu": neu_oov,
|
|
638
|
+
"pos": round(pos_sum / sent_total, 3),
|
|
639
|
+
"compound": compound,
|
|
640
|
+
}
|
|
641
|
+
if return_sentiments:
|
|
642
|
+
out["_sentiments"] = sentiments
|
|
643
|
+
return out
|
|
644
|
+
|
|
645
|
+
# ── Public scoring API ────────────────────────────────────────────────────
|
|
646
|
+
|
|
647
|
+
def polarity_scores(
|
|
648
|
+
self,
|
|
649
|
+
text: str,
|
|
650
|
+
sentence_aware: bool = True,
|
|
651
|
+
*,
|
|
652
|
+
use_negation: bool = True,
|
|
653
|
+
use_boosters: bool = True,
|
|
654
|
+
use_caps: bool = True,
|
|
655
|
+
use_balance: bool = True,
|
|
656
|
+
use_subjectivity: bool = True,
|
|
657
|
+
use_nie_prefix: bool = True,
|
|
658
|
+
use_punct: bool = True,
|
|
659
|
+
use_contrastive: bool = True,
|
|
660
|
+
) -> dict:
|
|
661
|
+
"""
|
|
662
|
+
VADER-style scoring with all Polish modifier improvements.
|
|
663
|
+
|
|
664
|
+
Parameters
|
|
665
|
+
----------
|
|
666
|
+
text : str
|
|
667
|
+
Raw Polish text. Use preprocess_social() first for tweets.
|
|
668
|
+
sentence_aware : bool
|
|
669
|
+
When True (default), texts with multiple sentences are scored
|
|
670
|
+
sentence-by-sentence and the sentence compounds are averaged.
|
|
671
|
+
This prevents long mixed-sentiment texts from accumulating a
|
|
672
|
+
spuriously strong compound. Single-sentence texts are unaffected.
|
|
673
|
+
use_negation, use_boosters, use_caps, use_balance, use_subjectivity,
|
|
674
|
+
use_nie_prefix, use_punct, use_contrastive : bool
|
|
675
|
+
Ablation flags — disable individual modifier categories for
|
|
676
|
+
diagnostic purposes. All True by default (full modifier system).
|
|
677
|
+
|
|
678
|
+
Returns
|
|
679
|
+
-------
|
|
680
|
+
dict with keys 'neg', 'neu', 'pos', 'compound' (VADER contract).
|
|
681
|
+
"""
|
|
682
|
+
flags = dict(
|
|
683
|
+
use_negation=use_negation, use_boosters=use_boosters,
|
|
684
|
+
use_caps=use_caps, use_balance=use_balance,
|
|
685
|
+
use_subjectivity=use_subjectivity, use_nie_prefix=use_nie_prefix,
|
|
686
|
+
use_punct=use_punct, use_contrastive=use_contrastive,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
ep_count = min(text.count("!"), 3)
|
|
690
|
+
qm_count = text.count("?")
|
|
691
|
+
|
|
692
|
+
doc = self.nlp(text)
|
|
693
|
+
sents = list(doc.sents)
|
|
694
|
+
|
|
695
|
+
if sentence_aware and len(sents) > 1:
|
|
696
|
+
# Score each sentence independently, then aggregate.
|
|
697
|
+
compounds, neg_scores, pos_scores, neu_scores = [], [], [], []
|
|
698
|
+
for sent in sents:
|
|
699
|
+
s_surf = [t.text for t in sent]
|
|
700
|
+
s_lemmas = [t.lemma_.lower() for t in sent]
|
|
701
|
+
s_iw = [t.is_alpha for t in sent]
|
|
702
|
+
sd = self._score_token_lists(s_surf, s_lemmas, s_iw, **flags)
|
|
703
|
+
if sd["compound"] != 0.0:
|
|
704
|
+
compounds.append(sd["compound"])
|
|
705
|
+
neg_scores.append(sd["neg"])
|
|
706
|
+
pos_scores.append(sd["pos"])
|
|
707
|
+
neu_scores.append(sd["neu"]) # OOV rate per sentence
|
|
708
|
+
|
|
709
|
+
if not compounds:
|
|
710
|
+
neu_agg = round(float(sum(neu_scores) / len(neu_scores)), 3) if neu_scores else 0.0
|
|
711
|
+
return {"neg": 0.0, "neu": neu_agg, "pos": 0.0, "compound": 0.0}
|
|
712
|
+
|
|
713
|
+
avg = sum(compounds) / len(compounds)
|
|
714
|
+
if use_punct:
|
|
715
|
+
if avg > 0:
|
|
716
|
+
avg += ep_count * 0.292
|
|
717
|
+
elif avg < 0:
|
|
718
|
+
avg -= ep_count * 0.292
|
|
719
|
+
compound = round(max(-1.0, min(1.0, avg)), 4)
|
|
720
|
+
|
|
721
|
+
# Aggregate neg/pos as mean of per-sentence proportions;
|
|
722
|
+
# neu is mean OOV rate across all sentences (not derived from compound).
|
|
723
|
+
neg_ = round(float(sum(neg_scores) / len(neg_scores)), 3)
|
|
724
|
+
pos_ = round(float(sum(pos_scores) / len(pos_scores)), 3)
|
|
725
|
+
neu_ = round(float(sum(neu_scores) / len(neu_scores)), 3)
|
|
726
|
+
return {"neg": neg_, "neu": neu_, "pos": pos_, "compound": compound}
|
|
727
|
+
|
|
728
|
+
# Single-sequence scoring (or sentence_aware=False)
|
|
729
|
+
surfaces = [t.text for t in doc]
|
|
730
|
+
lemmas = [t.lemma_.lower() for t in doc]
|
|
731
|
+
is_word = [t.is_alpha for t in doc]
|
|
732
|
+
return self._score_token_lists(surfaces, lemmas, is_word, ep_count, qm_count, **flags)
|
|
733
|
+
|
|
734
|
+
def score_batch(
|
|
735
|
+
self,
|
|
736
|
+
texts: list[str],
|
|
737
|
+
sentence_aware: bool = True,
|
|
738
|
+
batch_size: int = 256,
|
|
739
|
+
n_process: int = 1,
|
|
740
|
+
desc: str | None = None,
|
|
741
|
+
*,
|
|
742
|
+
use_negation: bool = True,
|
|
743
|
+
use_boosters: bool = True,
|
|
744
|
+
use_caps: bool = True,
|
|
745
|
+
use_balance: bool = True,
|
|
746
|
+
use_subjectivity: bool = True,
|
|
747
|
+
use_nie_prefix: bool = True,
|
|
748
|
+
use_punct: bool = True,
|
|
749
|
+
use_contrastive: bool = True,
|
|
750
|
+
) -> list[dict]:
|
|
751
|
+
"""
|
|
752
|
+
Score a list of texts using nlp.pipe() for batched spaCy inference.
|
|
753
|
+
Produces identical output to calling polarity_scores() per text but
|
|
754
|
+
is 3–5× faster for large corpora.
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
texts : list[str]
|
|
759
|
+
Pre-processed texts (apply preprocess_social() before passing).
|
|
760
|
+
batch_size : int
|
|
761
|
+
spaCy pipe batch size (default 256).
|
|
762
|
+
n_process : int
|
|
763
|
+
Number of worker processes for spaCy (default 1; set -1 for all CPUs,
|
|
764
|
+
but fork-safety varies by platform).
|
|
765
|
+
desc : str | None
|
|
766
|
+
If given, show a tqdm progress bar with this label.
|
|
767
|
+
"""
|
|
768
|
+
try:
|
|
769
|
+
from tqdm import tqdm as _tqdm
|
|
770
|
+
_have_tqdm = True
|
|
771
|
+
except ImportError:
|
|
772
|
+
_have_tqdm = False
|
|
773
|
+
|
|
774
|
+
flags = dict(
|
|
775
|
+
use_negation=use_negation, use_boosters=use_boosters,
|
|
776
|
+
use_caps=use_caps, use_balance=use_balance,
|
|
777
|
+
use_subjectivity=use_subjectivity, use_nie_prefix=use_nie_prefix,
|
|
778
|
+
use_punct=use_punct, use_contrastive=use_contrastive,
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
ep_counts = [min(t.count("!"), 3) for t in texts]
|
|
782
|
+
qm_counts = [t.count("?") for t in texts]
|
|
783
|
+
|
|
784
|
+
results = []
|
|
785
|
+
docs = self.nlp.pipe(texts, batch_size=batch_size, n_process=n_process)
|
|
786
|
+
it = zip(docs, ep_counts, qm_counts)
|
|
787
|
+
if desc is not None and _have_tqdm:
|
|
788
|
+
it = _tqdm(it, total=len(texts), desc=desc, unit="text")
|
|
789
|
+
for doc, ep_count, qm_count in it:
|
|
790
|
+
sents = list(doc.sents)
|
|
791
|
+
if sentence_aware and len(sents) > 1:
|
|
792
|
+
compounds, neg_scores, pos_scores, neu_scores = [], [], [], []
|
|
793
|
+
for sent in sents:
|
|
794
|
+
s_surf = [t.text for t in sent]
|
|
795
|
+
s_lemmas = [t.lemma_.lower() for t in sent]
|
|
796
|
+
s_iw = [t.is_alpha for t in sent]
|
|
797
|
+
sd = self._score_token_lists(s_surf, s_lemmas, s_iw, **flags)
|
|
798
|
+
if sd["compound"] != 0.0:
|
|
799
|
+
compounds.append(sd["compound"])
|
|
800
|
+
neg_scores.append(sd["neg"])
|
|
801
|
+
pos_scores.append(sd["pos"])
|
|
802
|
+
neu_scores.append(sd["neu"])
|
|
803
|
+
|
|
804
|
+
if not compounds:
|
|
805
|
+
neu_agg = round(float(sum(neu_scores) / len(neu_scores)), 3) if neu_scores else 0.0
|
|
806
|
+
results.append({"neg": 0.0, "neu": neu_agg, "pos": 0.0, "compound": 0.0})
|
|
807
|
+
continue
|
|
808
|
+
|
|
809
|
+
avg = sum(compounds) / len(compounds)
|
|
810
|
+
if use_punct:
|
|
811
|
+
if avg > 0:
|
|
812
|
+
avg += ep_count * 0.292
|
|
813
|
+
elif avg < 0:
|
|
814
|
+
avg -= ep_count * 0.292
|
|
815
|
+
compound = round(max(-1.0, min(1.0, avg)), 4)
|
|
816
|
+
neg_ = round(float(sum(neg_scores) / len(neg_scores)), 3)
|
|
817
|
+
pos_ = round(float(sum(pos_scores) / len(pos_scores)), 3)
|
|
818
|
+
neu_ = round(float(sum(neu_scores) / len(neu_scores)), 3)
|
|
819
|
+
results.append({"neg": neg_, "neu": neu_, "pos": pos_, "compound": compound})
|
|
820
|
+
else:
|
|
821
|
+
surfaces = [t.text for t in doc]
|
|
822
|
+
lemmas = [t.lemma_.lower() for t in doc]
|
|
823
|
+
is_word = [t.is_alpha for t in doc]
|
|
824
|
+
results.append(self._score_token_lists(surfaces, lemmas, is_word, ep_count, qm_count, **flags))
|
|
825
|
+
return results
|
|
826
|
+
|
|
827
|
+
def score_token_cache(self, cache: list[dict]) -> list[dict]:
|
|
828
|
+
"""
|
|
829
|
+
Score from a pre-built token cache (output of build_token_cache).
|
|
830
|
+
Avoids re-running spaCy — used by calibrate_constants.py to sweep
|
|
831
|
+
(b_incr, n_scalar, alpha) without paying the NLP cost each time.
|
|
832
|
+
|
|
833
|
+
Each cache entry is a dict with keys:
|
|
834
|
+
surfaces, lemmas, is_word, ep_count, qm_count,
|
|
835
|
+
sents: list of (surf_list, lemma_list, is_word_list)
|
|
836
|
+
"""
|
|
837
|
+
results = []
|
|
838
|
+
for entry in cache:
|
|
839
|
+
sents = entry["sents"]
|
|
840
|
+
ep = entry["ep_count"]
|
|
841
|
+
qm = entry["qm_count"]
|
|
842
|
+
if len(sents) > 1:
|
|
843
|
+
compounds, neg_s, pos_s, neu_s = [], [], [], []
|
|
844
|
+
for s_surf, s_lem, s_iw in sents:
|
|
845
|
+
sd = self._score_token_lists(s_surf, s_lem, s_iw)
|
|
846
|
+
if sd["compound"] != 0.0:
|
|
847
|
+
compounds.append(sd["compound"])
|
|
848
|
+
neg_s.append(sd["neg"]); pos_s.append(sd["pos"]); neu_s.append(sd["neu"])
|
|
849
|
+
if not compounds:
|
|
850
|
+
neu_agg = round(float(sum(neu_s) / len(neu_s)), 3) if neu_s else 0.0
|
|
851
|
+
results.append({"neg": 0.0, "neu": neu_agg, "pos": 0.0, "compound": 0.0})
|
|
852
|
+
continue
|
|
853
|
+
avg = sum(compounds) / len(compounds)
|
|
854
|
+
if avg > 0: avg += ep * 0.292
|
|
855
|
+
elif avg < 0: avg -= ep * 0.292
|
|
856
|
+
compound = round(max(-1.0, min(1.0, avg)), 4)
|
|
857
|
+
results.append({
|
|
858
|
+
"neg": round(float(sum(neg_s)/len(neg_s)), 3),
|
|
859
|
+
"neu": round(float(sum(neu_s)/len(neu_s)), 3),
|
|
860
|
+
"pos": round(float(sum(pos_s)/len(pos_s)), 3),
|
|
861
|
+
"compound": compound,
|
|
862
|
+
})
|
|
863
|
+
else:
|
|
864
|
+
surf, lem, iw = entry["surfaces"], entry["lemmas"], entry["is_word"]
|
|
865
|
+
results.append(self._score_token_lists(surf, lem, iw, ep, qm))
|
|
866
|
+
return results
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for polVADER.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
def make_lex_dict(lexicon_full_filepath):
|
|
6
|
+
"""
|
|
7
|
+
Convert lexicon file to a dictionary.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
lexicon_full_filepath: Path to the lexicon file
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Dictionary with lexicon data
|
|
14
|
+
"""
|
|
15
|
+
pass
|