Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ML] Data Frame Analytics: ROC Curve Chart #89991

Merged
merged 27 commits into from
Feb 15, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7191305
[ML] Initial AUC ROC chart.
walterra Jan 27, 2021
a8e9ab0
[ML] Fetch data for AUC ROC chart.
walterra Feb 2, 2021
cfea3d9
[ML] Move data fetching to custom hooks.
walterra Feb 3, 2021
2603d1e
[ML] Improve error reporting.
walterra Feb 3, 2021
0616963
[ML] Lazy loading.
walterra Feb 3, 2021
2989959
[ML] Fix color legend.
walterra Feb 4, 2021
3570962
[ML] Fix license header.
walterra Feb 4, 2021
452530b
[ML] Refactor to share a common VegaChart component with lazy loading…
walterra Feb 4, 2021
6a5ecd6
[ML] Tweak types. Cleanup. Translations. Chart Tweaks.
walterra Feb 4, 2021
15092f0
[ML] Fix types.
walterra Feb 4, 2021
4f505d1
[ML] Fix types.
walterra Feb 5, 2021
058f657
Merge branch 'master' into ml-auc-roc
walterra Feb 8, 2021
9afd137
Merge branch 'master' into ml-auc-roc
walterra Feb 9, 2021
42aa75f
[ML] Tweak SCSS.
walterra Feb 9, 2021
c555fb8
[ML] Rename AucRoc to RocCurve.
walterra Feb 9, 2021
004e4a3
[ML] Use the dependent variable as the legend title.
walterra Feb 9, 2021
6bcca25
[ML] Adds AUC value to chart legend.
walterra Feb 9, 2021
826044a
[ML] Improve error handling.
walterra Feb 9, 2021
18f976c
[ML] Avoid unnecessary fetch when no fields for scatterplot matrix ar…
walterra Feb 9, 2021
ef37973
[ML] Update functional tests to assert RocCurveChart. Refactored serv…
walterra Feb 9, 2021
b38a8eb
[ML] Fix updated service API.
walterra Feb 9, 2021
98cfdb6
[ML] Fix SCSS variable used in calc().
walterra Feb 10, 2021
aa59327
Merge branch 'master' into ml-auc-roc
walterra Feb 10, 2021
128456c
Merge branch 'master' into ml-auc-roc
walterra Feb 15, 2021
7caf108
[ML] Fix imports for lazy loadable VegaChart component.
walterra Feb 15, 2021
d372518
[ML] Fix imports.
walterra Feb 15, 2021
ef23a14
[ML] Fix import.
walterra Feb 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,4 @@
*/

export { useScatterplotFieldOptions } from './use_scatterplot_field_options';
export { LEGEND_TYPES } from './scatterplot_matrix_vega_lite_spec';
export { ScatterplotMatrix } from './scatterplot_matrix';
export type { ScatterplotMatrixViewProps as ScatterplotMatrixProps } from './scatterplot_matrix_view';
export { ScatterplotMatrix, ScatterplotMatrixProps } from './scatterplot_matrix';
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,295 @@
* 2.0.
*/

import React, { FC, Suspense } from 'react';
import React, { useMemo, useEffect, useState, FC } from 'react';

import type { ScatterplotMatrixViewProps } from './scatterplot_matrix_view';
import { ScatterplotMatrixLoading } from './scatterplot_matrix_loading';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiSelect,
EuiSwitch,
} from '@elastic/eui';

const ScatterplotMatrixLazy = React.lazy(() => import('./scatterplot_matrix_view'));
import { i18n } from '@kbn/i18n';

export const ScatterplotMatrix: FC<ScatterplotMatrixViewProps> = (props) => (
<Suspense fallback={<ScatterplotMatrixLoading />}>
<ScatterplotMatrixLazy {...props} />
</Suspense>
);
import type { SearchResponse7 } from '../../../../common/types/es_client';
import type { ResultsSearchQuery } from '../../data_frame_analytics/common/analytics';

import { useMlApiContext } from '../../contexts/kibana';

