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

Merge branch 'fix/exareme2-integrations' into 'develop'

Exareme2 new algorithms integration

See merge request sibmip/gateway!80
parents 668bde53 19fe3e2c
No related branches found
No related tags found
No related merge requests found
Showing
with 282 additions and 105 deletions
api/assets/engines/default/favicon.ico

1.12 KiB | W: 16px | H: 16px

api/assets/engines/default/favicon.ico

4.19 KiB | W: 32px | H: 32px

api/assets/engines/default/favicon.ico
api/assets/engines/default/favicon.ico
api/assets/engines/default/favicon.ico
api/assets/engines/default/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
api/assets/engines/default/favicon.png

2.26 KiB | W: | H:

api/assets/engines/default/favicon.png

2.01 KiB | W: | H:

api/assets/engines/default/favicon.png
api/assets/engines/default/favicon.png
api/assets/engines/default/favicon.png
api/assets/engines/default/favicon.png
  • 2-up
  • Swipe
  • Onion skin
api/assets/engines/default/logo.png

29.5 KiB | W: | H:

api/assets/engines/default/logo.png

8.19 KiB | W: | H:

api/assets/engines/default/logo.png
api/assets/engines/default/logo.png
api/assets/engines/default/logo.png
api/assets/engines/default/logo.png
  • 2-up
  • Swipe
  • Onion skin
api/assets/engines/default/logo_small.png

3.22 KiB | W: | H:

api/assets/engines/default/logo_small.png

2.84 KiB | W: | H:

api/assets/engines/default/logo_small.png
api/assets/engines/default/logo_small.png
api/assets/engines/default/logo_small.png
api/assets/engines/default/logo_small.png
  • 2-up
  • Swipe
  • Onion skin
