mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-05-11 03:00:42 -04:00
feat(sentiment): Improve scoring and analysis
* Implement english-only scoring with wink https://github.com/winkjs/wink-sentiment (AFINN, emojis) * Implement english-only scoring with NLP.js https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md (AFINN, Senticon, Pattern) * Refactor language processing into standalone functions for future use * Add limited multi-langauge support * Can run sentiment with NLP.js on english, german, spanish, and french * Normalize all scores to range between -1 and +1 * Improve score accuracy by averaging all scores
This commit is contained in:
1865
package-lock.json
generated
1865
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -67,6 +67,7 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "^3.3.1",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-nlp": "^4.24.0",
|
||||
"normalize-url": "^6.1.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"p-event": "^4.2.0",
|
||||
@@ -92,6 +93,7 @@
|
||||
"typescript": "^4.3.4",
|
||||
"vader-sentiment": "^1.1.3",
|
||||
"webhook-discord": "^3.7.7",
|
||||
"wink-sentiment": "^5.0.2",
|
||||
"winston": "github:FoxxMD/winston#fbab8de969ecee578981c77846156c7f43b5f01e",
|
||||
"winston-daily-rotate-file": "^4.5.5",
|
||||
"winston-duplex": "^0.1.1",
|
||||
|
||||
@@ -190,29 +190,40 @@ export type ActionTypes =
|
||||
/**
|
||||
* Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language
|
||||
*
|
||||
* Sentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:
|
||||
* Sentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:
|
||||
*
|
||||
* * -0.5 => extremely negative
|
||||
* * -0.2 => very negative
|
||||
* * -0.5 => negative
|
||||
* * -0.6 => extremely negative
|
||||
* * -0.3 => very negative
|
||||
* * -0.1 => negative
|
||||
* * 0 => neutral
|
||||
* * 0.05 => positive
|
||||
* * 0.2 => very positive
|
||||
* * 0.5 => extremely positive
|
||||
* * 0.1 => positive
|
||||
* * 0.3 => very positive
|
||||
* * 0.6 => extremely positive
|
||||
*
|
||||
* The below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)
|
||||
*
|
||||
* * `>= 0.05` = `is positive`
|
||||
* * `<= 0.2` = `is very negative`
|
||||
* * `< 0.05` = `is not positive`
|
||||
* * `> -0.2` = `is not very negative`
|
||||
* * `>= 0.1` = `is positive`
|
||||
* * `<= 0.3` = `is very negative`
|
||||
* * `< 0.1` = `is not positive`
|
||||
* * `> -0.3` = `is not very negative`
|
||||
*
|
||||
* Special case:
|
||||
*
|
||||
* * `is neutral` equates to `> -0.5 and < 0.5`
|
||||
* * `is not neutral` equates to `< -0.5 or > 0.5`
|
||||
* * `is neutral` equates to `> -0.1 and < 0.1`
|
||||
* * `is not neutral` equates to `< -0.1 or > 0.1`
|
||||
*
|
||||
* More about VADER Sentiment: https://github.com/cjhutto/vaderSentiment
|
||||
* ContextMod uses a normalized, weighted average from these sentiment tools:
|
||||
*
|
||||
* * NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md
|
||||
* * (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/
|
||||
* * (english only) wink-sentiment https://github.com/winkjs/wink-sentiment
|
||||
*
|
||||
* More about the sentiment algorithms used:
|
||||
* * VADER https://github.com/cjhutto/vaderSentiment
|
||||
* * AFINN http://corpustext.com/reference/sentiment_afinn.html
|
||||
* * Senticon https://ieeexplore.ieee.org/document/8721408
|
||||
* * Pattern https://github.com/clips/pattern
|
||||
* * wink https://github.com/winkjs/wink-sentiment
|
||||
*
|
||||
* @pattern ((>|>=|<|<=)\s*(-?\d?\.?\d+))|((not)?\s*(very|extremely)?\s*(positive|neutral|negative))
|
||||
* @examples ["is negative", "> 0.2"]
|
||||
|
||||
267
src/Common/LangaugeProcessing.ts
Normal file
267
src/Common/LangaugeProcessing.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import {Language, SentimentManager} from 'node-nlp';
|
||||
import {SentimentIntensityAnalyzer} from 'vader-sentiment';
|
||||
import wink from 'wink-sentiment';
|
||||
import {SnoowrapActivity} from "./Infrastructure/Reddit";
|
||||
import {
|
||||
asGenericComparison,
|
||||
GenericComparison,
|
||||
parseGenericValueComparison,
|
||||
RangedComparison
|
||||
} from "./Infrastructure/Comparisons";
|
||||
import {asSubmission, between, comparisonTextOp} from "../util";
|
||||
import {CMError, MaybeSeriousErrorWithCause} from "../Utils/Errors";
|
||||
import InvalidRegexError from "../Utils/InvalidRegexError";
|
||||
import {StringOperator} from "./Infrastructure/Atomic";
|
||||
|
||||
export type SentimentAnalysisType = 'vader' | 'afinn' | 'senticon' | 'pattern' | 'wink';
|
||||
|
||||
export const sentimentQuantifier = {
|
||||
'extremely negative': -0.6,
|
||||
'very negative': -0.3,
|
||||
'negative': -0.1,
|
||||
'neutral': 0,
|
||||
'positive': 0.1,
|
||||
'very positive': 0.3,
|
||||
'extremely positive': 0.6,
|
||||
}
|
||||
|
||||
export const sentimentQuantifierRanges = [
|
||||
{
|
||||
range: [Number.MIN_SAFE_INTEGER, -0.6],
|
||||
quant: 'extremely negative'
|
||||
},
|
||||
{
|
||||
range: [-0.6, -0.3],
|
||||
quant: 'very negative'
|
||||
},
|
||||
{
|
||||
range: [-0.3, -0.1],
|
||||
quant: 'negative'
|
||||
},
|
||||
{
|
||||
range: [-0.1, 0.1],
|
||||
quant: 'neutral'
|
||||
},
|
||||
{
|
||||
range: [0.1, 0.3],
|
||||
quant: 'positive'
|
||||
},
|
||||
{
|
||||
range: [0.3, 0.6],
|
||||
quant: 'very positive'
|
||||
},
|
||||
{
|
||||
range: [0.6, Number.MAX_SAFE_INTEGER],
|
||||
quant: 'extremely positive'
|
||||
}
|
||||
]
|
||||
|
||||
const scoreToSentimentText = (val: number) => {
|
||||
for(const segment of sentimentQuantifierRanges) {
|
||||
if(between(val, segment.range[0], segment.range[1], false)) {
|
||||
return segment.quant;
|
||||
}
|
||||
}
|
||||
throw new Error('should not hit this!');
|
||||
}
|
||||
|
||||
export interface SentimentResult {
|
||||
comparative: number
|
||||
type: SentimentAnalysisType
|
||||
sentiment: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
export interface ActivitySentiment {
|
||||
results: SentimentResult[]
|
||||
score: number
|
||||
scoreWeighted: number
|
||||
sentiment: string
|
||||
sentimentWeighted: string
|
||||
activity: SnoowrapActivity
|
||||
language: string
|
||||
}
|
||||
|
||||
export interface ActivitySentimentTestResult extends ActivitySentiment {
|
||||
passes: boolean
|
||||
test: GenericComparison | RangedComparison
|
||||
}
|
||||
|
||||
export interface ActivitySentimentOptions {
|
||||
testOn?: ('title' | 'body')[]
|
||||
}
|
||||
|
||||
export type SentimentCriteriaTest = GenericComparison | RangedComparison;
|
||||
|
||||
export const availableSentimentLanguages = ['en', 'es', 'de', 'fr'];
|
||||
|
||||
export const textComparison = /(?<not>not)?\s*(?<modifier>very|extremely)?\s*(?<sentiment>positive|neutral|negative)/i;
|
||||
|
||||
export const parseTextToNumberComparison = (val: string): RangedComparison | GenericComparison => {
|
||||
|
||||
let genericError: Error | undefined;
|
||||
try {
|
||||
return parseGenericValueComparison(val);
|
||||
} catch (e) {
|
||||
genericError = e as Error;
|
||||
// now try text match
|
||||
}
|
||||
|
||||
const matches = val.match(textComparison);
|
||||
if (matches === null) {
|
||||
const textError = new InvalidRegexError(textComparison, val);
|
||||
throw new CMError(`Sentiment value did not match a valid numeric comparison or valid text: \n ${genericError.message} \n ${textError.message}`);
|
||||
}
|
||||
const groups = matches.groups as any;
|
||||
|
||||
const negate = groups.not !== undefined && groups.not !== '';
|
||||
|
||||
if(groups.sentiment === 'neutral') {
|
||||
if(negate) {
|
||||
return {
|
||||
displayText: 'not neutral (not -0.49 to 0.49)',
|
||||
range: [-1, 1],
|
||||
not: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
displayText: 'is neutral (-0.49 to 0.49)',
|
||||
range: [-1, 1],
|
||||
not: false
|
||||
}
|
||||
}
|
||||
|
||||
const compoundSentimentText = `${groups.modifier !== undefined && groups.modifier !== '' ? `${groups.modifier} ` : ''}${groups.sentiment}`.toLocaleLowerCase();
|
||||
// @ts-ignore
|
||||
const numericVal = sentimentQuantifier[compoundSentimentText] as number;
|
||||
if(numericVal === undefined) {
|
||||
throw new CMError(`Sentiment given did not match any known phrases: '${compoundSentimentText}'`);
|
||||
}
|
||||
|
||||
let operator: StringOperator;
|
||||
if(negate) {
|
||||
operator = numericVal > 0 ? '<' : '>';
|
||||
} else {
|
||||
operator = numericVal > 0 ? '>=' : '<=';
|
||||
}
|
||||
|
||||
return {
|
||||
operator,
|
||||
value: numericVal,
|
||||
isPercent: false,
|
||||
displayText: `is${negate ? ' not ': ' '}${compoundSentimentText} (${operator} ${numericVal})`
|
||||
}
|
||||
}
|
||||
|
||||
const nlpAnalyzer = new SentimentManager();
|
||||
const langDetect = new Language();
|
||||
|
||||
export const getActivitySentiment = async (item: SnoowrapActivity, options?: ActivitySentimentOptions): Promise<ActivitySentiment> => {
|
||||
|
||||
const {
|
||||
testOn = ['body', 'title']
|
||||
} = options || {};
|
||||
|
||||
// determine what content we are testing
|
||||
let contents: string[] = [];
|
||||
if (asSubmission(item)) {
|
||||
for (const l of testOn) {
|
||||
switch (l) {
|
||||
case 'title':
|
||||
contents.push(item.title);
|
||||
break;
|
||||
case 'body':
|
||||
if (item.is_self) {
|
||||
contents.push(item.selftext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents.push(item.body)
|
||||
}
|
||||
|
||||
const contentStr = contents.join(' ');
|
||||
|
||||
const guess = langDetect.guessBest(contentStr);
|
||||
|
||||
if (availableSentimentLanguages.includes(guess.alpha2)) {
|
||||
const results: SentimentResult[] = [];
|
||||
|
||||
const nlpResult = await nlpAnalyzer.process(guess.alpha2, contentStr);
|
||||
|
||||
results.push({
|
||||
comparative: nlpResult.comparative,
|
||||
type: nlpResult.type as SentimentAnalysisType,
|
||||
sentiment: scoreToSentimentText(nlpResult.comparative),
|
||||
weight: 1
|
||||
});
|
||||
|
||||
if (guess.alpha2 === 'en') {
|
||||
const score = SentimentIntensityAnalyzer.polarity_scores(contentStr);
|
||||
results.push({
|
||||
comparative: score.compound,
|
||||
type: 'vader',
|
||||
sentiment: scoreToSentimentText(score.compound),
|
||||
// may want to weight higher in the future...
|
||||
weight: 1
|
||||
});
|
||||
|
||||
const winkScore = wink(contentStr);
|
||||
// normalizedScore is range of -5 to +5 -- convert to -1 to +1
|
||||
const winkAdjusted = (winkScore.normalizedScore * 2) / 10;
|
||||
results.push({
|
||||
comparative: winkAdjusted,
|
||||
type: 'wink',
|
||||
sentiment: scoreToSentimentText(winkAdjusted),
|
||||
weight: 1
|
||||
})
|
||||
}
|
||||
|
||||
const score = results.reduce((acc, curr) => acc + curr.comparative, 0) / results.length;
|
||||
const sentiment = scoreToSentimentText(score);
|
||||
|
||||
const weightSum = results.reduce((acc, curr) => acc + curr.weight, 0);
|
||||
const weightedScores = results.reduce((acc, curr) => acc + (curr.weight * curr.comparative), 0);
|
||||
const weightedScore = weightedScores / weightSum;
|
||||
const weightedSentiment = scoreToSentimentText(weightedScore);
|
||||
|
||||
return {
|
||||
results,
|
||||
score,
|
||||
sentiment,
|
||||
scoreWeighted: weightedScore,
|
||||
sentimentWeighted: weightedSentiment,
|
||||
activity: item,
|
||||
language: guess.language
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new MaybeSeriousErrorWithCause(`Cannot test sentiment for unsupported language ${guess.language}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const testActivitySentiment = async (item: SnoowrapActivity, criteria: SentimentCriteriaTest, options?: ActivitySentimentOptions): Promise<ActivitySentimentTestResult> => {
|
||||
const sentimentResult = await getActivitySentiment(item, options);
|
||||
|
||||
if (asGenericComparison(criteria)) {
|
||||
return {
|
||||
passes: comparisonTextOp(sentimentResult.scoreWeighted, criteria.operator, criteria.value),
|
||||
test: criteria,
|
||||
...sentimentResult,
|
||||
}
|
||||
} else {
|
||||
if (criteria.not) {
|
||||
return {
|
||||
passes: sentimentResult.scoreWeighted < criteria.range[0] || sentimentResult.scoreWeighted > criteria.range[1],
|
||||
test: criteria,
|
||||
...sentimentResult,
|
||||
}
|
||||
}
|
||||
return {
|
||||
passes: sentimentResult.scoreWeighted >= criteria.range[0] || sentimentResult.scoreWeighted <= criteria.range[1],
|
||||
test: criteria,
|
||||
...sentimentResult,
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/Common/Typings/support.d.ts
vendored
20
src/Common/Typings/support.d.ts
vendored
@@ -32,3 +32,23 @@ declare module 'winston-null' {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'node-nlp' {
|
||||
export class Language {
|
||||
guess(val: string, allowedList?: string[] | null, limit: number): {alpha3: string, alpha2: string, language: string, score: number}[];
|
||||
guessBest(val: string, allowedList?: string[] | null): {alpha3: string, alpha2: string, language: string, score: number};
|
||||
}
|
||||
export class SentimentAnalyzer {
|
||||
constructor(settings?: {language?: string})
|
||||
getSentiment(phrase: string): Promise<{score: number, comparative: score, vote: 'string', numWords: number, numHits: number, type: string, language: string}>
|
||||
}
|
||||
|
||||
export class SentimentManager {
|
||||
process(locale: string, phrase: string): Promise<{score: number, comparative: score, vote: 'string', numWords: number, numHits: number, type: string, language: string}>
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'wink-sentiment' {
|
||||
function sentiment(phrase: string): {score: number, normalizedScore: number, tokenizedPhrase: any[]};
|
||||
export default sentiment;
|
||||
}
|
||||
|
||||
@@ -2,123 +2,26 @@ import {Rule, RuleJSONConfig, RuleOptions} from "./index";
|
||||
import {Comment} from "snoowrap";
|
||||
import Submission from "snoowrap/dist/objects/Submission";
|
||||
import {
|
||||
asSubmission,
|
||||
comparisonTextOp, formatNumber,
|
||||
parseStringToRegex,
|
||||
triggeredIndicator, windowConfigToWindowCriteria
|
||||
} from "../util";
|
||||
import {Scores, SentimentIntensityAnalyzer} from 'vader-sentiment';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import {CMError, SimpleError} from "../Utils/Errors";
|
||||
import InvalidRegexError from "../Utils/InvalidRegexError";
|
||||
import {map as mapAsync} from 'async';
|
||||
import {
|
||||
asGenericComparison,
|
||||
GenericComparison,
|
||||
parseGenericValueComparison, parseGenericValueOrPercentComparison,
|
||||
parseGenericValueOrPercentComparison,
|
||||
RangedComparison
|
||||
} from "../Common/Infrastructure/Comparisons";
|
||||
import {ActivityWindowConfig, ActivityWindowCriteria} from "../Common/Infrastructure/ActivityWindow";
|
||||
import {StringOperator, VaderSentimentComparison} from "../Common/Infrastructure/Atomic";
|
||||
import {VaderSentimentComparison} from "../Common/Infrastructure/Atomic";
|
||||
import {RuleResult} from "../Common/interfaces";
|
||||
import {SnoowrapActivity} from "../Common/Infrastructure/Reddit";
|
||||
|
||||
export const sentimentQuantifier = {
|
||||
'extremely negative': -0.5,
|
||||
'very negative': -0.2,
|
||||
'negative': -0.05,
|
||||
'neutral': 0,
|
||||
'positive': 0.05,
|
||||
'very positive': 0.2,
|
||||
'extremely positive': 0.5,
|
||||
}
|
||||
|
||||
const sentimentQuantifierEntries = Object.entries(sentimentQuantifier);
|
||||
|
||||
const scoreToSentimentText = (val: number) => {
|
||||
for(let i = 0; i < sentimentQuantifierEntries.length; i++) {
|
||||
// hit max score (extremely positive)
|
||||
if(i + 1 === sentimentQuantifierEntries.length && val > sentimentQuantifierEntries[i][1]) {
|
||||
return sentimentQuantifierEntries[i][0];
|
||||
}
|
||||
|
||||
// if score is between (inclusive start) current and next
|
||||
if(i + 1 <= sentimentQuantifierEntries.length && val >= sentimentQuantifierEntries[i][1] && val < sentimentQuantifierEntries[i + 1][1]) {
|
||||
return sentimentQuantifierEntries[i][0];
|
||||
}
|
||||
|
||||
// if neither of those is hit and i = 0 then min score (extremely negative)
|
||||
if(i === 0 && val < sentimentQuantifierEntries[i][1]) {
|
||||
return sentimentQuantifierEntries[i][0];
|
||||
}
|
||||
}
|
||||
throw new Error('should not hit this!');
|
||||
}
|
||||
|
||||
interface ActivitySentimentResult {
|
||||
passes: boolean,
|
||||
scores: Scores,
|
||||
sentiment: string
|
||||
activity: SnoowrapActivity
|
||||
}
|
||||
|
||||
export const textComparison = /(?<not>not)?\s*(?<modifier>very|extremely)?\s*(?<sentiment>positive|neutral|negative)/i;
|
||||
|
||||
export const parseTextToNumberComparison = (val: string): RangedComparison | GenericComparison => {
|
||||
|
||||
let genericError: Error | undefined;
|
||||
try {
|
||||
return parseGenericValueComparison(val);
|
||||
} catch (e) {
|
||||
genericError = e as Error;
|
||||
// now try text match
|
||||
}
|
||||
|
||||
const matches = val.match(textComparison);
|
||||
if (matches === null) {
|
||||
const textError = new InvalidRegexError(textComparison, val);
|
||||
throw new CMError(`Sentiment value did not match a valid numeric comparison or valid text: \n ${genericError.message} \n ${textError.message}`);
|
||||
}
|
||||
const groups = matches.groups as any;
|
||||
|
||||
const negate = groups.not !== undefined && groups.not !== '';
|
||||
|
||||
if(groups.sentiment === 'neutral') {
|
||||
if(negate) {
|
||||
return {
|
||||
displayText: 'not neutral (not -0.49 to 0.49)',
|
||||
range: [-0.49, 0.49],
|
||||
not: true,
|
||||
}
|
||||
}
|
||||
return {
|
||||
displayText: 'is neutral (-0.49 to 0.49)',
|
||||
range: [-0.49, 0.49],
|
||||
not: false
|
||||
}
|
||||
}
|
||||
|
||||
const compoundSentimentText = `${groups.modifier !== undefined && groups.modifier !== '' ? `${groups.modifier} ` : ''}${groups.sentiment}`.toLocaleLowerCase();
|
||||
// @ts-ignore
|
||||
const numericVal = sentimentQuantifier[compoundSentimentText] as number;
|
||||
if(numericVal === undefined) {
|
||||
throw new CMError(`Sentiment given did not match any known phrases: '${compoundSentimentText}'`);
|
||||
}
|
||||
|
||||
let operator: StringOperator;
|
||||
if(negate) {
|
||||
operator = numericVal > 0 ? '<' : '>';
|
||||
} else {
|
||||
operator = numericVal > 0 ? '>=' : '<=';
|
||||
}
|
||||
|
||||
return {
|
||||
operator,
|
||||
value: numericVal,
|
||||
isPercent: false,
|
||||
displayText: `is${negate ? ' not ': ' '}${compoundSentimentText} (${operator} ${numericVal})`
|
||||
}
|
||||
}
|
||||
import {
|
||||
ActivitySentimentTestResult,
|
||||
parseTextToNumberComparison,
|
||||
testActivitySentiment
|
||||
} from "../Common/LangaugeProcessing";
|
||||
|
||||
export class SentimentRule extends Rule {
|
||||
|
||||
@@ -166,10 +69,8 @@ export class SentimentRule extends Rule {
|
||||
|
||||
protected async process(item: Submission | Comment): Promise<[boolean, RuleResult]> {
|
||||
|
||||
let criteriaResults = [];
|
||||
|
||||
let ogResult = this.testActivity(item, this.sentiment);
|
||||
let historicResults: ActivitySentimentResult[] | undefined;
|
||||
let ogResult = await this.testActivity(item, this.sentiment);
|
||||
let historicResults: ActivitySentimentTestResult[] | undefined;
|
||||
|
||||
if(this.historical !== undefined && (!this.historical.mustMatchCurrent || ogResult.passes)) {
|
||||
const {
|
||||
@@ -178,7 +79,7 @@ export class SentimentRule extends Rule {
|
||||
} = this.historical;
|
||||
const history = await this.resources.getAuthorActivities(item.author, window);
|
||||
|
||||
historicResults = history.map(x => this.testActivity(x, sentiment));
|
||||
historicResults = await mapAsync(history, async (x: SnoowrapActivity) => await this.testActivity(x, sentiment)); // history.map(x => this.testActivity(x, sentiment));
|
||||
}
|
||||
|
||||
|
||||
@@ -197,8 +98,8 @@ export class SentimentRule extends Rule {
|
||||
|
||||
if(historicResults === undefined) {
|
||||
triggered = ogResult.passes;
|
||||
averageScore = ogResult.scores.compound;
|
||||
logSummary.push(`${triggeredIndicator(triggered)} Current Activity Sentiment '${ogResult.sentiment} (${ogResult.scores.compound})' ${triggered ? 'PASSED' : 'DID NOT PASS'} sentiment test '${sentimentTest}'`);
|
||||
averageScore = ogResult.scoreWeighted;
|
||||
logSummary.push(`${triggeredIndicator(triggered)} Current Activity Sentiment '${ogResult.sentiment} (${ogResult.scoreWeighted})' ${triggered ? 'PASSED' : 'DID NOT PASS'} sentiment test '${sentimentTest}'`);
|
||||
if(!triggered && this.historical !== undefined && this.historical.mustMatchCurrent) {
|
||||
logSummary.push(`Did not check Historical because 'mustMatchCurrent' is true`);
|
||||
}
|
||||
@@ -212,8 +113,8 @@ export class SentimentRule extends Rule {
|
||||
totalMatchingText = totalMatching.displayText;
|
||||
const allResults = historicResults
|
||||
const passed = allResults.filter(x => x.passes);
|
||||
averageScore = passed.reduce((acc, curr) => acc + curr.scores.compound,0) / passed.length;
|
||||
averageWindowScore = allResults.reduce((acc, curr) => acc + curr.scores.compound,0) / allResults.length;
|
||||
averageScore = passed.reduce((acc, curr) => acc + curr.scoreWeighted,0) / passed.length;
|
||||
averageWindowScore = allResults.reduce((acc, curr) => acc + curr.scoreWeighted,0) / allResults.length;
|
||||
|
||||
const firstActivity = allResults[0].activity;
|
||||
const lastActivity = allResults[allResults.length - 1].activity;
|
||||
@@ -253,53 +154,8 @@ export class SentimentRule extends Rule {
|
||||
})]);
|
||||
}
|
||||
|
||||
protected testActivity(a: (Submission | Comment), criteria: GenericComparison | RangedComparison): ActivitySentimentResult {
|
||||
// determine what content we are testing
|
||||
let contents: string[] = [];
|
||||
if (asSubmission(a)) {
|
||||
for (const l of this.testOn) {
|
||||
switch (l) {
|
||||
case 'title':
|
||||
contents.push(a.title);
|
||||
break;
|
||||
case 'body':
|
||||
if (a.is_self) {
|
||||
contents.push(a.selftext);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
contents.push(a.body)
|
||||
}
|
||||
|
||||
const contentStr = contents.join(' ');
|
||||
const scores = SentimentIntensityAnalyzer.polarity_scores(contentStr);
|
||||
const textSentimentScore = scoreToSentimentText(scores.compound);
|
||||
|
||||
if (asGenericComparison(criteria)) {
|
||||
return {
|
||||
passes: comparisonTextOp(scores.compound, criteria.operator, criteria.value),
|
||||
scores,
|
||||
sentiment: textSentimentScore,
|
||||
activity: a
|
||||
}
|
||||
} else {
|
||||
if (criteria.not) {
|
||||
return {
|
||||
passes: scores.compound < criteria.range[0] || scores.compound > criteria.range[1],
|
||||
scores,
|
||||
sentiment: textSentimentScore,
|
||||
activity: a
|
||||
}
|
||||
}
|
||||
return {
|
||||
passes: scores.compound >= criteria.range[0] || scores.compound <= criteria.range[1],
|
||||
scores,
|
||||
sentiment: textSentimentScore,
|
||||
activity: a
|
||||
}
|
||||
}
|
||||
protected async testActivity(a: (Submission | Comment), criteria: GenericComparison | RangedComparison): Promise<ActivitySentimentTestResult> {
|
||||
return await testActivitySentiment(a, criteria, {testOn: this.testOn});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2926,7 +2926,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
@@ -5244,7 +5244,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
|
||||
@@ -1309,7 +1309,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
@@ -2855,7 +2855,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
|
||||
@@ -1280,7 +1280,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
@@ -2826,7 +2826,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"sentiment": {
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -0.5 to 0.5:\n\n* -0.5 => extremely negative\n* -0.2 => very negative\n* -0.5 => negative\n* 0 => neutral\n* 0.05 => positive\n* 0.2 => very positive\n* 0.5 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.05` = `is positive`\n* `<= 0.2` = `is very negative`\n* `< 0.05` = `is not positive`\n* `> -0.2` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.5 and < 0.5`\n* `is not neutral` equates to `< -0.5 or > 0.5`\n\nMore about VADER Sentiment: https://github.com/cjhutto/vaderSentiment",
|
||||
"description": "Test the calculated VADER sentiment (compound) score for an Activity using this comparison. Can be either a numerical or natural language\n\nSentiment values range from extremely negative to extremely positive in a numerical range of -1 to +1:\n\n* -0.6 => extremely negative\n* -0.3 => very negative\n* -0.1 => negative\n* 0 => neutral\n* 0.1 => positive\n* 0.3 => very positive\n* 0.6 => extremely positive\n\nThe below examples are all equivocal. You can use either set of values as the value for `sentiment` (numerical comparisons or natural langauge)\n\n* `>= 0.1` = `is positive`\n* `<= 0.3` = `is very negative`\n* `< 0.1` = `is not positive`\n* `> -0.3` = `is not very negative`\n\nSpecial case:\n\n* `is neutral` equates to `> -0.1 and < 0.1`\n* `is not neutral` equates to `< -0.1 or > 0.1`\n\nContextMod uses a normalized, weighted average from these sentiment tools:\n\n* NLP.js (english, french, german, and spanish) https://github.com/axa-group/nlp.js/blob/master/docs/v3/sentiment-analysis.md\n* (english only) vaderSentiment-js https://github.com/vaderSentiment/vaderSentiment-js/\n* (english only) wink-sentiment https://github.com/winkjs/wink-sentiment\n\nMore about the sentiment algorithms used:\n* VADER https://github.com/cjhutto/vaderSentiment\n* AFINN http://corpustext.com/reference/sentiment_afinn.html\n* Senticon https://ieeexplore.ieee.org/document/8721408\n* Pattern https://github.com/clips/pattern\n* wink https://github.com/winkjs/wink-sentiment",
|
||||
"examples": [
|
||||
"is negative",
|
||||
"> 0.2"
|
||||
|
||||
@@ -2757,3 +2757,11 @@ export const filterByTimeRequirement = (satisfiedEndtime: Dayjs, listSlice: Snoo
|
||||
|
||||
return [truncatedItems.length !== listSlice.length, truncatedItems]
|
||||
}
|
||||
|
||||
export const between = (val: number, a: number, b: number, inclusive: boolean): boolean => {
|
||||
var min = Math.min(a, b),
|
||||
max = Math.max(a, b);
|
||||
|
||||
return inclusive ? val >= min && val <= max : val > min && val < max;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user