import { getProcessedFields } from '../data_grid';
import { useCurrentEuiTheme } from '../color_range_legend';
import { LegendType, VegaChart, VegaChartLoading } from '../vega_chart';

import {
getScatterplotMatrixVegaLiteSpec,
OUTLIER_SCORE_FIELD,
} from './scatterplot_matrix_vega_lite_spec';

import './scatterplot_matrix.scss';

const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4;
const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000;
const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE = 1;
const SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE = 10000;

const TOGGLE_ON = i18n.translate('xpack.ml.splom.toggleOn', {
defaultMessage: 'On',
});
const TOGGLE_OFF = i18n.translate('xpack.ml.splom.toggleOff', {
defaultMessage: 'Off',
});

const sampleSizeOptions = [100, 1000, 10000].map((d) => ({ value: d, text: '' + d }));

export interface ScatterplotMatrixProps {
fields: string[];
index: string;
resultsField?: string;
color?: string;
legendType?: LegendType;
searchQuery?: ResultsSearchQuery;
}

export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
fields: allFields,
index,
resultsField,
color,
legendType,
searchQuery,
}) => {
const { esSearch } = useMlApiContext();

// dynamicSize is optionally used for outlier charts where the scatterplot marks
// are sized according to outlier_score
const [dynamicSize, setDynamicSize] = useState<boolean>(false);

// used to give the use the option to customize the fields used for the matrix axes
const [fields, setFields] = useState<string[]>([]);

useEffect(() => {
const defaultFields =
allFields.length > SCATTERPLOT_MATRIX_DEFAULT_FIELDS
? allFields.slice(0, SCATTERPLOT_MATRIX_DEFAULT_FIELDS)
: allFields;
setFields(defaultFields);
}, [allFields]);

// the amount of documents to be fetched
const [fetchSize, setFetchSize] = useState<number>(SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE);
// flag to add a random score to the ES query to fetch documents
const [randomizeQuery, setRandomizeQuery] = useState<boolean>(false);

const [isLoading, setIsLoading] = useState<boolean>(false);

// contains the fetched documents and columns to be passed on to the Vega spec.
const [splom, setSplom] = useState<{ items: any[]; columns: string[] } | undefined>();

// formats the array of field names for EuiComboBox
const fieldOptions = useMemo(
() =>
allFields.map((d) => ({
label: d,
})),
[allFields]
);

const fieldsOnChange = (newFields: EuiComboBoxOptionOption[]) => {
setFields(newFields.map((d) => d.label));
};

const fetchSizeOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFetchSize(
Math.min(
Math.max(parseInt(e.target.value, 10), SCATTERPLOT_MATRIX_DEFAULT_FETCH_MIN_SIZE),
SCATTERPLOT_MATRIX_DEFAULT_FETCH_MAX_SIZE
)
);
};

const randomizeQueryOnChange = () => {
setRandomizeQuery(!randomizeQuery);
};

const dynamicSizeOnChange = () => {
setDynamicSize(!dynamicSize);
};

const { euiTheme } = useCurrentEuiTheme();

useEffect(() => {
async function fetchSplom(options: { didCancel: boolean }) {
setIsLoading(true);
try {
const queryFields = [
...fields,
...(color !== undefined ? [color] : []),
...(legendType !== undefined ? [] : [`${resultsField}.${OUTLIER_SCORE_FIELD}`]),
];

const queryFallback = searchQuery !== undefined ? searchQuery : { match_all: {} };
const query = randomizeQuery
? {
function_score: {
query: queryFallback,
random_score: { seed: 10, field: '_seq_no' },
},
}
: queryFallback;

const resp: SearchResponse7 = await esSearch({
index,
body: {
fields: queryFields,
_source: false,
query,
from: 0,
size: fetchSize,
},
});

if (!options.didCancel) {
const items = resp.hits.hits.map((d) =>
getProcessedFields(d.fields, (key: string) =>
key.startsWith(`${resultsField}.feature_importance`)
)
);

setSplom({ columns: fields, items });
setIsLoading(false);
}
} catch (e) {
// TODO error handling
setIsLoading(false);
}
}

const options = { didCancel: false };
fetchSplom(options);
return () => {
options.didCancel = true;
};
// stringify the fields array and search, otherwise the comparator will trigger on new but identical instances.
}, [fetchSize, JSON.stringify({ fields, searchQuery }), index, randomizeQuery, resultsField]);