......@@ -111,6 +111,8 @@ export const isPlainObject = (fn: any): fn is 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 formatNumber = (val: any, precision = 4): number | string =>
isNumber(val) ? val.toPrecision(precision) : val;
export const isConstructor = (val: any): boolean => val === 'constructor';
export const isNil = (val: any): val is null | undefined =>
isUndefined(val) || val === null;
......
......@@ -2,6 +2,7 @@ import { Experiment } from '../../../../models/experiment/experiment.model';
import { HeatMapResult } from '../../../../models/result/heat-map-result.model';
import handlers from '..';
import { BarChartResult } from '../../../../models/result/bar-chart-result.model';
import { Domain } from '../../../../models/domain.model';
const createExperiment = (): Experiment => ({
id: 'dummy-id',
......@@ -24,6 +25,16 @@ const createExperiment = (): Experiment => ({
results: [],
});
const domain: Domain = {
id: 'dummy-id',
groups: [],
rootGroup: {
id: 'dummy-id',
},
datasets: [{ id: 'desd-synthdata', label: 'Dead Synthdata' }],
variables: [],
};
const data = [
{
n_obs: 920,
......@@ -80,7 +91,7 @@ const data = [
describe('PCA result handler', () => {
it('Test PCA handler with regular data (no edge cases)', () => {
const exp = createExperiment();
handlers(exp, data, null);
handlers(exp, data, domain);
expect(exp.results.length).toBeGreaterThanOrEqual(2);
exp.results.forEach((result) => {
......
import { Domain } from 'src/engine/models/domain.model';
import { Variable } from 'src/engine/models/variable.model';
import { Domain } from '../../../../models/domain.model';
import { Experiment } from '../../../../models/experiment/experiment.model';
import { BarChartResult } from '../../../../models/result/bar-chart-result.model';
import {
......@@ -18,18 +19,13 @@ export default class PCAHandler extends BaseHandler {
);
}
handle(exp: Experiment, data: any, domain?: Domain): void {
if (!this.canHandle(exp.algorithm.name, data))
return this.next?.handle(exp, data, domain);
const extractedData = data[0];
const barChar: BarChartResult = {
private getBarChartResult(data: any): BarChartResult {
const barChart: BarChartResult = {
name: 'Eigen values',
barValues: extractedData['eigenvalues'],
barValues: data['eigenvalues'],
xAxis: {
label: 'Dimensions',
categories: exp.variables.map((_, i) => i + 1).map(String),
categories: data['eigenvalues'].map((_: unknown, i: number) => i + 1),
},
hasConnectedBars: true,
yAxis: {
......@@ -37,17 +33,18 @@ export default class PCAHandler extends BaseHandler {
},
};
if (barChar.barValues && barChar.barValues.length > 0)
exp.results.push(barChar);
return barChart;
}
const matrix = extractedData['eigenvectors'] as number[][];
private getHeatMapResult(data: any, variables: Variable[]): HeatMapResult {
const matrix = data['eigenvectors'] as number[][];
const heatMapChart: HeatMapResult = {
name: 'Eigen vectors',
matrix,
heatMapStyle: HeatMapStyle.BUBBLE,
yAxis: {
categories: exp.variables,
categories: variables.map((v) => v.label ?? v.id),
},
xAxis: {
categories: [...Array(matrix.length).keys()]
......@@ -62,8 +59,27 @@ export default class PCAHandler extends BaseHandler {
);
}
return heatMapChart;
}
handle(exp: Experiment, data: any, domain: Domain): void {
if (!this.canHandle(exp.algorithm.name, data))
return this.next?.handle(exp, data, domain);
const extractedData = data[0];
const variables =
exp.variables
?.map((v) => domain.variables.find((v2) => v2.id === v) ?? { id: v })
.filter((v) => v) ?? [];
const barChart = this.getBarChartResult(extractedData);
if (barChart.barValues && barChart.barValues.length > 0)
exp.results.push(barChart);
const heatMapChart = this.getHeatMapResult(extractedData, variables);
if (heatMapChart.matrix) exp.results.push(heatMapChart);
this.next?.handle(exp, data);
this.next?.handle(exp, data, domain);
}
}
import { Domain } from '../../../../models/domain.model';
import handlers from '..';
import { Experiment } from '../../../../models/experiment/experiment.model';
import AnovaOneWayHandler from './anova-one-way.handler';
......@@ -15,11 +16,24 @@ const createExperiment = (): Experiment => ({
results: [],
});
const domain: Domain = {
id: 'dummy-id',
groups: [],
rootGroup: {
id: 'dummy-id',
},
datasets: [{ id: 'desd-synthdata', label: 'Dead Synthdata' }],
variables: [
{ id: 'rightcerebralwhitematter', label: 'Example label' },
{ id: 'ppmicategory', label: 'Example label 2' },
],
};
const data = [
{
anova_table: {
x_label: 'Variable X',
y_label: 'Variable Y',
x_label: 'Example label 2',
y_label: 'Example label',
df_residual: 1424.0,
df_explained: 3.0,
ss_residual: 1941.1517872154072,
......@@ -130,16 +144,16 @@ describe('Anova oneway result handler', () => {
it('Test anova 1 way handler', () => {
const exp = createExperiment();
const table1 = anovaHandler.getSummaryTable(data[0], exp.coVariables[0]);
const table1 = anovaHandler.getSummaryTable(
data[0],
data[0].anova_table.x_label,
);
const table2 = anovaHandler.getTuckeyTable(data[0]);
const meanPlot = anovaHandler.getMeanPlot(data[0]);
handlers(exp, data, null);
handlers(exp, data, domain);
expect(exp.results.length).toBeGreaterThanOrEqual(3);
expect(exp.results).toContainEqual(table1);
expect(exp.results).toContainEqual(table2);
expect(exp.results).toContainEqual(meanPlot);
expect(table1.data[0].length).toEqual(6);
expect(table2.headers.length).toEqual(8);
......
import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
import { Domain } from 'src/engine/models/domain.model';
import { MeanChartResult } from 'src/engine/models/result/means-chart-result.model';
import { formatNumber } from '../../../../../common/utils/shared.utils';
import { Domain } from '../../../../models/domain.model';
import { MeanChartResult } from '../../../../models/result/means-chart-result.model';
import { Experiment } from '../../../../models/experiment/experiment.model';
import {
TableResult,
TableStyle,
} from '../../../../models/result/table-result.model';
import BaseHandler from '../base.handler';
export default class AnovaOneWayHandler extends BaseHandler {
public static readonly ALGO_NAME = 'anova_oneway';
private static readonly tuckeyTransform = jsonata(`
{
(
$format:= "#0.0000";
{
"name": 'Tuckey Honest Significant Differences',
"headers": [
{"name": 'A', "type": 'string'},
......@@ -22,18 +26,27 @@ export default class AnovaOneWayHandler extends BaseHandler {
{"name": 'T value', "type": 'string'},
{"name": 'P value', "type": 'string'}
],
"data": tuckey_test.[$.groupA, $.groupB, $.meanA, $.meanB, $.diff, $.se, $.t_stat, $.p_tuckey][]
}
"data": tuckey_test.[$lookup($$.categories, $.groupA),
$lookup($$.categories, $.groupB),
$formatNumber($.meanA, $format),
$formatNumber($.meanB, $format),
$formatNumber($.diff, $format),
$formatNumber($.se, $format),
$formatNumber($.t_stat, $format),
$formatNumber($.p_tuckey, $format)
][]
})
`);
private static readonly meanPlotTransform = jsonata(`
(
$cats:= $keys(ci_info.means);
{
"name": "Mean Plot: " & anova_table.y_label & ' ~ ' & anova_table.x_label,
"xAxis": {
"label": anova_table.x_label,
"categories": $cats
"categories": $cats.($lookup($$.categories, $))
},
"yAxis": {
"label": '95% CI: ' & anova_table.y_label
......@@ -46,8 +59,13 @@ export default class AnovaOneWayHandler extends BaseHandler {
})
`);
canHandle(algorithm: string): boolean {
return algorithm.toLocaleLowerCase() === AnovaOneWayHandler.ALGO_NAME;
canHandle(algorithm: string, data: any): boolean {
return (
data &&
data.length !== 0 &&
data[0]['anova_table'] &&
algorithm.toLocaleLowerCase() === AnovaOneWayHandler.ALGO_NAME
);
}
getTuckeyTable(data: unknown): TableResult | undefined {
......@@ -75,18 +93,18 @@ export default class AnovaOneWayHandler extends BaseHandler {
[
varname,
data['anova_table']['df_explained'],
data['anova_table']['ss_explained'],
data['anova_table']['ms_explained'],
data['anova_table']['p_value'],
data['anova_table']['f_stat'],
formatNumber(data['anova_table']['ss_explained']),
formatNumber(data['anova_table']['ms_explained']),
formatNumber(data['anova_table']['f_stat']),
formatNumber(data['anova_table']['p_value']),
],
[
'Residual',
data['anova_table']['df_residual'],
data['anova_table']['ss_residual'],
data['anova_table']['ms_residual'],
'',
'',
formatNumber(data['anova_table']['ss_residual']),
formatNumber(data['anova_table']['ms_residual']),
'N/A',
'N/A',
],
],
};
......@@ -99,14 +117,38 @@ export default class AnovaOneWayHandler extends BaseHandler {
}
handle(exp: Experiment, data: any, domain: Domain): void {
if (!data || data.length === 0) return super.handle(exp, data, domain);
if (!this.canHandle(exp.algorithm.name))
if (!this.canHandle(exp.algorithm.name, data))
return super.handle(exp, data, domain);
const result = data[0];
const summaryTable = this.getSummaryTable(result, exp.coVariables[0]);
const varIds = [...exp.variables, ...(exp.coVariables ?? [])];
const variables = domain.variables.filter((v) => varIds.includes(v.id));
const [variable, coVariate] = variables;
if (variable) result.anova_table.y_label = variable.label ?? variable.id;
if (coVariate) result.anova_table.x_label = coVariate.label ?? coVariate.id;
if (coVariate && coVariate.enumerations) {
result.categories = coVariate.enumerations.reduce((p, e) => {
p[e.value] = e.label ?? e.value;
return p;
}, {});
} else {
result.categories = result['min_max_per_group']['categories'].reduce(
(p: { [x: string]: string }, e: string) => {
p[e] = e;
return p;
},
{},
);
}
const summaryTable = this.getSummaryTable(
result,
result.anova_table.x_label,
);
if (summaryTable) exp.results.push(summaryTable);
const tuckeyTable = this.getTuckeyTable(result);
......
import { HeatMapResult } from 'src/engine/models/result/heat-map-result.model';
import { Domain } from '../../../../models/domain.model';
import handlers from '..';
import { Experiment } from '../../../../models/experiment/experiment.model';
import PearsonHandler from './pearson.handler';
......@@ -42,8 +42,21 @@ const createExperiment = (): Experiment => ({
results: [],
});
describe('Pearson result handler', () => {
const data = {
const domain: Domain = {
id: 'dummy-id',
groups: [],
rootGroup: {
id: 'dummy-id',
},
datasets: [{ id: 'desd-synthdata', label: 'Dead Synthdata' }],
variables: [
{ id: 'rightcerebralwhitematter', label: 'Example label' },
{ id: 'ppmicategory', label: 'Example label 2' },
],
};
const data = [
{
n_obs: 1840,
correlations: {
variables: [
......@@ -413,24 +426,16 @@ describe('Pearson result handler', () => {
0.5096526209667468, 0.5533006654400224,
],
},
};
},
];
describe('Pearson result handler', () => {
it('Test pearson handler with regular data', () => {
const exp = createExperiment();
const names = [
'correlations',
'p_values',
'low_confidence_intervals',
'high_confidence_intervals',
];
handlers(exp, data, null);
const results = exp.results as HeatMapResult[];
const heatmaps = names.map((name) =>
results.find((it) => it.name === name),
);
handlers(exp, data, domain);
const results = exp.results;
expect(heatmaps.length).toBeGreaterThanOrEqual(4);
expect(results.length).toBe(2);
});
});
import * as jsonata from 'jsonata'; // old import style needed due to 'export = jsonata'
import { Expression } from 'jsonata';
import { Domain } from 'src/engine/models/domain.model';
import { formatNumber } from '../../../../../common/utils/shared.utils';
import { Domain } from '../../../../models/domain.model';
import {
TableResult,
TableStyle,
} from '../../../../models/result/table-result.model';
import { Experiment } from '../../../../models/experiment/experiment.model';
import { HeatMapResult } from '../../../../models/result/heat-map-result.model';
import BaseHandler from '../base.handler';
type Lookup = {
[key: string]: string;
};
export default class PearsonHandler extends BaseHandler {
public static readonly ALGO_NAME = 'pearson_correlation';
private static readonly transform: Expression = jsonata(`
(
$params := ['correlations', 'p_values', 'ci_lo', 'ci_hi'];
$params := ['correlations'];
$dictName := {
"correlations": "Correlations",
"p_values": "P values",
......@@ -22,16 +30,58 @@ export default class PearsonHandler extends BaseHandler {
{
'name': $lookup($dictName, $k),
'xAxis': {
'categories': $v.variables
'categories': $v.variables.($lookup($$.lookupVars, $))
},
'yAxis': {
'categories': $reverse($v.variables)
'categories': $reverse($keys($v.$sift(function($val, $key) {$key ~> /^(?!variables$)/}))).($lookup($$.lookupVars, $))
},
'matrix': $v.$sift(function($val, $key) {$key ~> /^(?!variables$)/}).$each(function($val, $key) {$val})[]
}
})
})[]
)`);
private getTableResult(data: any, lookup: Lookup): TableResult {
const elements = [...data['correlations']['variables']];
const keys = [
...Object.keys(data['correlations']).filter((k) => k !== 'variables'),
];
const tableData = [];
const doneMap = {};
while (keys.length > 0) {
const key = keys.shift();
elements.forEach((elem, i) => {
const token = [key, elem].sort().join();
if (elem === key || doneMap[token]) return;
doneMap[token] = true;
tableData.push([
lookup[key] ?? key,
lookup[elem] ?? elem,
formatNumber(data['correlations'][key][i]),
formatNumber(data['p_values'][key][i]),
formatNumber(data['ci_lo'][key][i]),
formatNumber(data['ci_hi'][key][i]),
]);
});
}
const tableResult: TableResult = {
name: 'Pearson summary',
tableStyle: TableStyle.DEFAULT,
headers: [
{ name: 'Variable 1', type: 'string' },
{ name: 'Variable 2', type: 'string' },
{ name: 'Correlation', type: 'string' },
{ name: 'P value', type: 'string' },
{ name: 'Low CI', type: 'string' },
{ name: 'High CI', type: 'string' },
],
data: tableData,
};
return tableResult;
}
/**
* This function returns true if the algorithm is Pearson.
* @param {string} algorithm - The name of the algorithm to use.
......@@ -47,12 +97,28 @@ export default class PearsonHandler extends BaseHandler {
);
}
handle(exp: Experiment, data: any, domain?: Domain): void {
handle(exp: Experiment, data: any, domain: Domain): void {
if (!this.canHandle(exp.algorithm.name, data))
return super.handle(exp, data, domain);
const extData = data[0];
const varIds = [...exp.variables, ...(exp.coVariables ?? [])];
const lookup: Lookup = varIds.reduce((acc, curr) => {
acc[curr] = curr;
return acc;
}, {}); // fallback to original ids if domain is empty
domain.variables
.filter((v) => varIds.includes(v.id))
.forEach((v) => {
lookup[v.id] = v.label ?? v.id;
});
extData.lookupVars = lookup;
const tableResult = this.getTableResult(extData, lookup);
if (tableResult.data.length > 0) exp.results.push(tableResult);
const results = PearsonHandler.transform.evaluate(
extData,
) as HeatMapResult[];
......@@ -60,6 +126,6 @@ export default class PearsonHandler extends BaseHandler {
.filter((heatMap) => heatMap.matrix.length > 0 && heatMap.name)
.forEach((heatMap) => exp.results.push(heatMap));
this.next?.handle(exp, data);
this.next?.handle(exp, data, domain);
}
}
......@@ -2,17 +2,19 @@ import { TableResult } from '../../../../models/result/table-result.model';
import { Experiment } from '../../../../models/experiment/experiment.model';
import TtestOnesampleHandler from './ttest-onesample.handler';
const data = {
n_obs: 1991,
t_value: 304.98272738655413,
p_value: 0.0,
df: 1990.0,
mean_diff: 220.17867654445,
se_diff: 0.7464781919192859,
ci_upper: 221.64263732187715,
ci_lower: 218.71471576702288,
cohens_d: 6.835017232945105,
};
const data = [
{
n_obs: 1991,
t_value: 304.98272738655413,
p_value: 0.0,
df: 1990.0,
mean_diff: 220.17867654445,
se_diff: 0.7464781919192859,
ci_upper: 221.64263732187715,
ci_lower: 218.71471576702288,
cohens_d: 6.835017232945105,
},
];
const createExperiment = (): Experiment => ({
id: 'dummy-id',
......
......@@ -31,13 +31,18 @@ const isNumberPrecision = (value: any, name: string) => {
export default class TtestOnesampleHandler extends BaseHandler {
public static readonly ALGO_NAME = 'ttest_onesample';
private canHandle(algoId: string) {
return algoId.toLocaleLowerCase() === TtestOnesampleHandler.ALGO_NAME;
private canHandle(algoId: string, data: any) {
return (
data &&
data[0] &&
data[0]['t_value'] &&
algoId.toLowerCase() === TtestOnesampleHandler.ALGO_NAME
);
}
private getTable(data: any): TableResult {
const tableModel: TableResult = {
name: 'T-test',
name: 'Results',
tableStyle: TableStyle.NORMAL,
headers: ['name', 'value'].map((name) => ({ name, type: 'string' })),
data: [
......@@ -62,10 +67,12 @@ export default class TtestOnesampleHandler extends BaseHandler {
}
handle(experiment: Experiment, data: any, domain?: Domain): void {
if (!this.canHandle(experiment.algorithm.name))
if (!this.canHandle(experiment.algorithm.name, data))
return super.handle(experiment, data, domain);
const tableModel = this.getTable(data);
const extData = data[0];
const tableModel = this.getTable(extData);
if (tableModel) experiment.results.push(tableModel);
}
......
......@@ -2,22 +2,24 @@ import { TableResult } from '../../../../models/result/table-result.model';
import { Experiment } from '../../../../models/experiment/experiment.model';
import TTestPairedHandler from './ttest-paired.handler';
const data = {
t_stat: -97.35410837992711,
p: 1.0,
df: 144.0,
mean_diff: -66.00088551724139,
se_diff: 0.6779465871093092,
ci_upper: 'Infinity',
ci_lower: -67.12322892404309,
cohens_d: -11.456478738682357,
};
const data = [
{
t_stat: -97.35410837992711,
p: 1.0,
df: 144.0,
mean_diff: -66.00088551724139,
se_diff: 0.6779465871093092,
ci_upper: 'Infinity',
ci_lower: -67.12322892404309,
cohens_d: -11.456478738682357,
},
];
const createExperiment = (): Experiment => ({
id: 'dummy-id',
name: 'Testing purpose',
algorithm: {
name: 'TTEST_PAIRED',
name: TTestPairedHandler.ALGO_NAME.toUpperCase(),
},
datasets: ['desd-synthdata'],
domain: 'dementia',
......
......@@ -20,14 +20,21 @@ const lookupDict = {
const NUMBER_PRECISION = 4;
export default class TTestPairedHandler extends BaseHandler {
private canHandle(experimentId: string) {
return experimentId.toLocaleLowerCase() === 'ttest_paired';
public static readonly ALGO_NAME = 'paired_ttest';
private canHandle(algoName: string, data: any) {
return (
data &&
data[0] &&
data[0]['t_stat'] &&
algoName.toLowerCase() === TTestPairedHandler.ALGO_NAME
);
}
private getTable(data: any): TableResult {
const tableModel: TableResult = {
name: 'T-test',
tableStyle: TableStyle.NORMAL,
name: 'Results',
tableStyle: TableStyle.DEFAULT,
headers: ['name', 'value'].map((name) => ({ name, type: 'string' })),
data: [
't_stat',
......@@ -50,10 +57,12 @@ export default class TTestPairedHandler extends BaseHandler {
}
handle(experiment: Experiment, data: any, domain?: Domain): void {
if (!this.canHandle(experiment.algorithm.name))
if (!this.canHandle(experiment.algorithm.name, data))
return super.handle(experiment, data, domain);
const tableModel = this.getTable(data);
const extData = data[0];
const tableModel = this.getTable(extData);
if (tableModel) experiment.results.push(tableModel);
}
......
......@@ -8,10 +8,10 @@ import LogisticRegressionHandler from './algorithms/logistic-regression.handler'
import PCAHandler from './algorithms/PCA.handler';
import PearsonHandler from './algorithms/pearson.handler';
import RawHandler from './algorithms/raw.handler';
import TtestOnesampleHandler from './algorithms/ttest-onesample.handler';
import TTestPairedHandler from './algorithms/ttest-paired.handler';
import ResultHandler from './result-handler.interface';
const start = new PearsonHandler() as ResultHandler;
const start = new PearsonHandler();
start
.setNext(new DescriptiveHandler())
......@@ -21,6 +21,7 @@ start
.setNext(new LinearRegressionCVHandler())
.setNext(new LogisticRegressionHandler())
.setNext(new TTestPairedHandler())
.setNext(new TtestOnesampleHandler())
.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, domain: Domain): Experiment => {
......
import { Domain } from 'src/engine/models/domain.model';
import { Domain } from '../../../models/domain.model';
import { Experiment } from '../../../models/experiment/experiment.model';
// produce algo handler
export default interface ResultHandler {
setNext(h: ResultHandler): ResultHandler;
handle(partialExperiment: Experiment, data: unknown, domain?: Domain): void;
handle(partialExperiment: Experiment, data: unknown, domain: Domain): void;
}
......@@ -12,7 +12,7 @@ const transformToAlgorithms = jsonata(`
'LOGISTIC_REGRESSION','TTEST_INDEPENDENT','TTEST_PAIRED',
'PEARSON','ID3','KMEANS','NAIVE_BAYES',
'TTEST_ONESAMPLE','PCA','CALIBRATION_BELT','CART',
'KAPLAN_MEIER','THREE_C', 'ONE_WAY_ANOVA', 'PEARSON', 'LINEAR_REGRESSION_CV'];
'KAPLAN_MEIER','THREE_C', 'ONE_WAY_ANOVA', 'PEARSON_CORRELATION', 'LINEAR_REGRESSION_CV', 'TTEST_ONESAMPLE', 'PAIRED_TTEST'];
$linkedVars:= ['positive_class', 'positive_level', 'negative_level', 'outcome_neg', 'outcome_pos'];
$linkedCoVars:= ['referencevalues', 'xlevels'];
$truthy:= function($val) {(
......
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