diff --git a/.github/workflows/radicalbit-bot.yaml b/.github/workflows/radicalbit-bot.yaml index 2401e460..c4cc350a 100644 --- a/.github/workflows/radicalbit-bot.yaml +++ b/.github/workflows/radicalbit-bot.yaml @@ -20,7 +20,7 @@ jobs: ORGANIZATION: ${{ secrets.DOCKER_HUB_ORG }} build-ui: - if: ${{ false && github.event.comment.body == '/build-ui' || github.event.comment.body == '/build-all' }} + if: ${{ github.event.comment.body == '/build-ui' || github.event.comment.body == '/build-all' }} uses: radicalbit/radicalbit-github-workflows/.github/workflows/docker.yaml@v1 with: push: false diff --git a/README.md b/README.md index 11da716e..45ac70ae 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ # Radicalbit AI Monitoring # 👋 Welcome! -The **Radicalbit AI Monitoring Platform** provides a comprehensive solution for monitoring your Artificial Intelligence models in production. +The **Radicalbit AI Monitoring Platform** provides a comprehensive solution for monitoring your Machine Learning and Large Language models in production. ## 🤔 Why Monitor AI Models? While models often perform well during development and validation, their effectiveness can degrade over time in production due to various factors like data shifts or concept drift. The Radicalbit AI Monitor platform helps you proactively identify and address potential performance issues. ## 🗝️ Key Functionalities -The platform provides extensive monitoring capabilities to ensure optimal performance of your AI models in production. It analyzes both your reference dataset (used for pre-production validation) and the current datasets, allowing you to put under control: +The platform provides extensive monitoring capabilities to ensure optimal performance of your AI models in production. It analyzes both your reference dataset (used for pre-production validation) and the current datasets, allowing you to control: * **Data Quality** * **Model Quality** * **Model Drift** @@ -36,7 +36,7 @@ This repository contains all the files and projects to run Radicalbit AI Monitor ## 🚀 Installation using Docker compose -In this repository a docker compose file is available to run the platform in local with a K3s cluster where we can deploy Spark jobs. +This repository provides a Docker Compose file for running the platform locally with a K3s cluster. This setup allows you to deploy Spark jobs. To run, simply: @@ -50,11 +50,11 @@ If the UI is needed: docker compose --profile ui up ``` -After all containers are up & running, you can go to [http://localhost:5173](http://127.0.0.1:5173) to play with the app. +Once all containers are up & running, you can go to [http://localhost:5173](http://127.0.0.1:5173) to play with the app. ### Interacting with K3s cluster -In the compose file is present a [k9s](https://k9scli.io/) container that can be used to monitor the K3s cluster. +The compose file includes a [k9s](https://k9scli.io/) container that can be used to monitor the K3s cluster. ```bash docker compose up k9s -d && docker attach radicalbit-ai-monitoring-k9s-1 @@ -62,13 +62,13 @@ docker compose up k9s -d && docker attach radicalbit-ai-monitoring-k9s-1 #### Other tools -In order to connect and interact with the K3s cluster from the local machine (for example with Lens or `kubectl`) is necessary to create another file starting from `./docker/k3s_data/kubeconfig/kubeconfig.yaml` (that is automatically generated when the docker compose is up and running). +In order to connect and interact with the K3s cluster from the local machine (for example with Lens or `kubectl`), it is necessary to create another file starting from `./docker/k3s_data/kubeconfig/kubeconfig.yaml` (that is automatically generated when the docker compose is up and running). Copy the above file and modify `https://k3s:6443` with `https://127.0.0.1:6443` and use this new file to interact with the cluster from the local machine ### Real AWS -In order to use a real AWS instead of Minio is necessary to modify the environment variables of the api container, putting real `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` and `S3_BUCKET_NAME` and removing `S3_ENDPOINT_URL`. +In order to use a real AWS instead of Minio it is necessary to modify the environment variables of the api container, putting real `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION` and `S3_BUCKET_NAME` and removing `S3_ENDPOINT_URL`. ### Teardown diff --git a/docker-compose.yaml b/docker-compose.yaml index 6a717779..c2889932 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,7 +3,7 @@ services: profiles: ["ui"] build: context: ./ui - target: build + target: dev command: yarn start:local develop: watch: diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 78df8539..4789fe94 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -8,23 +8,88 @@ :root { --ifm-color-primary: #3695D9; --ifm-color-primary-dark: #0A71BB; - --ifm-color-primary-darker: #182336; - --ifm-color-primary-darkest: #182336; + --ifm-color-primary-darker: #252329; + --ifm-color-primary-darkest: #252329; --ifm-color-primary-light: #fff; --ifm-color-primary-lighter: #fff; --ifm-color-primary-lightest: #fff; --ifm-code-font-size: 95%; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-menu-color-background-active: transparent; + --ifm-menu-color-background-hover: transparent; + --ifm-breadcrumb-item-background-active: transparent; + + --ifm-font-family-base: 'Open Sans', Helvetica, Arial, Lucida, sans-serif; + --ifm-footer-padding-vertical: 8rem; + + --rdb-font-family-headers: 'Montserrat', Helvetica, Arial, Lucida, sans-serif; } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { --ifm-color-primary: #fff; - --ifm-color-primary-dark: #182336; - --ifm-color-primary-darker: #182336; - --ifm-color-primary-darkest: #182336; + --ifm-color-primary-dark: #3E3E3E; + --ifm-color-primary-darker: #252329; + --ifm-color-primary-darkest: #252329; --ifm-color-primary-light: #73B2E0; --ifm-color-primary-lighter: #73B2E0; --ifm-color-primary-lightest: #73B2E0; --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } + + +/* Radicalbit corporate elements */ + +@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'); + +h1, h2, h3 { + font-family: var(--rdb-font-family-headers); + font-weight: normal; + color: var(--ifm-color-primary); +} + +.breadcrumbs__link { + padding: 0; +} + +.navbar__title { + font-family: var(--rdb-font-family-headers); +} + +.pagination-nav__link { + border: 0; +} + +.theme-doc-sidebar-container { + background-image: linear-gradient(180deg, rgba(54, 149, 217, 0.18) 0%, transparent 80%) !important; +} + +.footer { + background-image: linear-gradient(191deg, rgba(115, 177, 222, 1) 0%, #3a92d0 24%, #1171c2 46%, #095bbf 69%, #053189 100%) !important; + position: relative; + font-size: .8rem; +} + +.footer::before { + background-image: url(img/rdb-page-divisor--light.svg); + background-size: cover; + top: 0; + left: 0; + height: 94px; + width: 100%; + z-index: 1; + content: ' '; + position: absolute; +} + +.footer__copyright { + border-top: 1px solid var(--docusaurus-highlighted-code-line-bg); + padding-top: 1rem; + margin-top: 2rem; + font-size: .8em; +} + +[data-theme='dark'] .footer::before { + background-image: url(img/rdb-page-divisor--dark.svg); +} diff --git a/docs/src/css/img/rdb-page-divisor--dark.svg b/docs/src/css/img/rdb-page-divisor--dark.svg new file mode 100644 index 00000000..19395cbc --- /dev/null +++ b/docs/src/css/img/rdb-page-divisor--dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/src/css/img/rdb-page-divisor--light.svg b/docs/src/css/img/rdb-page-divisor--light.svg new file mode 100644 index 00000000..10ff3830 --- /dev/null +++ b/docs/src/css/img/rdb-page-divisor--light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/.env.production b/ui/.env.production new file mode 100644 index 00000000..a52a885c --- /dev/null +++ b/ui/.env.production @@ -0,0 +1 @@ +VITE_BASE_URL=APP_BASE_URL \ No newline at end of file diff --git a/ui/Dockerfile b/ui/Dockerfile index b415b1f4..7ca4f55f 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,16 +1,26 @@ -FROM node:20.10.0-bullseye AS build +FROM node:20.10.0-bullseye AS dev WORKDIR /app COPY . . RUN npm install --global --force yarn@1.22.21 && \ - yarn install && \ - yarn build:dev + yarn install + +FROM node:20.10.0-bullseye AS build + +WORKDIR /app + +COPY . . +COPY --from=dev /app/node_modules /app/node_modules + +RUN yarn build:prod FROM nginx:alpine3.19 EXPOSE 80 -COPY --from=build /app/dist /usr/share/nginx/html -COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file +COPY --from=build /app/dist /usr/share/nginx/html/ +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY env.sh /docker-entrypoint.d/ +RUN chmod +x /docker-entrypoint.d/env.sh \ No newline at end of file diff --git a/ui/env.sh b/ui/env.sh new file mode 100644 index 00000000..7298f9cd --- /dev/null +++ b/ui/env.sh @@ -0,0 +1,10 @@ +#! /bin/sh + +for i in $(env | grep "APP_"); do + key=$(echo "$i" | cut -d '=' -f 1) + value=$(echo "$i" | cut -d '=' -f 2-) + + echo "$key=$value" + + find "/usr/share/nginx/html" -type f -exec sed -i "s|${key}|${value}|g" '{}' + +done \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 853b4c08..ab5d6d3d 100644 --- a/ui/package.json +++ b/ui/package.json @@ -8,6 +8,7 @@ "start:dev": "NODE_ENV=development && vite --host 0.0.0.0", "build:local": "NODE_ENV=local && vite build", "build:dev": "NODE_ENV=development && vite build", + "build:prod": "vite build --mode production", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "lint:fix": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix", "preview": "vite preview" diff --git a/ui/src/components/modals/add-new-model/modal-context-provider.jsx b/ui/src/components/modals/add-new-model/modal-context-provider.jsx index 75556c70..6762e0cf 100644 --- a/ui/src/components/modals/add-new-model/modal-context-provider.jsx +++ b/ui/src/components/modals/add-new-model/modal-context-provider.jsx @@ -1,3 +1,4 @@ +import { DataTypeEnum, ModelTypeEnum } from '@State/models/constants'; import useFormbit from '@radicalbit/formbit'; import { createContext, @@ -5,7 +6,6 @@ import { useMemo, useState, } from 'react'; -import { DataTypeEnum, ModelTypeEnum } from '@State/models/constants'; import schemaStepFour from './step-four/schema'; import schemaStepOne from './step-one/schema'; import schemaStepThree from './step-three/schema'; diff --git a/ui/src/components/modals/add-new-model/step-four/form-fields.jsx b/ui/src/components/modals/add-new-model/step-four/form-fields.jsx index a18eafc0..f2f65be7 100644 --- a/ui/src/components/modals/add-new-model/step-four/form-fields.jsx +++ b/ui/src/components/modals/add-new-model/step-four/form-fields.jsx @@ -1,4 +1,8 @@ -import { FormField, Select, Tooltip } from '@radicalbit/radicalbit-design-system'; +import { + FormField, + Select, + Tooltip, +} from '@radicalbit/radicalbit-design-system'; import { ModelTypeEnum } from '@Src/store/state/models/constants'; import { useModalContext } from '../modal-context-provider'; @@ -261,46 +265,64 @@ function Probability() { ); } -const targetValidTypes = ['int', 'float', 'double']; +const targetValidTypes = { + [ModelTypeEnum.BINARY_CLASSIFICATION]: ['int', 'float', 'double'], + [ModelTypeEnum.MULTI_CLASSIFICATION]: ['int', 'float', 'double'], + [ModelTypeEnum.REGRESSION]: ['int', 'float', 'double'], +}; const useGetTargets = () => { - const { useFormbit } = useModalContext(); + const { useFormbit, useFormbitStepOne } = useModalContext(); const { form } = useFormbit; - return form.features.filter(({ type }) => targetValidTypes.includes(type)); + const { form: formStepOne } = useFormbitStepOne; + const { modelType } = formStepOne; + + return form.features.filter(({ type }) => targetValidTypes[modelType].includes(type)); }; -const predictionValidTypes = ['int', 'float', 'double']; +const predictionValidTypes = { + [ModelTypeEnum.BINARY_CLASSIFICATION]: ['int', 'float', 'double'], + [ModelTypeEnum.MULTI_CLASSIFICATION]: ['int', 'float', 'double', 'string'], + [ModelTypeEnum.REGRESSION]: ['int', 'float', 'double'], +}; const useGetPredictions = () => { - const { useFormbit } = useModalContext(); + const { useFormbit, useFormbitStepOne } = useModalContext(); const { form } = useFormbit; - return form.outputs.filter(({ type }) => predictionValidTypes.includes(type)); + const { form: formStepOne } = useFormbitStepOne; + const { modelType } = formStepOne; + + return form.outputs.filter(({ type }) => predictionValidTypes[modelType].includes(type)); }; -const binaryClassificationProbabilityValidTypes = ['float', 'double']; +const binaryClassificationProbabilityValidTypes = { + [ModelTypeEnum.BINARY_CLASSIFICATION]: ['float', 'double'], + [ModelTypeEnum.MULTI_CLASSIFICATION]: ['float', 'double'], + [ModelTypeEnum.REGRESSION]: ['float', 'double'], +}; const useGetProbabilities = () => { const { useFormbitStepOne, useFormbit } = useModalContext(); const { form } = useFormbit; - const { form: formStepOne } = useFormbitStepOne; - if (formStepOne.modelType === ModelTypeEnum.BINARY_CLASSIFICATION) { - return form.outputs.filter(({ type }) => binaryClassificationProbabilityValidTypes.includes(type)); - } + const { form: formStepOne } = useFormbitStepOne; + const { modelType } = formStepOne; - return form.outputs; + return form.outputs.filter(({ type }) => binaryClassificationProbabilityValidTypes[modelType].includes(type)); }; -const timestampValidTypes = ['datetime']; +const timestampValidTypes = { + [ModelTypeEnum.BINARY_CLASSIFICATION]: ['datetime'], + [ModelTypeEnum.MULTI_CLASSIFICATION]: ['datetime'], + [ModelTypeEnum.REGRESSION]: ['datetime'], +}; const useGetTimestapValidFeatures = () => { const { useFormbitStepOne, useFormbit } = useModalContext(); const { form } = useFormbit; - const { form: formStepOne } = useFormbitStepOne; - if (formStepOne.modelType === ModelTypeEnum.BINARY_CLASSIFICATION) { - return form?.features.filter(({ type }) => timestampValidTypes.includes(type)); - } + const { form: formStepOne } = useFormbitStepOne; + const { modelType } = formStepOne; - return form.outputs; + return form?.features.filter(({ type }) => timestampValidTypes[modelType].includes(type)); }; export { diff --git a/ui/src/components/modals/current-import-detail-modal/body.jsx b/ui/src/components/modals/current-import-detail-modal/body.jsx index c8b2bd26..d3e894bd 100644 --- a/ui/src/components/modals/current-import-detail-modal/body.jsx +++ b/ui/src/components/modals/current-import-detail-modal/body.jsx @@ -1,27 +1,16 @@ import { Tabs } from '@radicalbit/radicalbit-design-system'; -import { useSearchParams } from 'react-router-dom'; +import { useParams, useSearchParams } from 'react-router-dom'; import { METRICS_TABS } from '@Container/models/Details/constants'; -import DataQualityMetrics from '@Container/models/Details/current/data-quality'; -import ModelQualityMetrics from '@Container/models/Details/current/model-quality'; -import DataDriftMetrics from '@Container/models/Details/current/data-drift'; - -const tabs = [ - { - label: METRICS_TABS.DATA_QUALITIY, - key: METRICS_TABS.DATA_QUALITIY, - children: , - }, - { - label: METRICS_TABS.MODEL_QUALITY, - key: METRICS_TABS.MODEL_QUALITY, - children: , - }, - { - label: METRICS_TABS.DATA_DRIFT, - key: METRICS_TABS.DATA_DRIFT, - children: , - }, -]; +import BinaryClassificationDataQualityMetrics from '@Container/models/Details/binary-classification/current/data-quality'; +import BinaryClassificationModelQualityMetrics from '@Container/models/Details/binary-classification/current/model-quality'; +import BinaryClassificationDataDriftMetrics from '@Container/models/Details/binary-classification/current/data-drift'; +import MultiClassificationDataQualityMetrics from '@Container/models/Details/multi-classification/current/data-quality'; +import MultiClassificationModelQualityMetrics from '@Container/models/Details/multi-classification/current/model-quality'; +import MultiClassificationDataDriftMetrics from '@Container/models/Details/multi-classification/current/data-drift'; +import { modelsApiSlice } from '@Src/store/state/models/api'; +import { ModelTypeEnum } from '@Src/store/state/models/constants'; + +const { useGetModelByUUIDQuery } = modelsApiSlice; function Body() { const [searchParams, setSearchParams] = useSearchParams(); @@ -37,6 +26,8 @@ function Body() { setTab(e); }; + const tabs = useGetTabs(); + return (
@@ -51,4 +42,54 @@ function Body() { ); } +function useGetTabs() { + const { uuid } = useParams(); + const { data } = useGetModelByUUIDQuery({ uuid }); + + const modelType = data?.modelType; + + switch (modelType) { + case ModelTypeEnum.BINARY_CLASSIFICATION: + return [ + { + label: METRICS_TABS.DATA_QUALITIY, + key: METRICS_TABS.DATA_QUALITIY, + children: , + }, + { + label: METRICS_TABS.MODEL_QUALITY, + key: METRICS_TABS.MODEL_QUALITY, + children: , + }, + { + label: METRICS_TABS.DATA_DRIFT, + key: METRICS_TABS.DATA_DRIFT, + children: , + }, + ]; + + case ModelTypeEnum.MULTI_CLASSIFICATION: + return [ + { + label: METRICS_TABS.DATA_QUALITIY, + key: METRICS_TABS.DATA_QUALITIY, + children: , + }, + { + label: METRICS_TABS.MODEL_QUALITY, + key: METRICS_TABS.MODEL_QUALITY, + children: , + }, + { + label: METRICS_TABS.DATA_DRIFT, + key: METRICS_TABS.DATA_DRIFT, + children: , + }, + ]; + + default: + return false; + } +} + export default Body; diff --git a/ui/src/container/models/Details/current/data-drift/binary-classification/header/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-drift/header/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-drift/binary-classification/header/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-drift/header/index.jsx diff --git a/ui/src/container/models/Details/binary-classification/current/data-drift/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-drift/index.jsx new file mode 100644 index 00000000..dc9791e2 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/current/data-drift/index.jsx @@ -0,0 +1,54 @@ +import SomethingWentWrong from '@Components/ErrorPage/something-went-wrong'; +import JobStatus from '@Components/JobStatus'; +import { JOB_STATUS } from '@Src/constants'; +import { useGetCurrentDriftQueryWithPolling } from '@State/models/polling-hook'; +import { FormbitContextProvider } from '@radicalbit/formbit'; +import { Spinner } from '@radicalbit/radicalbit-design-system'; + +import DataDriftList from './list'; +import DataDriftHeader from './header'; +import SearchFeatureList from './search-filter'; + +const initialValues = { + __metadata: { + selectedFeatures: [], + isNumericalSelected: true, + isCategoricalSelected: true, + }, +}; + +function BinaryClassificationDataDriftMetrics() { + const { data, isError, isLoading } = useGetCurrentDriftQueryWithPolling(); + + const jobStatus = data?.jobStatus; + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + if (!data) { + return ; + } + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + return ( + +
+ + + + + +
+
+ ); + } + + return (); +} + +export default BinaryClassificationDataDriftMetrics; diff --git a/ui/src/container/models/Details/current/data-drift/binary-classification/list/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-drift/list/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-drift/binary-classification/list/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-drift/list/index.jsx diff --git a/ui/src/container/models/Details/current/data-drift/binary-classification/search-filter/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-drift/search-filter/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-drift/binary-classification/search-filter/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-drift/search-filter/index.jsx diff --git a/ui/src/container/models/Details/current/data-drift/binary-classification/use-get-filtered-features.js b/ui/src/container/models/Details/binary-classification/current/data-drift/use-get-filtered-features.js similarity index 100% rename from ui/src/container/models/Details/current/data-drift/binary-classification/use-get-filtered-features.js rename to ui/src/container/models/Details/binary-classification/current/data-drift/use-get-filtered-features.js diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-point-distribution/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-point-distribution/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-point-distribution/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-point-distribution/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-point-distribution/options.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-point-distribution/options.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-point-distribution/options.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-point-distribution/options.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/left-table/columns.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/left-table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/left-table/columns.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/left-table/columns.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/left-table/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/left-table/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/left-table/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/left-table/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/right-table/columns.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/right-table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/right-table/columns.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/right-table/columns.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/right-table/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/right-table/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/categorical/right-table/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/categorical/right-table/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/chart/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/chart/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/chart/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/chart/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/chart/options.js b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/chart/options.js similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/chart/options.js rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/chart/options.js diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/table/columns.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/table/columns.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/table/columns.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/table/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/table/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/data-quality-list/numerical/table/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/data-quality-list/numerical/table/index.jsx diff --git a/ui/src/container/models/Details/binary-classification/current/data-quality/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/index.jsx new file mode 100644 index 00000000..098462c3 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/current/data-quality/index.jsx @@ -0,0 +1,55 @@ +import SomethingWentWrong from '@Components/ErrorPage/something-went-wrong'; +import JobStatus from '@Components/JobStatus'; +import { JOB_STATUS } from '@Src/constants'; +import { useGetCurrentDataQualityQueryWithPolling } from '@State/models/polling-hook'; +import { FormbitContextProvider } from '@radicalbit/formbit'; +import { Spinner } from '@radicalbit/radicalbit-design-system'; +import { memo } from 'react'; +import DataPointDistribution from './data-point-distribution'; +import SearchFeatureList from './search-filter'; +import DataQualityList from './data-quality-list'; + +const initialValues = { + __metadata: { + selectedFeatures: [], + isNumericalSelected: true, + isCategoricalSelected: true, + }, +}; + +function BinaryClassificationDataQualityMetrics() { + const { data, isError, isLoading } = useGetCurrentDataQualityQueryWithPolling(); + const jobStatus = data?.jobStatus; + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + if (!data) { + return ; + } + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + return ( + + +
+ + + + + +
+ +
+ ); + } + + return (); +} + +export default memo(BinaryClassificationDataQualityMetrics); diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/search-filter/index.jsx b/ui/src/container/models/Details/binary-classification/current/data-quality/search-filter/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/search-filter/index.jsx rename to ui/src/container/models/Details/binary-classification/current/data-quality/search-filter/index.jsx diff --git a/ui/src/container/models/Details/current/data-quality/binary-classification/use-get-filtered-features.js b/ui/src/container/models/Details/binary-classification/current/data-quality/use-get-filtered-features.js similarity index 100% rename from ui/src/container/models/Details/current/data-quality/binary-classification/use-get-filtered-features.js rename to ui/src/container/models/Details/binary-classification/current/data-quality/use-get-filtered-features.js diff --git a/ui/src/container/models/Details/current/imports/columns.jsx b/ui/src/container/models/Details/binary-classification/current/imports/columns.jsx similarity index 100% rename from ui/src/container/models/Details/current/imports/columns.jsx rename to ui/src/container/models/Details/binary-classification/current/imports/columns.jsx diff --git a/ui/src/container/models/Details/current/imports/index.jsx b/ui/src/container/models/Details/binary-classification/current/imports/index.jsx similarity index 100% rename from ui/src/container/models/Details/current/imports/index.jsx rename to ui/src/container/models/Details/binary-classification/current/imports/index.jsx diff --git a/ui/src/container/models/Details/binary-classification/current/index.jsx b/ui/src/container/models/Details/binary-classification/current/index.jsx new file mode 100644 index 00000000..a2ffeaaa --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/current/index.jsx @@ -0,0 +1,63 @@ +import JobStatus from '@Components/JobStatus'; +import { METRICS_TABS } from '@Container/models/Details/constants'; +import { JOB_STATUS } from '@Src/constants'; +import { useGetReferenceDataQualityQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { Tabs } from '@radicalbit/radicalbit-design-system'; +import { useSearchParams } from 'react-router-dom'; +import BinaryClassificationDataQualityMetrics from './data-quality'; +import Imports from './imports'; +import BinaryClassificationModelQualityMetrics from './model-quality'; +import BinaryClassificationDataDriftMetrics from './data-drift'; + +const tabs = [ + { + label: METRICS_TABS.DATA_QUALITIY, + key: METRICS_TABS.DATA_QUALITIY, + children: , + }, + { + label: METRICS_TABS.MODEL_QUALITY, + key: METRICS_TABS.MODEL_QUALITY, + children: , + }, + { + label: METRICS_TABS.DATA_DRIFT, + key: METRICS_TABS.DATA_DRIFT, + children: , + }, + { + label: METRICS_TABS.IMPORT, + key: METRICS_TABS.IMPORT, + children: , + }, + +]; + +export default function CurrentDashboard() { + const [searchParams, setSearchParams] = useSearchParams(); + + const { data } = useGetReferenceDataQualityQueryWithPolling(); + const jobStatus = data?.jobStatus; + + const activeTab = searchParams.get('tab-metrics') || METRICS_TABS.METRICS; + + const onChangeTab = (value) => { + searchParams.set('tab-metrics', value); + setSearchParams(searchParams); + }; + + if (jobStatus !== JOB_STATUS.SUCCEEDED) { + return ; + } + + return ( +
+ +
+ ); +} diff --git a/ui/src/container/models/Details/current/model-quality/binary-classification/charts.jsx b/ui/src/container/models/Details/binary-classification/current/model-quality/charts.jsx similarity index 100% rename from ui/src/container/models/Details/current/model-quality/binary-classification/charts.jsx rename to ui/src/container/models/Details/binary-classification/current/model-quality/charts.jsx diff --git a/ui/src/container/models/Details/current/model-quality/binary-classification/columns.jsx b/ui/src/container/models/Details/binary-classification/current/model-quality/columns.jsx similarity index 100% rename from ui/src/container/models/Details/current/model-quality/binary-classification/columns.jsx rename to ui/src/container/models/Details/binary-classification/current/model-quality/columns.jsx diff --git a/ui/src/container/models/Details/binary-classification/current/model-quality/index.jsx b/ui/src/container/models/Details/binary-classification/current/model-quality/index.jsx new file mode 100644 index 00000000..b99e4f45 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/current/model-quality/index.jsx @@ -0,0 +1,193 @@ +import SomethingWentWrong from '@Components/ErrorPage/something-went-wrong'; +import JobStatus from '@Components/JobStatus'; +import ConfusionMatrix from '@Container/models/Details/charts/confusion-matrix-chart'; +import { CHART_COLOR, MODEL_QUALITY_FIELD } from '@Container/models/Details/constants'; +import { JOB_STATUS } from '@Src/constants'; +import { modelsApiSlice } from '@State/models/api'; +import { useGetCurrentModelQualityQueryWithPolling } from '@State/models/polling-hook'; +import { + Board, DataTable, SectionTitle, Spinner, +} from '@radicalbit/radicalbit-design-system'; +import { memo } from 'react'; +import { useParams } from 'react-router'; +import { + AccuracyChart, + AreaUnderPrChart, + AreaUnderRocChart, + F1Chart, + FalsePositiveRateChart, + PrecisionChart, + RecallChart, + TruePositiveRateChart, +} from './charts'; +import columns from './columns'; + +const { useGetReferenceModelQualityQuery } = modelsApiSlice; + +function BinaryClassificationModelQualityMetrics() { + const { data, isLoading, isError } = useGetCurrentModelQualityQueryWithPolling(); + + const jobStatus = data?.jobStatus; + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + if (!data) { + return ; + } + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + const confusionMatrixLabel = { + xAxisLabel: ['Predicted: 1', 'Predicted: 0'], + yAxisLabel: ['Actual: 0', 'Actual: 1'], + }; + + const confusionMatrixData = [ + [data?.modelQuality.globalMetrics.truePositiveCount, data?.modelQuality.globalMetrics.falseNegativeCount], + [data?.modelQuality.globalMetrics.falsePositiveCount, data?.modelQuality.globalMetrics.trueNegativeCount], + ]; + + return ( + +
+ + + + + + + + + + + + + + + + + + + +
+
+ ); + } + + return (); +} + +function PerformanceBoard() { + const { uuid } = useParams(); + + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceAccuracy = referenceData?.modelQuality?.accuracy; + const referencePrecision = referenceData?.modelQuality?.precision; + const referenceRecall = referenceData?.modelQuality?.recall; + const referenceF1 = referenceData?.modelQuality?.f1; + const referenceFalsePositiveRate = referenceData?.modelQuality?.falsePositiveRate; + const referenceTruePositiveRate = referenceData?.modelQuality?.truePositiveRate; + const referenceAreaUnderRoc = referenceData?.modelQuality?.areaUnderRoc; + const referenceAreaUnderPr = referenceData?.modelQuality?.areaUnderPr; + + const leftTableData = currentData ? [ + { + label: MODEL_QUALITY_FIELD.ACCURACY, + referenceValue: referenceAccuracy, + currentValue: currentData.modelQuality.globalMetrics.accuracy, + }, + { + label: MODEL_QUALITY_FIELD.PRECISION, + referenceValue: referencePrecision, + currentValue: currentData.modelQuality.globalMetrics.precision, + }, + { + label: MODEL_QUALITY_FIELD.RECALL, + referenceValue: referenceRecall, + currentValue: currentData.modelQuality.globalMetrics.recall, + }, + { + label: MODEL_QUALITY_FIELD.F1, + referenceValue: referenceF1, + currentValue: currentData.modelQuality.globalMetrics.f1, + }, + ] : []; + + const centerTableData = currentData ? [ + { + label: MODEL_QUALITY_FIELD.FALSE_POSITIVE_RATE, + referenceValue: referenceFalsePositiveRate, + currentValue: currentData.modelQuality.globalMetrics.falsePositiveRate, + }, + { + label: MODEL_QUALITY_FIELD.TRUE_POSITIVE_RATE, + referenceValue: referenceTruePositiveRate, + currentValue: currentData.modelQuality.globalMetrics.truePositiveRate, + }, + ] : []; + + const rightTableData = currentData ? [ + { + label: MODEL_QUALITY_FIELD.AREA_UNDER_ROC, + referenceValue: referenceAreaUnderRoc, + currentValue: currentData.modelQuality.globalMetrics.areaUnderRoc, + }, + { + label: MODEL_QUALITY_FIELD.AREA_UNDER_PR, + referenceValue: referenceAreaUnderPr, + currentValue: currentData.modelQuality.globalMetrics.areaUnderPr, + }, + ] : []; + + return ( + } + main={( +
+ label} + size="small" + /> + + label} + size="small" + /> + + label} + size="small" + /> +
+ )} + modifier="shadow" + size="small" + type="primary-light" + /> + ); +} + +export default memo(BinaryClassificationModelQualityMetrics); diff --git a/ui/src/container/models/Details/binary-classification/index.jsx b/ui/src/container/models/Details/binary-classification/index.jsx new file mode 100644 index 00000000..48eff735 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/index.jsx @@ -0,0 +1,37 @@ +import NotFound from '@Components/ErrorPage/not-found'; +import { modelsApiSlice } from '@State/models/api'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { MODEL_TABS_ENUM } from '@Container/models/Details/constants'; +import Current from './current'; +import Overview from './overview'; +import ReferenceDashboard from './reference'; + +const { useGetModelByUUIDQuery } = modelsApiSlice; + +function BinaryClassificationMetrics() { + const { uuid } = useParams(); + + const [searchParams] = useSearchParams(); + const activeKey = searchParams.get('tab') || MODEL_TABS_ENUM.OVERVIEW; + + const { error } = useGetModelByUUIDQuery({ uuid }); + const status = error?.status; + + if (status === 404) { + return ; + } + + if (activeKey === MODEL_TABS_ENUM.OVERVIEW) { + return (); + } + + if (activeKey === MODEL_TABS_ENUM.REFERENCE_DASHBOARD) { + return (); + } + + if (activeKey === MODEL_TABS_ENUM.CURRENT_DASHBOARD) { + return (); + } +} + +export default BinaryClassificationMetrics; diff --git a/ui/src/container/models/Details/overview/index.jsx b/ui/src/container/models/Details/binary-classification/overview/index.jsx similarity index 100% rename from ui/src/container/models/Details/overview/index.jsx rename to ui/src/container/models/Details/binary-classification/overview/index.jsx diff --git a/ui/src/container/models/Details/overview/outputs-tab/columns.jsx b/ui/src/container/models/Details/binary-classification/overview/outputs-tab/columns.jsx similarity index 100% rename from ui/src/container/models/Details/overview/outputs-tab/columns.jsx rename to ui/src/container/models/Details/binary-classification/overview/outputs-tab/columns.jsx diff --git a/ui/src/container/models/Details/overview/outputs-tab/index.jsx b/ui/src/container/models/Details/binary-classification/overview/outputs-tab/index.jsx similarity index 100% rename from ui/src/container/models/Details/overview/outputs-tab/index.jsx rename to ui/src/container/models/Details/binary-classification/overview/outputs-tab/index.jsx diff --git a/ui/src/container/models/Details/overview/summary-tab/columns.jsx b/ui/src/container/models/Details/binary-classification/overview/summary-tab/columns.jsx similarity index 100% rename from ui/src/container/models/Details/overview/summary-tab/columns.jsx rename to ui/src/container/models/Details/binary-classification/overview/summary-tab/columns.jsx diff --git a/ui/src/container/models/Details/overview/summary-tab/index.jsx b/ui/src/container/models/Details/binary-classification/overview/summary-tab/index.jsx similarity index 100% rename from ui/src/container/models/Details/overview/summary-tab/index.jsx rename to ui/src/container/models/Details/binary-classification/overview/summary-tab/index.jsx diff --git a/ui/src/container/models/Details/overview/variables-tab/columns.jsx b/ui/src/container/models/Details/binary-classification/overview/variables-tab/columns.jsx similarity index 100% rename from ui/src/container/models/Details/overview/variables-tab/columns.jsx rename to ui/src/container/models/Details/binary-classification/overview/variables-tab/columns.jsx diff --git a/ui/src/container/models/Details/overview/variables-tab/index.jsx b/ui/src/container/models/Details/binary-classification/overview/variables-tab/index.jsx similarity index 100% rename from ui/src/container/models/Details/overview/variables-tab/index.jsx rename to ui/src/container/models/Details/binary-classification/overview/variables-tab/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-point-distribution/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-point-distribution/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-point-distribution/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-point-distribution/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-point-distribution/options.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-point-distribution/options.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-point-distribution/options.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-point-distribution/options.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/left-table/columns.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/left-table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/left-table/columns.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/left-table/columns.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/left-table/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/left-table/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/left-table/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/left-table/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/right-table/columns.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/right-table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/right-table/columns.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/right-table/columns.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/right-table/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/right-table/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/categorical/right-table/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/categorical/right-table/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/chart/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/chart/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/chart/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/chart/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/chart/options.js b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/chart/options.js similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/chart/options.js rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/chart/options.js diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/table/columns.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/table/columns.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/table/columns.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/table/columns.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/table/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/table/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/data-quality-list/numerical/table/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/data-quality-list/numerical/table/index.jsx diff --git a/ui/src/container/models/Details/binary-classification/reference/data-quality/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/index.jsx new file mode 100644 index 00000000..9f7b19d3 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/reference/data-quality/index.jsx @@ -0,0 +1,52 @@ +import SomethingWentWrong from '@Components/ErrorPage/something-went-wrong'; +import JobStatus from '@Components/JobStatus'; +import { JOB_STATUS } from '@Src/constants'; +import { modelsApiSlice } from '@State/models/api'; +import { useGetReferenceDataQualityQueryWithPolling } from '@State/models/polling-hook'; +import { FormbitContextProvider } from '@radicalbit/formbit'; +import { memo } from 'react'; +import { useParams } from 'react-router'; +import DataPointDistribution from './data-point-distribution'; +import SearchFeatureList from './search-filter'; +import DataQualityList from './data-quality-list'; + +const { useGetReferenceDataQualityQuery } = modelsApiSlice; + +const initialValues = { + __metadata: { + selectedFeatures: [], + isNumericalSelected: true, + isCategoricalSelected: true, + }, +}; + +function BinaryClassificationDataQualityMetrics() { + useGetReferenceDataQualityQueryWithPolling(); + const { uuid } = useParams(); + const { data, isError } = useGetReferenceDataQualityQuery({ uuid }); + const jobStatus = data?.jobStatus; + + if (isError) { + return ; + } + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + return ( + + +
+ + + + + +
+ +
+ ); + } + + return (); +} + +export default memo(BinaryClassificationDataQualityMetrics); diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/search-filter/index.jsx b/ui/src/container/models/Details/binary-classification/reference/data-quality/search-filter/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/search-filter/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/data-quality/search-filter/index.jsx diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/use-get-filtered-features.js b/ui/src/container/models/Details/binary-classification/reference/data-quality/use-get-filtered-features.js similarity index 100% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/use-get-filtered-features.js rename to ui/src/container/models/Details/binary-classification/reference/data-quality/use-get-filtered-features.js diff --git a/ui/src/container/models/Details/reference/imports/columns.jsx b/ui/src/container/models/Details/binary-classification/reference/imports/columns.jsx similarity index 100% rename from ui/src/container/models/Details/reference/imports/columns.jsx rename to ui/src/container/models/Details/binary-classification/reference/imports/columns.jsx diff --git a/ui/src/container/models/Details/reference/imports/index.jsx b/ui/src/container/models/Details/binary-classification/reference/imports/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/imports/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/imports/index.jsx diff --git a/ui/src/container/models/Details/reference/index.jsx b/ui/src/container/models/Details/binary-classification/reference/index.jsx similarity index 100% rename from ui/src/container/models/Details/reference/index.jsx rename to ui/src/container/models/Details/binary-classification/reference/index.jsx diff --git a/ui/src/container/models/Details/reference/model-quality/binary-classification/columns.jsx b/ui/src/container/models/Details/binary-classification/reference/model-quality/columns.jsx similarity index 100% rename from ui/src/container/models/Details/reference/model-quality/binary-classification/columns.jsx rename to ui/src/container/models/Details/binary-classification/reference/model-quality/columns.jsx diff --git a/ui/src/container/models/Details/binary-classification/reference/model-quality/index.jsx b/ui/src/container/models/Details/binary-classification/reference/model-quality/index.jsx new file mode 100644 index 00000000..41befa04 --- /dev/null +++ b/ui/src/container/models/Details/binary-classification/reference/model-quality/index.jsx @@ -0,0 +1,97 @@ +import JobStatus from '@Components/JobStatus'; +import ConfusionMatrix from '@Container/models/Details/charts/confusion-matrix-chart'; +import { CHART_COLOR, MODEL_QUALITY_FIELD } from '@Container/models/Details/constants'; +import { JOB_STATUS } from '@Src/constants'; +import { useGetReferenceModelQualityQueryWithPolling } from '@State/models/polling-hook'; +import { + Board, DataTable, SectionTitle, Spinner, +} from '@radicalbit/radicalbit-design-system'; +import { memo } from 'react'; +import columns from './columns'; + +function BinaryClassificationModelQualityMetrics() { + const { data, isLoading } = useGetReferenceModelQualityQueryWithPolling(); + + const jobStatus = data?.jobStatus; + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + const leftTableData = data ? [ + { label: MODEL_QUALITY_FIELD.ACCURACY, value: data.modelQuality.accuracy }, + { label: MODEL_QUALITY_FIELD.PRECISION, value: data.modelQuality.precision }, + { label: MODEL_QUALITY_FIELD.RECALL, value: data.modelQuality.recall }, + { label: MODEL_QUALITY_FIELD.F1, value: data.modelQuality.f1 }, + ] : []; + + const centerTableData = data ? [ + { label: 'False positive rate', value: data.modelQuality.falsePositiveRate }, + { label: 'True positive rate', value: data.modelQuality.truePositiveRate }, + ] : []; + + const rightTableData = data ? [ + { label: MODEL_QUALITY_FIELD.AREA_UNDER_ROC, value: data.modelQuality.areaUnderRoc }, + { label: MODEL_QUALITY_FIELD.AREA_UNDER_PR, value: data.modelQuality.areaUnderPr }, + ] : []; + + const confusionMatrixLabel = { + xAxisLabel: ['Predicted: 1', 'Predicted: 0'], + yAxisLabel: ['Actual: 0', 'Actual: 1'], + }; + + const confusionMatrixData = [ + [data.modelQuality.truePositiveCount, data.modelQuality.falsePositiveCount], + [data.modelQuality.falseNegativeCount, data.modelQuality.trueNegativeCount], + ]; + + return ( + +
+ } + main={( +
+ label} + size="small" + /> + + label} + size="small" + /> + + label} + size="small" + /> +
+ )} + size="small" + type="secondary" + /> + + +
+
+ ); + } + + return (); +} + +export default memo(BinaryClassificationModelQualityMetrics); diff --git a/ui/src/container/models/Details/current/data-drift/index.jsx b/ui/src/container/models/Details/current/data-drift/index.jsx deleted file mode 100644 index ce97591e..00000000 --- a/ui/src/container/models/Details/current/data-drift/index.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { modelsApiSlice } from '@Src/store/state/models/api'; -import { ModelTypeEnum } from '@State/models/constants'; -import { memo } from 'react'; -import { useParams } from 'react-router'; -import BinaryClassificationMetrics from './binary-classification'; - -const { useGetModelByUUIDQuery } = modelsApiSlice; - -function DataDriftMetrics() { - const { uuid } = useParams(); - const { data } = useGetModelByUUIDQuery({ uuid }); - - const modelType = data?.modelType; - - switch (modelType) { - case ModelTypeEnum.BINARY_CLASSIFICATION: - return ; - - default: - return false; - } -} - -export default memo(DataDriftMetrics); diff --git a/ui/src/container/models/Details/current/data-quality/index.jsx b/ui/src/container/models/Details/current/data-quality/index.jsx deleted file mode 100644 index f5a7b539..00000000 --- a/ui/src/container/models/Details/current/data-quality/index.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { modelsApiSlice } from '@Src/store/state/models/api'; -import { ModelTypeEnum } from '@State/models/constants'; -import { memo } from 'react'; -import { useParams } from 'react-router'; -import BinaryClassificationMetrics from './binary-classification'; - -const { useGetModelByUUIDQuery } = modelsApiSlice; - -function DataQualityMetrics() { - const { uuid } = useParams(); - const { data } = useGetModelByUUIDQuery({ uuid }); - - const modelType = data?.modelType; - - switch (modelType) { - case ModelTypeEnum.BINARY_CLASSIFICATION: - return ; - - default: - return false; - } -} - -export default memo(DataQualityMetrics); diff --git a/ui/src/container/models/Details/current/model-quality/index.jsx b/ui/src/container/models/Details/current/model-quality/index.jsx deleted file mode 100644 index a43bcdd5..00000000 --- a/ui/src/container/models/Details/current/model-quality/index.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import { modelsApiSlice } from '@Src/store/state/models/api'; -import { ModelTypeEnum } from '@State/models/constants'; -import { memo } from 'react'; -import { useParams } from 'react-router'; -import BinaryClassificationMetrics from './binary-classification'; - -const { useGetModelByUUIDQuery } = modelsApiSlice; - -function ModelQualityMetrics() { - const { uuid } = useParams(); - - const { data } = useGetModelByUUIDQuery({ uuid }); - const modelType = data?.modelType; - - switch (modelType) { - case ModelTypeEnum.BINARY_CLASSIFICATION: - return ; - - default: - return false; - } -} - -export default memo(ModelQualityMetrics); diff --git a/ui/src/container/models/Details/index.jsx b/ui/src/container/models/Details/index.jsx index dd00f04b..071e03df 100644 --- a/ui/src/container/models/Details/index.jsx +++ b/ui/src/container/models/Details/index.jsx @@ -1,35 +1,25 @@ -import NotFound from '@Components/ErrorPage/not-found'; +import { ModelTypeEnum } from '@Src/store/state/models/constants'; import { modelsApiSlice } from '@State/models/api'; -import { useParams, useSearchParams } from 'react-router-dom'; -import { MODEL_TABS_ENUM } from '@Container/models/Details/constants'; -import Current from './current'; -import Overview from './overview'; -import ReferenceDashboard from './reference'; +import { useParams } from 'react-router-dom'; +import BinaryClassificationMetrics from './binary-classification'; +import MultiClassificationMetrics from './multi-classification'; const { useGetModelByUUIDQuery } = modelsApiSlice; export function ModelDetails() { const { uuid } = useParams(); + const { data } = useGetModelByUUIDQuery({ uuid }); - const [searchParams] = useSearchParams(); - const activeKey = searchParams.get('tab') || MODEL_TABS_ENUM.OVERVIEW; + const modelType = data?.modelType; - const { error } = useGetModelByUUIDQuery({ uuid }); - const status = error?.status; + switch (modelType) { + case ModelTypeEnum.BINARY_CLASSIFICATION: + return ; - if (status === 404) { - return ; - } - - if (activeKey === MODEL_TABS_ENUM.OVERVIEW) { - return (); - } - - if (activeKey === MODEL_TABS_ENUM.REFERENCE_DASHBOARD) { - return (); - } + case ModelTypeEnum.MULTI_CLASSIFICATION: + return ; - if (activeKey === MODEL_TABS_ENUM.CURRENT_DASHBOARD) { - return (); + default: + return false; } } diff --git a/ui/src/container/models/Details/multi-classification/current/data-drift/header/index.jsx b/ui/src/container/models/Details/multi-classification/current/data-drift/header/index.jsx new file mode 100644 index 00000000..bb1a69d8 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/data-drift/header/index.jsx @@ -0,0 +1,180 @@ +import { DRIFT_TEST_ENUM } from '@Src/constants'; +import { useGetCurrentDriftQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { Board, SectionTitle } from '@radicalbit/radicalbit-design-system'; + +function DataDriftHeader() { + return ( +
+
+ +
+ +
+ +
+ +
+ +
+
+ ); +} + +function TotalFeaturesCounter() { + const { data } = useGetCurrentDriftQueryWithPolling(); + const featureMetrics = data?.drift.featureMetrics.filter((feature) => feature.driftCalc.hasDrift) ?? []; + const featuresWithDriftCounter = featureMetrics.length; + const featuresCounter = data?.drift.featureMetrics.length; + + if (featuresCounter === 0) { + return ( + } + main={( +
+ {/* FIXME: inline style */} +
--
+ +
Features with drift
+
+ )} + modifier="h-full shadow" + size="small" + /> + ); + } + + return ( + } + main={( +
+ {/* FIXME: inline style */} +
+ {featuresWithDriftCounter} + + + / + {featuresCounter} + +
+ +
Features with drift
+
+ )} + modifier="h-full shadow" + size="small" + type="primary" + /> + + ); +} + +function CategoricalFeaturesCounter() { + const { data } = useGetCurrentDriftQueryWithPolling(); + + const featureMetrics = data?.drift.featureMetrics ?? []; + const categoricalWithDriftCounter = featureMetrics.filter((feature) => feature.driftCalc.type === DRIFT_TEST_ENUM.CHI2 && feature.driftCalc.hasDrift).length; + const categoricalCounter = featureMetrics.filter((feature) => feature.driftCalc.type === DRIFT_TEST_ENUM.CHI2).length; + + if (categoricalCounter === 0) { + return ( + } + main={( +
+ + {/* FIXME: inline style */} +
--
+ +
Categorical with drift
+
+ )} + modifier="h-full" + size="small" + /> + ); + } + + return ( + } + main={( +
+
+ {/* FIXME: inline style */} +
+ {categoricalWithDriftCounter} + + + / + {categoricalCounter} + +
+ +
+ +
Categorical with drift
+
+ )} + modifier="h-full" + size="small" + /> + ); +} + +function NumericalFeaturesCounter() { + const { data } = useGetCurrentDriftQueryWithPolling(); + + const featureMetrics = data?.drift.featureMetrics ?? []; + + const numericalWithDriftCounter = featureMetrics.filter((feature) => feature.driftCalc.type === DRIFT_TEST_ENUM.KS && feature.driftCalc.hasDrift).length; + const numericalCounter = featureMetrics.filter((feature) => feature.driftCalc.type === DRIFT_TEST_ENUM.KS).length; + + if (numericalCounter === 0) { + return ( + } + main={( +
+ + {/* FIXME: inline style */} +
--
+ +
Numerical with drift
+
+ )} + modifier="h-full shadow" + size="small" + /> + ); + } + + return ( + } + main={( +
+
+ {/* FIXME: inline style */} +
+ {numericalWithDriftCounter} + + + / + {numericalCounter} + +
+ +
+ +
Numerical with drift
+
+ )} + modifier="h-full shadow" + size="small" + /> + ); +} + +export default DataDriftHeader; diff --git a/ui/src/container/models/Details/current/data-drift/binary-classification/index.jsx b/ui/src/container/models/Details/multi-classification/current/data-drift/index.jsx similarity index 92% rename from ui/src/container/models/Details/current/data-drift/binary-classification/index.jsx rename to ui/src/container/models/Details/multi-classification/current/data-drift/index.jsx index f7897cf1..6653bf02 100644 --- a/ui/src/container/models/Details/current/data-drift/binary-classification/index.jsx +++ b/ui/src/container/models/Details/multi-classification/current/data-drift/index.jsx @@ -4,8 +4,9 @@ import { JOB_STATUS } from '@Src/constants'; import { useGetCurrentDriftQueryWithPolling } from '@State/models/polling-hook'; import { FormbitContextProvider } from '@radicalbit/formbit'; import { Spinner } from '@radicalbit/radicalbit-design-system'; -import DataDriftHeader from './header'; + import DataDriftList from './list'; +import DataDriftHeader from './header'; import SearchFeatureList from './search-filter'; const initialValues = { @@ -16,7 +17,7 @@ const initialValues = { }, }; -function DataDriftMetrics() { +function MultiClassificationDataDriftMetrics() { const { data, isError, isLoading } = useGetCurrentDriftQueryWithPolling(); const jobStatus = data?.jobStatus; @@ -50,4 +51,4 @@ function DataDriftMetrics() { return (); } -export default DataDriftMetrics; +export default MultiClassificationDataDriftMetrics; diff --git a/ui/src/container/models/Details/multi-classification/current/data-drift/list/index.jsx b/ui/src/container/models/Details/multi-classification/current/data-drift/list/index.jsx new file mode 100644 index 00000000..64baa814 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/data-drift/list/index.jsx @@ -0,0 +1,114 @@ +import { + DRIFT_FEATURE_TYPE_ENUM, DRIFT_TEST_ENUM, DRIFT_TEST_ENUM_LABEL, numberFormatter, +} from '@Src/constants'; +import { fa1, faC } from '@fortawesome/free-solid-svg-icons'; +import { + Board, + Button, + FontAwesomeIcon, + NewHeader, + Pin, + SectionTitle, + Spinner, + Tag, +} from '@radicalbit/radicalbit-design-system'; +import { Virtuoso } from 'react-virtuoso'; +import useGetFilteredFeatures from '../use-get-filtered-features'; + +function DataDriftList() { + const items = useGetFilteredFeatures(); + + return ( + + + + ()} + totalCount={items.length} + /> + + ); +} + +function CountLabel() { + const items = useGetFilteredFeatures(); + const label = items.length <= 1 ? 'Record' : 'Records'; + + return ( + + ); +} + +function FeatureRow({ item }) { + const pinType = (item.driftCalc.hasDrift) ? 'filled-error' : 'filled'; + const isError = (item.driftCalc.hasDrift) ? 'is-error' : ''; + const value = (item.driftCalc.value > 0) ? numberFormatter().format(item.driftCalc.value) : '--'; + const buttonIcon = getButtonIcon(item.driftCalc.type); + + return ( + + + {DRIFT_TEST_ENUM_LABEL[item.driftCalc.type]} + +

+ + {value} + + {' '} + + +

+
+ ), + two: ( + + ), + }} + title={( +
+ + + {DRIFT_FEATURE_TYPE_ENUM[item.driftCalc.type].toUpperCase()}} + /> +
+ )} + /> + )} + modifier="my-4 " + size="small" + /> + ); +} + +const getButtonIcon = (value) => { + switch (value) { + case DRIFT_TEST_ENUM.KS: + return fa1; + + case DRIFT_TEST_ENUM.CHI2: + return faC; + + default: + return ''; + } +}; + +export default DataDriftList; diff --git a/ui/src/container/models/Details/multi-classification/current/data-drift/search-filter/index.jsx b/ui/src/container/models/Details/multi-classification/current/data-drift/search-filter/index.jsx new file mode 100644 index 00000000..7dba3e25 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/data-drift/search-filter/index.jsx @@ -0,0 +1,90 @@ +import { useGetCurrentDriftQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { fa1, faC, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { useFormbitContext } from '@radicalbit/formbit'; +import { + Button, FontAwesomeIcon, FormField, Select, Toggle, + Tooltip, +} from '@radicalbit/radicalbit-design-system'; + +function SearchFeatureList() { + const { write } = useFormbitContext(); + + const { data } = useGetCurrentDriftQueryWithPolling(); + const items = data?.drift.featureMetrics ?? []; + const options = items.map((i) => ({ label: i.featureName, value: i.featureName })); + + const handleOnSelect = (value) => { + write('__metadata.selectedFeatures', value); + }; + + return ( +
+
+ + } + style={{ width: '100%' }} + /> + +
+ + + + +
+ ); +} + +function NumericalFilter() { + const { form, write } = useFormbitContext(); + const { __metadata: { isNumericalSelected } } = form; + + const type = isNumericalSelected ? 'primary' : 'secondary'; + const title = isNumericalSelected ? 'Numerical' : 'Show numerical'; + + const handleOnClick = () => { + write('__metadata.isNumericalSelected', !isNumericalSelected); + }; + + return ( + + + + + + ); +} + +function CategoricalFilter() { + const { form, write } = useFormbitContext(); + const { __metadata: { isCategoricalSelected } } = form; + + const type = isCategoricalSelected ? 'primary' : 'secondary'; + const title = isCategoricalSelected ? 'Categorical' : 'Show categorical'; + + const handleOnClick = () => { + write('__metadata.isCategoricalSelected', !isCategoricalSelected); + }; + + return ( + + + + + + ); +} + +export default SearchFeatureList; diff --git a/ui/src/container/models/Details/multi-classification/current/data-quality/use-get-filtered-features.js b/ui/src/container/models/Details/multi-classification/current/data-quality/use-get-filtered-features.js new file mode 100644 index 00000000..b0636046 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/data-quality/use-get-filtered-features.js @@ -0,0 +1,64 @@ +import { FEATURE_TYPE } from '@Container/models/Details/constants'; +import { modelsApiSlice } from '@Src/store/state/models/api'; +import { useGetCurrentDataQualityQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { useFormbitContext } from '@radicalbit/formbit'; +import { useParams } from 'react-router'; + +const { useGetReferenceDataQualityQuery } = modelsApiSlice; + +export default () => { + const { uuid } = useParams(); + + const { data: currentData } = useGetCurrentDataQualityQueryWithPolling(); + const items = currentData?.dataQuality?.featureMetrics ?? []; + + const { data: referenceData } = useGetReferenceDataQualityQuery({ uuid }); + const referenceItems = referenceData?.dataQuality?.featureMetrics ?? []; + + const { form: { __metadata: { isNumericalSelected, isCategoricalSelected, selectedFeatures } } } = useFormbitContext(); + + if (!currentData) { + return []; + } + + if (!isNumericalSelected && !isCategoricalSelected) { + return []; + } + + const filteredFeatures = selectedFeatures?.length > 0 + ? items.filter(({ featureName, type }) => { + const isSelected = selectedFeatures.includes(featureName); + const isNumerical = isNumericalSelected && type === FEATURE_TYPE.NUMERICAL; + const isCategorical = isCategoricalSelected && type === FEATURE_TYPE.CATEGORICAL; + + return isSelected && (isNumerical || isCategorical); + }) + : items.filter(({ type }) => { + const isNumerical = isNumericalSelected && type === FEATURE_TYPE.NUMERICAL; + const isCategorical = isCategoricalSelected && type === FEATURE_TYPE.CATEGORICAL; + + return isNumerical || isCategorical; + }); + + return filteredFeatures.map((feature) => { + if (feature.type === FEATURE_TYPE.CATEGORICAL) { + const referenceCategoryFrequency = referenceItems?.find((r) => r.type === FEATURE_TYPE.CATEGORICAL && r.featureName === feature.featureName)?.categoryFrequency; + const categoryFrequencyUpdated = feature.categoryFrequency.map((m) => { + const rFounded = referenceCategoryFrequency.find((r) => r.name === m.name); + + return { + ...m, + referenceCount: rFounded?.count, + referenceFrequency: rFounded?.frequency, + }; + }); + + return { + ...feature, + categoryFrequency: categoryFrequencyUpdated, + }; + } + + return feature; + }); +}; diff --git a/ui/src/container/models/Details/multi-classification/current/imports/columns.jsx b/ui/src/container/models/Details/multi-classification/current/imports/columns.jsx new file mode 100644 index 00000000..deb5f7a6 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/imports/columns.jsx @@ -0,0 +1,59 @@ +import { columnFactory } from '@Components/smart-table/utils'; +import useModals from '@Hooks/use-modals'; +import { JOB_STATUS, ModalsEnum } from '@Src/constants'; +import { faExternalLink, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons'; +import moment from 'moment'; +import { DataTableAction, FontAwesomeIcon, Spinner } from '@radicalbit/radicalbit-design-system'; + +export const getColumns = (activeFilters, activeSorter) => [ + columnFactory({ + title: 'File path', + dataIndex: 'path', + key: 'path', + activeFilters, + activeSorter, + }), + columnFactory({ + title: 'Imported at', + dataIndex: 'date', + key: 'date', + activeFilters, + activeSorter, + sorter: true, + render: (date) => moment(date).format('DD MMM YYYY HH:mm:ss').toString(), + }), + columnFactory({ + title: '', + key: 'status', + activeFilters, + activeSorter, + render: (data) => { + switch (data.status) { + case JOB_STATUS.IMPORTING: + return (); + + case JOB_STATUS.ERROR: + return (); + + case JOB_STATUS.SUCCEEDED: + return (); + + default: + return false; + } + }, + + }), + +]; + +function ImportActions({ data }) { + const { showModal } = useModals(); + const showImportDetailModal = () => showModal(ModalsEnum.CURRENT_IMPORT_DETAIL, data); + + return ( +
+ +
+ ); +} diff --git a/ui/src/container/models/Details/multi-classification/current/imports/index.jsx b/ui/src/container/models/Details/multi-classification/current/imports/index.jsx new file mode 100644 index 00000000..9af58b43 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/imports/index.jsx @@ -0,0 +1,93 @@ +import SmartTable from '@Components/smart-table'; +// @ts-ignore +import ImportCurrentDatasetButton from '@Components/ImportButton/import-current-button'; +import LogoSquared from '@Img/logo-collapsed.svg'; +import { NamespaceEnum } from '@Src/constants'; +import { useGetCurrentImportsQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { modelsApiSlice } from '@State/models/api'; +import { Spin, Void } from '@radicalbit/radicalbit-design-system'; +import { useParams } from 'react-router'; +import { useSelector } from 'react-redux'; +import { selectors as contextConfigurationSelectors } from '@State/context-configuration'; +import { getColumns } from './columns'; + +const { useGetCurrentImportsQuery } = modelsApiSlice; + +export default function Imports() { + useGetCurrentImportsQueryWithPolling(); + + const { uuid } = useParams(); + const { data, isLoading } = useGetCurrentImportsQuery({ uuid }); + const importList = data?.items ?? []; + + if (isLoading) { + return (); + } + + if (importList.length === 0) { + return ( + + )} + description={( + <> + Ther is no data available +
+ + {'Import a dataset or with Python following these '} + + instructions + + )} + image={} + title="No current dataset imported yet" + /> + ); + } + + return ( +
+ + + +
+ ); +} + +function FeedbackTable() { + const { uuid: modelUUID } = useParams(); + const queryParams = useSelector((state) => contextConfigurationSelectors.selectQueryParamsSelector(state, NamespaceEnum.CURRENT_IMPORT)); + + const { data } = useGetCurrentImportsQuery({ uuid: modelUUID, queryParams }); + const items = data?.items || []; + const recordCount = data?.total; + + return ( + `${uuid}`} + /> + + ); +} + +function FeedBackHeader() { + const { uuid } = useParams(); + + const { data } = useGetCurrentImportsQuery({ uuid }); + const totalCounter = data?.total; + + return ( +
+
+ {`${totalCounter} Dataset`} +
+ + +
+ ); +} diff --git a/ui/src/container/models/Details/current/index.jsx b/ui/src/container/models/Details/multi-classification/current/index.jsx similarity index 79% rename from ui/src/container/models/Details/current/index.jsx rename to ui/src/container/models/Details/multi-classification/current/index.jsx index b092e556..f2c2c137 100644 --- a/ui/src/container/models/Details/current/index.jsx +++ b/ui/src/container/models/Details/multi-classification/current/index.jsx @@ -4,26 +4,26 @@ import { JOB_STATUS } from '@Src/constants'; import { useGetReferenceDataQualityQueryWithPolling } from '@Src/store/state/models/polling-hook'; import { Tabs } from '@radicalbit/radicalbit-design-system'; import { useSearchParams } from 'react-router-dom'; -import DataDriftMetrics from './data-drift/binary-classification'; -import DataQualityMetrics from './data-quality'; +import MultiClassificationDataQualityMetrics from './data-quality'; import Imports from './imports'; -import ModelQualityMetrics from './model-quality'; +import MultiClassificationModelQualityMetrics from './model-quality'; +import MultiClassificationDataDriftMetrics from './data-drift'; const tabs = [ { label: METRICS_TABS.DATA_QUALITIY, key: METRICS_TABS.DATA_QUALITIY, - children: , + children: , }, { label: METRICS_TABS.MODEL_QUALITY, key: METRICS_TABS.MODEL_QUALITY, - children: , + children: , }, { label: METRICS_TABS.DATA_DRIFT, key: METRICS_TABS.DATA_DRIFT, - children: , + children: , }, { label: METRICS_TABS.IMPORT, diff --git a/ui/src/container/models/Details/multi-classification/current/model-quality/charts.jsx b/ui/src/container/models/Details/multi-classification/current/model-quality/charts.jsx new file mode 100644 index 00000000..bc71b714 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/model-quality/charts.jsx @@ -0,0 +1,199 @@ +import LineChart from '@Container/models/Details/charts/line-chart'; +import { CHART_COLOR, MODEL_QUALITY_FIELD } from '@Container/models/Details/constants'; +import { useGetCurrentModelQualityQueryWithPolling } from '@Src/store/state/models/polling-hook'; +import { modelsApiSlice } from '@State/models/api'; +import { useParams } from 'react-router'; + +const { useGetReferenceModelQualityQuery } = modelsApiSlice; + +function AccuracyChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceAccuracy = referenceData?.modelQuality?.accuracy; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.accuracy; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceAccuracy })); + + return ( + + ); + } + + return false; +} + +function PrecisionChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referencePrecision = referenceData?.modelQuality?.precision; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.precision; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referencePrecision })); + + return ( + + ); + } + return false; +} + +function RecallChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceRecall = referenceData?.modelQuality?.recall; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.recall; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceRecall })); + + return ( + + ); + } + return false; +} + +function F1Chart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceF1 = referenceData?.modelQuality?.f1; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.f1; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceF1 })); + + return ( + + ); + } + return false; +} + +function FalsePositiveRateChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceFalsePositiveRate = referenceData?.modelQuality?.falsePositiveRate; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.falsePositiveRate; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceFalsePositiveRate })); + + return ( + + ); + } + + return false; +} + +function TruePositiveRateChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceTruePositiveRate = referenceData?.modelQuality?.truePositiveRate; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.truePositiveRate; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceTruePositiveRate })); + + return ( + + ); + } + + return false; +} + +function AreaUnderRocChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceAreaUnderRoc = referenceData?.modelQuality?.areaUnderRoc; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.areaUnderRoc; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceAreaUnderRoc })); + + return ( + + ); + } + return false; +} +function AreaUnderPrChart() { + const { uuid } = useParams(); + const { data: currentData } = useGetCurrentModelQualityQueryWithPolling(); + const { data: referenceData } = useGetReferenceModelQualityQuery({ uuid }); + + const referenceAreaUnderPr = referenceData?.modelQuality?.areaUnderPr; + const currentSeries = currentData?.modelQuality?.groupedMetrics?.areaUnderPr; + + if (currentSeries && currentSeries !== null) { + const referenceSeries = currentSeries.map((o) => ({ ...o, value: referenceAreaUnderPr })); + + return ( + + ); + } + return false; +} + +export { + AccuracyChart, AreaUnderPrChart, AreaUnderRocChart, F1Chart, + FalsePositiveRateChart, PrecisionChart, + RecallChart, TruePositiveRateChart, +}; diff --git a/ui/src/container/models/Details/multi-classification/current/model-quality/columns.jsx b/ui/src/container/models/Details/multi-classification/current/model-quality/columns.jsx new file mode 100644 index 00000000..af9e84cf --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/current/model-quality/columns.jsx @@ -0,0 +1,25 @@ +import { numberFormatter } from '@Src/constants'; + +export default [ + { + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, + }, + { + title: 'Current', + key: 'currentValue', + dataIndex: 'currentValue', + align: 'right', + render: (currentValue) => numberFormatter().format(currentValue), + }, + { + title: 'Reference', + key: 'referenceValue', + dataIndex: 'referenceValue', + align: 'right', + render: (referenceValue) => (referenceValue) ? numberFormatter().format(referenceValue) : '--', + }, + +]; diff --git a/ui/src/container/models/Details/current/model-quality/binary-classification/index.jsx b/ui/src/container/models/Details/multi-classification/current/model-quality/index.jsx similarity index 98% rename from ui/src/container/models/Details/current/model-quality/binary-classification/index.jsx rename to ui/src/container/models/Details/multi-classification/current/model-quality/index.jsx index 1b5238d9..c2bd971e 100644 --- a/ui/src/container/models/Details/current/model-quality/binary-classification/index.jsx +++ b/ui/src/container/models/Details/multi-classification/current/model-quality/index.jsx @@ -24,7 +24,7 @@ import columns from './columns'; const { useGetReferenceModelQualityQuery } = modelsApiSlice; -function BinaryClassificationMetrics() { +function MultiClassificationModelQualityMetrics() { const { data, isLoading, isError } = useGetCurrentModelQualityQueryWithPolling(); const jobStatus = data?.jobStatus; @@ -190,4 +190,4 @@ function PerformanceBoard() { ); } -export default memo(BinaryClassificationMetrics); +export default memo(MultiClassificationModelQualityMetrics); diff --git a/ui/src/container/models/Details/multi-classification/index.jsx b/ui/src/container/models/Details/multi-classification/index.jsx new file mode 100644 index 00000000..659f5dae --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/index.jsx @@ -0,0 +1,37 @@ +import NotFound from '@Components/ErrorPage/not-found'; +import { modelsApiSlice } from '@State/models/api'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { MODEL_TABS_ENUM } from '@Container/models/Details/constants'; +import Current from './current'; +import Overview from './overview'; +import ReferenceDashboard from './reference'; + +const { useGetModelByUUIDQuery } = modelsApiSlice; + +function MultiClassificationMetrics() { + const { uuid } = useParams(); + + const [searchParams] = useSearchParams(); + const activeKey = searchParams.get('tab') || MODEL_TABS_ENUM.OVERVIEW; + + const { error } = useGetModelByUUIDQuery({ uuid }); + const status = error?.status; + + if (status === 404) { + return ; + } + + if (activeKey === MODEL_TABS_ENUM.OVERVIEW) { + return (); + } + + if (activeKey === MODEL_TABS_ENUM.REFERENCE_DASHBOARD) { + return (); + } + + if (activeKey === MODEL_TABS_ENUM.CURRENT_DASHBOARD) { + return (); + } +} + +export default MultiClassificationMetrics; diff --git a/ui/src/container/models/Details/multi-classification/overview/index.jsx b/ui/src/container/models/Details/multi-classification/overview/index.jsx new file mode 100644 index 00000000..b5b050c2 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/index.jsx @@ -0,0 +1,58 @@ +import { + Tabs, +} from '@radicalbit/radicalbit-design-system'; +import { useSearchParams } from 'react-router-dom'; +import { MODEL_TABS_ENUM, OVERVIEW_TABS_ENUM } from '@Container/models/Details/constants'; +import OutputsTab from './outputs-tab'; +import SummaryTab from './summary-tab'; +import VariablesTab from './variables-tab'; + +function Overview() { + const [searchParams, setSearchParams] = useSearchParams(); + + const activeKey = useGetTabParam(); + + const handleOnChange = (key) => { + searchParams.set(MODEL_TABS_ENUM.OVERVIEW, key); + setSearchParams(searchParams); + }; + + const tabs = [ + { + label: 'Summary', + key: OVERVIEW_TABS_ENUM.SUMMARY, + children: , + }, + { + label: 'Variables', + key: OVERVIEW_TABS_ENUM.VARIABLES, + children: , + }, + { + label: 'Outputs', + key: OVERVIEW_TABS_ENUM.OUTPUTS, + children: , + }, + ]; + + return ( + + ); +} + +const useGetTabParam = () => { + const [searchParams] = useSearchParams(); + + const tab = searchParams.get(MODEL_TABS_ENUM.OVERVIEW); + + return tab ? OVERVIEW_TABS_ENUM[tab.toUpperCase()] : OVERVIEW_TABS_ENUM.SUMMARY; +}; + +export default Overview; diff --git a/ui/src/container/models/Details/multi-classification/overview/outputs-tab/columns.jsx b/ui/src/container/models/Details/multi-classification/overview/outputs-tab/columns.jsx new file mode 100644 index 00000000..e72c5fac --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/outputs-tab/columns.jsx @@ -0,0 +1,36 @@ +import { Tag } from '@radicalbit/radicalbit-design-system'; +import { OVERVIEW_ROW_TYPE } from '@Container/models/Details/constants'; + +const outputsColumns = (dataSource) => [ + { + title: '', + key: 'index', + width: '30px', + render: (_, record) => , + }, { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, { + title: 'Type', + dataIndex: 'type', + key: 'type', + }, { + title: '', + dataIndex: 'outputType', + key: 'outputType', + render: (_, record) => { + if (record.outputType.length > 0) { + const tagType = record.outputType === OVERVIEW_ROW_TYPE.PREDICTION ? 'full' : ''; + return ( +
+ {record.outputType} +
+ ); + } + + return false; + }, + }, +]; +export default outputsColumns; diff --git a/ui/src/container/models/Details/multi-classification/overview/outputs-tab/index.jsx b/ui/src/container/models/Details/multi-classification/overview/outputs-tab/index.jsx new file mode 100644 index 00000000..d2a33ee7 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/outputs-tab/index.jsx @@ -0,0 +1,46 @@ +import { DataTable } from '@radicalbit/radicalbit-design-system'; +import { memo } from 'react'; +import { useParams } from 'react-router'; +import { modelsApiSlice } from '@Store/state/models/api'; +import { OVERVIEW_ROW_TYPE } from '@Container/models/Details/constants'; +import outputsColumns from './columns'; + +const { useGetModelByUUIDQuery } = modelsApiSlice; + +function OutputsTab() { + const { uuid } = useParams(); + const { data } = useGetModelByUUIDQuery({ uuid }); + + const predictionProba = data?.outputs.predictionProba; + const prediction = data?.outputs.prediction; + + const outputs = data?.outputs.output.map((output) => { + const rowOutputType = (function getRowType({ name }) { + if (name === predictionProba?.name) { + return OVERVIEW_ROW_TYPE.PROBABILITY; + } + + if (name === prediction.name) { + return OVERVIEW_ROW_TYPE.PREDICTION; + } + + return ''; + }(output)); + + return { ...output, outputType: rowOutputType }; + }) ?? []; + + const handleRowClassName = ({ name }) => (name === predictionProba?.name) || (name === prediction.name) ? DataTable.ROW_PRIMARY_LIGHT : ''; + + return ( + b.outputType.length - a.outputType.length)} + pagination={{ pageSize: 200, hideOnSinglePage: true }} + rowClassName={handleRowClassName} + rowKey={({ name }) => name} + /> + ); +} + +export default memo(OutputsTab); diff --git a/ui/src/container/models/Details/multi-classification/overview/summary-tab/columns.jsx b/ui/src/container/models/Details/multi-classification/overview/summary-tab/columns.jsx new file mode 100644 index 00000000..e04664a0 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/summary-tab/columns.jsx @@ -0,0 +1,107 @@ +import { numberFormatter } from '@Src/constants'; + +const currentColumns = [{ + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, +}, { + title: 'Current', + key: 'currentValue', + dataIndex: 'currentValue', + align: 'right', + render: (value) => numberFormatter().format(value), +}]; + +const referenceColumns = [{ + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, +}, { + title: 'Reference', + key: 'referenceValue', + dataIndex: 'referenceValue', + align: 'right', + render: (value) => numberFormatter().format(value), +}]; + +const columnsComparison = [{ + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, +}, +{ + title: 'Current', + key: 'currentValue', + dataIndex: 'currentValue', + align: 'right', + render: (currentValue) => numberFormatter().format(currentValue), +}, +{ + title: 'Reference', + key: 'referenceValue', + dataIndex: 'referenceValue', + align: 'right', + render: (referenceValue) => numberFormatter().format(referenceValue), +}]; + +const leftDataSourceTable = ({ reference, current }) => [ + { + label: 'Number of variables', + referenceValue: reference ? reference.statistics.nVariables : undefined, + currentValue: current ? current.statistics?.nVariables : undefined, + }, + { + label: 'Number of observations', + referenceValue: reference ? reference.statistics.nObservations : undefined, + currentValue: current ? current.statistics?.nObservations : undefined, + }, + { + label: 'Missing cells', + referenceValue: reference ? reference.statistics.missingCells : undefined, + currentValue: current ? current.statistics?.missingCells : undefined, + }, + { + label: 'Missing cells (%)', + referenceValue: reference ? reference.statistics.missingCellsPerc : undefined, + currentValue: current ? current.statistics?.missingCellsPerc : undefined, + }, + { + label: 'Duplicated rows', + referenceValue: reference ? reference.statistics.duplicateRows : undefined, + currentValue: current ? current.statistics?.duplicateRows : undefined, + }, + { + label: 'Duplicated rows (%)', + referenceValue: reference ? reference.statistics.duplicateRowsPerc : undefined, + currentValue: current ? current.statistics?.duplicateRowsPerc : undefined, + }, +]; + +const rigthDataSourceTable = ({ reference, current }) => [ + { + label: 'Numerical', + referenceValue: reference ? reference.statistics.numeric : undefined, + currentValue: current ? current.statistics?.numeric : undefined, + }, + { + label: 'Categorical', + referenceValue: reference ? reference.statistics.categorical : undefined, + currentValue: current ? current.statistics?.categorical : undefined, + }, + { + label: 'Datetime', + referenceValue: reference ? reference.statistics.datetime : undefined, + currentValue: current ? current.statistics?.datetime : undefined, + }, +]; + +export { + currentColumns, + referenceColumns, + columnsComparison, + leftDataSourceTable, + rigthDataSourceTable, +}; diff --git a/ui/src/container/models/Details/multi-classification/overview/summary-tab/index.jsx b/ui/src/container/models/Details/multi-classification/overview/summary-tab/index.jsx new file mode 100644 index 00000000..74205a71 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/summary-tab/index.jsx @@ -0,0 +1,190 @@ +import JobStatus from '@Components/JobStatus'; +import { JOB_STATUS } from '@Src/constants'; +import { useGetCurrentImportsQueryWithPolling, useGetCurrentStatisticsQueryWithPolling, useGetReferenceStatisticsQueryWithPolling } from '@State/models/polling-hook'; +import { modelsApiSlice } from '@Store/state/models/api'; +import { + Collapse, + DataTable, + Spinner, +} from '@radicalbit/radicalbit-design-system'; +import moment from 'moment'; +import { useParams } from 'react-router'; +import { Virtuoso } from 'react-virtuoso'; +import { + columnsComparison, + currentColumns, + leftDataSourceTable, + referenceColumns, + rigthDataSourceTable, +} from './columns'; + +const { Panel } = Collapse; + +const { useGetCurrentStatisticsByUUIDQuery } = modelsApiSlice; + +function SummaryTab() { + const { data, isLoading } = useGetReferenceStatisticsQueryWithPolling(); + const jobStatus = data?.jobStatus; + + if (isLoading) { + return ; + } + + if (jobStatus === JOB_STATUS.SUCCEEDED) { + return ( + <> + + + + + ); + } + + return (); +} + +function CurrentList() { + const { data: currents } = useGetCurrentImportsQueryWithPolling(); + + const [, ...rest] = currents?.items || []; + + if (rest.length === 0) { + return false; + } + + return ( + ()} + totalCount={rest.length} + /> + ); +} + +function ReferenceCurrentLatestComparison() { + const { data: reference, isSuccess: isReferenceSuccess } = useGetReferenceStatisticsQueryWithPolling(); + const date = reference?.date; + + const { data: latestCurrent } = useGetCurrentStatisticsQueryWithPolling(); + const latestStatistics = latestCurrent?.statistics; + + if (!isReferenceSuccess) { + return false; + } + + if (latestStatistics) { + return ( + + +
+ DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> + + DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> +
+
+
+ ); + } + + return ( + + +
+ DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> + + DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> +
+
+
+ ); +} + +function Currents({ currentUUID }) { + const { uuid } = useParams(); + + const { data: current, isSuccess } = useGetCurrentStatisticsByUUIDQuery({ uuid, currentUUID }); + const date = current?.date; + + if (!isSuccess) { + return false; + } + + return ( + + +
+ DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> + + DataTable.ROW_NOT_CLICKABLE} + rowKey={({ label }) => label} + size="small" + /> +
+
+
+ ); +} + +export default SummaryTab; diff --git a/ui/src/container/models/Details/multi-classification/overview/variables-tab/columns.jsx b/ui/src/container/models/Details/multi-classification/overview/variables-tab/columns.jsx new file mode 100644 index 00000000..eae59cad --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/variables-tab/columns.jsx @@ -0,0 +1,35 @@ +import { Tag } from '@radicalbit/radicalbit-design-system'; + +const featuresColumns = (dataSource) => [ + { + title: '', + key: 'index', + width: '30px', + render: (_, record) => , + }, { + title: 'Name', + dataIndex: 'name', + key: 'name', + }, { + title: 'Type', + dataIndex: 'type', + key: 'type', + }, + { + title: '', + dataIndex: 'type', + key: 'type', + render: (_, record) => { + if (record.rowType) { + return ( +
+ {record.rowType} +
+ ); + } + return false; + }, + }, +]; + +export default featuresColumns; diff --git a/ui/src/container/models/Details/multi-classification/overview/variables-tab/index.jsx b/ui/src/container/models/Details/multi-classification/overview/variables-tab/index.jsx new file mode 100644 index 00000000..01ac6efb --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/overview/variables-tab/index.jsx @@ -0,0 +1,34 @@ +import { DataTable } from '@radicalbit/radicalbit-design-system'; +import { useParams } from 'react-router'; +import { memo } from 'react'; +import { modelsApiSlice } from '@Store/state/models/api'; +import { OVERVIEW_ROW_TYPE } from '@Container/models/Details/constants'; +import featuresColumns from './columns'; + +const { useGetModelByUUIDQuery } = modelsApiSlice; + +function VariablesTab() { + const { uuid } = useParams(); + const { data } = useGetModelByUUIDQuery({ uuid }); + + const features = data?.features ?? []; + const target = data?.target ?? []; + const variables = [target].concat(features).map((f) => ({ + ...f, + rowType: f.name === target.name ? OVERVIEW_ROW_TYPE.GROUND_TRUTH : '', + })); + + const handleRowClassName = ({ rowType }) => rowType.length > 0 ? DataTable.ROW_PRIMARY_LIGHT : ''; + + return ( + b.rowType.length - a.rowType.length)} + pagination={{ pageSize: 200, hideOnSinglePage: true }} + rowClassName={handleRowClassName} + rowKey={({ name }) => name} + /> + ); +} + +export default memo(VariablesTab); diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/index.jsx new file mode 100644 index 00000000..f99a6ec4 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/index.jsx @@ -0,0 +1,122 @@ +import { modelsApiSlice } from '@Src/store/state/models/api'; +import ReactEchartsCore from 'echarts-for-react/lib/core'; +import { BarChart } from 'echarts/charts'; +import { + GridComponent, + LegendComponent, + MarkLineComponent, + MarkPointComponent, +} from 'echarts/components'; +import * as echarts from 'echarts/lib/echarts'; +import { + Board, + SectionTitle, +} from '@radicalbit/radicalbit-design-system'; +import { useParams } from 'react-router'; +import chartOptions from './options'; + +echarts.use([ + GridComponent, + MarkPointComponent, + MarkLineComponent, + LegendComponent, + BarChart, +]); + +const { useGetReferenceDataQualityQuery, useGetModelByUUIDQuery } = modelsApiSlice; + +const numberFormatter = (value) => new Intl.NumberFormat('it-IT').format(value); + +const numberCompactFormatter = (value, maximumSignificantDigits) => { + const str = Intl.NumberFormat('en-EN', { notation: 'compact', maximumSignificantDigits }).format(value); + + const re = /(\d*\.?\d+)([a-zA-Z]*)/; + const [, figures, letter] = re.exec(str); + + return { figures, letter }; +}; + +function DataPointDistribution() { + return ( +
+
+ +
+ + + +
+ ); +} + +function DataPointDistributionCounter() { + const { uuid } = useParams(); + + const { data } = useGetReferenceDataQualityQuery({ uuid }); + const nObservations = data?.dataQuality.nObservations ?? 0; + + const { figures, letter } = numberCompactFormatter(nObservations); + const fullNumber = numberFormatter(nObservations); + + return ( + } + main={( +
+
+ + {/* FIXME: inline style */} +
{figures}
+ +
{letter}
+
+ + + +
+ )} + modifier="h-full shadow" + size="small" + type="secondary" + /> + ); +} + +function DataPointDistributionChart() { + const { uuid } = useParams(); + + const { data: model } = useGetModelByUUIDQuery({ uuid }); + const title = model?.target.name; + + const { data } = useGetReferenceDataQualityQuery({ uuid }); + + const classMetrics = data?.dataQuality.classMetrics ?? {}; + + const handleOnChartReady = (echart) => { + // To handle the second opening of a modal when the rtkq hook read from cache + // and the echart graph will render immediately. + setTimeout(echart.resize); + }; + + return ( + } + main={( +
+ +
+ )} + modifier="w-full h-full shadow" + size="small" + /> + ); +} + +export default DataPointDistribution; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/options.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/options.jsx new file mode 100644 index 00000000..d42874d0 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-point-distribution/options.jsx @@ -0,0 +1,58 @@ +import { CHART_COLOR } from '@Container/models/Details/constants'; +import { numberFormatter } from '@Src/constants'; + +export default function chartOptions(title, dataset) { + const yAxisLabel = dataset.map(({ name }) => name); + + return { + grid: { + left: 0, + right: 20, + bottom: 0, + top: 0, + containLabel: true, + }, + xAxis: { + type: 'value', + boundaryGap: true, + axisLabel: { + fontSize: 9, + color: '#9b99a1', + }, + }, + yAxis: { + type: 'category', + data: yAxisLabel, + boundaryGap: true, + axisTick: { show: false }, + axisLine: { show: false }, + splitLine: { show: false }, + axisLabel: { + fontSize: 10, + }, + }, + emphasis: { + disabled: true, + }, + barCategoryGap: '21%', + overflow: 'truncate', + lineOverflow: 'truncate', + series: [ + { + name: title, + type: 'bar', + color: CHART_COLOR.REFERENCE_LIGHT, + emphasis: { + focus: 'series', + }, + label: { + show: true, + position: 'insideRight', + fontWeight: 'bold', + formatter: (el) => (el.data.count > 0) ? `${el.data.count} (${numberFormatter().format(el.data.percentage)}%)` : '', + }, + data: dataset.map(({ count, percentage }) => ({ percentage, count, value: count })), + }, + ], + }; +} diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/columns.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/columns.jsx new file mode 100644 index 00000000..6c96de48 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/columns.jsx @@ -0,0 +1,16 @@ +const columns = [ + { + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, + }, + { + title: '', + key: 'value', + dataIndex: 'value', + align: 'right', + }, +]; + +export default columns; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/index.jsx new file mode 100644 index 00000000..ba1d5f08 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/left-table/index.jsx @@ -0,0 +1,25 @@ +import { DataTable } from '@radicalbit/radicalbit-design-system'; +import { numberFormatter } from '@Src/constants'; +import columns from './columns'; + +function CategoricalLeftTable({ data }) { + const dataSource = (el) => [ + { label: 'Miss_val', value: el.missingValue?.count ?? '--' }, + { label: '%_miss_val', value: el.missingValue?.percentage ?? '--' }, + { label: 'Dist_val', value: el?.distinctValue ?? '--' }, + ].map((o) => ({ ...o, value: (o.value !== '--') ? numberFormatter().format(o.value) : '--' })); + + return ( + label} + size="small" + /> + ); +} + +export default CategoricalLeftTable; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/columns.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/columns.jsx new file mode 100644 index 00000000..243ffe33 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/columns.jsx @@ -0,0 +1,33 @@ +import { BarChart } from '@radicalbit/radicalbit-design-system'; + +const columns = [ + { + title: 'Name', + key: 'name', + dataIndex: 'name', + }, + { + title: 'Count', + key: 'count', + width: '8rem', + dataIndex: 'count', + }, + { + title: 'Frequency (%)', + key: 'frequency', + width: '12rem', + render: ({ frequency }) => { + const formattedFrequency = Math.floor(frequency * 100); + return ( + + ); + }, + }, +]; + +export default columns; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/index.jsx new file mode 100644 index 00000000..17010598 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/categorical/right-table/index.jsx @@ -0,0 +1,23 @@ +import { DataTable } from '@radicalbit/radicalbit-design-system'; +import columns from './columns'; + +function CategoricalRightTable({ data }) { + if (!data) { + return false; + } + + return ( + DataTable.ROW_NOT_CLICKABLE} + rowKey={({ name }) => name} + scroll={{ y: '12rem' }} + size="small" + /> + ); +} + +export default CategoricalRightTable; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/index.jsx new file mode 100644 index 00000000..0e8e8b26 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/index.jsx @@ -0,0 +1,152 @@ +import { FEATURE_TYPE } from '@Container/models/Details/constants'; +import { + fa1, + faC, +} from '@fortawesome/free-solid-svg-icons'; +import { + Board, + Button, + FontAwesomeIcon, + NewHeader, + SectionTitle, + Spinner, + Tag, +} from '@radicalbit/radicalbit-design-system'; +import { Virtuoso } from 'react-virtuoso'; +import useGetFilteredFeatures from '../use-get-filtered-features'; +import CategoricalLeftTable from './categorical/left-table/index'; +import CategoricalRightTable from './categorical/right-table'; +import NumericalBarChart from './numerical/chart/index'; +import NumericalTable from './numerical/table/index'; + +function DataQualityList() { + const items = useGetFilteredFeatures(); + + return ( + + + + { + switch (item.type) { + case FEATURE_TYPE.NUMERICAL: + return (); + + case FEATURE_TYPE.CATEGORICAL: + return (); + + default: + return false; + } + }} + totalCount={items.length} + /> + + ); +} + +function CountLabel() { + const items = useGetFilteredFeatures(); + const label = items.length <= 1 ? 'Record' : 'Records'; + + return ( + + ); +} + +function NumericalFeature({ item }) { + const dataset = item?.histogram ?? []; + + return ( + + + + ), + }} + title={( + {item.type.toUpperCase()}} + /> + )} + /> + )} + main={( +
+
+ +
+ +
+ + +
+
+ )} + modifier="my-4 " + size="small" + + /> + ); +} + +function CategoricalFeature({ item }) { + const dataset = item?.categoryFrequency ?? []; + + return ( + + + + ), + }} + title={( + {item.type.toUpperCase()}} + /> + )} + /> + )} + main={( +
+
+ +
+ +
+ +
+
+ )} + modifier="my-4 " + size="small" + /> + ); +} + +export default DataQualityList; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/index.jsx new file mode 100644 index 00000000..ee944f58 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/index.jsx @@ -0,0 +1,43 @@ +import { CHART_COLOR } from '@Container/models/Details/constants'; +import ReactEchartsCore from 'echarts-for-react/lib/core'; +import { BarChart } from 'echarts/charts'; +import { + GridComponent, + MarkLineComponent, + MarkPointComponent, +} from 'echarts/components'; +import * as echarts from 'echarts/lib/echarts'; +import { memo } from 'react'; +import chartOptions from './options'; + +echarts.use([ + GridComponent, + MarkPointComponent, + MarkLineComponent, + BarChart, +]); + +function NumericalBarChart({ dataset }) { + const handleOnChartReady = (echart) => { + // To handle the second opening of a modal when the rtkq hook read from cache + // and the echart graph will render immediately. + setTimeout(echart.resize); + }; + + if (!dataset) { + return false; + } + + return ( +
+ +
+ ); +} + +export default memo(NumericalBarChart); diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/options.js b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/options.js new file mode 100644 index 00000000..8b164a76 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/chart/options.js @@ -0,0 +1,44 @@ +import { numberFormatter } from '@Src/constants'; + +export default function chartOptions(dataset, color) { + const dataFormatted = dataset.buckets.map((value) => numberFormatter().format(value)); + const lastElementData = dataFormatted.pop(); + const xAxisData = dataFormatted.map((el, idx) => `[${dataFormatted[idx]}${(idx < dataFormatted.length - 1) ? `-${dataFormatted[idx + 1]})` : (idx === dataFormatted.length - 1) ? `-${lastElementData}]` : ''} `); + + return { + grid: { + left: 0, + right: 20, + bottom: 0, + top: 10, + containLabel: true, + }, + xAxis: { + type: 'category', + data: xAxisData, + axisTick: { show: false }, + axisLine: { show: false }, + splitLine: { show: false }, + axisLabel: { + fontSize: 12, + interval: 0, + rotate: 20, + color: '#9b99a1', + }, + }, + yAxis: { + type: 'value', + axisLabel: { + fontSize: 9, + color: '#9b99a1', + }, + }, + series: [ + { + data: dataset.referenceValues, + type: 'bar', + itemStyle: { color }, + }, + ], + }; +} diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/columns.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/columns.jsx new file mode 100644 index 00000000..6c96de48 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/columns.jsx @@ -0,0 +1,16 @@ +const columns = [ + { + title: '', + key: 'label', + dataIndex: 'label', + render: (label) =>
{label}
, + }, + { + title: '', + key: 'value', + dataIndex: 'value', + align: 'right', + }, +]; + +export default columns; diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/index.jsx new file mode 100644 index 00000000..31753a25 --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/data-quality-list/numerical/table/index.jsx @@ -0,0 +1,66 @@ +import { DataTable } from '@radicalbit/radicalbit-design-system'; +import { numberFormatter } from '@Src/constants'; +import columns from './columns'; + +function NumericalTable({ data }) { + if (!data) { + return false; + } + + const leftTableData = (el) => [ + { label: 'AVG', value: el?.mean ?? '--' }, + { label: 'STD', value: el?.std ?? '--' }, + { label: 'Min', value: el?.min ?? '--' }, + { label: 'Max', value: el?.max ?? '--' }, + ].map((o) => ({ ...o, value: (o.value !== '--') ? numberFormatter().format(o.value) : '--' })); + + const centerTableData = (el) => [ + { label: 'Perc_25', value: el?.medianMetrics?.perc25 ?? '--' }, + { label: 'Median', value: el?.medianMetrics?.median ?? '--' }, + { label: 'Perc_75', value: el?.medianMetrics?.perc75 ?? '--' }, + ].map((o) => ({ ...o, value: (o.value !== '--') ? numberFormatter().format(o.value) : '--' })); + + const rigthTableData = (el) => [ + { label: 'Miss_val', value: el?.missingValue?.count ?? '--' }, + { label: '%_miss_val', value: el?.missingValue?.percentage ?? '--' }, + ].map((o) => ({ ...o, value: (o.value !== '--') ? numberFormatter().format(o.value) : '--' })); + + return ( +
+ label} + size="small" + /> + + label} + size="small" + /> + + label} + size="small" + /> +
+ ); +} + +export default NumericalTable; diff --git a/ui/src/container/models/Details/reference/data-quality/binary-classification/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/index.jsx similarity index 92% rename from ui/src/container/models/Details/reference/data-quality/binary-classification/index.jsx rename to ui/src/container/models/Details/multi-classification/reference/data-quality/index.jsx index 2f9979c7..8388eda4 100644 --- a/ui/src/container/models/Details/reference/data-quality/binary-classification/index.jsx +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/index.jsx @@ -7,8 +7,8 @@ import { FormbitContextProvider } from '@radicalbit/formbit'; import { memo } from 'react'; import { useParams } from 'react-router'; import DataPointDistribution from './data-point-distribution'; -import DataQualityList from './data-quality-list'; import SearchFeatureList from './search-filter'; +import DataQualityList from './data-quality-list'; const { useGetReferenceDataQualityQuery } = modelsApiSlice; @@ -20,7 +20,7 @@ const initialValues = { }, }; -function BinaryClassificationMetrics() { +function MultiClassificationDataQualityMetrics() { useGetReferenceDataQualityQueryWithPolling(); const { uuid } = useParams(); const { data, isError } = useGetReferenceDataQualityQuery({ uuid }); @@ -49,4 +49,4 @@ function BinaryClassificationMetrics() { return (); } -export default memo(BinaryClassificationMetrics); +export default memo(MultiClassificationDataQualityMetrics); diff --git a/ui/src/container/models/Details/multi-classification/reference/data-quality/search-filter/index.jsx b/ui/src/container/models/Details/multi-classification/reference/data-quality/search-filter/index.jsx new file mode 100644 index 00000000..544fca4d --- /dev/null +++ b/ui/src/container/models/Details/multi-classification/reference/data-quality/search-filter/index.jsx @@ -0,0 +1,95 @@ +import { modelsApiSlice } from '@State/models/api'; +import { fa1, faC, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { useFormbitContext } from '@radicalbit/formbit'; +import { + Button, FontAwesomeIcon, FormField, Select, Toggle, + Tooltip, +} from '@radicalbit/radicalbit-design-system'; +import { useParams } from 'react-router'; + +const { useGetReferenceDataQualityQuery } = modelsApiSlice; + +function SearchFeatureList() { + const { uuid } = useParams(); + const { write } = useFormbitContext(); + + const { data } = useGetReferenceDataQualityQuery({ uuid }); + const items = data?.dataQuality.featureMetrics ?? []; + const options = items.map((i) => ({ label: i.featureName, value: i.featureName })); + + const handleOnSelect = (value) => { + write('__metadata.selectedFeatures', value); + }; + + return ( +
+
+ +