diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index fc651f0..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "src/aggregation"] - path = src/aggregation - url = ./src/aggregation/ diff --git a/index.js b/index.js index 9c43315..377fbad 100644 --- a/index.js +++ b/index.js @@ -5,7 +5,7 @@ import { geometricMeanOfOdds, extremizedGeometricMeanOfOdds, neyman, -} from "./src/aggregation/index.js"; +} from "./packages/aggregation"; export { median, diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000..a2bb50b --- /dev/null +++ b/lerna.json @@ -0,0 +1,6 @@ +{ + "packages": [ + "packages/*" + ], + "version": "independent" +} diff --git a/package.json b/package.json index 0a64f8d..110bafa 100644 --- a/package.json +++ b/package.json @@ -14,5 +14,8 @@ "license": "ISC", "dependencies": { "@forecasting/aggregation": "^0.0.1" + }, + "devDependencies": { + "lerna": "^4.0.0" } } diff --git a/packages/aggregation/README.md b/packages/aggregation/README.md new file mode 100644 index 0000000..ab6a495 --- /dev/null +++ b/packages/aggregation/README.md @@ -0,0 +1,76 @@ +## About + +![](decision-method.png) + +This package contains a series of utilities for forecast aggregation. It is currently in _alpha_, meaning that the code hasn't been tested much. + +For an introduction to different aggregation methods, see Jaime Sevilla's [Aggregation](https://forum.effectivealtruism.org/s/hjiBqAJNKhfJFq7kf) series. For an explanation of the neyman method, see [here](https://forum.effectivealtruism.org/s/hjiBqAJNKhfJFq7kf/p/biL94PKfeHmgHY6qe). + +## Built with + +- vanilla javascript +- [Best readme template](https://github.com/othneildrew/Best-README-Template) + +## Getting started + +### Installation + +```sh +npm install @forecasting/aggregation +``` + +### Usage + +```js +import { + median, + arithmeticMean, + geometricMean, + geometricMeanOfOdds, + extremizedGeometricMeanOfOdds, + neyman, +} from "@forecasting/aggregation"; + +let ps = [0.1, 0.2, 0.4, 0.5]; +console.log(ps); + +console.log(median(ps)); +console.log(arithmeticMean(ps)); +console.log(geometricMean(ps)); +console.log(geometricMeanOfOdds(ps)); +console.log(extremizedGeometricMeanOfOdds(ps, 1.5)); // 1.5 is the extremization factor +console.log(extremizedGeometricMeanOfOdds(ps, 2.5)); +console.log(neyman(ps)); + +// invalid inputs, will return -1 +let notArrayOfProbabilities0 = "Hello world!"; +console.log(arithmeticMean(notArrayOfProbabilities0)); // -1 +let notArrayOfProbabilities1 = []; +console.log(arithmeticMean(notArrayOfProbabilities1)); // -1 +let notArrayOfProbabilities2 = ["a"]; +console.log(arithmeticMean(notArrayOfProbabilities2)); // -1 +let notArrayOfProbabilities3 = [2, 4, 5]; +console.log(arithmeticMean(notArrayOfProbabilities3)); // -1 +let notArrayOfProbabilities4 = [0.2, 4, 5]; +console.log(arithmeticMean(notArrayOfProbabilities4)); // -1 + +const chosenAggregationMethod = neyman; +const getAggregatedProbabilities = (array) => { + let result = neyman(array); + if (result == -1) { + // handle case somehow; maybe throw an error, e.g.: + // throw new Error("Invalid array of probabilities") + } else { + return result; + } +}; +``` + +## Roadmap + +- [x] validate probability (must be 0<= p <=1) +- [x] Decide on a return type if probabilities are not validated (-1? / null?) +- [x] Write wrapper code for validation +- [x] Validate that array.length > 0 +- [ ] add weighting? by recency? +- [ ] filter outliers? diff --git a/packages/aggregation/decision-method.png b/packages/aggregation/decision-method.png new file mode 100644 index 0000000..3292f90 Binary files /dev/null and b/packages/aggregation/decision-method.png differ diff --git a/packages/aggregation/index.js b/packages/aggregation/index.js new file mode 100644 index 0000000..051bffa --- /dev/null +++ b/packages/aggregation/index.js @@ -0,0 +1,74 @@ +// Helpers +const sum = (array) => array.reduce((a, b) => a + b, 0); +const probabilityToOdds = (p) => p / (1 - p); +const oddsToProbability = (o) => o / (1 + o); +const validateArray = (arr) => + Array.isArray(arr) && + arr.length > 0 && + arr.reduce((a, b) => a && typeof b == "number" && b >= 0 && b <= 1, true); + +// Main functions +export const median = (array) => { + if (!validateArray(array)) return -1; + // needs validation array not empty + let midway = Math.floor(array.length) / 2; + let arrayToBeSorted = [...array]; + // sorting mutates the array, which I am averse to + let arraySorted = arrayToBeSorted.sort((a, b) => a - b); + if (midway % 2) { + return arraySorted[midway]; + } else { + return (arraySorted[midway - 1] + arraySorted[midway]) / 2; + } +}; + +export const arithmeticMean = (array) => { + if (!validateArray(array)) return -1; + let result = sum(array) / array.length; + return result; +}; + +export const geometricMean = (array) => { + if (!validateArray(array)) return -1; + // sum of logs seems more numerically stable than multiplying a lot of numbers 0<=p<=1 + let arrayAsLog = array.map((p) => Math.log(p)); + let sumOfLogs = sum(arrayAsLog) / arrayAsLog.length; + let result = Math.exp(sumOfLogs); + return result; +}; + +export const geometricMeanOfOdds = (array) => { + if (!validateArray(array)) return -1; + let arrayOfOdds = array.map((p) => probabilityToOdds(p)); + let arrayOfLogsOfOdds = arrayOfOdds.map((p) => Math.log(p)); + let sumOfLogsOfOdds = sum(arrayOfLogsOfOdds) / arrayOfLogsOfOdds.length; + let geomMeanOfOdds = Math.exp(sumOfLogsOfOdds); + let result = oddsToProbability(geomMeanOfOdds); + return result; +}; + +export const extremizedGeometricMeanOfOdds = ( + array, + extremizationParameter = 1.5 +) => { + if (!validateArray(array)) return -1; + let arrayOfOdds = array.map((p) => probabilityToOdds(p)); + let arrayOfLogsOfOdds = arrayOfOdds.map((p) => Math.log(p)); + let extremizedSumOfLogsOfOdds = + (extremizationParameter * sum(arrayOfLogsOfOdds)) / + arrayOfLogsOfOdds.length; + let extremizedGeomMeanOfOdds = Math.exp(extremizedSumOfLogsOfOdds); + let result = oddsToProbability(extremizedGeomMeanOfOdds); + return result; +}; + +export const neyman = (array) => { + if (!validateArray(array)) return -1; + let n = array.length; + + let d = + (n * (Math.sqrt(3 * Math.pow(n, 2) - 3 * n + 1) - 2)) / + (Math.pow(n, 2) - n - 1); + let result = extremizedGeometricMeanOfOdds(array, d); + return result; +}; diff --git a/packages/aggregation/package.json b/packages/aggregation/package.json new file mode 100644 index 0000000..ff0d663 --- /dev/null +++ b/packages/aggregation/package.json @@ -0,0 +1,19 @@ +{ + "name": "@forecasting/aggregation", + "version": "1.0.1", + "description": "Forecasting aggregation utilities", + "main": "index.js", + "scripts": { + "test": "node tests.js" + }, + "keywords": [ + "forecasting", + "aggregation", + "prediction", + "prediction", + "markets" + ], + "type": "module", + "author": "Nuño Sempere", + "license": "MIT" +} diff --git a/packages/aggregation/tests.js b/packages/aggregation/tests.js new file mode 100644 index 0000000..7070bf1 --- /dev/null +++ b/packages/aggregation/tests.js @@ -0,0 +1,31 @@ +import { + median, + arithmeticMean, + geometricMean, + geometricMeanOfOdds, + extremizedGeometricMeanOfOdds, + neyman, +} from "./index.js"; + +let ps = [0.1, 0.2, 0.4, 0.5]; +console.log(ps); + +console.log(median(ps)); +console.log(arithmeticMean(ps)); +console.log(geometricMean(ps)); +console.log(geometricMeanOfOdds(ps)); +console.log(extremizedGeometricMeanOfOdds(ps, 1.5)); +console.log(extremizedGeometricMeanOfOdds(ps, 2.5)); +console.log(neyman(ps)); + +// invalid inputs, will return -1 +let notArrayOfProbabilities0 = "Hello world!"; +console.log(arithmeticMean(notArrayOfProbabilities0)); // -1 +let notArrayOfProbabilities1 = []; +console.log(arithmeticMean(notArrayOfProbabilities1)); // -1 +let notArrayOfProbabilities2 = ["a"]; +console.log(arithmeticMean(notArrayOfProbabilities2)); // -1 +let notArrayOfProbabilities3 = [2, 4, 5]; +console.log(arithmeticMean(notArrayOfProbabilities3)); // -1 +let notArrayOfProbabilities4 = [0.2, 4, 5]; +console.log(arithmeticMean(notArrayOfProbabilities4)); // -1 diff --git a/src/aggregation b/src/aggregation deleted file mode 160000 index 359897c..0000000 --- a/src/aggregation +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 359897cc5221b0f6ee2f428484daa05253a94364