[ML] DF Analytics: create classification jobs results view (#52584)
* wip: create classification results page + table and evaluate panel * enable view link for classification jobs * wip: fetch classification eval data * wip: display confusion matrix in datagrid * evaluate panel: add heatmap for cells and doc count * Update use of loadEvalData in expanded row component * Add metric type for evaluate endpoint and fix localization error * handle no incorrect prediction classes case for confusion matrix. remove unused translation * setCellProps needs to be called from a lifecycle method - wrap in useEffect * TypeScript improvements * fix datagrid column resize affecting results table * allow custom prediction field for classification jobs * ensure values are rounded correctly and add tooltip * temp workaroun for datagrid width issues
This commit is contained in:
parent
79a8528646
commit
0cd5bb0ca9
|
@ -1,5 +1,6 @@
|
|||
@import 'pages/analytics_exploration/components/exploration/index';
|
||||
@import 'pages/analytics_exploration/components/regression_exploration/index';
|
||||
@import 'pages/analytics_exploration/components/classification_exploration/index';
|
||||
@import 'pages/analytics_management/components/analytics_list/index';
|
||||
@import 'pages/analytics_management/components/create_analytics_form/index';
|
||||
@import 'pages/analytics_management/components/create_analytics_flyout/index';
|
||||
|
|
|
@ -35,6 +35,7 @@ interface ClassificationAnalysis {
|
|||
dependent_variable: string;
|
||||
training_percent?: number;
|
||||
num_top_classes?: string;
|
||||
prediction_field_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -74,13 +75,33 @@ export interface RegressionEvaluateResponse {
|
|||
};
|
||||
}
|
||||
|
||||
export interface PredictedClass {
|
||||
predicted_class: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ConfusionMatrix {
|
||||
actual_class: string;
|
||||
actual_class_doc_count: number;
|
||||
predicted_classes: PredictedClass[];
|
||||
other_predicted_class_doc_count: number;
|
||||
}
|
||||
|
||||
export interface ClassificationEvaluateResponse {
|
||||
classification: {
|
||||
multiclass_confusion_matrix: {
|
||||
confusion_matrix: ConfusionMatrix[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface GenericAnalysis {
|
||||
[key: string]: Record<string, any>;
|
||||
}
|
||||
|
||||
interface LoadEvaluateResult {
|
||||
success: boolean;
|
||||
eval: RegressionEvaluateResponse | null;
|
||||
eval: RegressionEvaluateResponse | ClassificationEvaluateResponse | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
|
@ -109,6 +130,7 @@ export const getAnalysisType = (analysis: AnalysisConfig) => {
|
|||
|
||||
export const getDependentVar = (analysis: AnalysisConfig) => {
|
||||
let depVar = '';
|
||||
|
||||
if (isRegressionAnalysis(analysis)) {
|
||||
depVar = analysis.regression.dependent_variable;
|
||||
}
|
||||
|
@ -124,17 +146,26 @@ export const getPredictionFieldName = (analysis: AnalysisConfig) => {
|
|||
let predictionFieldName;
|
||||
if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) {
|
||||
predictionFieldName = analysis.regression.prediction_field_name;
|
||||
} else if (
|
||||
isClassificationAnalysis(analysis) &&
|
||||
analysis.classification.prediction_field_name !== undefined
|
||||
) {
|
||||
predictionFieldName = analysis.classification.prediction_field_name;
|
||||
}
|
||||
return predictionFieldName;
|
||||
};
|
||||
|
||||
export const getPredictedFieldName = (resultsField: string, analysis: AnalysisConfig) => {
|
||||
export const getPredictedFieldName = (
|
||||
resultsField: string,
|
||||
analysis: AnalysisConfig,
|
||||
forSort?: boolean
|
||||
) => {
|
||||
// default is 'ml'
|
||||
const predictionFieldName = getPredictionFieldName(analysis);
|
||||
const defaultPredictionField = `${getDependentVar(analysis)}_prediction`;
|
||||
const predictedField = `${resultsField}.${
|
||||
predictionFieldName ? predictionFieldName : defaultPredictionField
|
||||
}`;
|
||||
}${isClassificationAnalysis(analysis) && !forSort ? '.keyword' : ''}`;
|
||||
return predictedField;
|
||||
};
|
||||
|
||||
|
@ -153,13 +184,32 @@ export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysi
|
|||
return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION;
|
||||
};
|
||||
|
||||
export const isRegressionResultsSearchBoolQuery = (
|
||||
arg: any
|
||||
): arg is RegressionResultsSearchBoolQuery => {
|
||||
export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => {
|
||||
const keys = Object.keys(arg);
|
||||
return keys.length === 1 && keys[0] === 'bool';
|
||||
};
|
||||
|
||||
export const isRegressionEvaluateResponse = (arg: any): arg is RegressionEvaluateResponse => {
|
||||
const keys = Object.keys(arg);
|
||||
return (
|
||||
keys.length === 1 &&
|
||||
keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION &&
|
||||
arg?.regression?.mean_squared_error !== undefined &&
|
||||
arg?.regression?.r_squared !== undefined
|
||||
);
|
||||
};
|
||||
|
||||
export const isClassificationEvaluateResponse = (
|
||||
arg: any
|
||||
): arg is ClassificationEvaluateResponse => {
|
||||
const keys = Object.keys(arg);
|
||||
return (
|
||||
keys.length === 1 &&
|
||||
keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION &&
|
||||
arg?.classification?.multiclass_confusion_matrix !== undefined
|
||||
);
|
||||
};
|
||||
|
||||
export interface DataFrameAnalyticsConfig {
|
||||
id: DataFrameAnalyticsId;
|
||||
// Description attribute is not supported yet
|
||||
|
@ -254,17 +304,14 @@ export function getValuesFromResponse(response: RegressionEvaluateResponse) {
|
|||
|
||||
return { meanSquaredError, rSquared };
|
||||
}
|
||||
interface RegressionResultsSearchBoolQuery {
|
||||
interface ResultsSearchBoolQuery {
|
||||
bool: Dictionary<any>;
|
||||
}
|
||||
interface RegressionResultsSearchTermQuery {
|
||||
interface ResultsSearchTermQuery {
|
||||
term: Dictionary<any>;
|
||||
}
|
||||
|
||||
export type RegressionResultsSearchQuery =
|
||||
| RegressionResultsSearchBoolQuery
|
||||
| RegressionResultsSearchTermQuery
|
||||
| SavedSearchQuery;
|
||||
export type ResultsSearchQuery = ResultsSearchBoolQuery | ResultsSearchTermQuery | SavedSearchQuery;
|
||||
|
||||
export function getEvalQueryBody({
|
||||
resultsField,
|
||||
|
@ -274,16 +321,16 @@ export function getEvalQueryBody({
|
|||
}: {
|
||||
resultsField: string;
|
||||
isTraining: boolean;
|
||||
searchQuery?: RegressionResultsSearchQuery;
|
||||
searchQuery?: ResultsSearchQuery;
|
||||
ignoreDefaultQuery?: boolean;
|
||||
}) {
|
||||
let query: RegressionResultsSearchQuery = {
|
||||
let query: ResultsSearchQuery = {
|
||||
term: { [`${resultsField}.is_training`]: { value: isTraining } },
|
||||
};
|
||||
|
||||
if (searchQuery !== undefined && ignoreDefaultQuery === true) {
|
||||
query = searchQuery;
|
||||
} else if (searchQuery !== undefined && isRegressionResultsSearchBoolQuery(searchQuery)) {
|
||||
} else if (searchQuery !== undefined && isResultsSearchBoolQuery(searchQuery)) {
|
||||
const searchQueryClone = cloneDeep(searchQuery);
|
||||
searchQueryClone.bool.must.push(query);
|
||||
query = searchQueryClone;
|
||||
|
@ -291,6 +338,27 @@ export function getEvalQueryBody({
|
|||
return query;
|
||||
}
|
||||
|
||||
interface EvaluateMetrics {
|
||||
classification: {
|
||||
multiclass_confusion_matrix: object;
|
||||
};
|
||||
regression: {
|
||||
r_squared: object;
|
||||
mean_squared_error: object;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoadEvalDataConfig {
|
||||
isTraining: boolean;
|
||||
index: string;
|
||||
dependentVariable: string;
|
||||
resultsField: string;
|
||||
predictionFieldName?: string;
|
||||
searchQuery?: ResultsSearchQuery;
|
||||
ignoreDefaultQuery?: boolean;
|
||||
jobType: ANALYSIS_CONFIG_TYPE;
|
||||
}
|
||||
|
||||
export const loadEvalData = async ({
|
||||
isTraining,
|
||||
index,
|
||||
|
@ -299,34 +367,38 @@ export const loadEvalData = async ({
|
|||
predictionFieldName,
|
||||
searchQuery,
|
||||
ignoreDefaultQuery,
|
||||
}: {
|
||||
isTraining: boolean;
|
||||
index: string;
|
||||
dependentVariable: string;
|
||||
resultsField: string;
|
||||
predictionFieldName?: string;
|
||||
searchQuery?: RegressionResultsSearchQuery;
|
||||
ignoreDefaultQuery?: boolean;
|
||||
}) => {
|
||||
jobType,
|
||||
}: LoadEvalDataConfig) => {
|
||||
const results: LoadEvaluateResult = { success: false, eval: null, error: null };
|
||||
const defaultPredictionField = `${dependentVariable}_prediction`;
|
||||
const predictedField = `${resultsField}.${
|
||||
let predictedField = `${resultsField}.${
|
||||
predictionFieldName ? predictionFieldName : defaultPredictionField
|
||||
}`;
|
||||
|
||||
if (jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) {
|
||||
predictedField = `${predictedField}.keyword`;
|
||||
}
|
||||
|
||||
const query = getEvalQueryBody({ resultsField, isTraining, searchQuery, ignoreDefaultQuery });
|
||||
|
||||
const metrics: EvaluateMetrics = {
|
||||
classification: {
|
||||
multiclass_confusion_matrix: {},
|
||||
},
|
||||
regression: {
|
||||
r_squared: {},
|
||||
mean_squared_error: {},
|
||||
},
|
||||
};
|
||||
|
||||
const config = {
|
||||
index,
|
||||
query,
|
||||
evaluation: {
|
||||
regression: {
|
||||
[jobType]: {
|
||||
actual_field: dependentVariable,
|
||||
predicted_field: predictedField,
|
||||
metrics: {
|
||||
r_squared: {},
|
||||
mean_squared_error: {},
|
||||
},
|
||||
metrics: metrics[jobType as keyof EvaluateMetrics],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -341,3 +413,57 @@ export const loadEvalData = async ({
|
|||
return results;
|
||||
}
|
||||
};
|
||||
|
||||
interface TrackTotalHitsSearchResponse {
|
||||
hits: {
|
||||
total: {
|
||||
value: number;
|
||||
relation: string;
|
||||
};
|
||||
hits: any[];
|
||||
};
|
||||
}
|
||||
|
||||
interface LoadDocsCountConfig {
|
||||
ignoreDefaultQuery?: boolean;
|
||||
isTraining: boolean;
|
||||
searchQuery: SavedSearchQuery;
|
||||
resultsField: string;
|
||||
destIndex: string;
|
||||
}
|
||||
|
||||
interface LoadDocsCountResponse {
|
||||
docsCount: number | null;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export const loadDocsCount = async ({
|
||||
ignoreDefaultQuery = true,
|
||||
isTraining,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex,
|
||||
}: LoadDocsCountConfig): Promise<LoadDocsCountResponse> => {
|
||||
const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery });
|
||||
|
||||
try {
|
||||
const body: SearchQuery = {
|
||||
track_total_hits: true,
|
||||
query,
|
||||
};
|
||||
|
||||
const resp: TrackTotalHitsSearchResponse = await ml.esSearch({
|
||||
index: destIndex,
|
||||
size: 0,
|
||||
body,
|
||||
});
|
||||
|
||||
const docsCount = resp.hits.total && resp.hits.total.value;
|
||||
return { docsCount, success: docsCount !== undefined };
|
||||
} catch (e) {
|
||||
return {
|
||||
docsCount: null,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -77,7 +77,7 @@ export const sortRegressionResultsFields = (
|
|||
) => {
|
||||
const dependentVariable = getDependentVar(jobConfig.analysis);
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis);
|
||||
const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true);
|
||||
if (a === `${resultsField}.is_training`) {
|
||||
return -1;
|
||||
}
|
||||
|
@ -96,6 +96,14 @@ export const sortRegressionResultsFields = (
|
|||
if (b === dependentVariable) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a === `${resultsField}.prediction_probability`) {
|
||||
return -1;
|
||||
}
|
||||
if (b === `${resultsField}.prediction_probability`) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
|
||||
|
@ -107,7 +115,7 @@ export const sortRegressionResultsColumns = (
|
|||
) => (a: string, b: string) => {
|
||||
const dependentVariable = getDependentVar(jobConfig.analysis);
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis);
|
||||
const predictedField = getPredictedFieldName(resultsField, jobConfig.analysis, true);
|
||||
|
||||
const typeofA = typeof obj[a];
|
||||
const typeofB = typeof obj[b];
|
||||
|
@ -136,6 +144,14 @@ export const sortRegressionResultsColumns = (
|
|||
return 1;
|
||||
}
|
||||
|
||||
if (a === `${resultsField}.prediction_probability`) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (b === `${resultsField}.prediction_probability`) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (typeofA !== 'string' && typeofB === 'string') {
|
||||
return 1;
|
||||
}
|
||||
|
@ -184,6 +200,43 @@ export function getFlattenedFields(obj: EsDocSource, resultsField: string): EsFi
|
|||
return flatDocFields.filter(f => f !== ML__ID_COPY);
|
||||
}
|
||||
|
||||
export const getDefaultClassificationFields = (
|
||||
docs: EsDoc[],
|
||||
jobConfig: DataFrameAnalyticsConfig
|
||||
): EsFieldName[] => {
|
||||
if (docs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
const newDocFields = getFlattenedFields(docs[0]._source, resultsField);
|
||||
return newDocFields
|
||||
.filter(k => {
|
||||
if (k === `${resultsField}.is_training`) {
|
||||
return true;
|
||||
}
|
||||
// predicted value of dependent variable
|
||||
if (k === getPredictedFieldName(resultsField, jobConfig.analysis, true)) {
|
||||
return true;
|
||||
}
|
||||
// actual value of dependent variable
|
||||
if (k === getDependentVar(jobConfig.analysis)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (k === `${resultsField}.prediction_probability`) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (k.split('.')[0] === resultsField) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return docs.some(row => row._source[k] !== null);
|
||||
})
|
||||
.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig))
|
||||
.slice(0, DEFAULT_REGRESSION_COLUMNS);
|
||||
};
|
||||
|
||||
export const getDefaultRegressionFields = (
|
||||
docs: EsDoc[],
|
||||
jobConfig: DataFrameAnalyticsConfig
|
||||
|
|
|
@ -20,6 +20,7 @@ export {
|
|||
RegressionEvaluateResponse,
|
||||
getValuesFromResponse,
|
||||
loadEvalData,
|
||||
loadDocsCount,
|
||||
Eval,
|
||||
getPredictedFieldName,
|
||||
INDEX_STATUS,
|
||||
|
@ -31,6 +32,7 @@ export {
|
|||
export {
|
||||
getDefaultSelectableFields,
|
||||
getDefaultRegressionFields,
|
||||
getDefaultClassificationFields,
|
||||
getFlattenedFields,
|
||||
sortColumns,
|
||||
sortRegressionResultsColumns,
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.euiFormRow.mlDataFrameAnalyticsClassification__actualLabel {
|
||||
padding-top: $euiSize * 4;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
@import 'classification_exploration';
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 React, { FC, Fragment, useState, useEffect } from 'react';
|
||||
import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { DataFrameAnalyticsConfig } from '../../../../common';
|
||||
import { EvaluatePanel } from './evaluate_panel';
|
||||
import { ResultsTable } from './results_table';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
|
||||
import { LoadingPanel } from '../loading_panel';
|
||||
|
||||
interface GetDataFrameAnalyticsResponse {
|
||||
count: number;
|
||||
data_frame_analytics: DataFrameAnalyticsConfig[];
|
||||
}
|
||||
|
||||
export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.classificationExploration.tableJobIdTitle', {
|
||||
defaultMessage: 'Destination index for classification job ID {jobId}',
|
||||
values: { jobId },
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
);
|
||||
|
||||
interface Props {
|
||||
jobId: string;
|
||||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
}
|
||||
|
||||
export const ClassificationExploration: FC<Props> = ({ jobId, jobStatus }) => {
|
||||
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
|
||||
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
|
||||
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
|
||||
|
||||
const loadJobConfig = async () => {
|
||||
setIsLoadingJobConfig(true);
|
||||
try {
|
||||
const analyticsConfigs: GetDataFrameAnalyticsResponse = await ml.dataFrameAnalytics.getDataFrameAnalytics(
|
||||
jobId
|
||||
);
|
||||
if (
|
||||
Array.isArray(analyticsConfigs.data_frame_analytics) &&
|
||||
analyticsConfigs.data_frame_analytics.length > 0
|
||||
) {
|
||||
setJobConfig(analyticsConfigs.data_frame_analytics[0]);
|
||||
setIsLoadingJobConfig(false);
|
||||
} else {
|
||||
setJobConfigErrorMessage(
|
||||
i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationNoResultsMessage',
|
||||
{
|
||||
defaultMessage: 'No results found.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message !== undefined) {
|
||||
setJobConfigErrorMessage(e.message);
|
||||
} else {
|
||||
setJobConfigErrorMessage(JSON.stringify(e));
|
||||
}
|
||||
setIsLoadingJobConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadJobConfig();
|
||||
}, []);
|
||||
|
||||
if (jobConfigErrorMessage !== undefined) {
|
||||
return (
|
||||
<EuiPanel grow={false}>
|
||||
<ExplorationTitle jobId={jobId} />
|
||||
<EuiSpacer />
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.jobConfigurationFetchError',
|
||||
{
|
||||
defaultMessage:
|
||||
'Unable to fetch results. An error occurred loading the job configuration data.',
|
||||
}
|
||||
)}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>{jobConfigErrorMessage}</p>
|
||||
</EuiCallOut>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
|
||||
{isLoadingJobConfig === false && jobConfig !== undefined && (
|
||||
<EvaluatePanel jobConfig={jobConfig} jobStatus={jobStatus} searchQuery={searchQuery} />
|
||||
)}
|
||||
<EuiSpacer />
|
||||
{isLoadingJobConfig === true && jobConfig === undefined && <LoadingPanel />}
|
||||
{isLoadingJobConfig === false && jobConfig !== undefined && (
|
||||
<ResultsTable
|
||||
jobConfig={jobConfig}
|
||||
jobStatus={jobStatus}
|
||||
setEvaluateSearchQuery={setSearchQuery}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { ConfusionMatrix, PredictedClass } from '../../../../common/analytics';
|
||||
|
||||
interface ColumnData {
|
||||
actual_class: string;
|
||||
actual_class_doc_count: number;
|
||||
predicted_class?: string;
|
||||
count?: number;
|
||||
error_count?: number;
|
||||
}
|
||||
|
||||
export function getColumnData(confusionMatrixData: ConfusionMatrix[]) {
|
||||
const colData: Partial<ColumnData[]> = [];
|
||||
|
||||
confusionMatrixData.forEach((classData: any) => {
|
||||
const correctlyPredictedClass = classData.predicted_classes.find(
|
||||
(pc: PredictedClass) => pc.predicted_class === classData.actual_class
|
||||
);
|
||||
const incorrectlyPredictedClass = classData.predicted_classes.find(
|
||||
(pc: PredictedClass) => pc.predicted_class !== classData.actual_class
|
||||
);
|
||||
|
||||
let accuracy;
|
||||
if (correctlyPredictedClass !== undefined) {
|
||||
accuracy = correctlyPredictedClass.count / classData.actual_class_doc_count;
|
||||
// round to 2 decimal places without converting to string;
|
||||
accuracy = Math.round(accuracy * 100) / 100;
|
||||
}
|
||||
|
||||
let error;
|
||||
if (incorrectlyPredictedClass !== undefined) {
|
||||
error = incorrectlyPredictedClass.count / classData.actual_class_doc_count;
|
||||
error = Math.round(error * 100) / 100;
|
||||
}
|
||||
|
||||
let col: any = {
|
||||
actual_class: classData.actual_class,
|
||||
actual_class_doc_count: classData.actual_class_doc_count,
|
||||
};
|
||||
|
||||
if (correctlyPredictedClass !== undefined) {
|
||||
col = {
|
||||
...col,
|
||||
predicted_class: correctlyPredictedClass.predicted_class,
|
||||
[correctlyPredictedClass.predicted_class]: accuracy,
|
||||
count: correctlyPredictedClass.count,
|
||||
accuracy,
|
||||
};
|
||||
}
|
||||
|
||||
if (incorrectlyPredictedClass !== undefined) {
|
||||
col = {
|
||||
...col,
|
||||
[incorrectlyPredictedClass.predicted_class]: error,
|
||||
error_count: incorrectlyPredictedClass.count,
|
||||
};
|
||||
}
|
||||
|
||||
colData.push(col);
|
||||
});
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
id: 'actual_class',
|
||||
display: <span />,
|
||||
},
|
||||
];
|
||||
|
||||
colData.forEach((data: any) => {
|
||||
columns.push({ id: data.predicted_class });
|
||||
});
|
||||
|
||||
return { columns, columnData: colData };
|
||||
}
|
|
@ -0,0 +1,343 @@
|
|||
/*
|
||||
* 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 React, { FC, useState, useEffect, Fragment } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import {
|
||||
EuiDataGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { ErrorCallout } from '../error_callout';
|
||||
import {
|
||||
getDependentVar,
|
||||
getPredictionFieldName,
|
||||
loadEvalData,
|
||||
loadDocsCount,
|
||||
DataFrameAnalyticsConfig,
|
||||
} from '../../../../common';
|
||||
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import {
|
||||
isResultsSearchBoolQuery,
|
||||
isClassificationEvaluateResponse,
|
||||
ConfusionMatrix,
|
||||
ResultsSearchQuery,
|
||||
ANALYSIS_CONFIG_TYPE,
|
||||
} from '../../../../common/analytics';
|
||||
import { LoadingPanel } from '../loading_panel';
|
||||
import { getColumnData } from './column_data';
|
||||
|
||||
const defaultPanelWidth = 500;
|
||||
|
||||
interface Props {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
searchQuery: ResultsSearchQuery;
|
||||
}
|
||||
|
||||
export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery }) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [confusionMatrixData, setConfusionMatrixData] = useState<ConfusionMatrix[]>([]);
|
||||
const [columns, setColumns] = useState<any>([]);
|
||||
const [columnsData, setColumnsData] = useState<any>([]);
|
||||
const [popoverContents, setPopoverContents] = useState<any>([]);
|
||||
const [docsCount, setDocsCount] = useState<null | number>(null);
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const [panelWidth, setPanelWidth] = useState<number>(defaultPanelWidth);
|
||||
|
||||
// Column visibility
|
||||
const [visibleColumns, setVisibleColumns] = useState(() =>
|
||||
columns.map(({ id }: { id: string }) => id)
|
||||
);
|
||||
|
||||
const index = jobConfig.dest.index;
|
||||
const dependentVariable = getDependentVar(jobConfig.analysis);
|
||||
const predictionFieldName = getPredictionFieldName(jobConfig.analysis);
|
||||
// default is 'ml'
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
|
||||
const loadData = async ({
|
||||
isTrainingClause,
|
||||
ignoreDefaultQuery = true,
|
||||
}: {
|
||||
isTrainingClause: { query: string; operator: string };
|
||||
ignoreDefaultQuery?: boolean;
|
||||
}) => {
|
||||
setIsLoading(true);
|
||||
|
||||
const evalData = await loadEvalData({
|
||||
isTraining: false,
|
||||
index,
|
||||
dependentVariable,
|
||||
resultsField,
|
||||
predictionFieldName,
|
||||
searchQuery,
|
||||
ignoreDefaultQuery,
|
||||
jobType: ANALYSIS_CONFIG_TYPE.CLASSIFICATION,
|
||||
});
|
||||
|
||||
const docsCountResp = await loadDocsCount({
|
||||
isTraining: false,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex: jobConfig.dest.index,
|
||||
});
|
||||
|
||||
if (
|
||||
evalData.success === true &&
|
||||
evalData.eval &&
|
||||
isClassificationEvaluateResponse(evalData.eval)
|
||||
) {
|
||||
const confusionMatrix =
|
||||
evalData.eval?.classification?.multiclass_confusion_matrix?.confusion_matrix;
|
||||
setError(null);
|
||||
setConfusionMatrixData(confusionMatrix || []);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setConfusionMatrixData([]);
|
||||
setError(evalData.error);
|
||||
}
|
||||
|
||||
if (docsCountResp.success === true) {
|
||||
setDocsCount(docsCountResp.docsCount);
|
||||
} else {
|
||||
setDocsCount(null);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeHandler = () => {
|
||||
const tablePanelWidth: number =
|
||||
document.getElementById('mlDataFrameAnalyticsTableResultsPanel')?.clientWidth ||
|
||||
defaultPanelWidth;
|
||||
// Keep the evaluate panel width slightly smaller than the results table
|
||||
// to ensure results table can resize correctly. Temporary workaround DataGrid issue with flex
|
||||
const newWidth = tablePanelWidth - 8;
|
||||
setPanelWidth(newWidth);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeHandler();
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (confusionMatrixData.length > 0) {
|
||||
const { columns: derivedColumns, columnData } = getColumnData(confusionMatrixData);
|
||||
// Initialize all columns as visible
|
||||
setVisibleColumns(() => derivedColumns.map(({ id }: { id: string }) => id));
|
||||
setColumns(derivedColumns);
|
||||
setColumnsData(columnData);
|
||||
setPopoverContents({
|
||||
numeric: ({
|
||||
cellContentsElement,
|
||||
children,
|
||||
}: {
|
||||
cellContentsElement: any;
|
||||
children: any;
|
||||
}) => {
|
||||
const rowIndex = children?.props?.rowIndex;
|
||||
const colId = children?.props?.columnId;
|
||||
const gridItem = columnData[rowIndex];
|
||||
|
||||
if (gridItem !== undefined) {
|
||||
const count = colId === gridItem.actual_class ? gridItem.count : gridItem.error_count;
|
||||
return `${count} / ${gridItem.actual_class_doc_count} * 100 = ${cellContentsElement.textContent}`;
|
||||
}
|
||||
|
||||
return cellContentsElement.textContent;
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [confusionMatrixData]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasIsTrainingClause =
|
||||
isResultsSearchBoolQuery(searchQuery) &&
|
||||
searchQuery.bool.must.filter(
|
||||
(clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined
|
||||
);
|
||||
const isTrainingClause =
|
||||
hasIsTrainingClause &&
|
||||
hasIsTrainingClause[0] &&
|
||||
hasIsTrainingClause[0].match[`${resultsField}.is_training`];
|
||||
|
||||
loadData({ isTrainingClause });
|
||||
}, [JSON.stringify(searchQuery)]);
|
||||
|
||||
const renderCellValue = ({
|
||||
rowIndex,
|
||||
columnId,
|
||||
setCellProps,
|
||||
}: {
|
||||
rowIndex: number;
|
||||
columnId: string;
|
||||
setCellProps: any;
|
||||
}) => {
|
||||
const cellValue = columnsData[rowIndex][columnId];
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
setCellProps({
|
||||
style: {
|
||||
backgroundColor: `rgba(0, 179, 164, ${cellValue})`,
|
||||
},
|
||||
});
|
||||
}, [rowIndex, columnId, setCellProps]);
|
||||
return (
|
||||
<span>{typeof cellValue === 'number' ? `${Math.round(cellValue * 100)}%` : cellValue}</span>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading === true) {
|
||||
return <LoadingPanel />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel style={{ width: `${panelWidth}px` }}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.evaluateJobIdTitle',
|
||||
{
|
||||
defaultMessage: 'Evaluation of classification job ID {jobId}',
|
||||
values: { jobId: jobConfig.id },
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>{getTaskStateBadge(jobStatus)}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{error !== null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<ErrorCallout error={error} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{error === null && (
|
||||
<Fragment>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiTitle size="xxs">
|
||||
<span>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText',
|
||||
{
|
||||
defaultMessage: 'Normalized confusion matrix',
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
anchorClassName="mlDataFrameAnalyticsClassificationInfoTooltip"
|
||||
content={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'The multi-class confusion matrix contains the number of occurrences where the analysis classified data points correctly with their actual class as well as the number of occurrences where it misclassified them with another class',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{docsCount !== null && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analytics.classificationExploration.generalizationDocsCount"
|
||||
defaultMessage="{docsCount, plural, one {# doc} other {# docs}} evaluated"
|
||||
values={{ docsCount }}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{/* BEGIN TABLE ELEMENTS */}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s" style={{ paddingLeft: '10%', paddingRight: '10%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
className="mlDataFrameAnalyticsClassification__actualLabel"
|
||||
helpText={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixActualLabel',
|
||||
{
|
||||
defaultMessage: 'Actual label',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Fragment />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{columns.length > 0 && columnsData.length > 0 && (
|
||||
<Fragment>
|
||||
<EuiFlexGroup direction="column" justifyContent="center" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSpacer />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixPredictedLabel',
|
||||
{
|
||||
defaultMessage: 'Predicted label',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Fragment />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ width: '90%' }}>
|
||||
<EuiDataGrid
|
||||
aria-label="Data grid demo"
|
||||
columns={columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns }}
|
||||
rowCount={columnsData.length}
|
||||
renderCellValue={renderCellValue}
|
||||
inMemory={{ level: 'sorting' }}
|
||||
toolbarVisibility={false}
|
||||
popoverContents={popoverContents}
|
||||
gridStyle={{ rowHover: 'none' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</Fragment>
|
||||
)}
|
||||
{/* END TABLE ELEMENTS */}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { ClassificationExploration } from './classification_exploration';
|
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
* 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 React, { Fragment, FC, useEffect, useState } from 'react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonIcon,
|
||||
EuiCallOut,
|
||||
EuiCheckbox,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPanel,
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiProgress,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
Query,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { Query as QueryType } from '../../../analytics_management/components/analytics_list/common';
|
||||
|
||||
import {
|
||||
ColumnType,
|
||||
mlInMemoryTableBasicFactory,
|
||||
OnTableChangeArg,
|
||||
SortingPropType,
|
||||
SORT_DIRECTION,
|
||||
} from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../util/date_utils';
|
||||
import { SavedSearchQuery } from '../../../../../contexts/kibana';
|
||||
|
||||
import {
|
||||
sortRegressionResultsColumns,
|
||||
sortRegressionResultsFields,
|
||||
toggleSelectedField,
|
||||
DataFrameAnalyticsConfig,
|
||||
EsFieldName,
|
||||
EsDoc,
|
||||
MAX_COLUMNS,
|
||||
getPredictedFieldName,
|
||||
INDEX_STATUS,
|
||||
SEARCH_SIZE,
|
||||
defaultSearchQuery,
|
||||
} from '../../../../common';
|
||||
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
|
||||
import { useExploreData, TableItem } from './use_explore_data';
|
||||
import { ExplorationTitle } from './classification_exploration';
|
||||
|
||||
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
|
||||
|
||||
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
|
||||
interface Props {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
setEvaluateSearchQuery: React.Dispatch<React.SetStateAction<object>>;
|
||||
}
|
||||
|
||||
export const ResultsTable: FC<Props> = React.memo(
|
||||
({ jobConfig, jobStatus, setEvaluateSearchQuery }) => {
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(25);
|
||||
const [selectedFields, setSelectedFields] = useState([] as EsFieldName[]);
|
||||
const [isColumnsPopoverVisible, setColumnsPopoverVisible] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState<SavedSearchQuery>(defaultSearchQuery);
|
||||
const [searchError, setSearchError] = useState<any>(undefined);
|
||||
const [searchString, setSearchString] = useState<string | undefined>(undefined);
|
||||
|
||||
function toggleColumnsPopover() {
|
||||
setColumnsPopoverVisible(!isColumnsPopoverVisible);
|
||||
}
|
||||
|
||||
function closeColumnsPopover() {
|
||||
setColumnsPopoverVisible(false);
|
||||
}
|
||||
|
||||
function toggleColumn(column: EsFieldName) {
|
||||
if (tableItems.length > 0 && jobConfig !== undefined) {
|
||||
// spread to a new array otherwise the component wouldn't re-render
|
||||
setSelectedFields([...toggleSelectedField(selectedFields, column)]);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
errorMessage,
|
||||
loadExploreData,
|
||||
sortField,
|
||||
sortDirection,
|
||||
status,
|
||||
tableItems,
|
||||
} = useExploreData(jobConfig, selectedFields, setSelectedFields);
|
||||
|
||||
let docFields: EsFieldName[] = [];
|
||||
let docFieldsCount = 0;
|
||||
if (tableItems.length > 0) {
|
||||
docFields = Object.keys(tableItems[0]);
|
||||
docFields.sort((a, b) => sortRegressionResultsFields(a, b, jobConfig));
|
||||
docFieldsCount = docFields.length;
|
||||
}
|
||||
|
||||
const columns: Array<ColumnType<TableItem>> = [];
|
||||
|
||||
if (jobConfig !== undefined && selectedFields.length > 0 && tableItems.length > 0) {
|
||||
columns.push(
|
||||
...selectedFields.sort(sortRegressionResultsColumns(tableItems[0], jobConfig)).map(k => {
|
||||
const column: ColumnType<TableItem> = {
|
||||
field: k,
|
||||
name: k,
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
};
|
||||
|
||||
const render = (d: any, fullItem: EsDoc) => {
|
||||
if (Array.isArray(d) && d.every(item => typeof item === 'string')) {
|
||||
// If the cells data is an array of strings, return as a comma separated list.
|
||||
// The list will get limited to 5 items with `…` at the end if there's more in the original array.
|
||||
return `${d.slice(0, 5).join(', ')}${d.length > 5 ? ', …' : ''}`;
|
||||
} else if (Array.isArray(d)) {
|
||||
// If the cells data is an array of e.g. objects, display a 'array' badge with a
|
||||
// tooltip that explains that this type of field is not supported in this table.
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayToolTipContent',
|
||||
{
|
||||
defaultMessage:
|
||||
'The full content of this array based column cannot be displayed.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiBadge>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.indexArrayBadgeContent',
|
||||
{
|
||||
defaultMessage: 'array',
|
||||
}
|
||||
)}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
} else if (typeof d === 'object' && d !== null) {
|
||||
// If the cells data is an object, display a 'object' badge with a
|
||||
// tooltip that explains that this type of field is not supported in this table.
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.indexObjectToolTipContent',
|
||||
{
|
||||
defaultMessage:
|
||||
'The full content of this object based column cannot be displayed.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
<EuiBadge>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.indexObjectBadgeContent',
|
||||
{
|
||||
defaultMessage: 'object',
|
||||
}
|
||||
)}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return d;
|
||||
};
|
||||
|
||||
let columnType;
|
||||
|
||||
if (tableItems.length > 0) {
|
||||
columnType = typeof tableItems[0][k];
|
||||
}
|
||||
|
||||
if (typeof columnType !== 'undefined') {
|
||||
switch (columnType) {
|
||||
case 'boolean':
|
||||
column.dataType = 'boolean';
|
||||
break;
|
||||
case 'Date':
|
||||
column.align = 'right';
|
||||
column.render = (d: any) =>
|
||||
formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000);
|
||||
break;
|
||||
case 'number':
|
||||
column.dataType = 'number';
|
||||
column.render = render;
|
||||
break;
|
||||
default:
|
||||
column.render = render;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
column.render = render;
|
||||
}
|
||||
|
||||
return column;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (jobConfig !== undefined) {
|
||||
const predictedFieldName = getPredictedFieldName(
|
||||
jobConfig.dest.results_field,
|
||||
jobConfig.analysis
|
||||
);
|
||||
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
|
||||
|
||||
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
|
||||
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
|
||||
loadExploreData({ field, direction, searchQuery });
|
||||
}
|
||||
}, [JSON.stringify(searchQuery)]);
|
||||
|
||||
useEffect(() => {
|
||||
// by default set the sorting to descending on the prediction field (`<dependent_varible or prediction_field_name>_prediction`).
|
||||
// if that's not available sort ascending on the first column.
|
||||
// also check if the current sorting field is still available.
|
||||
if (jobConfig !== undefined && columns.length > 0 && !selectedFields.includes(sortField)) {
|
||||
const predictedFieldName = getPredictedFieldName(
|
||||
jobConfig.dest.results_field,
|
||||
jobConfig.analysis
|
||||
);
|
||||
const predictedFieldSelected = selectedFields.includes(predictedFieldName);
|
||||
|
||||
const field = predictedFieldSelected ? predictedFieldName : selectedFields[0];
|
||||
const direction = predictedFieldSelected ? SORT_DIRECTION.DESC : SORT_DIRECTION.ASC;
|
||||
loadExploreData({ field, direction, searchQuery });
|
||||
}
|
||||
}, [jobConfig, columns.length, sortField, sortDirection, tableItems.length]);
|
||||
|
||||
let sorting: SortingPropType = false;
|
||||
let onTableChange;
|
||||
|
||||
if (columns.length > 0 && sortField !== '' && sortField !== undefined) {
|
||||
sorting = {
|
||||
sort: {
|
||||
field: sortField,
|
||||
direction: sortDirection,
|
||||
},
|
||||
};
|
||||
|
||||
onTableChange = ({
|
||||
page = { index: 0, size: 10 },
|
||||
sort = { field: sortField, direction: sortDirection },
|
||||
}: OnTableChangeArg) => {
|
||||
const { index, size } = page;
|
||||
setPageIndex(index);
|
||||
setPageSize(size);
|
||||
|
||||
if (sort.field !== sortField || sort.direction !== sortDirection) {
|
||||
loadExploreData({ ...sort, searchQuery });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
initialPageIndex: pageIndex,
|
||||
initialPageSize: pageSize,
|
||||
totalItemCount: tableItems.length,
|
||||
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
||||
hidePerPageOptions: false,
|
||||
};
|
||||
|
||||
const onQueryChange = ({ query, error }: { query: QueryType; error: any }) => {
|
||||
if (error) {
|
||||
setSearchError(error.message);
|
||||
} else {
|
||||
try {
|
||||
const esQueryDsl = Query.toESQuery(query);
|
||||
setSearchQuery(esQueryDsl);
|
||||
setSearchString(query.text);
|
||||
setSearchError(undefined);
|
||||
// set query for use in evaluate panel
|
||||
setEvaluateSearchQuery(esQueryDsl);
|
||||
} catch (e) {
|
||||
setSearchError(e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const search = {
|
||||
onChange: onQueryChange,
|
||||
defaultQuery: searchString,
|
||||
box: {
|
||||
incremental: false,
|
||||
placeholder: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.searchBoxPlaceholder',
|
||||
{
|
||||
defaultMessage: 'E.g. avg>0.5',
|
||||
}
|
||||
),
|
||||
},
|
||||
filters: [
|
||||
{
|
||||
type: 'field_value_toggle_group',
|
||||
field: `${jobConfig.dest.results_field}.is_training`,
|
||||
items: [
|
||||
{
|
||||
value: false,
|
||||
name: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.isTestingLabel',
|
||||
{
|
||||
defaultMessage: 'Testing',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
value: true,
|
||||
name: i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.isTrainingLabel',
|
||||
{
|
||||
defaultMessage: 'Training',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (jobConfig === undefined) {
|
||||
return null;
|
||||
}
|
||||
// if it's a searchBar syntax error leave the table visible so they can try again
|
||||
if (status === INDEX_STATUS.ERROR && !errorMessage.includes('parsing_exception')) {
|
||||
return (
|
||||
<EuiPanel grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ExplorationTitle jobId={jobConfig.id} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>{getTaskStateBadge(jobStatus)}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.indexError', {
|
||||
defaultMessage: 'An error occurred loading the index data.',
|
||||
})}
|
||||
color="danger"
|
||||
iconType="cross"
|
||||
>
|
||||
<p>{errorMessage}</p>
|
||||
</EuiCallOut>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const tableError =
|
||||
status === INDEX_STATUS.ERROR && errorMessage.includes('parsing_exception')
|
||||
? errorMessage
|
||||
: searchError;
|
||||
|
||||
return (
|
||||
<EuiPanel grow={false} id="mlDataFrameAnalyticsTableResultsPanel">
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ExplorationTitle jobId={jobConfig.id} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<span>{getTaskStateBadge(jobStatus)}</span>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem style={{ textAlign: 'right' }}>
|
||||
{docFieldsCount > MAX_COLUMNS && (
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.fieldSelection',
|
||||
{
|
||||
defaultMessage:
|
||||
'{selectedFieldsLength, number} of {docFieldsCount, number} {docFieldsCount, plural, one {field} other {fields}} selected',
|
||||
values: { selectedFieldsLength: selectedFields.length, docFieldsCount },
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">
|
||||
<EuiPopover
|
||||
id="popover"
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
iconType="gear"
|
||||
onClick={toggleColumnsPopover}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.selectColumnsAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Select columns',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
isOpen={isColumnsPopoverVisible}
|
||||
closePopover={closeColumnsPopover}
|
||||
ownFocus
|
||||
>
|
||||
<EuiPopoverTitle>
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.selectFieldsPopoverTitle',
|
||||
{
|
||||
defaultMessage: 'Select fields',
|
||||
}
|
||||
)}
|
||||
</EuiPopoverTitle>
|
||||
<div style={{ maxHeight: '400px', overflowY: 'scroll' }}>
|
||||
{docFields.map(d => (
|
||||
<EuiCheckbox
|
||||
key={d}
|
||||
id={d}
|
||||
label={d}
|
||||
checked={selectedFields.includes(d)}
|
||||
onChange={() => toggleColumn(d)}
|
||||
disabled={selectedFields.includes(d) && selectedFields.length === 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{status === INDEX_STATUS.LOADING && <EuiProgress size="xs" color="accent" />}
|
||||
{status !== INDEX_STATUS.LOADING && (
|
||||
<EuiProgress size="xs" color="accent" max={1} value={0} />
|
||||
)}
|
||||
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && (
|
||||
<Fragment>
|
||||
{tableItems.length === SEARCH_SIZE && (
|
||||
<EuiFormRow
|
||||
helpText={i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.classificationExploration.documentsShownHelpText',
|
||||
{
|
||||
defaultMessage: 'Showing first {searchSize} documents',
|
||||
values: { searchSize: SEARCH_SIZE },
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Fragment />
|
||||
</EuiFormRow>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<MlInMemoryTableBasic
|
||||
allowNeutralSort={false}
|
||||
columns={columns}
|
||||
compressed
|
||||
hasActions={false}
|
||||
isSelectable={false}
|
||||
items={tableItems}
|
||||
onTableChange={onTableChange}
|
||||
pagination={pagination}
|
||||
responsive={false}
|
||||
search={search}
|
||||
error={tableError}
|
||||
sorting={sorting}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* 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 React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SearchResponse } from 'elasticsearch';
|
||||
|
||||
import { SortDirection, SORT_DIRECTION } from '../../../../../components/ml_in_memory_table';
|
||||
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { getNestedProperty } from '../../../../../util/object_utils';
|
||||
import { SavedSearchQuery } from '../../../../../contexts/kibana';
|
||||
|
||||
import {
|
||||
getDefaultClassificationFields,
|
||||
getFlattenedFields,
|
||||
DataFrameAnalyticsConfig,
|
||||
EsFieldName,
|
||||
getPredictedFieldName,
|
||||
INDEX_STATUS,
|
||||
SEARCH_SIZE,
|
||||
defaultSearchQuery,
|
||||
SearchQuery,
|
||||
} from '../../../../common';
|
||||
|
||||
export type TableItem = Record<string, any>;
|
||||
|
||||
interface LoadExploreDataArg {
|
||||
field: string;
|
||||
direction: SortDirection;
|
||||
searchQuery: SavedSearchQuery;
|
||||
}
|
||||
export interface UseExploreDataReturnType {
|
||||
errorMessage: string;
|
||||
loadExploreData: (arg: LoadExploreDataArg) => void;
|
||||
sortField: EsFieldName;
|
||||
sortDirection: SortDirection;
|
||||
status: INDEX_STATUS;
|
||||
tableItems: TableItem[];
|
||||
}
|
||||
|
||||
export const useExploreData = (
|
||||
jobConfig: DataFrameAnalyticsConfig | undefined,
|
||||
selectedFields: EsFieldName[],
|
||||
setSelectedFields: React.Dispatch<React.SetStateAction<EsFieldName[]>>
|
||||
): UseExploreDataReturnType => {
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [status, setStatus] = useState(INDEX_STATUS.UNUSED);
|
||||
const [tableItems, setTableItems] = useState<TableItem[]>([]);
|
||||
const [sortField, setSortField] = useState<string>('');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>(SORT_DIRECTION.ASC);
|
||||
|
||||
const loadExploreData = async ({ field, direction, searchQuery }: LoadExploreDataArg) => {
|
||||
if (jobConfig !== undefined) {
|
||||
setErrorMessage('');
|
||||
setStatus(INDEX_STATUS.LOADING);
|
||||
|
||||
try {
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
const body: SearchQuery = {
|
||||
query: searchQuery,
|
||||
};
|
||||
|
||||
if (field !== undefined) {
|
||||
body.sort = [
|
||||
{
|
||||
[field]: {
|
||||
order: direction,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const resp: SearchResponse<any> = await ml.esSearch({
|
||||
index: jobConfig.dest.index,
|
||||
size: SEARCH_SIZE,
|
||||
body,
|
||||
});
|
||||
|
||||
setSortField(field);
|
||||
setSortDirection(direction);
|
||||
|
||||
const docs = resp.hits.hits;
|
||||
|
||||
if (docs.length === 0) {
|
||||
setTableItems([]);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedFields.length === 0) {
|
||||
const newSelectedFields = getDefaultClassificationFields(docs, jobConfig);
|
||||
setSelectedFields(newSelectedFields);
|
||||
}
|
||||
|
||||
// Create a version of the doc's source with flattened field names.
|
||||
// This avoids confusion later on if a field name has dots in its name
|
||||
// or is a nested fields when displaying it via EuiInMemoryTable.
|
||||
const flattenedFields = getFlattenedFields(docs[0]._source, resultsField);
|
||||
const transformedTableItems = docs.map(doc => {
|
||||
const item: TableItem = {};
|
||||
flattenedFields.forEach(ff => {
|
||||
item[ff] = getNestedProperty(doc._source, ff);
|
||||
if (item[ff] === undefined) {
|
||||
// If the attribute is undefined, it means it was not a nested property
|
||||
// but had dots in its actual name. This selects the property by its
|
||||
// full name and assigns it to `item[ff]`.
|
||||
item[ff] = doc._source[`"${ff}"`];
|
||||
}
|
||||
if (item[ff] === undefined) {
|
||||
const parts = ff.split('.');
|
||||
if (parts[0] === resultsField && parts.length >= 2) {
|
||||
parts.shift();
|
||||
if (doc._source[resultsField] !== undefined) {
|
||||
item[ff] = doc._source[resultsField][parts.join('.')];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return item;
|
||||
});
|
||||
|
||||
setTableItems(transformedTableItems);
|
||||
setStatus(INDEX_STATUS.LOADED);
|
||||
} catch (e) {
|
||||
if (e.message !== undefined) {
|
||||
setErrorMessage(e.message);
|
||||
} else {
|
||||
setErrorMessage(JSON.stringify(e));
|
||||
}
|
||||
setTableItems([]);
|
||||
setStatus(INDEX_STATUS.ERROR);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (jobConfig !== undefined) {
|
||||
loadExploreData({
|
||||
field: getPredictedFieldName(jobConfig.dest.results_field, jobConfig.analysis),
|
||||
direction: SORT_DIRECTION.DESC,
|
||||
searchQuery: defaultSearchQuery,
|
||||
});
|
||||
}
|
||||
}, [jobConfig && jobConfig.id]);
|
||||
|
||||
return { errorMessage, loadExploreData, sortField, sortDirection, status, tableItems };
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { ErrorCallout } from './error_callout';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { LoadingPanel } from './loading_panel';
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 React, { FC } from 'react';
|
||||
import { EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
|
||||
|
||||
export const LoadingPanel: FC = () => (
|
||||
<EuiPanel className="eui-textCenter">
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiPanel>
|
||||
);
|
|
@ -8,40 +8,30 @@ import React, { FC, Fragment, useEffect, useState } from 'react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
|
||||
import { ErrorCallout } from './error_callout';
|
||||
import { ErrorCallout } from '../error_callout';
|
||||
import {
|
||||
getValuesFromResponse,
|
||||
getDependentVar,
|
||||
getPredictionFieldName,
|
||||
loadEvalData,
|
||||
loadDocsCount,
|
||||
Eval,
|
||||
DataFrameAnalyticsConfig,
|
||||
} from '../../../../common';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { EvaluateStat } from './evaluate_stat';
|
||||
import {
|
||||
getEvalQueryBody,
|
||||
isRegressionResultsSearchBoolQuery,
|
||||
RegressionResultsSearchQuery,
|
||||
SearchQuery,
|
||||
isResultsSearchBoolQuery,
|
||||
isRegressionEvaluateResponse,
|
||||
ResultsSearchQuery,
|
||||
ANALYSIS_CONFIG_TYPE,
|
||||
} from '../../../../common/analytics';
|
||||
|
||||
interface Props {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
searchQuery: RegressionResultsSearchQuery;
|
||||
}
|
||||
|
||||
interface TrackTotalHitsSearchResponse {
|
||||
hits: {
|
||||
total: {
|
||||
value: number;
|
||||
relation: string;
|
||||
};
|
||||
hits: any[];
|
||||
};
|
||||
searchQuery: ResultsSearchQuery;
|
||||
}
|
||||
|
||||
const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
|
||||
|
@ -60,40 +50,6 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
// default is 'ml'
|
||||
const resultsField = jobConfig.dest.results_field;
|
||||
|
||||
const loadDocsCount = async ({
|
||||
ignoreDefaultQuery = true,
|
||||
isTraining,
|
||||
}: {
|
||||
ignoreDefaultQuery?: boolean;
|
||||
isTraining: boolean;
|
||||
}): Promise<{
|
||||
docsCount: number | null;
|
||||
success: boolean;
|
||||
}> => {
|
||||
const query = getEvalQueryBody({ resultsField, isTraining, ignoreDefaultQuery, searchQuery });
|
||||
|
||||
try {
|
||||
const body: SearchQuery = {
|
||||
track_total_hits: true,
|
||||
query,
|
||||
};
|
||||
|
||||
const resp: TrackTotalHitsSearchResponse = await ml.esSearch({
|
||||
index: jobConfig.dest.index,
|
||||
size: 0,
|
||||
body,
|
||||
});
|
||||
|
||||
const docsCount = resp.hits.total && resp.hits.total.value;
|
||||
return { docsCount, success: true };
|
||||
} catch (e) {
|
||||
return {
|
||||
docsCount: null,
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const loadGeneralizationData = async (ignoreDefaultQuery: boolean = true) => {
|
||||
setIsLoadingGeneralization(true);
|
||||
|
||||
|
@ -105,9 +61,14 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
predictionFieldName,
|
||||
searchQuery,
|
||||
ignoreDefaultQuery,
|
||||
jobType: ANALYSIS_CONFIG_TYPE.REGRESSION,
|
||||
});
|
||||
|
||||
if (genErrorEval.success === true && genErrorEval.eval) {
|
||||
if (
|
||||
genErrorEval.success === true &&
|
||||
genErrorEval.eval &&
|
||||
isRegressionEvaluateResponse(genErrorEval.eval)
|
||||
) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
|
||||
setGeneralizationEval({
|
||||
meanSquaredError,
|
||||
|
@ -136,9 +97,14 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
predictionFieldName,
|
||||
searchQuery,
|
||||
ignoreDefaultQuery,
|
||||
jobType: ANALYSIS_CONFIG_TYPE.REGRESSION,
|
||||
});
|
||||
|
||||
if (trainingErrorEval.success === true && trainingErrorEval.eval) {
|
||||
if (
|
||||
trainingErrorEval.success === true &&
|
||||
trainingErrorEval.eval &&
|
||||
isRegressionEvaluateResponse(trainingErrorEval.eval)
|
||||
) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
|
||||
setTrainingEval({
|
||||
meanSquaredError,
|
||||
|
@ -165,7 +131,13 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
if (isTrainingClause !== undefined && isTrainingClause.query === 'false') {
|
||||
loadGeneralizationData();
|
||||
|
||||
const docsCountResp = await loadDocsCount({ isTraining: false });
|
||||
const docsCountResp = await loadDocsCount({
|
||||
isTraining: false,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex: jobConfig.dest.index,
|
||||
});
|
||||
|
||||
if (docsCountResp.success === true) {
|
||||
setGeneralizationDocsCount(docsCountResp.docsCount);
|
||||
} else {
|
||||
|
@ -182,7 +154,13 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
// searchBar query is filtering for training data
|
||||
loadTrainingData();
|
||||
|
||||
const docsCountResp = await loadDocsCount({ isTraining: true });
|
||||
const docsCountResp = await loadDocsCount({
|
||||
isTraining: true,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex: jobConfig.dest.index,
|
||||
});
|
||||
|
||||
if (docsCountResp.success === true) {
|
||||
setTrainingDocsCount(docsCountResp.docsCount);
|
||||
} else {
|
||||
|
@ -201,6 +179,9 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
const genDocsCountResp = await loadDocsCount({
|
||||
ignoreDefaultQuery: false,
|
||||
isTraining: false,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex: jobConfig.dest.index,
|
||||
});
|
||||
if (genDocsCountResp.success === true) {
|
||||
setGeneralizationDocsCount(genDocsCountResp.docsCount);
|
||||
|
@ -212,6 +193,9 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
const trainDocsCountResp = await loadDocsCount({
|
||||
ignoreDefaultQuery: false,
|
||||
isTraining: true,
|
||||
searchQuery,
|
||||
resultsField,
|
||||
destIndex: jobConfig.dest.index,
|
||||
});
|
||||
if (trainDocsCountResp.success === true) {
|
||||
setTrainingDocsCount(trainDocsCountResp.docsCount);
|
||||
|
@ -223,7 +207,7 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
|
||||
useEffect(() => {
|
||||
const hasIsTrainingClause =
|
||||
isRegressionResultsSearchBoolQuery(searchQuery) &&
|
||||
isResultsSearchBoolQuery(searchQuery) &&
|
||||
searchQuery.bool.must.filter(
|
||||
(clause: any) => clause.match && clause.match[`${resultsField}.is_training`] !== undefined
|
||||
);
|
||||
|
@ -241,10 +225,13 @@ export const EvaluatePanel: FC<Props> = ({ jobConfig, jobStatus, searchQuery })
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', {
|
||||
defaultMessage: 'Regression job ID {jobId}',
|
||||
{i18n.translate(
|
||||
'xpack.ml.dataframe.analytics.regressionExploration.evaluateJobIdTitle',
|
||||
{
|
||||
defaultMessage: 'Evaluation of regression job ID {jobId}',
|
||||
values: { jobId: jobConfig.id },
|
||||
})}
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -5,31 +5,26 @@
|
|||
*/
|
||||
|
||||
import React, { FC, Fragment, useState, useEffect } from 'react';
|
||||
import { EuiCallOut, EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ml } from '../../../../../services/ml_api_service';
|
||||
import { DataFrameAnalyticsConfig } from '../../../../common';
|
||||
import { EvaluatePanel } from './evaluate_panel';
|
||||
import { ResultsTable } from './results_table';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { RegressionResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
|
||||
import { ResultsSearchQuery, defaultSearchQuery } from '../../../../common/analytics';
|
||||
import { LoadingPanel } from '../loading_panel';
|
||||
|
||||
interface GetDataFrameAnalyticsResponse {
|
||||
count: number;
|
||||
data_frame_analytics: DataFrameAnalyticsConfig[];
|
||||
}
|
||||
|
||||
const LoadingPanel: FC = () => (
|
||||
<EuiPanel className="eui-textCenter">
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</EuiPanel>
|
||||
);
|
||||
|
||||
export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => (
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle', {
|
||||
defaultMessage: 'Regression job ID {jobId}',
|
||||
{i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.tableJobIdTitle', {
|
||||
defaultMessage: 'Destination index for regression job ID {jobId}',
|
||||
values: { jobId },
|
||||
})}
|
||||
</span>
|
||||
|
@ -45,7 +40,7 @@ export const RegressionExploration: FC<Props> = ({ jobId, jobStatus }) => {
|
|||
const [jobConfig, setJobConfig] = useState<DataFrameAnalyticsConfig | undefined>(undefined);
|
||||
const [isLoadingJobConfig, setIsLoadingJobConfig] = useState<boolean>(false);
|
||||
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState<RegressionResultsSearchQuery>(defaultSearchQuery);
|
||||
const [searchQuery, setSearchQuery] = useState<ResultsSearchQuery>(defaultSearchQuery);
|
||||
|
||||
const loadJobConfig = async () => {
|
||||
setIsLoadingJobConfig(true);
|
||||
|
|
|
@ -60,6 +60,8 @@ import { ExplorationTitle } from './regression_exploration';
|
|||
|
||||
const PAGE_SIZE_OPTIONS = [5, 10, 25, 50];
|
||||
|
||||
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
|
||||
|
||||
interface Props {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus: DATA_FRAME_TASK_STATE;
|
||||
|
@ -363,8 +365,6 @@ export const ResultsTable: FC<Props> = React.memo(
|
|||
? errorMessage
|
||||
: searchError;
|
||||
|
||||
const MlInMemoryTableBasic = mlInMemoryTableBasicFactory<TableItem>();
|
||||
|
||||
return (
|
||||
<EuiPanel grow={false}>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween" responsive={false}>
|
||||
|
|
|
@ -3,11 +3,6 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
/*
|
||||
* 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 React, { useEffect, useState } from 'react';
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import { NavigationMenu } from '../../../components/navigation_menu';
|
|||
|
||||
import { Exploration } from './components/exploration';
|
||||
import { RegressionExploration } from './components/regression_exploration';
|
||||
import { ClassificationExploration } from './components/classification_exploration';
|
||||
|
||||
import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics';
|
||||
import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common';
|
||||
|
@ -72,6 +73,9 @@ export const Page: FC<{
|
|||
{analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && (
|
||||
<RegressionExploration jobId={jobId} jobStatus={jobStatus} />
|
||||
)}
|
||||
{analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && (
|
||||
<ClassificationExploration jobId={jobId} jobStatus={jobStatus} />
|
||||
)}
|
||||
</EuiPageContentBody>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
getAnalysisType,
|
||||
isRegressionAnalysis,
|
||||
isOutlierAnalysis,
|
||||
isClassificationAnalysis,
|
||||
} from '../../../../common/analytics';
|
||||
|
||||
import { getResultsUrl, isDataFrameAnalyticsRunning, DataFrameAnalyticsListRow } from './common';
|
||||
|
@ -31,7 +32,9 @@ export const AnalyticsViewAction = {
|
|||
const analysisType = getAnalysisType(item.config.analysis);
|
||||
const jobStatus = item.stats.state;
|
||||
const isDisabled =
|
||||
!isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis);
|
||||
!isRegressionAnalysis(item.config.analysis) &&
|
||||
!isOutlierAnalysis(item.config.analysis) &&
|
||||
!isClassificationAnalysis(item.config.analysis);
|
||||
|
||||
const url = getResultsUrl(item.id, analysisType, jobStatus);
|
||||
return (
|
||||
|
|
|
@ -25,7 +25,11 @@ import {
|
|||
Eval,
|
||||
} from '../../../../common';
|
||||
import { isCompletedAnalyticsJob } from './common';
|
||||
import { isRegressionAnalysis } from '../../../../common/analytics';
|
||||
import {
|
||||
isRegressionAnalysis,
|
||||
ANALYSIS_CONFIG_TYPE,
|
||||
isRegressionEvaluateResponse,
|
||||
} from '../../../../common/analytics';
|
||||
import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
|
||||
|
||||
function getItemDescription(value: any) {
|
||||
|
@ -81,9 +85,14 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
|
|||
dependentVariable,
|
||||
resultsField,
|
||||
predictionFieldName,
|
||||
jobType: ANALYSIS_CONFIG_TYPE.REGRESSION,
|
||||
});
|
||||
|
||||
if (genErrorEval.success === true && genErrorEval.eval) {
|
||||
if (
|
||||
genErrorEval.success === true &&
|
||||
genErrorEval.eval &&
|
||||
isRegressionEvaluateResponse(genErrorEval.eval)
|
||||
) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
|
||||
setGeneralizationEval({
|
||||
meanSquaredError,
|
||||
|
@ -106,9 +115,14 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
|
|||
dependentVariable,
|
||||
resultsField,
|
||||
predictionFieldName,
|
||||
jobType: ANALYSIS_CONFIG_TYPE.REGRESSION,
|
||||
});
|
||||
|
||||
if (trainingErrorEval.success === true && trainingErrorEval.eval) {
|
||||
if (
|
||||
trainingErrorEval.success === true &&
|
||||
trainingErrorEval.eval &&
|
||||
isRegressionEvaluateResponse(trainingErrorEval.eval)
|
||||
) {
|
||||
const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
|
||||
setTrainingEval({
|
||||
meanSquaredError,
|
||||
|
|
|
@ -7864,7 +7864,6 @@
|
|||
"xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "データの読み込み中にエラーが発生しました。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.generalError": "データの読み込み中にエラーが発生しました。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "一般化エラー",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "ジョブ ID {jobId}",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "平均二乗エラー",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "インデックスのクエリが結果を返しませんでした。ジョブが完了済みで、インデックスにドキュメントがあることを確認してください。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空のインデックスクエリ結果。",
|
||||
|
|
|
@ -7957,7 +7957,6 @@
|
|||
"xpack.ml.dataframe.analytics.regressionExploration.evaluateError": "加载数据时出错。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.generalError": "加载数据时出错。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.generalizationErrorTitle": "泛化误差",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.jobIdTitle": "作业 ID {jobId}",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText": "均方误差",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutBody": "该索引的查询未返回结果。请确保作业已完成且索引包含文档。",
|
||||
"xpack.ml.dataframe.analytics.regressionExploration.noDataCalloutTitle": "空的索引查询结果。",
|
||||
|
|
Loading…
Reference in a new issue