From 4537911640dc0232ac9e5af61f3905448811c44b Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 2 Dec 2022 10:31:22 +0000 Subject: [PATCH 1/4] [ML] Use anomaly score explanation for chart tooltip multi-bucket impact --- .../common/constants/multi_bucket_impact.ts | 19 --- x-pack/plugins/ml/common/types/results.ts | 1 + .../ml/common/util/anomaly_utils.test.ts | 156 ++++++++++++++++-- .../plugins/ml/common/util/anomaly_utils.ts | 68 +++++--- .../anomalies_table_columns.js | 4 +- .../anomalies_table/anomaly_details_utils.tsx | 17 +- .../severity_cell/severity_cell.test.tsx | 4 +- .../severity_cell/severity_cell.tsx | 13 +- .../explorer_chart_single_metric.js | 5 +- .../timeseries_chart/timeseries_chart.js | 6 +- .../timeseriesexplorer_help_popover.tsx | 2 +- .../timeseriesexplorer_utils.js | 10 +- .../ml/public/application/util/chart_utils.js | 30 +++- .../application/util/chart_utils.test.js | 33 +--- .../models/results_service/anomaly_charts.ts | 10 +- 15 files changed, 251 insertions(+), 127 deletions(-) delete mode 100644 x-pack/plugins/ml/common/constants/multi_bucket_impact.ts diff --git a/x-pack/plugins/ml/common/constants/multi_bucket_impact.ts b/x-pack/plugins/ml/common/constants/multi_bucket_impact.ts deleted file mode 100644 index c708f15862163..0000000000000 --- a/x-pack/plugins/ml/common/constants/multi_bucket_impact.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * Thresholds for indicating the impact of multi-bucket features in an anomaly. - * As a rule-of-thumb, a threshold value T corresponds to the multi-bucket probability - * being 1000^(T/5) times smaller than the single bucket probability. - * So, for example, for HIGH it is 63 times smaller. - */ -export const MULTI_BUCKET_IMPACT = { - HIGH: 3, - MEDIUM: 2, - LOW: 1, - NONE: -5, -}; diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index 9997bc4751b41..cf25fc6081e16 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -99,6 +99,7 @@ export interface ChartPoint { anomalyScore?: number; actual?: number[]; multiBucketImpact?: number; + isMultiBucketAnomaly?: boolean; typical?: number[]; value?: number | null; entity?: string; diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.test.ts b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts index 24eb06d8bd053..b707a61d5b9bc 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.test.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.test.ts @@ -12,13 +12,13 @@ import { getEntityFieldList, getEntityFieldName, getEntityFieldValue, - getMultiBucketImpactLabel, getSeverity, getSeverityWithLow, getSeverityColor, isRuleSupported, showActualForFunction, showTypicalForFunction, + isMultiBucketAnomaly, } from './anomaly_utils'; describe('ML - anomaly utils', () => { @@ -260,30 +260,152 @@ describe('ML - anomaly utils', () => { }); }); - describe('getMultiBucketImpactLabel', () => { - test('returns high for 3 <= score <= 5', () => { - expect(getMultiBucketImpactLabel(3)).toBe('high'); - expect(getMultiBucketImpactLabel(5)).toBe('high'); + describe('isMultiBucketAnomaly', () => { + const singleBucketAnomaly: AnomalyRecordDoc = { + job_id: 'farequote_sb', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + anomaly_score_explanation: { + single_bucket_impact: 65, + multi_bucket_impact: 14, + lower_confidence_bound: 94.79879269994528, + typical_value: 100.26620234643129, + upper_confidence_bound: 106.04564690901603, + }, + }; + + const multiBucketAnomaly: AnomalyRecordDoc = { + job_id: 'farequote_mb', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + anomaly_score_explanation: { + single_bucket_impact: 14, + multi_bucket_impact: 65, + lower_confidence_bound: 94.79879269994528, + typical_value: 100.26620234643129, + upper_confidence_bound: 106.04564690901603, + }, + }; + + const multiBucketAnomaly2: AnomalyRecordDoc = { + job_id: 'farequote_mb2', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + anomaly_score_explanation: { + multi_bucket_impact: 65, + lower_confidence_bound: 94.79879269994528, + typical_value: 100.26620234643129, + upper_confidence_bound: 106.04564690901603, + }, + }; + + const noASEAnomaly: AnomalyRecordDoc = { + job_id: 'farequote_ase', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + }; + + const noMBIAnomaly: AnomalyRecordDoc = { + job_id: 'farequote_sbi', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + anomaly_score_explanation: { + single_bucket_impact: 65, + lower_confidence_bound: 94.79879269994528, + typical_value: 100.26620234643129, + upper_confidence_bound: 106.04564690901603, + }, + }; + + const singleBucketAnomaly2: AnomalyRecordDoc = { + job_id: 'farequote_sb2', + result_type: 'record', + probability: 0.0191711, + record_score: 4.38431, + initial_record_score: 19.654, + bucket_span: 300, + detector_index: 0, + is_interim: false, + timestamp: 1454890500000, + function: 'mean', + function_description: 'mean', + field_name: 'responsetime', + anomaly_score_explanation: { + single_bucket_impact: 65, + multi_bucket_impact: 65, + lower_confidence_bound: 94.79879269994528, + typical_value: 100.26620234643129, + upper_confidence_bound: 106.04564690901603, + }, + }; + + test('returns false when single_bucket_impact much larger than multi_bucket_impact', () => { + expect(isMultiBucketAnomaly(singleBucketAnomaly)).toBe(false); + }); + + test('returns true when multi_bucket_impact much larger than single_bucket_impact', () => { + expect(isMultiBucketAnomaly(multiBucketAnomaly)).toBe(true); }); - test('returns medium for 2 <= score < 3', () => { - expect(getMultiBucketImpactLabel(2)).toBe('medium'); - expect(getMultiBucketImpactLabel(2.99)).toBe('medium'); + test('returns true when multi_bucket_impact > 0 and single_bucket_impact undefined', () => { + expect(isMultiBucketAnomaly(multiBucketAnomaly2)).toBe(true); }); - test('returns low for 1 <= score < 2', () => { - expect(getMultiBucketImpactLabel(1)).toBe('low'); - expect(getMultiBucketImpactLabel(1.99)).toBe('low'); + test('returns false when anomaly_score_explanation undefined', () => { + expect(isMultiBucketAnomaly(noASEAnomaly)).toBe(false); }); - test('returns none for -5 <= score < 1', () => { - expect(getMultiBucketImpactLabel(-5)).toBe('none'); - expect(getMultiBucketImpactLabel(0.99)).toBe('none'); + test('returns false when multi_bucket_impact undefined', () => { + expect(isMultiBucketAnomaly(noMBIAnomaly)).toBe(false); }); - test('returns expected label when impact outside normal bounds', () => { - expect(getMultiBucketImpactLabel(10)).toBe('high'); - expect(getMultiBucketImpactLabel(-10)).toBe('none'); + test('returns false when multi_bucket_impact === single_bucket_impact', () => { + expect(isMultiBucketAnomaly(singleBucketAnomaly2)).toBe(false); }); }); diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index cbbec963b0c3d..852ac85b5c55f 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -12,7 +12,6 @@ import { i18n } from '@kbn/i18n'; import { CONDITIONS_NOT_SUPPORTED_FUNCTIONS } from '../constants/detector_rule'; -import { MULTI_BUCKET_IMPACT } from '../constants/multi_bucket_impact'; import { ANOMALY_SEVERITY, ANOMALY_THRESHOLD, SEVERITY_COLORS } from '../constants/anomalies'; import type { AnomaliesTableRecord, AnomalyRecordDoc } from '../types/anomalies'; @@ -119,6 +118,10 @@ function getSeverityTypes() { }); } +/** + * Returns whether the anomaly is in a categorization analysis. + * @param anomaly Anomaly table record + */ export function isCategorizationAnomaly(anomaly: AnomaliesTableRecord): boolean { return anomaly.entityName === 'mlcategory'; } @@ -219,29 +222,50 @@ export function getSeverityColor(normalizedScore: number): string { } /** - * Returns a label to use for the multi-bucket impact of an anomaly - * according to the value of the multi_bucket_impact field of a record, - * which ranges from -5 to +5. - * @param multiBucketImpact - Value of the multi_bucket_impact field of a record, from -5 to +5 + * Returns whether the anomaly record should be indicated in the UI as a multi-bucket anomaly, + * for example in anomaly charts with a cross-shaped marker. + * @param anomaly Anomaly table record */ -export function getMultiBucketImpactLabel(multiBucketImpact: number): string { - if (multiBucketImpact >= MULTI_BUCKET_IMPACT.HIGH) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.highLabel', { - defaultMessage: 'high', - }); - } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel', { - defaultMessage: 'medium', - }); - } else if (multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW) { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.lowLabel', { - defaultMessage: 'low', - }); - } else { - return i18n.translate('xpack.ml.anomalyUtils.multiBucketImpact.noneLabel', { - defaultMessage: 'none', - }); +export function isMultiBucketAnomaly(anomaly: AnomalyRecordDoc): boolean { + if (anomaly.anomaly_score_explanation === undefined) { + return false; } + + const sb = anomaly.anomaly_score_explanation.single_bucket_impact; + const mb = anomaly.anomaly_score_explanation.multi_bucket_impact; + + if (mb === undefined || mb === 0) { + return false; + } + + if (sb !== undefined && sb > mb) { + return false; + } + + if ((sb === undefined || sb === 0) && mb > 0) { + return true; + } + + if (sb !== undefined && mb > sb) { + return (((mb - sb) * mb) / sb) * 1.7 >= 2; + } + + return false; +} + +/** + * Returns the value on a scale of 1 to 5, from a log based scaled value for an + * anomaly score explanation impact field, such as anomaly_characteristics_impact, + * single_bucket_impact or multi_bucket_impact. + * @param score value from an impact field from the anomaly_score_explanation. + * @returns numeric value on an integer scale of 1 (low) to 5 (high). + */ +export function getAnomalyScoreExplanationImpactValue(score: number): number { + if (score < 2) return 1; + if (score < 4) return 2; + if (score < 7) return 3; + if (score < 12) return 4; + return 5; } /** diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js index 07f52b03e6221..c8381726ee86a 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomalies_table_columns.js @@ -27,7 +27,7 @@ import { InfluencersCell } from './influencers_cell'; import { LinksMenu } from './links_menu'; import { checkPermission } from '../../capabilities/check_capabilities'; import { mlFieldFormatService } from '../../services/field_format_service'; -import { isRuleSupported } from '../../../../common/util/anomaly_utils'; +import { isRuleSupported, isMultiBucketAnomaly } from '../../../../common/util/anomaly_utils'; import { formatValue } from '../../formatters/format_value'; import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS } from './anomalies_table_constants'; import { SeverityCell } from './severity_cell'; @@ -133,7 +133,7 @@ export function getColumns( ), render: (score, item) => ( - + ), sortable: true, }, diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx index a049ee2c48ea6..0254c27f67906 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx @@ -26,7 +26,10 @@ import { import { AnomaliesTableRecord, MLAnomalyDoc } from '../../../../common/types/anomalies'; import { formatValue } from '../../formatters/format_value'; import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; -import { getSeverityColor } from '../../../../common/util/anomaly_utils'; +import { + getAnomalyScoreExplanationImpactValue, + getSeverityColor, +} from '../../../../common/util/anomaly_utils'; const TIME_FIELD_NAME = 'timestamp'; @@ -597,14 +600,6 @@ function getAnomalyType(explanation: MLAnomalyDoc['anomaly_score_explanation']) return explanation.anomaly_type === 'dip' ? dip : spike; } -function getImpactValue(score: number) { - if (score < 2) return 1; - if (score < 4) return 2; - if (score < 7) return 3; - if (score < 12) return 4; - return 5; -} - const impactTooltips = { anomaly_characteristics: { low: i18n.translate( @@ -681,7 +676,7 @@ function getImpactTooltip( score: number, type: 'anomaly_characteristics' | 'single_bucket' | 'multi_bucket' ) { - const value = getImpactValue(score); + const value = getAnomalyScoreExplanationImpactValue(score); if (value < 3) { return impactTooltips[type].low; @@ -698,7 +693,7 @@ const ImpactVisual: FC<{ score: number }> = ({ score }) => { euiTheme: { colors }, } = useEuiTheme(); - const impact = getImpactValue(score); + const impact = getAnomalyScoreExplanationImpactValue(score); const boxPx = '10px'; const emptyBox = colors.lightShade; const fullBox = colors.primary; diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx index f58fe20facbf6..e9166c0c6c28c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.test.tsx @@ -13,7 +13,7 @@ describe('SeverityCell', () => { test('should render a single-bucket marker with rounded severity score', () => { const props = { score: 75.2, - multiBucketImpact: -2, + isMultiBucketAnomaly: false, }; const { container } = render(); expect(container.textContent).toBe('75'); @@ -24,7 +24,7 @@ describe('SeverityCell', () => { test('should render a multi-bucket marker with low severity score', () => { const props = { score: 0.8, - multiBucketImpact: 4, + isMultiBucketAnomaly: true, }; const { container } = render(); expect(container.textContent).toBe('< 1'); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx index b761599a447b7..555110e4a3e4d 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/severity_cell/severity_cell.tsx @@ -7,7 +7,6 @@ import React, { FC, memo } from 'react'; import { EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { MULTI_BUCKET_IMPACT } from '../../../../../common/constants/multi_bucket_impact'; import { getSeverityColor, getFormattedSeverityScore, @@ -19,21 +18,19 @@ interface SeverityCellProps { */ score: number; /** - * Multi-bucket impact score from –5 to 5. - * Anomalies with a multi-bucket impact value of greater than or equal - * to 2 are indicated with a plus shaped symbol in the cell. + * Flag to indicate whether the anomaly should be displayed in the cell as a + * multi-bucket anomaly with a plus-shaped symbol. */ - multiBucketImpact: number; + isMultiBucketAnomaly: boolean; } /** * Renders anomaly severity score with single or multi-bucket impact marker. */ -export const SeverityCell: FC = memo(({ score, multiBucketImpact }) => { +export const SeverityCell: FC = memo(({ score, isMultiBucketAnomaly }) => { const severity = getFormattedSeverityScore(score); const color = getSeverityColor(score); - const isMultiBucket = multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM; - return isMultiBucket ? ( + return isMultiBucketAnomaly ? ( diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js index 6ce196393b97e..23f1b91910887 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_single_metric.js @@ -24,7 +24,6 @@ import { getFormattedSeverityScore, getSeverityColor, getSeverityWithLow, - getMultiBucketImpactLabel, } from '../../../../common/util/anomaly_utils'; import { LINE_CHART_ANOMALY_RADIUS, @@ -36,6 +35,7 @@ import { removeLabelOverlap, showMultiBucketAnomalyMarker, showMultiBucketAnomalyTooltip, + getMultiBucketImpactTooltipValue, } from '../../util/chart_utils'; import { LoadingIndicator } from '../../components/loading_indicator/loading_indicator'; import { mlFieldFormatService } from '../../services/field_format_service'; @@ -461,6 +461,7 @@ export class ExplorerChartSingleMetric extends React.Component { if (marker.anomalyScore !== undefined) { const score = parseInt(marker.anomalyScore); + tooltipData.push({ label: i18n.translate('xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', { defaultMessage: 'anomaly score', @@ -478,7 +479,7 @@ export class ExplorerChartSingleMetric extends React.Component { label: i18n.translate('xpack.ml.explorer.singleMetricChart.multiBucketImpactLabel', { defaultMessage: 'multi-bucket impact', }), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), + value: getMultiBucketImpactTooltipValue(marker), seriesIdentifier: { key: seriesKey, }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js index 8daf2e8f86891..707511c1f22d4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart.js @@ -21,7 +21,6 @@ import { i18n } from '@kbn/i18n'; import { getFormattedSeverityScore, getSeverityWithLow, - getMultiBucketImpactLabel, } from '../../../../../common/util/anomaly_utils'; import { formatValue } from '../../../formatters/format_value'; import { @@ -34,6 +33,7 @@ import { numTicksForDateFormat, showMultiBucketAnomalyMarker, showMultiBucketAnomalyTooltip, + getMultiBucketImpactTooltipValue, } from '../../../util/chart_utils'; import { formatHumanReadableDateTimeSeconds } from '../../../../../common/util/date_utils'; import { getTimeBucketsFromCache } from '../../../util/time_buckets'; @@ -1509,12 +1509,12 @@ class TimeseriesChartIntl extends Component { if (showMultiBucketAnomalyTooltip(marker) === true) { tooltipData.push({ label: i18n.translate( - 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel', + 'xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketAnomalyLabel', { defaultMessage: 'multi-bucket impact', } ), - value: getMultiBucketImpactLabel(marker.multiBucketImpact), + value: getMultiBucketImpactTooltipValue(marker), seriesIdentifier: { key: seriesKey, }, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx index 4313c9b228825..5cdfc8951e84e 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx @@ -27,7 +27,7 @@ export const TimeSeriesExplorerHelpPopover = () => {

diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js index 5a508cbf4bb41..cd7c12e5c3430 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/timeseriesexplorer_utils.js @@ -14,6 +14,7 @@ import { each, get, find } from 'lodash'; import moment from 'moment-timezone'; +import { isMultiBucketAnomaly } from '../../../../common/util/anomaly_utils'; import { isTimeSeriesViewJob } from '../../../../common/util/job_utils'; import { parseInterval } from '../../../../common/util/parse_interval'; @@ -193,9 +194,14 @@ export function processDataForFocusAnomalies( } } - if (record.multi_bucket_impact !== undefined) { - chartPoint.multiBucketImpact = record.multi_bucket_impact; + if ( + record.anomaly_score_explanation !== undefined && + record.anomaly_score_explanation.multi_bucket_impact !== undefined + ) { + chartPoint.multiBucketImpact = record.anomaly_score_explanation.multi_bucket_impact; } + + chartPoint.isMultiBucketAnomaly = isMultiBucketAnomaly(record); } } }); diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 4936f80f1911c..4b08bf1162e76 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -7,7 +7,7 @@ import d3 from 'd3'; import { calculateTextWidth } from './string_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; +import { getAnomalyScoreExplanationImpactValue } from '../../../common/util/anomaly_utils'; import moment from 'moment'; import { CHART_TYPE } from '../explorer/explorer_constants'; import { ML_PAGES } from '../../../common/constants/locator'; @@ -224,17 +224,29 @@ export async function getExploreSeriesLink(mlLocator, series, timeRange) { } export function showMultiBucketAnomalyMarker(point) { - // TODO - test threshold with real use cases - return ( - point.multiBucketImpact !== undefined && point.multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM - ); + return point.isMultiBucketAnomaly === true; } export function showMultiBucketAnomalyTooltip(point) { - // TODO - test threshold with real use cases - return ( - point.multiBucketImpact !== undefined && point.multiBucketImpact >= MULTI_BUCKET_IMPACT.LOW - ); + return point.isMultiBucketAnomaly === true; +} + +export function getMultiBucketImpactTooltipValue(point) { + const numFilledSquares = + point.multiBucketImpact !== undefined + ? getAnomalyScoreExplanationImpactValue(point.multiBucketImpact) + : 0; + const numHollowSquares = 5 - numFilledSquares; + + let tooltip = ''; + for (let i = 0; i < numFilledSquares; i++) { + tooltip += '\u25A0 '; // Unicode filled square + } + for (let i = 0; i < numHollowSquares; i++) { + tooltip += '\u25A1 '; // Unicode hollow square + } + + return tooltip; } export function numTicks(axisWidth) { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index 83c7b58afbefd..f7bc96e1c18d1 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -44,7 +44,6 @@ import { showMultiBucketAnomalyTooltip, } from './chart_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; import { CHART_TYPE } from '../explorer/explorer_constants'; timefilter.setTime({ @@ -159,44 +158,24 @@ describe('ML - chart utils', () => { }); describe('showMultiBucketAnomalyMarker', () => { - test('returns true for points with multiBucketImpact at or above medium impact', () => { - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( - true - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( - true - ); + test('returns true for points with isMultiBucketAnomaly=true', () => { + expect(showMultiBucketAnomalyMarker({ isMultiBucketAnomaly: true })).toBe(true); }); test('returns false for points with multiBucketImpact missing or below medium impact', () => { expect(showMultiBucketAnomalyMarker({})).toBe(false); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( - false - ); - expect(showMultiBucketAnomalyMarker({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( - false - ); + expect(showMultiBucketAnomalyMarker({ isMultiBucketAnomaly: false })).toBe(false); }); }); describe('showMultiBucketAnomalyTooltip', () => { - test('returns true for points with multiBucketImpact at or above low impact', () => { - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.HIGH })).toBe( - true - ); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.MEDIUM })).toBe( - true - ); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.LOW })).toBe( - true - ); + test('returns true for points with isMultiBucketAnomaly=true', () => { + expect(showMultiBucketAnomalyTooltip({ isMultiBucketAnomaly: true })).toBe(true); }); test('returns false for points with multiBucketImpact missing or below medium impact', () => { expect(showMultiBucketAnomalyTooltip({})).toBe(false); - expect(showMultiBucketAnomalyTooltip({ multiBucketImpact: MULTI_BUCKET_IMPACT.NONE })).toBe( - false - ); + expect(showMultiBucketAnomalyTooltip({ isMultiBucketAnomaly: false })).toBe(false); }); }); diff --git a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts index eb85ec82763e6..dbbf95d4806c2 100644 --- a/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts +++ b/x-pack/plugins/ml/server/models/results_service/anomaly_charts.ts @@ -37,6 +37,7 @@ import { aggregationTypeTransform, EntityField, getEntityFieldList, + isMultiBucketAnomaly, } from '../../../common/util/anomaly_utils'; import { InfluencersFilterQuery } from '../../../common/types/es_client'; import { isDefined } from '../../../common/types/guards'; @@ -1101,9 +1102,14 @@ export function anomalyChartsDataProvider(mlClient: MlClient, client: IScopedClu } } - if (record.multi_bucket_impact !== undefined) { - chartPoint.multiBucketImpact = record.multi_bucket_impact; + if ( + record.anomaly_score_explanation !== undefined && + record.anomaly_score_explanation.multi_bucket_impact !== undefined + ) { + chartPoint.multiBucketImpact = record.anomaly_score_explanation.multi_bucket_impact; } + + chartPoint.isMultiBucketAnomaly = isMultiBucketAnomaly(record); } }); From 0df33adc50d38dda3a29c0e67673faf561481100 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Fri, 2 Dec 2022 11:10:24 +0000 Subject: [PATCH 2/4] [ML] Fix translations --- x-pack/plugins/translations/translations/fr-FR.json | 5 ----- x-pack/plugins/translations/translations/ja-JP.json | 5 ----- x-pack/plugins/translations/translations/zh-CN.json | 5 ----- 3 files changed, 15 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index fece10374f8cb..359bd000274ed 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -19647,10 +19647,6 @@ "xpack.ml.anomalyResultsViewSelector.buttonGroupLegend": "Sélecteur de vue des résultats d'anomalie", "xpack.ml.anomalyResultsViewSelector.singleMetricViewerLabel": "Voir les résultats dans Single Metric Viewer", "xpack.ml.anomalySwimLane.noOverallDataMessage": "Aucune anomalie trouvée dans les résultats de groupe généraux pour cette plage temporelle", - "xpack.ml.anomalyUtils.multiBucketImpact.highLabel": "élevé", - "xpack.ml.anomalyUtils.multiBucketImpact.lowLabel": "bas", - "xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel": "moyen", - "xpack.ml.anomalyUtils.multiBucketImpact.noneLabel": "aucun", "xpack.ml.anomalyUtils.severity.criticalLabel": "critique", "xpack.ml.anomalyUtils.severity.majorLabel": "majeur", "xpack.ml.anomalyUtils.severity.minorLabel": "mineure", @@ -21504,7 +21500,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel": "réel", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel": "limites inférieures", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel": "limites supérieures", - "xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel": "impact sur plusieurs compartiments", "xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel": "typique", "xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel": "valeur", "xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel": "prédiction", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5cf72f934ec56..0e7c87147991a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19628,10 +19628,6 @@ "xpack.ml.anomalyResultsViewSelector.buttonGroupLegend": "異常結果ビューセレクター", "xpack.ml.anomalyResultsViewSelector.singleMetricViewerLabel": "シングルメトリックビューアーで結果を表示", "xpack.ml.anomalySwimLane.noOverallDataMessage": "この時間範囲のバケット結果全体で異常が見つかりませんでした", - "xpack.ml.anomalyUtils.multiBucketImpact.highLabel": "高", - "xpack.ml.anomalyUtils.multiBucketImpact.lowLabel": "低", - "xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel": "中", - "xpack.ml.anomalyUtils.multiBucketImpact.noneLabel": "なし", "xpack.ml.anomalyUtils.severity.criticalLabel": "致命的", "xpack.ml.anomalyUtils.severity.majorLabel": "メジャー", "xpack.ml.anomalyUtils.severity.minorLabel": "マイナー", @@ -21485,7 +21481,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel": "実際", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel": "下の境界", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel": "上の境界", - "xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel": "複数バケットの影響", "xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel": "通常", "xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel": "値", "xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel": "予測", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4330b9d74fbab..7724cf7cbfe2b 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19658,10 +19658,6 @@ "xpack.ml.anomalyResultsViewSelector.buttonGroupLegend": "异常结果视图选择器", "xpack.ml.anomalyResultsViewSelector.singleMetricViewerLabel": "在 Single Metric Viewer 中查看结果", "xpack.ml.anomalySwimLane.noOverallDataMessage": "此时间范围的总体存储桶中未发现异常", - "xpack.ml.anomalyUtils.multiBucketImpact.highLabel": "高", - "xpack.ml.anomalyUtils.multiBucketImpact.lowLabel": "低", - "xpack.ml.anomalyUtils.multiBucketImpact.mediumLabel": "中", - "xpack.ml.anomalyUtils.multiBucketImpact.noneLabel": "无", "xpack.ml.anomalyUtils.severity.criticalLabel": "紧急", "xpack.ml.anomalyUtils.severity.majorLabel": "重大", "xpack.ml.anomalyUtils.severity.minorLabel": "轻微", @@ -21515,7 +21511,6 @@ "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.actualLabel": "实际", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.lowerBoundsLabel": "下边界", "xpack.ml.timeSeriesExplorer.timeSeriesChart.modelPlotEnabled.upperBoundsLabel": "上边界", - "xpack.ml.timeSeriesExplorer.timeSeriesChart.multiBucketImpactLabel": "多存储桶影响", "xpack.ml.timeSeriesExplorer.timeSeriesChart.typicalLabel": "典型", "xpack.ml.timeSeriesExplorer.timeSeriesChart.valueLabel": "值", "xpack.ml.timeSeriesExplorer.timeSeriesChart.withoutAnomalyScore.predictionLabel": "预测", From aecb00b038199a7bd88cb7da27e486a2c06ff78b Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 7 Dec 2022 10:53:16 +0000 Subject: [PATCH 3/4] [ML] Address review feedback on help text and comments. --- x-pack/plugins/ml/common/util/anomaly_utils.ts | 4 ++++ .../timeseriesexplorer/timeseriesexplorer_help_popover.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/ml/common/util/anomaly_utils.ts b/x-pack/plugins/ml/common/util/anomaly_utils.ts index 852ac85b5c55f..31a2a5fe49ca5 100644 --- a/x-pack/plugins/ml/common/util/anomaly_utils.ts +++ b/x-pack/plugins/ml/common/util/anomaly_utils.ts @@ -246,6 +246,10 @@ export function isMultiBucketAnomaly(anomaly: AnomalyRecordDoc): boolean { return true; } + // Basis of use of 1.7 comes from the backend calculation for + // the single- and multi-bucket impacts + // 1.7 = 5.0/lg(e)/ln(1000) + // with the computation of the logarithm basis changed from e to 10. if (sb !== undefined && mb > sb) { return (((mb - sb) * mb) / sb) * 1.7 >= 2; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx index 5cdfc8951e84e..afd93fd5acee1 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_help_popover.tsx @@ -27,7 +27,7 @@ export const TimeSeriesExplorerHelpPopover = () => {

From 6a4a0ba837dd9e016f56d06433da0a85eb916036 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 7 Dec 2022 11:28:20 +0000 Subject: [PATCH 4/4] [ML] Simmplify code for outputting squares in tooltip --- .../ml/public/application/util/chart_utils.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 4b08bf1162e76..ab842fe6f688d 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -236,17 +236,10 @@ export function getMultiBucketImpactTooltipValue(point) { point.multiBucketImpact !== undefined ? getAnomalyScoreExplanationImpactValue(point.multiBucketImpact) : 0; - const numHollowSquares = 5 - numFilledSquares; - - let tooltip = ''; - for (let i = 0; i < numFilledSquares; i++) { - tooltip += '\u25A0 '; // Unicode filled square - } - for (let i = 0; i < numHollowSquares; i++) { - tooltip += '\u25A1 '; // Unicode hollow square - } - - return tooltip; + return new Array(5) + .fill('\u25A0 ', 0, numFilledSquares) // Unicode filled square + .fill('\u25A1 ', numFilledSquares) // Unicode hollow square + .join(''); } export function numTicks(axisWidth) {