mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 01:16:31 -06:00
Transformations: Add regression analysis transformation (#78457)
* regression analysis first dragt * Swap to better regression libraries * fix name * Interpolate x points instead of using source x points * clean up ui and add feature toggle * fix merge error * change to loop for finding min max, rename resolution * Add docs * add docs and tests * change name to regression analysis * update docs * Fix editor labels * add regression images * fix docs
This commit is contained in:
parent
7fa73d2b21
commit
ab982e7bd3
@ -1266,6 +1266,19 @@ For each generated **Trend** field value, a calculation function can be selected
|
||||
|
||||
> **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it.
|
||||
|
||||
### Regression analysis
|
||||
|
||||
Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline.
|
||||
|
||||
There are two different models:
|
||||
|
||||
- **Linear regression** - Fits a linear function to the data.
|
||||
{{< figure src="/static/img/docs/transformations/linear-regression.png" class="docs-image--no-shadow" max-width= "1100px" >}}
|
||||
- **Polynomial regression** - Fits a polynomial function to the data.
|
||||
{{< figure src="/static/img/docs/transformations/polynomial-regression.png" class="docs-image--no-shadow" max-width= "1100px" >}}
|
||||
|
||||
> **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `regressionTransformation` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||
|
||||
{{% docs/reference %}}
|
||||
[Table panel]: "/docs/grafana/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/table"
|
||||
[Table panel]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/table"
|
||||
|
@ -164,6 +164,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
||||
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected |
|
||||
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
|
||||
| `regressionTransformation` | Enables regression analysis transformation |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
@ -343,6 +343,8 @@
|
||||
"marked": "5.1.1",
|
||||
"marked-mangle": "1.1.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"ml-regression-polynomial": "^3.0.0",
|
||||
"ml-regression-simple-linear": "^3.0.0",
|
||||
"moment": "2.29.4",
|
||||
"moment-timezone": "0.5.43",
|
||||
"monaco-editor": "0.34.0",
|
||||
|
@ -39,4 +39,5 @@ export enum DataTransformerID {
|
||||
timeSeriesTable = 'timeSeriesTable',
|
||||
formatTime = 'formatTime',
|
||||
formatString = 'formatString',
|
||||
regression = 'regression',
|
||||
}
|
||||
|
@ -164,4 +164,5 @@ export interface FeatureToggles {
|
||||
alertingSimplifiedRouting?: boolean;
|
||||
logRowsPopoverMenu?: boolean;
|
||||
pluginsSkipHostEnvVars?: boolean;
|
||||
regressionTransformation?: boolean;
|
||||
}
|
||||
|
@ -1075,6 +1075,13 @@ var (
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
},
|
||||
{
|
||||
Name: "regressionTransformation",
|
||||
Description: "Enables regression analysis transformation",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaBiSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -145,3 +145,4 @@ datatrails,experimental,@grafana/dashboards-squad,false,false,false,true
|
||||
alertingSimplifiedRouting,experimental,@grafana/alerting-squad,false,false,false,false
|
||||
logRowsPopoverMenu,experimental,@grafana/observability-logs,false,false,false,true
|
||||
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false,false
|
||||
regressionTransformation,experimental,@grafana/grafana-bi-squad,false,false,false,true
|
||||
|
|
@ -590,4 +590,8 @@ const (
|
||||
// FlagPluginsSkipHostEnvVars
|
||||
// Disables passing host environment variable to plugin processes
|
||||
FlagPluginsSkipHostEnvVars = "pluginsSkipHostEnvVars"
|
||||
|
||||
// FlagRegressionTransformation
|
||||
// Enables regression analysis transformation
|
||||
FlagRegressionTransformation = "regressionTransformation"
|
||||
)
|
||||
|
@ -1377,6 +1377,27 @@ export const transformationDocsContent: TransformationDocsContentType = {
|
||||
},
|
||||
],
|
||||
},
|
||||
regression: {
|
||||
name: 'Regression analysis',
|
||||
getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) {
|
||||
return `
|
||||
Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline.
|
||||
|
||||
There are two different models:
|
||||
|
||||
- **Linear regression** - Fits a linear function to the data.
|
||||
${buildImageContent('/static/img/docs/transformations/linear-regression.png', imageRenderType, 'Linear regression')}
|
||||
- **Polynomial regression** - Fits a polynomial function to the data.
|
||||
${buildImageContent(
|
||||
'/static/img/docs/transformations/polynomial-regression.png',
|
||||
imageRenderType,
|
||||
'Polynomial regression'
|
||||
)}
|
||||
|
||||
> **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the \`regressionTransformation\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud.
|
||||
`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function getLinkToDocs(): string {
|
||||
|
142
public/app/features/transformers/regression/regression.test.ts
Normal file
142
public/app/features/transformers/regression/regression.test.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import {
|
||||
DataFrame,
|
||||
DataFrameDTO,
|
||||
DataTransformContext,
|
||||
Field,
|
||||
FieldType,
|
||||
toDataFrame,
|
||||
toDataFrameDTO,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression';
|
||||
|
||||
describe('Regression transformation', () => {
|
||||
it('it should predict a linear regression to exactly fit the data when the data is f(x) = x', () => {
|
||||
const source = [
|
||||
toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config: RegressionTransformerOptions = {
|
||||
modelType: ModelType.linear,
|
||||
predictionCount: 6,
|
||||
xFieldName: 'time',
|
||||
yFieldName: 'value',
|
||||
};
|
||||
|
||||
expect(toEquableDataFrames(RegressionTransformer.transformer(config, {} as DataTransformContext)(source))).toEqual(
|
||||
toEquableDataFrames([
|
||||
toEquableDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
],
|
||||
length: 6,
|
||||
}),
|
||||
toEquableDataFrame({
|
||||
name: 'linear regression',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
{ name: 'value predicted', type: FieldType.number, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
],
|
||||
length: 6,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
it('it should predict a linear regression where f(x) = 1', () => {
|
||||
const source = [
|
||||
toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config: RegressionTransformerOptions = {
|
||||
modelType: ModelType.linear,
|
||||
predictionCount: 6,
|
||||
xFieldName: 'time',
|
||||
yFieldName: 'value',
|
||||
};
|
||||
|
||||
expect(toEquableDataFrames(RegressionTransformer.transformer(config, {} as DataTransformContext)(source))).toEqual(
|
||||
toEquableDataFrames([
|
||||
toEquableDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0], config: {} },
|
||||
],
|
||||
length: 6,
|
||||
}),
|
||||
toEquableDataFrame({
|
||||
name: 'linear regression',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} },
|
||||
{ name: 'value predicted', type: FieldType.number, values: [1, 1, 1, 1, 1, 1], config: {} },
|
||||
],
|
||||
length: 6,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('it should predict a quadratic function', () => {
|
||||
const source = [
|
||||
toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0] },
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
const config: RegressionTransformerOptions = {
|
||||
modelType: ModelType.polynomial,
|
||||
degree: 2,
|
||||
predictionCount: 6,
|
||||
xFieldName: 'time',
|
||||
yFieldName: 'value',
|
||||
};
|
||||
|
||||
const result = RegressionTransformer.transformer(config, {} as DataTransformContext)(source);
|
||||
|
||||
expect(result[1].fields[1].values[0]).toBeCloseTo(-0.1, 1);
|
||||
expect(result[1].fields[1].values[1]).toBeCloseTo(1.2, 1);
|
||||
expect(result[1].fields[1].values[2]).toBeCloseTo(1.86, 1);
|
||||
expect(result[1].fields[1].values[3]).toBeCloseTo(1.86, 1);
|
||||
expect(result[1].fields[1].values[4]).toBeCloseTo(1.2, 1);
|
||||
expect(result[1].fields[1].values[5]).toBeCloseTo(-0.1, 1);
|
||||
});
|
||||
});
|
||||
|
||||
function toEquableDataFrame(source: DataFrame): DataFrame {
|
||||
return toDataFrame({
|
||||
...source,
|
||||
fields: source.fields.map((field: Field) => {
|
||||
return {
|
||||
...field,
|
||||
config: {},
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function toEquableDataFrames(data: DataFrame[]): DataFrameDTO[] {
|
||||
return data.map((frame) => toDataFrameDTO(frame));
|
||||
}
|
122
public/app/features/transformers/regression/regression.ts
Normal file
122
public/app/features/transformers/regression/regression.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { PolynomialRegression } from 'ml-regression-polynomial';
|
||||
import { SimpleLinearRegression } from 'ml-regression-simple-linear';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataTransformerID,
|
||||
FieldMatcherID,
|
||||
FieldType,
|
||||
SynchronousDataTransformerInfo,
|
||||
fieldMatchers,
|
||||
} from '@grafana/data';
|
||||
|
||||
export enum ModelType {
|
||||
linear = 'linear',
|
||||
polynomial = 'polynomial',
|
||||
}
|
||||
|
||||
export interface RegressionTransformerOptions {
|
||||
modelType?: ModelType;
|
||||
degree?: number;
|
||||
xFieldName?: string;
|
||||
yFieldName?: string;
|
||||
predictionCount?: number;
|
||||
}
|
||||
|
||||
export const DEFAULTS = { predictionCount: 100, modelType: ModelType.linear, degree: 2 };
|
||||
|
||||
export const RegressionTransformer: SynchronousDataTransformerInfo<RegressionTransformerOptions> = {
|
||||
id: DataTransformerID.regression,
|
||||
name: 'Regression analysis',
|
||||
operator: (options, ctx) => (source) =>
|
||||
source.pipe(map((data) => RegressionTransformer.transformer(options, ctx)(data))),
|
||||
transformer: (options, ctx) => {
|
||||
return (frames: DataFrame[]) => {
|
||||
const { predictionCount, modelType, degree } = { ...DEFAULTS, ...options };
|
||||
if (frames.length === 0) {
|
||||
return frames;
|
||||
}
|
||||
const matchesY = fieldMatchers.get(FieldMatcherID.byName).get(options.yFieldName);
|
||||
const matchesX = fieldMatchers.get(FieldMatcherID.byName).get(options.xFieldName);
|
||||
|
||||
let xField;
|
||||
let yField;
|
||||
for (const frame of frames) {
|
||||
const fy = frame.fields.find((f) => matchesY(f, frame, frames));
|
||||
if (fy) {
|
||||
yField = fy;
|
||||
const fx = frame.fields.find((f) => matchesX(f, frame, frames));
|
||||
if (fx) {
|
||||
xField = fx;
|
||||
break;
|
||||
} else {
|
||||
throw 'X and Y fields must be part of the same frame';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!xField || !yField) {
|
||||
return frames;
|
||||
}
|
||||
|
||||
let xMin = xField.values[0];
|
||||
let xMax = xField.values[0];
|
||||
|
||||
for (let i = 1; i < xField.values.length; i++) {
|
||||
if (xField.values[i] < xMin) {
|
||||
xMin = xField.values[i];
|
||||
}
|
||||
if (xField.values[i] > xMax) {
|
||||
xMax = xField.values[i];
|
||||
}
|
||||
}
|
||||
|
||||
const resolution = (xMax - xMin + 1) / predictionCount;
|
||||
|
||||
// These are the X values for which we should predict Y
|
||||
const predictionPoints = [...[...Array(predictionCount - 1).keys()].map((_, i) => i * resolution + xMin), xMax];
|
||||
|
||||
// If X is a time field we normalize the time to the start of the timeseries data
|
||||
const normalizationSubtrahend = xField.type === FieldType.time ? xMin : 0;
|
||||
|
||||
const yValues = [];
|
||||
const xValues = [];
|
||||
|
||||
for (let i = 0; i < xField.values.length; i++) {
|
||||
if (yField.values[i] !== null) {
|
||||
xValues.push(xField.values[i] - normalizationSubtrahend);
|
||||
yValues.push(yField.values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
let result: PolynomialRegression | SimpleLinearRegression;
|
||||
switch (modelType) {
|
||||
case ModelType.linear:
|
||||
result = new SimpleLinearRegression(xValues, yValues);
|
||||
break;
|
||||
case ModelType.polynomial:
|
||||
result = new PolynomialRegression(xValues, yValues, degree);
|
||||
break;
|
||||
default:
|
||||
return frames;
|
||||
}
|
||||
|
||||
const newFrame: DataFrame = {
|
||||
name: `${modelType} regression`,
|
||||
length: predictionPoints.length,
|
||||
fields: [
|
||||
{ name: xField.name, type: xField.type, values: predictionPoints, config: {} },
|
||||
{
|
||||
name: `${yField.name} predicted`,
|
||||
type: yField.type,
|
||||
values: predictionPoints.map((x) => result.predict(x - normalizationSubtrahend)),
|
||||
config: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return [...frames, newFrame];
|
||||
};
|
||||
},
|
||||
};
|
@ -0,0 +1,87 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { FieldType, toDataFrame } from '@grafana/data';
|
||||
|
||||
import { ModelType } from './regression';
|
||||
import { RegressionTransformerEditor } from './regressionEditor';
|
||||
|
||||
describe('FieldToConfigMappingEditor', () => {
|
||||
it('Should try to set the first time field as X and first number field as Y', async () => {
|
||||
const onChangeMock = jest.fn();
|
||||
|
||||
const df = toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'not this', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'not this either', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
|
||||
render(<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{}} />);
|
||||
|
||||
expect(onChangeMock).toBeCalledTimes(1);
|
||||
expect(onChangeMock).toBeCalledWith({ xFieldName: 'time', yFieldName: 'value' });
|
||||
});
|
||||
|
||||
it('Should set the first field as X and the second as Y if there are no time fields', async () => {
|
||||
const onChangeMock = jest.fn();
|
||||
|
||||
const df = toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'not this', type: FieldType.string, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'not this either', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
|
||||
render(<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{}} />);
|
||||
|
||||
expect(onChangeMock).toBeCalledTimes(1);
|
||||
expect(onChangeMock).toBeCalledWith({ xFieldName: 'foo', yFieldName: 'bar' });
|
||||
});
|
||||
|
||||
it('should display degree if the model is polynomial', async () => {
|
||||
const onChangeMock = jest.fn();
|
||||
|
||||
const df = toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{ modelType: ModelType.polynomial }} />
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Degree')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display degree if the model is linear', async () => {
|
||||
const onChangeMock = jest.fn();
|
||||
|
||||
const df = toDataFrame({
|
||||
name: 'data',
|
||||
refId: 'A',
|
||||
fields: [
|
||||
{ name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
{ name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] },
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{ modelType: ModelType.linear }} />
|
||||
);
|
||||
|
||||
expect(await screen.queryByText('Degree')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
142
public/app/features/transformers/regression/regressionEditor.tsx
Normal file
142
public/app/features/transformers/regression/regressionEditor.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
DataTransformerID,
|
||||
TransformerRegistryItem,
|
||||
TransformerUIProps,
|
||||
TransformerCategory,
|
||||
fieldMatchers,
|
||||
FieldMatcherID,
|
||||
Field,
|
||||
} from '@grafana/data';
|
||||
import { InlineField, Select } from '@grafana/ui';
|
||||
import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
|
||||
import { getTransformationContent } from '../docs/getTransformationContent';
|
||||
|
||||
import { DEFAULTS, ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression';
|
||||
|
||||
const fieldNamePickerSettings = {
|
||||
editor: FieldNamePicker,
|
||||
id: '',
|
||||
name: '',
|
||||
settings: { width: 24, isClearable: false },
|
||||
};
|
||||
|
||||
const LABEL_WIDTH = 20;
|
||||
|
||||
export const RegressionTransformerEditor = ({
|
||||
input,
|
||||
options,
|
||||
onChange,
|
||||
}: TransformerUIProps<RegressionTransformerOptions>) => {
|
||||
const modelTypeOptions = [
|
||||
{ label: 'Linear', value: ModelType.linear },
|
||||
{ label: 'Polynomial', value: ModelType.polynomial },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
let x: Field | undefined;
|
||||
let y: Field | undefined;
|
||||
if (!options.xFieldName) {
|
||||
const timeMatcher = fieldMatchers.get(FieldMatcherID.firstTimeField).get({});
|
||||
for (const frame of input) {
|
||||
x = frame.fields.find((field) => timeMatcher(field, frame, input));
|
||||
if (x) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!x) {
|
||||
const firstMatcher = fieldMatchers.get(FieldMatcherID.numeric).get({});
|
||||
for (const frame of input) {
|
||||
x = frame.fields.find((field) => firstMatcher(field, frame, input));
|
||||
if (x) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!options.yFieldName) {
|
||||
const numberMatcher = fieldMatchers.get(FieldMatcherID.numeric).get({});
|
||||
for (const frame of input) {
|
||||
y = frame.fields.find((field) => numberMatcher(field, frame, input) && field !== x);
|
||||
if (y) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (x && y) {
|
||||
onChange({ ...options, xFieldName: x.name, yFieldName: y.name });
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineField labelWidth={LABEL_WIDTH} label="X field">
|
||||
<FieldNamePicker
|
||||
context={{ data: input }}
|
||||
value={options.xFieldName ?? ''}
|
||||
item={fieldNamePickerSettings}
|
||||
onChange={(v) => {
|
||||
onChange({ ...options, xFieldName: v });
|
||||
}}
|
||||
></FieldNamePicker>
|
||||
</InlineField>
|
||||
<InlineField labelWidth={LABEL_WIDTH} label="Y field">
|
||||
<FieldNamePicker
|
||||
context={{ data: input }}
|
||||
value={options.yFieldName ?? ''}
|
||||
item={fieldNamePickerSettings}
|
||||
onChange={(v) => {
|
||||
onChange({ ...options, yFieldName: v });
|
||||
}}
|
||||
></FieldNamePicker>
|
||||
</InlineField>
|
||||
<InlineField labelWidth={LABEL_WIDTH} label="Model type">
|
||||
<Select
|
||||
value={options.modelType ?? DEFAULTS.modelType}
|
||||
onChange={(v) => {
|
||||
onChange({ ...options, modelType: v.value ?? DEFAULTS.modelType });
|
||||
}}
|
||||
options={modelTypeOptions}
|
||||
></Select>
|
||||
</InlineField>
|
||||
<InlineField labelWidth={LABEL_WIDTH} label="Predicted points" tooltip={'Number of X,Y points to predict'}>
|
||||
<NumberInput
|
||||
value={options.predictionCount ?? DEFAULTS.predictionCount}
|
||||
onChange={(v) => {
|
||||
onChange({ ...options, predictionCount: v });
|
||||
}}
|
||||
></NumberInput>
|
||||
</InlineField>
|
||||
{options.modelType === ModelType.polynomial && (
|
||||
<InlineField labelWidth={LABEL_WIDTH} label="Degree">
|
||||
<Select<number>
|
||||
value={options.degree ?? DEFAULTS.degree}
|
||||
options={[
|
||||
{ label: 'Quadratic', value: 2 },
|
||||
{ label: 'Cubic', value: 3 },
|
||||
{ label: 'Quartic', value: 4 },
|
||||
{ label: 'Quintic', value: 5 },
|
||||
]}
|
||||
onChange={(v) => {
|
||||
onChange({ ...options, degree: v.value });
|
||||
}}
|
||||
></Select>
|
||||
</InlineField>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const regressionTransformerRegistryItem: TransformerRegistryItem<RegressionTransformerOptions> = {
|
||||
id: DataTransformerID.regression,
|
||||
editor: RegressionTransformerEditor,
|
||||
transformation: RegressionTransformer,
|
||||
name: RegressionTransformer.name,
|
||||
description: RegressionTransformer.description,
|
||||
categories: new Set([TransformerCategory.CalculateNewFields]),
|
||||
help: getTransformationContent(DataTransformerID.regression).helperDocs,
|
||||
};
|
@ -28,6 +28,7 @@ import { joinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTr
|
||||
import { fieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor';
|
||||
import { partitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor';
|
||||
import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor';
|
||||
import { regressionTransformerRegistryItem } from './regression/regressionEditor';
|
||||
import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor';
|
||||
import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor';
|
||||
import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor';
|
||||
@ -62,6 +63,7 @@ export const getStandardTransformers = (): Array<TransformerRegistryItem<any>> =
|
||||
joinByLabelsTransformRegistryItem,
|
||||
partitionByValuesTransformRegistryItem,
|
||||
...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []),
|
||||
...(config.featureToggles.regressionTransformation ? [regressionTransformerRegistryItem] : []),
|
||||
formatTimeTransformerRegistryItem,
|
||||
timeSeriesTableTransformRegistryItem,
|
||||
];
|
||||
|
41
public/img/transformations/dark/regression.svg
Normal file
41
public/img/transformations/dark/regression.svg
Normal file
@ -0,0 +1,41 @@
|
||||
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1834_14939)">
|
||||
<path d="M0 0.611699V7.64012H14.394V0H0.601504C0.441975 0 0.28898 0.0644467 0.176176 0.179162C0.0633725 0.293878 0 0.449466 0 0.611699Z" fill="url(#paint0_linear_1834_14939)"/>
|
||||
<path d="M33.606 7.64012H48V0.611699C48 0.449466 47.9366 0.293878 47.8238 0.179162C47.711 0.0644467 47.558 0 47.3985 0L33.606 0V7.64012Z" fill="url(#paint1_linear_1834_14939)"/>
|
||||
<path d="M31.194 0H16.8V7.64012H31.194V0Z" fill="url(#paint2_linear_1834_14939)"/>
|
||||
<path d="M31.194 10.0869H16.8V17.727H31.194V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M14.394 10.0869H0V17.727H14.394V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M48 10.0869H33.606V17.727H48V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M48 20.1799H33.606V27.8262H48V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M31.194 20.1799H16.8V27.8262H31.194V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M14.394 20.1799H0V27.8262H14.394V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M31.194 30.2668H16.8V37.907H31.194V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M48 30.2668H33.606V37.907H48V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M14.394 30.2668H0V37.907H14.394V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M31.194 40.3599H16.8V48H31.194V40.3599Z" fill="#3865AB"/>
|
||||
<path d="M48 47.3883V40.3721H33.606V48H47.3985C47.558 48 47.711 47.9355 47.8238 47.8208C47.9366 47.7061 48 47.5505 48 47.3883Z" fill="#3865AB"/>
|
||||
<path d="M14.394 48V40.3721H0V47.3883C0 47.5505 0.0633725 47.7061 0.176176 47.8208C0.28898 47.9355 0.441975 48 0.601504 48H14.394Z" fill="#3865AB"/>
|
||||
</g>
|
||||
<path d="M61.9067 30.4001C62.6011 30.4001 67.9327 26.1334 67.9327 24.0001C67.9327 21.8668 62.7357 17.6001 61.9067 17.6001C61.0777 17.6001 60.4023 18.1334 60.4023 19.1741C60.4023 20.2147 63.9067 22.8487 63.9067 22.8487C63.9067 22.8487 56.2539 22.1334 56 22.8487C55.7461 23.564 55.7461 24.4362 56 25.1515C56.2539 25.8668 63.9067 25.1515 63.9067 25.1515C63.9067 25.1515 60.4023 28.0001 60.4023 28.8323C60.4023 29.6644 61.2123 30.4001 61.9067 30.4001Z" fill="#CCCCDC"/>
|
||||
<path d="M93.9457 25.1875C93.9457 26.9347 93.4911 28.3835 92.4542 29.6477H90.5366C91.3178 28.7102 91.9002 26.8637 91.9002 25.1875C91.9002 23.5114 91.3178 21.6648 90.5366 20.7273H92.4542C93.4911 21.9915 93.9457 23.4404 93.9457 25.1875Z" fill="white"/>
|
||||
<path d="M86.6153 22.5457L87.4107 24.236L88.2488 22.5457H90.1948L88.7317 25.2729L90.2658 28.0002H88.334L87.4107 26.2672L86.5158 28.0002H84.5556L86.1039 25.2729L84.655 22.5457H86.6153Z" fill="white"/>
|
||||
<path d="M80.8757 25.1875C80.8757 23.4404 81.3303 21.9915 82.3672 20.7273H84.2848C83.5036 21.6648 82.9212 23.5114 82.9212 25.1875C82.9212 26.8637 83.5036 28.7102 84.2848 29.6477H82.3672C81.3303 28.3835 80.8757 26.9347 80.8757 25.1875Z" fill="white"/>
|
||||
<path d="M79.7642 22.5456V23.966H78.7699V28.0001H76.8097V23.966H76.1136V22.5456H76.8097V22.3893C76.8097 21.0399 77.5767 20.3865 78.7699 20.3865C79.2671 20.3865 79.7926 20.4859 80.0199 20.5427L79.7358 21.949C79.5938 21.9064 79.3807 21.878 79.2386 21.878C78.8693 21.878 78.7699 22.0484 78.7699 22.3041V22.5456H79.7642Z" fill="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1834_14939" x1="0" y1="3.82312" x2="14.394" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1834_14939" x1="33.606" y1="3.82312" x2="48" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1834_14939" x1="16.8" y1="3.82312" x2="31.2" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1834_14939">
|
||||
<rect width="48" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
41
public/img/transformations/light/regression.svg
Normal file
41
public/img/transformations/light/regression.svg
Normal file
@ -0,0 +1,41 @@
|
||||
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1834_45844)">
|
||||
<path d="M0 0.611699V7.64012H14.394V0H0.601504C0.441975 0 0.28898 0.0644467 0.176176 0.179162C0.0633725 0.293878 0 0.449466 0 0.611699Z" fill="url(#paint0_linear_1834_45844)"/>
|
||||
<path d="M33.606 7.64012H48V0.611699C48 0.449466 47.9366 0.293878 47.8238 0.179162C47.711 0.0644467 47.558 0 47.3985 0L33.606 0V7.64012Z" fill="url(#paint1_linear_1834_45844)"/>
|
||||
<path d="M31.194 0H16.8V7.64012H31.194V0Z" fill="url(#paint2_linear_1834_45844)"/>
|
||||
<path d="M31.194 10.0869H16.8V17.727H31.194V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M14.394 10.0869H0V17.727H14.394V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M48 10.0869H33.606V17.727H48V10.0869Z" fill="#84AFF1"/>
|
||||
<path d="M48 20.1799H33.606V27.8262H48V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M31.194 20.1799H16.8V27.8262H31.194V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M14.394 20.1799H0V27.8262H14.394V20.1799Z" fill="#3865AB"/>
|
||||
<path d="M31.194 30.2668H16.8V37.907H31.194V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M48 30.2668H33.606V37.907H48V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M14.394 30.2668H0V37.907H14.394V30.2668Z" fill="#84AFF1"/>
|
||||
<path d="M31.194 40.3599H16.8V48H31.194V40.3599Z" fill="#3865AB"/>
|
||||
<path d="M48 47.3883V40.3721H33.606V48H47.3985C47.558 48 47.711 47.9355 47.8238 47.8208C47.9366 47.7061 48 47.5505 48 47.3883Z" fill="#3865AB"/>
|
||||
<path d="M14.394 48V40.3721H0V47.3883C0 47.5505 0.0633725 47.7061 0.176176 47.8208C0.28898 47.9355 0.441975 48 0.601504 48H14.394Z" fill="#3865AB"/>
|
||||
</g>
|
||||
<path d="M61.9067 30.4001C62.6011 30.4001 67.9327 26.1334 67.9327 24.0001C67.9327 21.8668 62.7357 17.6001 61.9067 17.6001C61.0778 17.6001 60.4023 18.1334 60.4023 19.1741C60.4023 20.2147 63.9067 22.8487 63.9067 22.8487C63.9067 22.8487 56.2539 22.1334 56 22.8487C55.7461 23.564 55.7461 24.4362 56 25.1515C56.2539 25.8668 63.9067 25.1515 63.9067 25.1515C63.9067 25.1515 60.4023 28.0001 60.4023 28.8323C60.4023 29.6644 61.2124 30.4001 61.9067 30.4001Z" fill="#CCCCDC"/>
|
||||
<path d="M93.832 24.801C93.832 26.5482 93.3775 27.9971 92.3406 29.2613H90.4229C91.2042 28.3238 91.7866 26.4772 91.7866 24.801C91.7866 23.1249 91.2042 21.2783 90.4229 20.3408H92.3406C93.3775 21.605 93.832 23.0539 93.832 24.801Z" fill="#24292E"/>
|
||||
<path d="M86.5016 22.1592L87.297 23.8495L88.1351 22.1592H90.0811L88.6181 24.8865L90.1522 27.6137H88.2203L87.297 25.8808L86.4022 27.6137H84.4419L85.9902 24.8865L84.5414 22.1592H86.5016Z" fill="#24292E"/>
|
||||
<path d="M80.7621 24.801C80.7621 23.0539 81.2166 21.605 82.2536 20.3408H84.1712C83.3899 21.2783 82.8075 23.1249 82.8075 24.801C82.8075 26.4772 83.3899 28.3238 84.1712 29.2613H82.2536C81.2166 27.9971 80.7621 26.5482 80.7621 24.801Z" fill="#24292E"/>
|
||||
<path d="M79.6506 22.1591V23.5795H78.6563V27.6136H76.696V23.5795H76V22.1591H76.696V22.0028C76.696 20.6534 77.4631 20 78.6563 20C79.1534 20 79.679 20.0994 79.9063 20.1562L79.6222 21.5625C79.4801 21.5199 79.267 21.4915 79.125 21.4915C78.7557 21.4915 78.6563 21.6619 78.6563 21.9176V22.1591H79.6506Z" fill="#24292E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1834_45844" x1="0" y1="3.82312" x2="14.394" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1834_45844" x1="33.606" y1="3.82312" x2="48" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1834_45844" x1="16.8" y1="3.82312" x2="31.2" y2="3.82312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2CC0C"/>
|
||||
<stop offset="1" stop-color="#FF9830"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1834_45844">
|
||||
<rect width="48" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
86
yarn.lock
86
yarn.lock
@ -12138,6 +12138,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cheminfo-types@npm:^1.7.2":
|
||||
version: 1.7.2
|
||||
resolution: "cheminfo-types@npm:1.7.2"
|
||||
checksum: 26c4db9600c786aff28276c8dbc48779f6c20705c3673558344282340d3b9d14554f306994e627396678475b50728a96134d99a794271b86301f6945fab355de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3":
|
||||
version: 3.5.3
|
||||
resolution: "chokidar@npm:3.5.3"
|
||||
@ -17508,6 +17515,8 @@ __metadata:
|
||||
marked-mangle: "npm:1.1.0"
|
||||
memoize-one: "npm:6.0.0"
|
||||
mini-css-extract-plugin: "npm:2.7.6"
|
||||
ml-regression-polynomial: "npm:^3.0.0"
|
||||
ml-regression-simple-linear: "npm:^3.0.0"
|
||||
moment: "npm:2.29.4"
|
||||
moment-timezone: "npm:0.5.43"
|
||||
monaco-editor: "npm:0.34.0"
|
||||
@ -18717,6 +18726,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-any-array@npm:^2.0.0, is-any-array@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "is-any-array@npm:2.0.1"
|
||||
checksum: a2caaec75abb10ccb7e926aed322df5d2f206ae8645313771282702cf47d626832d9dc3318580e0fddbd04772d899263bebccb12c692e97988017ac549654cd4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "is-arguments@npm:1.1.1"
|
||||
@ -22223,6 +22239,76 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-array-max@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "ml-array-max@npm:1.2.4"
|
||||
dependencies:
|
||||
is-any-array: "npm:^2.0.0"
|
||||
checksum: d62689d349c825a36dae212ee109a7f81f2a3c1136cb79f50a90e6c48ae5acc78dacb1334864bfd9abf6f09b78ce6a68e2455b1259b775b3b05556362fbf8470
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-array-min@npm:^1.2.3":
|
||||
version: 1.2.3
|
||||
resolution: "ml-array-min@npm:1.2.3"
|
||||
dependencies:
|
||||
is-any-array: "npm:^2.0.0"
|
||||
checksum: bc6e0c69f20eb2b35c2c8d3a59f8c6289462683084e8fa50344ae4db3214b8b30854f96a8eaf22df510752c3065d3337cd996f98a80a95b47a88b38369beeb5b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-array-rescale@npm:^1.3.7":
|
||||
version: 1.3.7
|
||||
resolution: "ml-array-rescale@npm:1.3.7"
|
||||
dependencies:
|
||||
is-any-array: "npm:^2.0.0"
|
||||
ml-array-max: "npm:^1.2.4"
|
||||
ml-array-min: "npm:^1.2.3"
|
||||
checksum: 2f9883388ebb6c921c648a1cdcf1d853a35dff5f30e7204e142511abcafe05f40c1c1543232503aeff4ecdb3f57e958e7de8d070b973af3be7bfd27381b30528
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-matrix@npm:^6.10.5":
|
||||
version: 6.10.8
|
||||
resolution: "ml-matrix@npm:6.10.8"
|
||||
dependencies:
|
||||
is-any-array: "npm:^2.0.1"
|
||||
ml-array-rescale: "npm:^1.3.7"
|
||||
checksum: a13909330f119dffb5538b08036a635895f87e12b25e3a23b0662e7333cfbb50425cea8fca55b03cd3eb8eeaa2509a2c84e0c26a0ca89c718f97f4234e2008b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-regression-base@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "ml-regression-base@npm:3.0.0"
|
||||
dependencies:
|
||||
cheminfo-types: "npm:^1.7.2"
|
||||
is-any-array: "npm:^2.0.1"
|
||||
checksum: b737d4f70d6058efee607ab36a56cc92199111f6ea021e6290158792ee3f410c12b77d5dabfe6ccd55d02f5b6b072b83066a6e5cba909d4c91b85050ac15b491
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-regression-polynomial@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "ml-regression-polynomial@npm:3.0.0"
|
||||
dependencies:
|
||||
cheminfo-types: "npm:^1.7.2"
|
||||
ml-matrix: "npm:^6.10.5"
|
||||
ml-regression-base: "npm:^3.0.0"
|
||||
checksum: e5d3ee187d6f35e4e3b2191f52d7dc63a61f0b975cf2012ed20403420d6a65ae2ad7d51f80896daf778b39bdbd56d7fa9d4c17aac96a1669acea3fbd2a0a31d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ml-regression-simple-linear@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "ml-regression-simple-linear@npm:3.0.0"
|
||||
dependencies:
|
||||
cheminfo-types: "npm:^1.7.2"
|
||||
ml-regression-base: "npm:^3.0.0"
|
||||
checksum: 2e4650026bb793455788f31de79bd25f5325e97580f20af848c4c8af4d1f1e1d3b04fbdb0f1677071727da3ffe4a34c5d0e4a0533ad81050441b5e0d745bdc57
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mocha@npm:10.2.0":
|
||||
version: 10.2.0
|
||||
resolution: "mocha@npm:10.2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user