const vegaSpec = useMemo(() => {
if (splom === undefined) {
return;
}

const { items, columns } = splom;

const values =
resultsField !== undefined
? items
: items.map((d) => {
d[`${resultsField}.${OUTLIER_SCORE_FIELD}`] = 0;
return d;
});

return getScatterplotMatrixVegaLiteSpec(
values,
columns,
euiTheme,
resultsField,
color,
legendType,
dynamicSize
);
}, [resultsField, splom, color, legendType, dynamicSize]);

return (
<>
{splom === undefined || vegaSpec === undefined ? (
<VegaChartLoading />
) : (
<>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate('xpack.ml.splom.fieldSelectionLabel', {
defaultMessage: 'Fields',
})}
display="rowCompressed"
fullWidth
>
<EuiComboBox
compressed
fullWidth
placeholder={i18n.translate('xpack.ml.splom.fieldSelectionPlaceholder', {
defaultMessage: 'Select fields',
})}
options={fieldOptions}
selectedOptions={fields.map((d) => ({
label: d,
}))}
onChange={fieldsOnChange}
isClearable={true}
data-test-subj="mlScatterplotMatrixFieldsComboBox"
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ width: '200px' }} grow={false}>
<EuiFormRow
label={i18n.translate('xpack.ml.splom.sampleSizeLabel', {
defaultMessage: 'Sample size',
})}
display="rowCompressed"
fullWidth
>
<EuiSelect
compressed
options={sampleSizeOptions}
value={fetchSize}
onChange={fetchSizeOnChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ width: '120px' }} grow={false}>
<EuiFormRow
label={i18n.translate('xpack.ml.splom.randomScoringLabel', {
defaultMessage: 'Random scoring',
})}
display="rowCompressed"
fullWidth
>
<EuiSwitch
name="mlScatterplotMatrixRandomizeQuery"
label={randomizeQuery ? TOGGLE_ON : TOGGLE_OFF}
checked={randomizeQuery}
onChange={randomizeQueryOnChange}
disabled={isLoading}
/>
</EuiFormRow>
</EuiFlexItem>
{resultsField !== undefined && legendType === undefined && (
<EuiFlexItem style={{ width: '120px' }} grow={false}>
<EuiFormRow
label={i18n.translate('xpack.ml.splom.dynamicSizeLabel', {
defaultMessage: 'Dynamic size',
})}
display="rowCompressed"
fullWidth
>
<EuiSwitch
name="mlScatterplotMatrixDynamicSize"
label={dynamicSize ? TOGGLE_ON : TOGGLE_OFF}
checked={dynamicSize}
onChange={dynamicSizeOnChange}
disabled={isLoading}
/>
</EuiFormRow>
</EuiFlexItem>
)}
</EuiFlexGroup>

<VegaChart vegaSpec={vegaSpec} />
</>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { compile } from 'vega-lite/build-es5/vega-lite';

import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json';

import { LEGEND_TYPES } from '../vega_chart';

import {
getColorSpec,
getScatterplotMatrixVegaLiteSpec,
COLOR_OUTLIER,
COLOR_RANGE_NOMINAL,
DEFAULT_COLOR,
LEGEND_TYPES,
} from './scatterplot_matrix_vega_lite_spec';

describe('getColorSpec()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ import { euiPaletteColorBlind, euiPaletteNegative, euiPalettePositive } from '@e

import { i18n } from '@kbn/i18n';

export const LEGEND_TYPES = {
NOMINAL: 'nominal',
QUANTITATIVE: 'quantitative',
} as const;
export type LegendType = typeof LEGEND_TYPES[keyof typeof LEGEND_TYPES];
import { LegendType, LEGEND_TYPES } from '../vega_chart';

export const OUTLIER_SCORE_FIELD = 'outlier_score';

Expand Down
Loading