From e92456cd8420321057020fdf56b880a05d8abf92 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 17 Nov 2020 10:07:46 -0600 Subject: [PATCH] [ML] Add UI test for feature importance features (#82677) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../decision_path_chart.tsx | 102 ++++----- .../decision_path_classification.tsx | 1 + .../decision_path_popover.tsx | 5 +- .../feature_importance_summary.tsx | 58 ++--- .../timeseries_chart/timeseries_chart.js | 12 +- .../timeseriesexplorer/timeseriesexplorer.js | 1 + .../feature_importance.ts | 211 ++++++++++++++++++ .../apps/ml/data_frame_analytics/index.ts | 1 + .../ml/data_frame_analytics_results.ts | 56 +++++ 9 files changed, 367 insertions(+), 80 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx index 908755b197fd7..6bfd7a66331df 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -116,57 +116,59 @@ export const DecisionPathChart = ({ const tickFormatter = useCallback((d) => formatSingleValue(d, '').toString(), []); return ( - - - {baselineData && ( - - )} - - + + + {baselineData && ( + )} - showGridLines={false} - position={Position.Top} - showOverlappingTicks - domain={ - minDomain && maxDomain - ? { - min: minDomain, - max: maxDomain, - } - : undefined - } - /> - - - + /> + + + + ); }; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx index 496bc37f571ce..7b091a06d1064 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx @@ -98,6 +98,7 @@ export const ClassificationDecisionPath: FC = ( {options !== undefined && ( = ({ ]; return ( - <> +
{tabs.map((tab) => ( setSelectedTabId(tab.id)} key={tab.id} @@ -146,6 +147,6 @@ export const DecisionPathPopover: FC = ({ {selectedTabId === DECISION_PATH_TABS.JSON && ( )} - +
); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 64835e7ca4c6d..0fab1cf75259e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -210,6 +210,7 @@ export const FeatureImportanceSummaryPanel: FC - +
+ + - - - - + + + + +
) } /> 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 8df186c5c3c6e..b2d054becbb1a 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 @@ -51,6 +51,7 @@ import { unhighlightFocusChartAnnotation, ANNOTATION_MIN_WIDTH, } from './timeseries_chart_annotations'; +import { distinctUntilChanged } from 'rxjs/operators'; const focusZoomPanelHeight = 25; const focusChartHeight = 310; @@ -570,6 +571,7 @@ class TimeseriesChartIntl extends Component { } renderFocusChart() { + console.log('renderFocusChart'); const { focusAggregationInterval, focusAnnotationData: focusAnnotationDataOriginalPropValue, @@ -1798,7 +1800,15 @@ class TimeseriesChartIntl extends Component { } export const TimeseriesChart = (props) => { - const annotationProp = useObservable(annotation$); + const annotationProp = useObservable( + annotation$.pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ) + ); + if (annotationProp === undefined) { return null; } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index e3b6e38f47bab..f14f11e5d6149 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -1014,6 +1014,7 @@ export class TimeSeriesExplorer extends React.Component { this.previousShowForecast = showForecast; this.previousShowModelBounds = showModelBounds; + console.log('Timeseriesexplorer rerendered'); return ( {fieldNamesWithEmptyValues.length > 0 && ( diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts new file mode 100644 index 0000000000000..ff2bbd077fa8f --- /dev/null +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/feature_importance.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DeepPartial } from '../../../../../plugins/ml/common/types/common'; +import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + describe('total feature importance panel and decision path popover', function () { + const testDataList: Array<{ + suiteTitle: string; + archive: string; + indexPattern: { name: string; timeField: string }; + job: DeepPartial; + }> = (() => { + const timestamp = Date.now(); + + return [ + { + suiteTitle: 'binary classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_binary_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_binary_${timestamp}`; + }, + results_field: 'ml_central_air', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'YearBuilt', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + ], + }, + analysis: { + classification: { + dependent_variable: 'CentralAir', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'CentralAir_prediction', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'multi class classification job', + archive: 'ml/ihp_outlier', + indexPattern: { name: 'ft_ihp_outlier', timeField: '@timestamp' }, + job: { + id: `ihp_fi_multi_${timestamp}`, + description: + "Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '35'", + source: { + index: ['ft_ihp_outlier'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-ihp_fi_multi_${timestamp}`; + }, + results_field: 'ml_heating_qc', + }, + analyzed_fields: { + includes: [ + 'CentralAir', + 'GarageArea', + 'GarageCars', + 'Electrical', + 'Neighborhood', + 'Heating', + '1stFlrSF', + 'HeatingQC', + ], + }, + analysis: { + classification: { + dependent_variable: 'HeatingQC', + num_top_feature_importance_values: 5, + training_percent: 35, + prediction_field_name: 'heatingqc', + num_top_classes: -1, + }, + }, + model_memory_limit: '60mb', + allow_lazy_start: false, + }, + }, + { + suiteTitle: 'regression job', + archive: 'ml/egs_regression', + indexPattern: { name: 'ft_egs_regression', timeField: '@timestamp' }, + job: { + id: `egs_fi_reg_${timestamp}`, + description: 'This is the job description', + source: { + index: ['ft_egs_regression'], + query: { + match_all: {}, + }, + }, + dest: { + get index(): string { + return `user-egs_fi_reg_${timestamp}`; + }, + results_field: 'ml', + }, + analysis: { + regression: { + prediction_field_name: 'test', + dependent_variable: 'stab', + num_top_feature_importance_values: 5, + training_percent: 35, + }, + }, + analyzed_fields: { + includes: [ + 'g1', + 'g2', + 'g3', + 'g4', + 'p1', + 'p2', + 'p3', + 'p4', + 'stab', + 'tau1', + 'tau2', + 'tau3', + 'tau4', + ], + excludes: [], + }, + model_memory_limit: '20mb', + }, + }, + ]; + })(); + + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + for (const testData of testDataList) { + await esArchiver.loadIfNeeded(testData.archive); + await ml.testResources.createIndexPatternIfNeeded( + testData.indexPattern.name, + testData.indexPattern.timeField + ); + await ml.api.createAndRunDFAJob(testData.job as DataFrameAnalyticsConfig); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function () { + before(async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToDataFrameAnalytics(); + await ml.dataFrameAnalyticsTable.waitForAnalyticsToLoad(); + await ml.dataFrameAnalyticsTable.openResultsView(testData.job.id as string); + }); + + after(async () => { + await ml.api.deleteIndices(testData.job.dest!.index as string); + await ml.testResources.deleteIndexPatternByTitle(testData.job.dest!.index as string); + }); + + it('should display the total feature importance in the results view', async () => { + await ml.dataFrameAnalyticsResults.assertTotalFeatureImportanceEvaluatePanelExists(); + }); + + it('should display the feature importance decision path in the data grid', async () => { + await ml.dataFrameAnalyticsResults.assertResultsTableExists(); + await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); + await ml.dataFrameAnalyticsResults.openFeatureImportanceDecisionPathPopover(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathElementsExists(); + await ml.dataFrameAnalyticsResults.assertFeatureImportanceDecisionPathChartElementsExists(); + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts index 0202c8431ce34..a57d26b536b4f 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./regression_creation')); loadTestFile(require.resolve('./classification_creation')); loadTestFile(require.resolve('./cloning')); + loadTestFile(require.resolve('./feature_importance')); }); } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index 8781a2cd216f2..1ac11a0149897 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -5,12 +5,14 @@ */ import expect from '@kbn/expect'; +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; export function MachineLearningDataFrameAnalyticsResultsProvider({ getService, }: FtrProviderContext) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -60,5 +62,59 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ `DFA results table should have at least one row (got '${resultTableRows.length}')` ); }, + + async assertTotalFeatureImportanceEvaluatePanelExists() { + await testSubjects.existOrFail('mlDFExpandableSection-FeatureImportanceSummary'); + await testSubjects.existOrFail('mlTotalFeatureImportanceChart', { timeout: 5000 }); + }, + + async assertFeatureImportanceDecisionPathElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_chart', { + timeout: 5000, + }); + await testSubjects.existOrFail('mlDFADecisionPathPopoverTab-decision_path_json', { + timeout: 5000, + }); + }, + + async assertFeatureImportanceDecisionPathChartElementsExists() { + await testSubjects.existOrFail('mlDFADecisionPathChart', { + timeout: 5000, + }); + }, + + async openFeatureImportanceDecisionPathPopover() { + this.assertResultsTableNotEmpty(); + + const featureImportanceCell = await this.getFirstFeatureImportanceCell(); + const interactionButton = await featureImportanceCell.findByTagName('button'); + + // simulate hover and wait for button to appear + await featureImportanceCell.moveMouseTo(); + await this.waitForInteractionButtonToDisplay(interactionButton); + + // open popover + await interactionButton.click(); + await testSubjects.existOrFail('mlDFADecisionPathPopover'); + }, + + async getFirstFeatureImportanceCell(): Promise { + // get first row of the data grid + const firstDataGridRow = await testSubjects.find( + 'mlExplorationDataGrid loaded > dataGridRow' + ); + // find the feature importance cell in that row + const featureImportanceCell = await firstDataGridRow.findByCssSelector( + '[data-test-subj="dataGridRowCell"][class*="featureImportance"]' + ); + return featureImportanceCell; + }, + + async waitForInteractionButtonToDisplay(interactionButton: WebElementWrapper) { + await retry.tryForTime(5000, async () => { + const buttonVisible = await interactionButton.isDisplayed(); + expect(buttonVisible).to.equal(true, 'Expected data grid cell button to be visible'); + }); + }, }; }