import { useMemo } from 'react' ; import { i18n } from '@kbn/i18n' ; import { ClassificationFeatureImportanceBaseline , FeatureImportance , FeatureImportanceBaseline , isClassificationFeatureImportanceBaseline , isRegressionFeatureImportanceBaseline , TopClasses , } from '../../../../../../../common/types/feature_importance' ; import type { ExtendedFeatureImportance } from './decision_path_popover' ; export type DecisionPathPlotData = Array < [ string , number , number ] > ; interface UseDecisionPathDataParams { featureImportance : FeatureImportance [ ] ; baseline ? : FeatureImportanceBaseline ; predictedValue ? : string | number | undefined ; predictedProbability ? : number | undefined ; topClasses ? : TopClasses ; } interface RegressionDecisionPathProps { baseline ? : number ; predictedValue ? : number | undefined ; featureImportance : FeatureImportance [ ] ; topClasses ? : TopClasses ; } const FEATURE_NAME = 'feature_name' ; const FEATURE_IMPORTANCE = 'importance' ; const RESIDUAL_IMPORTANCE_ERROR_MARGIN = 1e-5 ; const decisionPathFeatureOtherTitle = i18n . translate ( 'xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle' , { defaultMessage : 'other' , } ) ; export const isDecisionPathData = ( decisionPathData ) : boolean => { return ( Array . isArray ( decisionPathData ) && decisionPathData . length > 0 && decisionPathData [ 0 ] . length === 3 ) ; } ; export const getStringBasedClassName = ( v ) : string => { if ( v === undefined || v === null ) { return '' ; } if ( typeof v === 'boolean' ) { return v ? 'True' : 'False' ; } if ( typeof v === 'number' ) { return v . toString ( ) ; } return v ; } ; export const formatValue = ( number , precision = 3 , fractionDigits = 1 ) : string => { if ( Math . abs ( number ) < 10 ) { return Number ( number . toPrecision ( precision ) ) . toString ( ) ; } return number . toFixed ( fractionDigits ) ; } ; export const useDecisionPathData = ( { baseline , featureImportance , predictedValue , predictedProbability , } ) : { decisionPathData : DecisionPathPlotData | undefined ; } => { const decisionPathData = useMemo ( ( ) => { if ( baseline === undefined ) return ; if ( isRegressionFeatureImportanceBaseline ( baseline ) ) { return buildRegressionDecisionPathData ( { baseline : baseline . baseline , featureImportance , predictedValue : predictedValue as number | undefined , } ) ; } if ( isClassificationFeatureImportanceBaseline ( baseline ) ) { return buildClassificationDecisionPathData ( { baselines : baseline . classes , featureImportance , currentClass : predictedValue as string | undefined , predictedProbability , } ) ; } } , [ baseline , featureImportance , predictedValue ] ) ; return { decisionPathData } ; } ; /**
 * Returns values to build decision path for regression jobs
 * where first data point of array is the final predicted value (end of decision path)
 */ export const buildRegressionDecisionPathData = ( { baseline , featureImportance , predictedValue , } ) : DecisionPathPlotData | undefined => { const mappedFeatureImportance = featureImportance . map ( ( d ) => ( { ... d , absImportance : Math . abs ( d [ FEATURE_IMPORTANCE ] as number ) , } ) ) ; if ( baseline && predictedValue !== undefined && Number . isFinite ( predictedValue ) ) { const adjustedImportance = predictedValue - mappedFeatureImportance . reduce ( ( accumulator , currentValue ) => accumulator + currentValue . importance ! , 0 ) - baseline ; mappedFeatureImportance . push ( { [ FEATURE_NAME ] : i18n . translate ( 'xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle' , { defaultMessage : 'baseline' , } ) , [ FEATURE_IMPORTANCE ] : baseline , absImportance : - 1 , } ) ; if ( Math . abs ( adjustedImportance ) > RESIDUAL_IMPORTANCE_ERROR_MARGIN ) { mappedFeatureImportance . push ( { [ FEATURE_NAME ] : decisionPathFeatureOtherTitle , [ FEATURE_IMPORTANCE ] : adjustedImportance , absImportance : 0 , } ) ; } } const filteredFeatureImportance = mappedFeatureImportance . filter ( ( f ) => f !== undefined ) as ExtendedFeatureImportance [ ] ; const finalResult = filteredFeatureImportance . sort ( ( a , b ) => b . absImportance - a . absImportance ) . map ( ( d ) => [ d [ FEATURE_NAME ] as string , d [ FEATURE_IMPORTANCE ] as number , NaN ] ) ; let cumulativeSum = 0 ; for ( let i = filteredFeatureImportance . length - 1 ; i >= 0 ; i -- ) { cumulativeSum += finalResult [ i ] [ 1 ] ; finalResult [ i ] [ 2 ] = cumulativeSum ; } return finalResult ; } ; export const addAdjustedProbability = ( { predictedProbability , decisionPlotData , } ) : DecisionPathPlotData | undefined => { if ( predictedProbability && decisionPlotData . length > 0 ) { const adjustedResidualImportance = predictedProbability - decisionPlotData [ 0 ] [ 2 ] ; if ( Math . abs ( adjustedResidualImportance ) > RESIDUAL_IMPORTANCE_ERROR_MARGIN ) { decisionPlotData . forEach ( ( row ) => ( row [ 2 ] = row [ 2 ] + adjustedResidualImportance ) ) ; decisionPlotData . push ( [ decisionPathFeatureOtherTitle , adjustedResidualImportance , decisionPlotData [ decisionPlotData . length - 1 ] [ 2 ] - adjustedResidualImportance , ] ) ; } } return decisionPlotData ; } ; export const processBinaryClassificationDecisionPathData = ( { decisionPlotData , startingBaseline , predictedProbability , } ) : DecisionPathPlotData | undefined => { const finalResult = decisionPlotData ; let logOddSoFar = startingBaseline ; for ( let i = finalResult . length - 1 ; i >= 0 ; i -- ) { logOddSoFar += finalResult [ i ] [ 1 ] ; const predictionProbabilitySoFar = Math . exp ( logOddSoFar ) / ( Math . exp ( logOddSoFar ) + 1 ) ; finalResult [ i ] [ 2 ] = predictionProbabilitySoFar ; } return addAdjustedProbability ( { predictedProbability , decisionPlotData : finalResult } ) ; } ; export const processMultiClassClassificationDecisionPathData = ( { baselines , decisionPlotData , startingBaseline , featureImportance , predictedProbability , } ) : DecisionPathPlotData | undefined => { const denominator = computeMultiClassImportanceDenominator ( { baselines , featureImportance } ) ; const baseline = startingBaseline ; let featureImportanceRunningSum = 0 ; for ( let i = decisionPlotData . length - 1 ; i >= 0 ; i -- ) { featureImportanceRunningSum += decisionPlotData [ i ] [ 1 ] ; const numerator = Math . exp ( baseline + featureImportanceRunningSum ) ; decisionPlotData [ i ] [ 2 ] = numerator / denominator ; } return addAdjustedProbability ( { predictedProbability , decisionPlotData } ) ; } ; /**
 * Compute the denominator used for multiclass classification
 * (\sum_{x\in{A,B,C}} exp(baseline(x) + \sum_{i=0}^j feature_importance_i(x)))
 */ export const computeMultiClassImportanceDenominator = ( { baselines , featureImportance , } ) : number => { let denominator = 0 ; for ( let x = 0 ; x < baselines . length ; x ++ ) { let featureImportanceOfClassX = 0 ; for ( let i = 0 ; i < featureImportance . length ; i ++ ) { const feature = featureImportance [ i ] ; const classFeatureImportance = Array . isArray ( feature . classes ) ? feature . classes . find ( ( c ) => getStringBasedClassName ( c . class_name ) === getStringBasedClassName ( baselines [ x ] . class_name ) ) : feature ; if ( classFeatureImportance && classFeatureImportance . importance !== undefined && typeof classFeatureImportance [ FEATURE_IMPORTANCE ] === 'number' ) { featureImportanceOfClassX += classFeatureImportance . importance ; } } denominator += Math . exp ( baselines [ x ] . baseline + featureImportanceOfClassX ) ; } return denominator ; } ; /**
 * Returns values to build decision path for classification jobs
 * where first data point of array is the final predicted probability (end of decision path)
 */ export const buildClassificationDecisionPathData = ( { baselines , featureImportance , currentClass , predictedProbability , } ) : DecisionPathPlotData | undefined => { if ( currentClass === undefined || ! ( Array . isArray ( baselines ) && baselines . length >= 2 ) ) return [ ] ; const mappedFeatureImportance = featureImportance . map ( ( feature ) => { const classFeatureImportance = Array . isArray ( feature . classes ) ? feature . classes . find ( ( c ) => getStringBasedClassName ( c . class_name ) === getStringBasedClassName ( currentClass ) ) : feature ; if ( classFeatureImportance && typeof classFeatureImportance [ FEATURE_IMPORTANCE ] === 'number' ) { return { [ FEATURE_NAME ] : feature [ FEATURE_NAME ] , [ FEATURE_IMPORTANCE ] : classFeatureImportance [ FEATURE_IMPORTANCE ] , absImportance : Math . abs ( classFeatureImportance [ FEATURE_IMPORTANCE ] as number ) , } ; } return undefined ; } ) ; const baselineClass = baselines . find ( ( bl ) => getStringBasedClassName ( bl . class_name ) === getStringBasedClassName ( currentClass ) ) ; const startingBaseline = baselineClass ?. baseline ? baselineClass ?. baseline : 0 ; const filteredFeatureImportance = mappedFeatureImportance . filter ( ( f ) => f !== undefined ) as ExtendedFeatureImportance [ ] ; const decisionPlotData = filteredFeatureImportance . sort ( ( a , b ) => b . absImportance - a . absImportance ) . map ( ( d ) => [ d [ FEATURE_NAME ] as string , d [ FEATURE_IMPORTANCE ] as number , NaN ] ) ; if ( baselines . length === 2 ) { return processBinaryClassificationDecisionPathData ( { startingBaseline , decisionPlotData , predictedProbability , } ) ; } return processMultiClassClassificationDecisionPathData ( { baselines , decisionPlotData , startingBaseline , featureImportance , predictedProbability , } ) ; } ;