apprecom
Version:
Location based app recommendation system.
390 lines (329 loc) • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
// CONSTANTS
var RULES_ENCODING = "utf-8";
/**
* <p>AppRecom is a tool for getting app recommendations based on a user's location.</p>
*
* <p>
* The two primary methods in this class are:
* <ol>
* <li>train()</li>
* <li>getApps()</li>
* </ol>
*
* For specific information about each method, check the method documentation.
*/
var AppRecom = function () {
/**
* Instantiate an AppRecom object for training and fetching recommendations.
* @param {Boolean} debug - option for console log debugging
*/
function AppRecom() {
var debug = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
_classCallCheck(this, AppRecom);
this.itemsets = [];
this.rules = [];
this.DEBUG = debug;
}
/**
* <p>Train the recommender against a data set. Entries in the data array look like:</p>
* <p>
* {pname: "Place Name", pcat: "Place Category", aname: "App Name", acat: "App Category"}
* </p>
*
* @param {Array<Object>} data - data to find association rules on.
* @param {Decimal} min_support - the minimum support percentage for an itemset (0.0 - 1.0)
* @param {Decimal} min_conf - the minimum confidence percentage for a rule (0.0 - 1.0)
* @param {Number} test_ratio - ratio of training data to test data (0.0 - 1.0)
* @returns rules
*/
_createClass(AppRecom, [{
key: "train",
value: function train(data) {
var min_support = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.02;
var min_conf = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0.8;
var test_ratio = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0.8;
// TRAIN AND TEST
this._testData(data, min_support, min_conf, test_ratio, 5);
// DONE TESTING
// Get final rules using all data
var optimalItemset = this._getOptimalItemset(data, min_support);
var rules = this._getRules(data, optimalItemset, min_conf); // get the rules
this.rules = rules;
}
/**
* <p>Retrieves app category recommendations that best fit this location as an array.</p>
*
* @param {String} locationCategory - the category of the location (e.g. 'cafe')
*/
}, {
key: "getApps",
value: function getApps(location) {
var appRecommendations = this.rules[location] ? this.rules[location] : [];
return appRecommendations;
}
/**
* Tests the learning by splitting into training and testing data and verifying results.
* @private
*/
}, {
key: "_testData",
value: function _testData(data, min_support, min_conf, test_ratio, rounds) {
var averageError = 0;
// Determines an integer # for training instances
var numTraining = Math.round(data.length * test_ratio);
for (var count = 0; count < rounds; count++) {
var shuffleData = data.slice();
shuffleData = shuffle(shuffleData);
// Training and Testing data
var trainingSet = [];
for (var i = 0; i <= numTraining; i++) {
var item = shuffleData.pop();
trainingSet.push(item);
}
var testingItemset = shuffleData;
// Training rules
var trainingItemset = this._getOptimalItemset(trainingSet, min_support);
var trainingRules = this._getRules(trainingSet, trainingItemset, min_conf);
// Logs
if (this.DEBUG) print("==============\nUNKNOWN TEST\ndata length: " + data.length + " - training length: " + trainingSet.length + " - testing length: " + testingItemset.length + "\n");
// Get the error rate for this data.
var error = this._testTrainingSet(trainingRules, testingItemset);
averageError += error;
if (this.DEBUG) print("Round " + (count + 1) + " unknown rate: " + error);
}
averageError = Math.round(averageError / rounds * 100) / 100;
if (this.DEBUG) print("\nAverage unknown rate: " + averageError);
}
/**
* Gets the optimal itemsets with the specified minimum support.
* @private
* @param {Array<Object>} data - the data to find itemsets on
* @param {Decimal} min_support - the minimum support percentage to include this item set
* @returns {Set} optimal itemset
*/
}, {
key: "_getOptimalItemset",
value: function _getOptimalItemset(data, min_support) {
return this._itemsetPrune(this._countItemsets(data), data.length, min_support);
}
/**
* Prunes the itemsets for those who match the min_support
* @private
* @param {Map<String, Number>} itemsets - item entry as JSON Array, item frequency
* @param {Number} length - length of the original data
* @param {Number} min_support - the minimum support accepted for an itemset
* @returns {Map<String, Number>} keepers - a map of rules and support
*/
}, {
key: "_itemsetPrune",
value: function _itemsetPrune(itemsetSupport, length, min_support) {
var keepers = new Map(itemsetSupport);
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = itemsetSupport[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var _ref = _step.value;
var _ref2 = _slicedToArray(_ref, 2),
instance = _ref2[0],
support = _ref2[1];
if (support / length < min_support) keepers.delete(instance); // keep this value because it satisfies the min support.
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
if (this.DEBUG) print("==============\nITEMSET DEBUG\n" + jstr([].concat(_toConsumableArray(itemsetSupport))).replace(/],\[\"\[/g, "],\n\[\"\[") + "\n=============="); // debug log
return keepers; // return the keepers with their supports
}
/**
* Counts the number of itemsets in the data.
* @private
* @param {Array<Object>} data - the data to count the itemsets on
* @returns {Map<String, Number>} itemsetSupport - the itemset numbers
*/
}, {
key: "_countItemsets",
value: function _countItemsets(data) {
var itemsetSupport = new Map();
data.forEach(function (instance) {
var mapKey = jstr([instance.pcat, instance.acat]);
if (!itemsetSupport.has(mapKey)) itemsetSupport.set(mapKey, 0); // fill in the map value
itemsetSupport.set(mapKey, itemsetSupport.get(mapKey) + 1); // increment this instance by one in the map
});
return itemsetSupport;
}
/**
* Determines the quality of the classifier by testing the trainingSet for the error rate.
* @private
* @returns ratio of incorrectly classified instances
*/
}, {
key: "_testTrainingSet",
value: function _testTrainingSet(rules, testingSet) {
var total = 0;
var numIncorrect = 0;
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = testingSet[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var _instance = _step2.value;
if (rules[_instance.pcat]) {
// if we have a rule for it, lets count it
total++;
var correct = false;
if (this.DEBUG) print(total + ". place cat: " + _instance.pcat);
correct = rules[_instance.pcat].indexOf(_instance.acat) != -1; // check the equality of the app part of the itemset
if (this.DEBUG) print("rule apps: " + rules[_instance.pcat] + "\ninstance app: " + _instance.acat);
if (!correct) numIncorrect++;
if (this.DEBUG) print((correct ? "FOUND" : "UNKNOWN") + "\n");
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
if (this.DEBUG) print("\nTotal: " + total + "\nUnknown: " + numIncorrect);
var errorrate = numIncorrect / total;
return errorrate != 0 ? Math.round(errorrate * 100) / 100 : 0;
}
/**
* Gets the rules from the itemsets according to the minimum confidence.
* @private
* @param {Array<Object>} ogData - the original data to count value frequencies on
* @param {Map<String, Number>} itemsets - the itemsets to fetch rules from.
* @param {Decimal} min_conf - the minimum confidence for a rule to be accepted
* @returns {Object} rules
*/
}, {
key: "_getRules",
value: function _getRules(ogData, itemsets, min_conf) {
var _this = this;
var rules = {};
[].concat(_toConsumableArray(itemsets.keys())).forEach(function (itemset) {
var arr = parse(itemset);
var hyp = arr[0];
var con = arr[1];
var hypFreq = _this._valueFreq(ogData, hyp);
var conFreq = _this._valueFreq(ogData, con);
var count = itemsets.get(itemset);
// print(`hypFreq: ${hypFreq} conFreq: ${conFreq}`);
if (conFreq / hypFreq >= min_conf) _this._addRules(rules, hyp, con, count);
});
if (this.DEBUG) print("==============\nRULES DEBUG\n");
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = Object.keys(rules)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var key = _step3.value;
rules[key].sort(function (a, b) {
return b.count - a.count;
}); // sort the rules on count
if (this.DEBUG) print(key + ":" + jstr(rules[key]).replace(/{/g, "{\n ").replace(/}/g, "\n }").replace(/],/g, "],\n") + "\n");
for (var app in rules[key]) {
rules[key][app] = rules[key][app].app; // strip object and count value
}
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
if (this.DEBUG) print("\n==============");
return rules;
}
/**
* Returns the frequency of a certain value in the original data
* @private
*/
}, {
key: "_valueFreq",
value: function _valueFreq(ogData, value) {
var count = 0;
ogData.forEach(function (instance) {
if (instance.pcat == value || instance.acat == value) count++;
});
return count;
}
/**
* Adds the rules to the rules array by their hypothesis conclusion format.
* @private
*/
}, {
key: "_addRules",
value: function _addRules(rules, hyp, con, count) {
var value = { app: con, count: count };
if (rules[hyp]) rules[hyp].push(value); // if a value already exists, append to the rules array
else rules[hyp] = [value]; // otherwise set
return rules;
}
}]);
return AppRecom;
}();
// HELPER FUNCTIONS
function print(str) {
console.log(str);
}
function jstr(obj) {
return JSON.stringify(obj);
}
function parse(string) {
return JSON.parse(string);
}
function shuffle(array) {
/*
Stole from Christoph on Stack Overflow:
https://stackoverflow.com/a/962890
*/
var tmp,
current,
top = array.length;
if (top) while (--top) {
current = Math.floor(Math.random() * (top + 1));
tmp = array[current];
array[current] = array[top];
array[top] = tmp;
}
return array;
}
// Module export
exports.default = AppRecom;