From 7a93b926e6da7f6572eb3463fd52519e81ac2582 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Fri, 3 May 2024 12:06:05 +0200 Subject: [PATCH] wip: need still to convert to typescript --- .eslintrc.yml | 1 + .github/workflows/nodejs-ts.yml | 14 ++ .github/workflows/release.yml | 16 ++ .github/workflows/typedoc.yml | 32 ++++ .gitignore | 41 +++++ .npmrc | 1 + .prettierrc.json | 7 + CHANGELOG.md | 1 + README.md | 68 +++++++ package.json | 52 ++++++ src/PolynomialRegression2D.ts | 180 +++++++++++++++++++ src/__tests__/PolynomialRegression2D.test.ts | 80 +++++++++ src/index.ts | 1 + tsconfig.cjs.json | 11 ++ tsconfig.esm.json | 7 + tsconfig.json | 14 ++ 16 files changed, 526 insertions(+) create mode 100644 .eslintrc.yml create mode 100644 .github/workflows/nodejs-ts.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/typedoc.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 package.json create mode 100644 src/PolynomialRegression2D.ts create mode 100644 src/__tests__/PolynomialRegression2D.test.ts create mode 100644 src/index.ts create mode 100644 tsconfig.cjs.json create mode 100644 tsconfig.esm.json create mode 100644 tsconfig.json diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..bc57d11 --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1 @@ +extends: cheminfo-typescript diff --git a/.github/workflows/nodejs-ts.yml b/.github/workflows/nodejs-ts.yml new file mode 100644 index 0000000..71e0af2 --- /dev/null +++ b/.github/workflows/nodejs-ts.yml @@ -0,0 +1,14 @@ +name: Node.js CI + +on: + push: + branches: + - main + pull_request: + +jobs: + nodejs: + # Documentation: https://github.com/zakodium/workflows#nodejs-ci + uses: zakodium/workflows/.github/workflows/nodejs.yml@nodejs-v1 + with: + lint-check-types: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7f5db58 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,16 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + release: + # Documentation: https://github.com/zakodium/workflows#release + uses: zakodium/workflows/.github/workflows/release.yml@release-v1 + with: + npm: true + secrets: + github-token: ${{ secrets.BOT_TOKEN }} + npm-token: ${{ secrets.NPM_BOT_TOKEN }} diff --git a/.github/workflows/typedoc.yml b/.github/workflows/typedoc.yml new file mode 100644 index 0000000..7f4a28c --- /dev/null +++ b/.github/workflows/typedoc.yml @@ -0,0 +1,32 @@ +name: Deploy TypeDoc on GitHub pages + +on: + workflow_dispatch: + release: + types: [published] + +env: + NODE_VERSION: 18.x + ENTRY_FILE: 'src/index.ts' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: Install dependencies + run: npm install + - name: Build documentation + uses: zakodium/typedoc-action@v2 + with: + entry: ${{ env.ENTRY_FILE }} + - name: Deploy to GitHub pages + uses: JamesIves/github-pages-deploy-action@v4 + with: + token: ${{ secrets.BOT_TOKEN }} + branch: gh-pages + folder: docs + clean: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52f2560 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +lib + +lib-esm diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a23e760 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all" +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bfc9b6 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# regression-polynomial-2d + +[![NPM version][npm-image]][npm-url] +[![npm download][download-image]][download-url] +[![build status][ci-image]][ci-url] +[![Test coverage][codecov-image]][codecov-url] + +Polynomial Regression. + +## Installation + +`$ npm i ml-regression-polynomial-2d` + +## Usage + +```js +import { PolynomialRegression } from 'ml-regression-polynomial-2d'; + +const x = [50, 50, 50, 70, 70, 70, 80, 80, 80, 90, 90, 90, 100, 100, 100]; +const y = [ + 3.3, 2.8, 2.9, 2.3, 2.6, 2.1, 2.5, 2.9, 2.4, 3.0, 3.1, 2.8, 3.3, 3.5, 3.0, +]; +const degree = 5; // setup the maximum degree of the polynomial + +const regression = new PolynomialRegression(x, y, degree); + +console.log(regression.predict(80)); // Apply the model to some x value. Prints 2.6. +console.log(regression.coefficients); // Prints the coefficients in increasing order of power (from 0 to degree). +console.log(regression.toString(3)); // Prints a human-readable version of the function. +console.log(regression.toLaTeX()); +console.log(regression.score(x, y)); +``` + +## Options + +An `interceptAtZero` option is available, to force $f(0) = 0$. Also, a "powers array" can be specified. + +- Using `interceptAtZero` + +```js +const regression = new PolynomialRegression(x, y, degree, { + interceptAtZero: true, +}); +``` + +- Using the powers array + +```js +const powers = [0, 1, 2, 3, 4, 5]; +const regression = new PolynomialRegression(x, y, powers); +``` + +`powers` could also be `[1,2,3,4,5]`or`[1,3,5]` and so on. + +For intercepting at zero using an array, skip the zero in the array (the option `interceptAtZero` is ignored in this case.) + +## License + +[MIT](./LICENSE) + +[npm-image]: https://img.shields.io/npm/v/ml-regression-polynomial-2d.svg?style=flat-square +[npm-url]: https://npmjs.org/package/ml-regression-polynomial-2d +[download-image]: https://img.shields.io/npm/dm/ml-regression-polynomial-2d.svg?style=flat-square +[download-url]: https://npmjs.org/package/ml-regression-polynomial-2d +[codecov-image]: https://img.shields.io/codecov/c/github/mljs/regression-polynomial-2d.svg +[codecov-url]: https://codecov.io/gh/mljs/regression-polynomial-2d +[ci-image]: https://github.com/mljs/regression-polynomial-2d/workflows/Node.js%20CI/badge.svg?branch=main +[ci-url]: https://github.com/mljs/regression-polynomial-2d/actions?query=workflow%3A%22Node.js+CI%22 diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f1b808 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "ml-regression-polynomial-2d-2d", + "version": "0.0.0", + "description": "Polynomial Regression 2D", + "types": "./lib/index.d.ts", + "main": "./lib/index.js", + "module": "./lib-esm/index.js", + "files": [ + "src", + "lib", + "lib-esm" + ], + "scripts": { + "check-types": "tsc --noEmit", + "clean": "rimraf lib lib-esm", + "eslint": "eslint src", + "eslint-fix": "npm run eslint -- --fix", + "prepack": "npm run tsc", + "prettier": "prettier --check src", + "prettier-write": "prettier --write src", + "test": "npm run test-only && npm run eslint && npm run prettier && npm run check-types", + "test-only": "vitest run --coverage", + "tsc": "npm run clean && npm run tsc-cjs && npm run tsc-esm", + "tsc-cjs": "tsc --project tsconfig.cjs.json", + "tsc-esm": "tsc --project tsconfig.esm.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mljs/regression-polynomial-2d.git" + }, + "keywords": [], + "license": "MIT", + "bugs": { + "url": "https://github.com/mljs/regression-polynomial-2d/issues" + }, + "homepage": "https://github.com/mljs/regression-polynomial-2d#readme", + "devDependencies": { + "@vitest/coverage-v8": "^1.2.2", + "eslint": "^8.56.0", + "eslint-config-cheminfo-typescript": "^12.1.0", + "ml-spectra-processing": "^12.10.1", + "prettier": "^3.2.4", + "rimraf": "^5.0.5", + "typescript": "^5.3.3", + "vitest": "^1.2.2" + }, + "dependencies": { + "cheminfo-types": "^1.7.2", + "ml-matrix": "^6.11.0", + "ml-regression-base": "^3.0.0" + } +} diff --git a/src/PolynomialRegression2D.ts b/src/PolynomialRegression2D.ts new file mode 100644 index 0000000..4afba4d --- /dev/null +++ b/src/PolynomialRegression2D.ts @@ -0,0 +1,180 @@ +import { Matrix, SVD } from 'ml-matrix'; +import BaseRegression from 'ml-regression-base'; + +const defaultOptions = { + order: 2, +}; +// Implements the Kernel ridge regression algorithm. +// http://www.ics.uci.edu/~welling/classnotes/papers_class/Kernel-Ridge.pdf +export class PolynomialRegression2D extends BaseRegression { + /** + * Constructor for the 2D polynomial fitting + * + * @param inputs + * @param outputs + * @param options + * @constructor + */ + constructor(inputs, outputs, options = {}) { + super(); + if (inputs === true) { + // reloading model + this.coefficients = Matrix.columnVector(outputs.coefficients); + this.order = outputs.order; + if (outputs.r) { + this.r = outputs.r; + this.r2 = outputs.r2; + } + if (outputs.chi2) { + this.chi2 = outputs.chi2; + } + } else { + options = { ...defaultOptions, ...options }; + this.order = options.order; + this.coefficients = []; + this.X = inputs; + this.y = outputs; + + this.train(this.X, this.y, options); + } + } + + /** + * Function that fits the model given the data(X) and predictions(y). + * The third argument is an object with the following options: + * * order: order of the polynomial to fit. + * + * @param {Matrix} X - A matrix with n rows and 2 columns. + * @param {Matrix} y - A vector of the prediction values. + */ + train(X, y) { + if (!Matrix.isMatrix(X)) X = new Matrix(X); + if (!Matrix.isMatrix(y)) y = Matrix.columnVector(y); + + if (y.rows !== X.rows) { + y = y.transpose(); + } + + if (X.columns !== 2) { + throw new RangeError( + `You give X with ${X.columns} columns and it must be 2`, + ); + } + if (X.rows !== y.rows) { + throw new RangeError('X and y must have the same rows'); + } + + const examples = X.rows; + const coefficients = ((this.order + 2) * (this.order + 1)) / 2; + this.coefficients = new Array(coefficients); + + const x1 = X.getColumnVector(0); + const x2 = X.getColumnVector(1); + + const scaleX1 = 1.0 / x1.clone().abs().max(); + const scaleX2 = 1.0 / x2.clone().abs().max(); + const scaleY = 1.0 / y.clone().abs().max(); + + x1.mulColumn(0, scaleX1); + x2.mulColumn(0, scaleX2); + y.mulColumn(0, scaleY); + + const A = new Matrix(examples, coefficients); + let col = 0; + + for (let i = 0; i <= this.order; ++i) { + const limit = this.order - i; + for (let j = 0; j <= limit; ++j) { + const result = powColVector(x1, i).mulColumnVector(powColVector(x2, j)); + A.setColumn(col, result); + col++; + } + } + + const svd = new SVD(A.transpose(), { + computeLeftSingularVectors: true, + computeRightSingularVectors: true, + autoTranspose: false, + }); + + let qqs = Matrix.rowVector(svd.diagonal); + qqs = qqs.apply((i, j) => { + if (qqs.get(i, j) >= 1e-15) qqs.set(i, j, 1 / qqs.get(i, j)); + else qqs.set(i, j, 0); + }); + + const qqs1 = Matrix.zeros(examples, coefficients); + for (let i = 0; i < coefficients; ++i) { + qqs1.set(i, i, qqs.get(0, i)); + } + + qqs = qqs1; + + const U = svd.rightSingularVectors; + const V = svd.leftSingularVectors; + + this.coefficients = V.mmul(qqs.transpose()).mmul(U.transpose()).mmul(y); + + col = 0; + + for (let i = 0; i <= coefficients; ++i) { + const limit = this.order - i; + for (let j = 0; j <= limit; ++j) { + this.coefficients.set( + col, + 0, + (this.coefficients.get(col, 0) * scaleX1 ** i * scaleX2 ** j) / + scaleY, + ); + col++; + } + } + } + + _predict(newInputs) { + const x1 = newInputs[0]; + const x2 = newInputs[1]; + + let y = 0; + let column = 0; + + for (let i = 0; i <= this.order; i++) { + for (let j = 0; j <= this.order - i; j++) { + y += x1 ** i * x2 ** j * this.coefficients.get(column, 0); + column++; + } + } + + return y; + } + + toJSON() { + return { + name: 'polyfit2D', + order: this.order, + coefficients: this.coefficients, + }; + } + + static load(json) { + if (json.name !== 'polyfit2D') { + throw new TypeError('not a polyfit2D model'); + } + return new PolynomialRegression2D(true, json); + } +} + +/** + * Function that given a column vector return this: vector^power + * + * @param x - Column vector. + * @param power - Pow number. + * @return {Suite|Matrix} + */ +function powColVector(x, power) { + const result = x.clone(); + for (let i = 0; i < x.rows; ++i) { + result.set(i, 0, result.get(i, 0) ** power); + } + return result; +} diff --git a/src/__tests__/PolynomialRegression2D.test.ts b/src/__tests__/PolynomialRegression2D.test.ts new file mode 100644 index 0000000..491641c --- /dev/null +++ b/src/__tests__/PolynomialRegression2D.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest'; + +import { PolynomialRegression2D } from '../PolynomialRegression2D'; + +describe('2D polinomial fit', () => { + const X = new Array(21); + const y = new Array(21); + for (let i = 0; i < 21; ++i) { + X[i] = [i, i + 10]; + y[i] = i + 20; + } + + const pf = new PolynomialRegression2D(X, y, { + order: 2, + }); + + it('Training coefficients', () => { + const estimatedCoefficients = [ + 1.5587e1, 3.8873e-1, 5.2582e-3, 4.8498e-1, 2.1127e-3, -7.3709e-3, + ]; + for (let i = 0; i < estimatedCoefficients.length; ++i) { + expect(pf.coefficients.get(i, 0)).toBeCloseTo( + estimatedCoefficients[i], + 1e-2, + ); + } + }); + + it('Prediction', () => { + const test = new Array(11); + let val = 0.5; + for (let i = 0; i < 11; ++i) { + test[i] = [val, val + 10]; + val++; + } + + const y = pf.predict(test); + + let j = 0; + for (let i = 20.5; i < 30.5; i++, j++) { + expect(y[j]).toBeCloseTo(i, 1e-2); + } + }); + + it('Other function test', () => { + const testValues = [ + 15.041667, 9.375, 5.041667, 2.041667, 0.375, 0.041667, 1.041667, 3.375, + 7.041667, 12.041667, + ]; + + const len = 21; + + const X = new Array(len); + let val = 5.0; + const y = new Array(len); + for (let i = 0; i < len; ++i, val += 0.5) { + X[i] = [val, val]; + y[i] = val * val + val * val; + } + + const polynomialRegression2D = new PolynomialRegression2D(X, y, { + order: 2, + }); + + const test = 10; + let x1 = -4.75; + let x2 = 4.75; + const X1 = new Array(test); + for (let i = 0; i < test; ++i) { + X1[i] = [x1, x2]; + x1++; + x2--; + } + + const predict = polynomialRegression2D.predict(X1); + for (let i = 0; i < testValues.length; ++i) { + expect(predict[i]).toBeCloseTo(testValues[i], 1e-2); + } + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d67a5b8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from './PolynomialRegression2D'; diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..3442d16 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "declarationMap": true + }, + "exclude": [ + "./src/**/__tests__" + ] +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..050b45d --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.cjs.json", + "compilerOptions": { + "module": "es2020", + "outDir": "lib-esm" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..66e32a3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "allowJs": true, + "esModuleInterop": true, + "moduleResolution": "node", + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2020" + }, + "include": [ + "./src/**/*" + ] +}