Skip to content
Snippets Groups Projects
Commit c0c65285 authored by Steve Reis's avatar Steve Reis
Browse files

feat(exareme2): Add linear regression handler

parent d952d8fc
No related branches found
No related tags found
No related merge requests found
Showing with 276 additions and 9 deletions
......@@ -10,7 +10,6 @@ import { Response, Request } from 'express';
import { CurrentUser } from '../common/decorators/user.decorator';
import { GQLRequest } from '../common/decorators/gql-request.decoractor';
import { GQLResponse } from '../common/decorators/gql-response.decoractor';
import { parseToBoolean } from '../common/utilities';
import {
ENGINE_MODULE_OPTIONS,
ENGINE_SERVICE,
......@@ -23,6 +22,7 @@ import { GlobalAuthGuard } from './guards/global-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { AuthenticationInput } from './inputs/authentication.input';
import { AuthenticationOutput } from './outputs/authentication.output';
import { parseToBoolean } from '../common/utils/shared.utils';
//Custom defined type because Pick<CookieOptions, 'sameSite'> does not work
type SameSiteType = boolean | 'lax' | 'strict' | 'none' | undefined;
......
......@@ -6,8 +6,7 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import axios from 'axios';
import { response } from 'express';
import { errorAxiosHandler, parseToBoolean } from './utilities';
import { errorAxiosHandler, parseToBoolean } from './shared.utils';
describe('Utility parseToBoolean testing', () => {
it('Parse true string to boolean', () => {
......
/* eslint-disable @typescript-eslint/ban-types */
/* eslint-disable @typescript-eslint/no-use-before-define */
import {
BadRequestException,
HttpException,
InternalServerErrorException,
NotFoundException,
......@@ -15,13 +19,39 @@ export const errorAxiosHandler = (e: any) => {
if (e.response.status === 404) throw new NotFoundException();
if (e.response.status === 408) throw new RequestTimeoutException();
if (e.response.status === 500) throw new InternalServerErrorException();
if (e.response.status && e.response.status)
if (e.response.status)
throw new HttpException(e.response.data, e.response.status);
}
throw new InternalServerErrorException('Unknown error');
};
/**
* It rounds a number to a given number of decimal places
* @param {number} val - the number to be rounded
* @param [decimal=2] - The number of decimal places to round to.
* @param [keepSmallNumber=true] - If true, it will keep the number in exponential form if it's smaller
* than 1/coef.
* @returns Formatted string number
*/
export const floatRound = (
val: number,
decimal = 2,
keepSmallNumber = true,
) => {
const n = Math.trunc(decimal);
if (n < 0) throw new Error('decimal cannot be negative number');
const coef = Math.pow(10, n);
if (keepSmallNumber && val !== 0 && val < 1 / coef) {
return val.toExponential(n);
}
return (Math.round(val * coef) / coef).toString();
};
/**
* Parse a string to a boolean
* @param {string} value - The value to parse.
......@@ -39,3 +69,37 @@ export const parseToBoolean = (
return defaultValue;
}
};
export const isUndefined = (obj: any): obj is undefined =>
typeof obj === 'undefined';
export const isObject = (fn: any): fn is object =>
!isNil(fn) && typeof fn === 'object';
export const isPlainObject = (fn: any): fn is object => {
if (!isObject(fn)) {
return false;
}
const proto = Object.getPrototypeOf(fn);
if (proto === null) {
return true;
}
const ctor =
Object.prototype.hasOwnProperty.call(proto, 'constructor') &&
proto.constructor;
return (
typeof ctor === 'function' &&
ctor instanceof ctor &&
Function.prototype.toString.call(ctor) ===
Function.prototype.toString.call(Object)
);
};
export const isFunction = (val: any): boolean => typeof val === 'function';
export const isString = (val: any): val is string => typeof val === 'string';
export const isNumber = (val: any): val is number => typeof val === 'number';
export const isConstructor = (val: any): boolean => val === 'constructor';
export const isNil = (val: any): val is null | undefined =>
isUndefined(val) || val === null;
export const isEmpty = (array: any): boolean => !(array && array.length > 0);
export const isSymbol = (val: any): val is symbol => typeof val === 'symbol';
import { registerAs } from '@nestjs/config';
import { parseToBoolean } from 'src/common/utilities';
import { parseToBoolean } from 'src/common/utils/shared.utils';
export default registerAs('matomo', () => {
return {
......
......@@ -11,7 +11,7 @@ import {
ExperimentResult,
MIME_TYPES,
} from 'src/common/interfaces/utilities.interface';
import { errorAxiosHandler } from 'src/common/utilities';
import { errorAxiosHandler } from 'src/common/utils/shared.utils';
import { ENGINE_MODULE_OPTIONS } from 'src/engine/engine.constants';
import {
IConfiguration,
......
......@@ -109,6 +109,6 @@ export default class AnovaOneWayHandler extends BaseHandler {
const meanPlot = this.getMeanPlot(data);
if (meanPlot && meanPlot.pointCIs) exp.results.push(meanPlot);
super.handle(exp, data); // continue request
return super.handle(exp, data); // continue request
}
}
import { Experiment } from '../../../../models/experiment/experiment.model';
import LinearRegressionHandler from './linear-regression.handler';
const data = {
dependent_var: 'lefthippocampus',
n_obs: 15431,
df_resid: 1540.0,
df_model: 2.0,
rse: 0.1270107560405171,
r_squared: 0.8772983534917347,
r_squared_adjusted: 0.8771390007040616,
f_stat: 5505.38441342865,
f_pvalue: 0.0,
indep_vars: ['Intercept', 'righthippocampus', 'leftamygdala'],
coefficients: [0.2185676251985193, 0.611894589820809, 1.0305204881766319],
std_err: [0.029052606790014847, 0.016978263425746872, 0.05180007458246667],
t_stats: [7.523167431352125, 36.03988078621131, 19.894189274496593],
pvalues: [
9.04278019740564e-14, 8.833386164556705e-207, 1.4580450464941301e-78,
],
lower_ci: [0.16158077395909892, 0.5785916308422961, 0.9289143512210847],
upper_ci: [0.2755544764379397, 0.6451975487993219, 1.132126625132179],
};
const createExperiment = (): Experiment => ({
id: 'dummy-id',
name: 'Testing purpose',
algorithm: {
name: 'LINEAR_REGRESSION',
},
datasets: ['desd-synthdata'],
domain: 'dementia',
variables: ['righthippocampus'],
coVariables: ['leftamygdala'],
results: [],
});
describe('Linear regression result handler', () => {
let linearHandler: LinearRegressionHandler;
let experiment: Experiment;
beforeEach(() => {
linearHandler = new LinearRegressionHandler();
experiment = createExperiment();
});
describe('Handle', () => {
it('with standard linear algo data', () => {
linearHandler.handle(experiment, data);
expect(experiment.results.length === 2);
});
it('Should be empty with another algo', () => {
experiment.algorithm.name = 'dummy_algo';
linearHandler.handle(experiment, data);
expect(experiment.results.length === 0);
});
});
});
import { isNumber } from '../../../../../common/utils/shared.utils';
import { Experiment } from '../../../../models/experiment/experiment.model';
import {
TableResult,
TableStyle,
} from '../../../../models/result/table-result.model';
import BaseHandler from '../base.handler';
const NUMBER_PRECISION = 4;
const ALGO_NANE = 'linear_regression';
const lookupDict = {
dependent_var: 'Dependent variable',
n_obs: 'Number of observations',
df_resid: 'Residual degrees of freedom',
df_model: 'Model degrees of freedom',
rse: 'Residual standard error',
r_squared: 'R-squared',
r_squared_adjusted: 'Adjusted R-squared',
f_stat: 'F statistic',
f_pvalue: 'P{>F}',
indep_vars: 'Independent variables',
coefficients: 'Coefficients',
std_err: 'Std.Err.',
t_stats: 't-statistics',
pvalues: 'P{>|t|}',
lower_ci: 'Lower 95% c.i.',
upper_ci: 'Upper 95% c.i.',
};
export default class LinearRegressionHandler extends BaseHandler {
private getModel(data: any): TableResult | undefined {
const excepts = ['n_obs'];
const tableModel: TableResult = {
name: 'Model',
tableStyle: TableStyle.NORMAL,
headers: ['name', 'value'].map((name) => ({ name, type: 'string' })),
data: [
'dependent_var',
'n_obs',
'df_resid',
'df_model',
'rse',
'r_squared',
'r_squared_adjusted',
'f_stat',
'f_pvalue',
].map((name) => [
lookupDict[name],
isNumber(data[name]) && !excepts.includes(name)
? data[name].toPrecision(NUMBER_PRECISION)
: data[name],
]),
};
return tableModel;
}
private getCoefficients(data: any): TableResult | undefined {
const keys = [
'indep_vars',
'coefficients',
'std_err',
't_stats',
'pvalues',
'lower_ci',
'upper_ci',
];
const tabKeys = keys.slice(1);
const tableCoef: TableResult = {
name: 'Coefficients',
tableStyle: TableStyle.NORMAL,
headers: keys.map((name) => ({
name: lookupDict[name],
type: 'string',
})),
data: data.indep_vars.map((variable, i) => {
const row = tabKeys
.map((key) => data[key][i])
.map((val) =>
isNumber(val) ? val.toPrecision(NUMBER_PRECISION) : val,
);
row.unshift(variable);
return row;
}),
};
return tableCoef;
}
handle(experiment: Experiment, data: any): void {
if (experiment.algorithm.name.toLowerCase() !== ALGO_NANE)
return super.handle(experiment, data);
const model = this.getModel(data);
if (model) experiment.results.push(model);
const coefs = this.getCoefficients(data);
if (coefs) experiment.results.push(coefs);
}
}
......@@ -12,7 +12,7 @@ export default abstract class BaseHandler implements ResultHandler {
return h;
}
handle(partialExperiment: Experiment, data: unknown): void {
this.next?.handle(partialExperiment, data);
handle(experiment: Experiment, data: unknown): void {
this.next?.handle(experiment, data);
}
}
......@@ -3,6 +3,7 @@ import AnovaOneWayHandler from './algorithms/anova-one-way.handler';
import AreaHandler from './algorithms/area.handler';
import DescriptiveHandler from './algorithms/descriptive.handler';
import HeatMapHandler from './algorithms/heat-map.handler';
import LinearRegressionHandler from './algorithms/linear-regression.handler';
import PCAHandler from './algorithms/PCA.handler';
import PearsonHandler from './algorithms/pearson.handler';
import RawHandler from './algorithms/raw.handler';
......@@ -15,6 +16,7 @@ start
.setNext(new HeatMapHandler())
.setNext(new AnovaOneWayHandler())
.setNext(new PCAHandler())
.setNext(new LinearRegressionHandler())
.setNext(new RawHandler()); // should be last handler as it works as a fallback (if other handlers could not process the results)
export default (exp: Experiment, data: unknown): Experiment => {
......
......@@ -4,10 +4,10 @@ import { Args, Query, Resolver } from '@nestjs/graphql';
import { Request } from 'express';
import { Public } from 'src/auth/decorators/public.decorator';
import { GlobalAuthGuard } from 'src/auth/guards/global-auth.guard';
import { parseToBoolean } from 'src/common/utils/shared.utils';
import { Md5 } from 'ts-md5';
import { authConstants } from '../auth/auth-constants';
import { GQLRequest } from '../common/decorators/gql-request.decoractor';
import { parseToBoolean } from '../common/utilities';
import {
ENGINE_MODULE_OPTIONS,
ENGINE_ONTOLOGY_URL,
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment