UNPKG

emotional

Version:

Subjectivtiy and sentiment/polarity analysis library for Node.js

402 lines (339 loc) 11.6 kB
/* * Emotional * * Node.js subjectivity and polarity/sentiment analysis tool * Partial port from the pattern.en python library: http://www.clips.ua.ac.be/pages/pattern-en of the University of Antwerp * All credits go to the original writer: Tom de Smedt */ var _ = require("underscore"); var _str = require("underscore.string"); var fs = require("fs"); var xml2js = require("xml2js"); var xmlParser = new xml2js.Parser(); var find_tokens = require("tokepi"); var EMOTICONS = require("emotional-emoticons"); var MOOD = "mood"; // emoticons, emojis var IRONY = "irony"; // sarcasm mark (!) /* * Sentiment Constructor * To be instantiated for a certain language */ function Sentiment(args) { this.path = args.path || ""; this.language = args.language; this.confidence = args.confidence || null; this.synset = args.synset; this.synsets = {}; this.labeler = {}; this.negations = def(args.negations, ["no", "not", "n't", "never"]); this.modifiers = def(args.modifiers, ["RB"]); this.modifier = def(args.modifier, function (w) { return _str.endsWith(w, "ly"); }); this.tokenizer = def(args.tokenizer, find_tokens); } // Load sentiment db path if given, otherwise load standard file provided by the language Sentiment.prototype.load = function(path, finish) { if (_.isFunction(path)) { finish = path; path = undefined; } var self = this; path = path || this.path; getXml(path, function (xml) { xml = xml.sentiment; var words = {}; var synsets = {}; var labels = {}; xml.word.forEach(function (word) { word = word.$; // skip if confidence threshold set and word is not belong threshold if (_.isNull(self.confidence) || self.confidence <= parseFloat(def(word.confidence, 0.0))) { var w = word.form; var pos = word.pos; var p = def(word.polarity, 0.0); var s = def(word.subjectivity, 0.0); var i = def(word.intensity, 1.0); var label = word.label; var synset = word[self.synset]; // wordnet_id, cornetto_id, ... var psi = [parseFloat(p), parseFloat(s), parseFloat(i)]; if (!_.isUndefined(w)) { setDefault(setDefault(words, w, {}), pos, []).push(psi); } if ((!_.isUndefined(w)) && (!_.isUndefined(label))) { labels[w] = label; } if (!_.isUndefined(synset)) { setDefault(synsets, synset, []).push(psi); } } }); self.language = xml.$.language || self.language; // Average scores of all word senses per part-of-speech tag. Object.keys(words).forEach(function (w) { Object.keys(words[w]).forEach(function (pos) { words[w][pos] = _.zip.apply(_, words[w][pos]).map(avg); }); }); // Average scores of all part-of-speech tags. Object.keys(words).forEach(function (w) { words[w][null] = _.zip.apply(_, _.values(words[w])).map(avg); }); // Average scores of all synonyms per synset. Object.keys(synsets).forEach(function (id) { var psi = synsets[id]; synsets[id] = _.zip.apply(_, psi).map(avg); }); self.words = words; self.labeler = labels; self.synsets = synsets; finish(); }); }; // Add given word with part-of-speach to the database with given polarity, subjectivity and intensity. Sentiment.prototype.annotate = function (w, pos, p, s, i, label) { var self = this; var entry = setDefault(self.words, w, {}); entry[pos] = entry[null] = [p, s, i]; if (!_.isUndefined(label)) { self.labeler[w] = label; } }; // Sentiment.prototype.getSynset = function (id, pos) { // var self = this; // pos = pos || ADJECTIVE; // if (_.keys(self.words).length === 0) { // throw Error("No sentiment corpus loaded"); // } // id = _str.pad(id.toString(), 8, "0"); // if (! (_str.startsWith(id, "n-") && // _str.startsWith(id, "v-") && // _str.startsWith(id, "a-") && // _str.startsWith(id, "r-"))) { // switch (pos) { // case NOUN: // id = "n-" + id; // break; // case VERB: // id = "v-" + id; // break; // case ADJECTIVE: // id = "a-" + id; // break; // case ADVERB: // id = "r-" + id; // break; // } // var syn = self.synsets[id]; // if (_.isUndefined(syn)) { // syn = def(self.synsets[id.replace(/-0+/, "-")], [0.0, 0.0]); // } // return syn.slice(0,2); // } // }; function avgAssessment(assessments, weighted) { var w; var s = 0; var n = 0; assessments.forEach(function (ws) { w = weighted(ws[0]); s += w * ws[1]; n += w; }); if (n === 0) { return 0; } else { return s / n; } } // Return the subjectivity and polarity/sentiment of given string Sentiment.prototype.get = function (s, negation, weight) { var self = this; weight = def(weight, (function () { return 1; })); var a; if (_.keys(self.words).length === 0) { throw Error("No sentiment database is loaded, please call 'load' first."); } if (!_.isString(s)) { throw new Error("unknown input " + s + " only know sentences of type string"); } var tokens = self.tokenizer(s); a = self.assessments(tokens.join(" ").split(" ").map(function (w) { return [w.toLowerCase(), null]; }), negation); return { polarity: avgAssessment(a.map(function (w) { return [w[0], w[1]]; }), weight), subjectivity: avgAssessment(a.map(function (w) { return [w[0], w[2]]; }), weight), assessments: a }; }; // Returns an array of [chunk, polarity, subjectivity, label] arrays for the given vector of words: // where chunk is a vector of successive words: a known word optionally // preceded by a modifier ("very good") or a negation ("not good"). Sentiment.prototype.assessments = function (words, negation) { var self = this; var prev, w, p, s, x; negation = _.isUndefined(negation) ? true : negation; var a = []; var m = null; // Preceding modifier (i.e., adverb or adjective). var n = null; // Preceding negation (e.g., "not beautiful"). words.forEach(function (wp) { var w = wp[0]; var pos = wp[1]; // will return arrays where indexes are as follows: // 0 = "w", 1 = "p", 2 = "s", 3 = "i", 4 = "n", 5 = "x" // Only assess known words, preferably by part-of-speech tag. // Including unknown words (polarity 0.0 and subjectivity 0.0) lowers the average. if (_.isNull(w)) return; // If we know the word from the sentimental corpus var entry = self.words[w]; if ((!_.isUndefined(entry)) && (!_.isUndefined(entry[pos]))) { var p = entry[pos][0]; var s = entry[pos][1]; var i = entry[pos][2]; // Known word not preceded by a modifier, e.g "good". if (_.isNull(m)) { a.push([[w], p, s, i, 1, self.labeler[w]]); } prev = a[a.length-1]; // Known word preceded by a modifier, e.g. "really good". if (!_.isNull(m)) { prev[0].push(w); prev[1] = Math.max(-1.0, Math.min(p * prev[3], +1.0)); prev[2] = Math.max(-1.0, Math.min(s * prev[3], +1.0)); prev[3] = i; prev[5] = self.labeler[w]; } // Known word preceded by a negation, e.g. "not really good". if (!_.isNull(n)) { prev[0] = [n].concat(prev[0]); prev[3] = 1.0 / prev[3]; prev[4] = -1; } // Known word may be a negation. // Known word may be modifying the next word (i.e., it is a known adverb). m = null; n = null; if ((!_.isUndefined(pos)) && (!_.isUndefined(self.modifiers[pos])) || _.any(self.modifiers.map(function (modifier) { return !_.isUndefined(entry[modifier]); }))) { m = [w, pos]; } if (negation && (!_.isUndefined(self.negations[w]))) { n = w; } // Unknown word } else { // negation if (negation && (!_.isUndefined(self.negations[w]))) { n = w; // Retain negation across small words ("not a good"). } else if ((!_.isNull(n)) && _str.strip(w, "'").length > 1) { n = null; } // May be a negation preceded by a modifier ("really not good"). if ((!_.isNull(n)) && (!_.isNull(m)) && ((!_.isUndefined(self.modifiers[pos])) || (!_.isUndefined(self.modifier(m[0]))))) { prev = a[a.length-1]; prev[0].push(n); prev[4] = -1; // Retain modifier across small words ("really is a good"). } else if ((!_.isNull(m)) && (w.length > 2)) { m = null; } // Exclamation mark boosts previous word if (w == "!" && a.length > 0) { prev = a[a.length-1]; prev[0].push("!"); prev[1] = Math.max(-1.0, Math.min(prev[1] * 1.25, +1.0)); } // Exclamation marks in parentheses indicate sarcasm. if (w == "(!)") { a.push([[w], 0.0, 1.0, 1.0, 1, IRONY]); } // EMOTICONS: {("grin", +1.0): set((":-D", ":D"))} // if ((!w.match(/^[0-9]+$/i)) && (w.length <= 5) && _str.include(PUNCTUATION, w)) { Object.keys(EMOTICONS).forEach(function (type) { if (_.contains(EMOTICONS[type].e, w.toLowerCase())) { a.push([[w], EMOTICONS[type].p, 1.0, 1.0, 1, MOOD]); } }); // } } }); for (var i=0; i<a.length; i++) { w = a[i][0]; p = a[i][1]; s = a[i][2]; n = a[i][4]; x = a[i][5]; // "not good" = slightly bad, "not bad" = slightly good. a[i] = [w, (n < 0 ? (p * -0.5) : p), s, x]; } return a; }; Sentiment.prototype.positive = function (s, threshold) { threshold = def(threshold, 0.1); var result = this.get(s); return (result.polarity >= threshold); }; // Initialize the sentiment analyser for english text var sentiment = new Sentiment({path: __dirname + "/en/en-sentiment.xml", synset: "wordnet_id"}); sentiment.load = function load(callback) { var self = this; Sentiment.prototype.load.call(self, function () { Object.keys(self.words).forEach(function (w) { Object.keys(self.words[w]).forEach(function (pos) { var nw = w; if (pos === "JJ") { if (_str.endsWith(w, "y")) { nw = w.slice(0, w.length-1) + "i"; } if (_str.endsWith(w, "le")) { nw = w.slice(0, w.length-2); } var entry = self.words[w][pos]; var p = entry[0]; var s = entry[1]; var i = entry[2]; self.annotate(nw+"ly", "RB", p, s, i); } }); }); callback(); }); }; /* * Auxiliary Functions */ // Average of number vector (0 if empty) function avg(vct) { if (vct.length === 0) { return 0; } return (vct.reduce(function (a, c) { return a + c; }, 0) / vct.length); } // Returns value if value is defined, otherwise defValue. function def(value, defValue) { if (_.isUndefined(value)) { return defValue; } return value; } // If given key is set in the object it returns the associated value, // otherwise it sets the value to val and returns it. function setDefault(obj, key, val) { if (_.isUndefined(obj[key])) { obj[key] = val; return val; } return obj[key]; } // Read and Parse XML file from given path, pass result to finish // Any error that occurs is simpy thrown. function getXml(path, finish) { fs.readFile(path, function(err, data) { if (err) throw err; xmlParser.parseString(data, function (err, result) { if (err) throw err; finish(result); }); }); } module.exports = sentiment;