Skip to content

Commit

Permalink
[ML] Fix formatting of values for time of day or week anomalies (elas…
Browse files Browse the repository at this point in the history
  • Loading branch information
peteharverson committed Mar 1, 2019
1 parent 9bf1906 commit 96d6cf2
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export function getColumns(
}),
render: (actual, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.actual, item.source.function, fieldFormat);
return formatValue(item.actual, item.source.function, fieldFormat, item.source);
},
sortable: true
});
Expand All @@ -185,7 +185,7 @@ export function getColumns(
}),
render: (typical, item) => {
const fieldFormat = mlFieldFormatService.getFieldFormat(item.jobId, item.source.detector_index);
return formatValue(item.typical, item.source.function, fieldFormat);
return formatValue(item.typical, item.source.function, fieldFormat, item.source);
},
sortable: true
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', {
defaultMessage: 'actual',
}),
description: formatValue(anomaly.actual, source.function)
description: formatValue(anomaly.actual, source.function, undefined, source)
});
}

Expand All @@ -161,7 +161,7 @@ function getDetailsItems(anomaly, examples, filter) {
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', {
defaultMessage: 'typical',
}),
description: formatValue(anomaly.typical, source.function)
description: formatValue(anomaly.typical, source.function, undefined, source)
});
}

Expand Down
58 changes: 43 additions & 15 deletions x-pack/plugins/ml/public/formatters/__tests__/format_value.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,55 @@


import expect from 'expect.js';
import moment from 'moment';
import moment from 'moment-timezone';
import { formatValue } from '../format_value';

describe('ML - formatValue formatter', () => {
const timeOfWeekRecord = {
job_id: 'gallery_time_of_week',
result_type: 'record',
probability: 0.012818,
record_score: 53.55134,
bucket_span: 900,
detector_index: 0,
timestamp: 1530155700000,
by_field_name: 'clientip',
by_field_value: '65.55.215.39',
function: 'time_of_week',
function_description: 'time'
};

// Just check the return value is in the expected format, and
// not the exact value as this will be timezone specific.
const timeOfDayRecord = {
job_id: 'gallery_time_of_day',
result_type: 'record',
probability: 0.012818,
record_score: 97.94245,
bucket_span: 900,
detector_index: 0,
timestamp: 1517472900000,
by_field_name: 'clientip',
by_field_value: '157.56.93.83',
function: 'time_of_day',
function_description: 'time'
};

// Set timezone to US/Eastern for time_of_day and time_of_week tests.
beforeEach(() => {
moment.tz.setDefault('US/Eastern');
});

afterEach(() => {
moment.tz.setDefault('Browser');
});

// For time_of_day and time_of_week test values which are offsets in seconds
// from UTC start of week / day are formatted correctly using the test timezone.
it('correctly formats time_of_week value from numeric input', () => {
const formattedValue = formatValue(1483228800, 'time_of_week');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue(359739, 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
});

it('correctly formats time_of_day value from numeric input', () => {
const formattedValue = formatValue(1483228800, 'time_of_day');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue(73781, 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
});

it('correctly formats number values from numeric input', () => {
Expand All @@ -37,15 +69,11 @@ describe('ML - formatValue formatter', () => {
});

it('correctly formats time_of_week value from array input', () => {
const formattedValue = formatValue([1483228800], 'time_of_week');
const result = moment(formattedValue, 'ddd hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue([359739], 'time_of_week', undefined, timeOfWeekRecord)).to.be('Wed 23:55');
});

it('correctly formats time_of_day value from array input', () => {
const formattedValue = formatValue([1483228800], 'time_of_day');
const result = moment(formattedValue, 'hh:mm', true).isValid();
expect(result).to.be(true);
expect(formatValue([73781], 'time_of_day', undefined, timeOfDayRecord)).to.be('15:29');
});

it('correctly formats number values from array input', () => {
Expand Down
34 changes: 23 additions & 11 deletions x-pack/plugins/ml/public/formatters/format_value.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,42 +24,54 @@ const SIGFIGS_IF_ROUNDING = 3; // Number of sigfigs to use for values < 10
// Formats the value of an actual or typical field from a machine learning anomaly record.
// mlFunction is the 'function' field from the ML record containing what the user entered e.g. 'high_count',
// (as opposed to the 'function_description' field which holds an ML-built display hint for the function e.g. 'count'.
export function formatValue(value, mlFunction, fieldFormat) {
// If a Kibana fieldFormat is not supplied, will fall back to default
// formatting depending on the magnitude of the value.
// For time_of_day or time_of_week functions the anomaly record
// containing the timestamp of the anomaly should be supplied in
// order to correctly format the day or week offset to the time of the anomaly.
export function formatValue(value, mlFunction, fieldFormat, record) {
// actual and typical values in anomaly record results will be arrays.
// Unless the array is multi-valued (as it will be for multi-variate analyses such as lat_long),
// simply return the formatted single value.
if (Array.isArray(value)) {
if (value.length === 1) {
return formatSingleValue(value[0], mlFunction, fieldFormat);
return formatSingleValue(value[0], mlFunction, fieldFormat, record);
} else {
// Return with array style formatting.
const values = value.map(val => formatSingleValue(val, mlFunction, fieldFormat));
const values = value.map(val => formatSingleValue(val, mlFunction, fieldFormat, record));
return `[${values}]`;
}
} else {
return formatSingleValue(value, mlFunction, fieldFormat);
return formatSingleValue(value, mlFunction, fieldFormat, record);
}
}

// Formats a single value according to the specified ML function.
// If a Kibana fieldFormat is not supplied, will fall back to default
// formatting depending on the magnitude of the value.
function formatSingleValue(value, mlFunction, fieldFormat) {
// For time_of_day or time_of_week functions the anomaly record
// containing the timestamp of the anomaly should be supplied in
// order to correctly format the day or week offset to the time of the anomaly.
function formatSingleValue(value, mlFunction, fieldFormat, record) {
if (value === undefined || value === null) {
return '';
}

// If the analysis function is time_of_week/day, format as day/time.
// For time_of_week / day, actual / typical is the UTC offset in seconds from the
// start of the week / day, so need to manipulate to UTC moment of the start of the week / day
// that the anomaly occurred using record timestamp if supplied, add on the offset, and finally
// revert back to configured timezone for formatting.
if (mlFunction === 'time_of_week') {
const d = new Date();
const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date());
const i = parseInt(value);
d.setTime(i * 1000);
return moment(d).format('ddd hh:mm');
const utcMoment = moment.utc(d).startOf('week').add(i, 's');
return moment(utcMoment.valueOf()).format('ddd HH:mm');
} else if (mlFunction === 'time_of_day') {
const d = new Date();
const d = ((record !== undefined && record.timestamp !== undefined) ? new Date(record.timestamp) : new Date());
const i = parseInt(value);
d.setTime(i * 1000);
return moment(d).format('hh:mm');
const utcMoment = moment.utc(d).startOf('day').add(i, 's');
return moment(utcMoment.valueOf()).format('HH:mm');
} else {
if (fieldFormat !== undefined) {
return fieldFormat.convert(value, 'text');
Expand Down

0 comments on commit 96d6cf2

Please sign in to comment.