From 48b6df4ab4e254f03a118dd298e82237c3186877 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Wed, 24 Jun 2026 17:31:15 -0400 Subject: [PATCH 01/28] fix: replace en dashes with hyphens in benchmark docs --- docs/docs/deep-dive/benchmark.md | 12 ++++++------ .../current/deep-dive/benchmark.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/docs/deep-dive/benchmark.md b/docs/docs/deep-dive/benchmark.md index d5761b4e8..69c21053f 100644 --- a/docs/docs/deep-dive/benchmark.md +++ b/docs/docs/deep-dive/benchmark.md @@ -30,13 +30,13 @@ KNIME and Orange use GPLv3; WEKA, GPL. All three licenses include copyleft on di dashAI exposes twelve base classes organized by functional role: `BaseModel`, `BaseMetric`, `BaseTask`, `BaseExplainer`, among others. A new component is implemented by subclassing the corresponding abstraction and declaring its parameters via a Pydantic schema. From the schema, the platform automatically generates the configuration form in the interface, without requiring any frontend code. The component is distributed via PyPI and installed directly from the dashAI interface. -The resulting boilerplate is approximately **20–40 lines of code** for simple components (metrics, basic classifiers) and **70–110 lines** for models with multiple hyperparameters, where most of the code corresponds to the parameter schema declaration. +The resulting boilerplate is approximately **20-40 lines of code** for simple components (metrics, basic classifiers) and **70-110 lines** for models with multiple hyperparameters, where most of the code corresponds to the parameter schema declaration. For reference, the extension mechanisms of the other evaluated platforms are as follows: -- **Orange** allows Python extensions, but each widget must couple to Qt/PyQt and manually instantiate interface controls (≈50–70 lines of code). -- **WEKA** is extended in Java by inheriting from `AbstractClassifier` and implementing `buildClassifier(Instances)` and `distributionForInstance(Instance)` over the `Instances` abstraction (≈80–120 lines of code). -- **KNIME**, via the official path, requires an OSGi/Eclipse plugin with four Java classes (`NodeFactory`, `NodeModel`, `NodeDialog`, and `NodeView`), plus `plugin.xml` and `MANIFEST.MF` descriptors, and a Maven/Tycho build (≈150–250 lines of code). Since version 4.6 there is an experimental Python path (Labs) that generates UI from parameter declarations, but it does not replace the Java path as the official approach and requires its own packaging tools (`pixi`, `knime.yml`). +- **Orange** allows Python extensions, but each widget must couple to Qt/PyQt and manually instantiate interface controls (≈50-70 lines of code). +- **WEKA** is extended in Java by inheriting from `AbstractClassifier` and implementing `buildClassifier(Instances)` and `distributionForInstance(Instance)` over the `Instances` abstraction (≈80-120 lines of code). +- **KNIME**, via the official path, requires an OSGi/Eclipse plugin with four Java classes (`NodeFactory`, `NodeModel`, `NodeDialog`, and `NodeView`), plus `plugin.xml` and `MANIFEST.MF` descriptors, and a Maven/Tycho build (≈150-250 lines of code). Since version 4.6 there is an experimental Python path (Labs) that generates UI from parameter declarations, but it does not replace the Java path as the official approach and requires its own packaging tools (`pixi`, `knime.yml`). ### Interface Architecture @@ -66,7 +66,7 @@ The interface is available in Spanish and English. | Abstractions by functional role | 1 (generic node) | 1 (generic widget) | 6 Java hierarchies | **12 base classes** | | Interface type | Desktop (Eclipse) | Desktop (PyQt) | Desktop (Java Swing) | **Web (React + FastAPI)** | | Autogenerated UI | Partial | No | No | **Yes** | -| Boilerplate per component | 150–250 LoC (Java) | 50–70 LoC | 80–120 LoC | **20–40 LoC** | +| Boilerplate per component | 150-250 LoC (Java) | 50-70 LoC | 80-120 LoC | **20-40 LoC** | | GPU support | Partial | Partial | Partial | **Partial** | | Multilingual interface (ES/EN) | No | No | No | **Yes** | | **Native Catalog** | | | | | @@ -85,7 +85,7 @@ The official KNIME path (Java) requires programming `NodeDialog` and `NodeView` ::: :::note Boilerplate per component -The 150–250 LoC for KNIME correspond to the official Java path. The Python path (Labs) reduces that number but adds its own configuration files (`knime.yml`, `pixi.toml`). In dashAI: ≈20–40 LoC for simple components; ≈70–110 LoC for complex models with multiple hyperparameters, where most of the code is the parameter schema declaration. +The 150-250 LoC for KNIME correspond to the official Java path. The Python path (Labs) reduces that number but adds its own configuration files (`knime.yml`, `pixi.toml`). In dashAI: ≈20-40 LoC for simple components; ≈70-110 LoC for complex models with multiple hyperparameters, where most of the code is the parameter schema declaration. ::: :::note Task overlap (dashAI) diff --git a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/benchmark.md b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/benchmark.md index e64ada972..f43f9526e 100644 --- a/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/benchmark.md +++ b/docs/i18n/es/docusaurus-plugin-content-docs/current/deep-dive/benchmark.md @@ -30,13 +30,13 @@ KNIME y Orange utilizan GPLv3; WEKA, GPL. Las tres licencias incluyen copyleft s dashAI expone doce clases base organizadas por rol funcional: `BaseModel`, `BaseMetric`, `BaseTask`, `BaseExplainer`, entre otras. Un nuevo componente se implementa subclasificando la abstracción correspondiente y declarando sus parámetros mediante un schema Pydantic. A partir del schema, la plataforma genera automáticamente el formulario de configuración en la interfaz, sin requerir código de frontend. El componente se distribuye vía PyPI e instala directamente desde la interfaz de dashAI. -El boilerplate resultante es de aproximadamente **20–40 líneas de código** para componentes simples (métricas, clasificadores básicos) y **70–110 líneas** para modelos con múltiples hiperparámetros, donde la mayor parte del código corresponde a la declaración del schema de parámetros. +El boilerplate resultante es de aproximadamente **20-40 líneas de código** para componentes simples (métricas, clasificadores básicos) y **70-110 líneas** para modelos con múltiples hiperparámetros, donde la mayor parte del código corresponde a la declaración del schema de parámetros. Para referencia, los mecanismos de extensión de las otras plataformas evaluadas son los siguientes: -- **Orange** permite extensiones en Python, pero cada widget requiere acoplarse a Qt/PyQt e instanciar manualmente los controles de interfaz (≈50–70 líneas de código). -- **WEKA** se extiende en Java heredando de `AbstractClassifier` e implementando `buildClassifier(Instances)` y `distributionForInstance(Instance)` sobre la abstracción `Instances` (≈80–120 líneas de código). -- **KNIME**, en su vía oficial, requiere un plugin OSGi/Eclipse con cuatro clases Java (`NodeFactory`, `NodeModel`, `NodeDialog` y `NodeView`), además de descriptores `plugin.xml` y `MANIFEST.MF`, y build con Maven/Tycho (≈150–250 líneas de código). Desde la versión 4.6 existe una vía experimental en Python (Labs) que genera UI desde declaraciones de parámetros, pero no reemplaza al camino Java como vía oficial y requiere herramientas de empaquetado propias (`pixi`, `knime.yml`). +- **Orange** permite extensiones en Python, pero cada widget requiere acoplarse a Qt/PyQt e instanciar manualmente los controles de interfaz (≈50-70 líneas de código). +- **WEKA** se extiende en Java heredando de `AbstractClassifier` e implementando `buildClassifier(Instances)` y `distributionForInstance(Instance)` sobre la abstracción `Instances` (≈80-120 líneas de código). +- **KNIME**, en su vía oficial, requiere un plugin OSGi/Eclipse con cuatro clases Java (`NodeFactory`, `NodeModel`, `NodeDialog` y `NodeView`), además de descriptores `plugin.xml` y `MANIFEST.MF`, y build con Maven/Tycho (≈150-250 líneas de código). Desde la versión 4.6 existe una vía experimental en Python (Labs) que genera UI desde declaraciones de parámetros, pero no reemplaza al camino Java como vía oficial y requiere herramientas de empaquetado propias (`pixi`, `knime.yml`). ### Arquitectura de interfaz @@ -66,7 +66,7 @@ La interfaz está disponible en español e inglés. | Abstracciones por rol funcional | 1 (nodo genérico) | 1 (widget genérico) | 6 jerarquías Java | **12 clases base** | | Tipo de interfaz | Desktop (Eclipse) | Desktop (PyQt) | Desktop (Java Swing) | **Web (React + FastAPI)** | | UI generada automáticamente | Parcial | No | No | **Sí** | -| Boilerplate por componente | 150–250 LdC (Java) | 50–70 LdC | 80–120 LdC | **20–40 LdC** | +| Boilerplate por componente | 150-250 LdC (Java) | 50-70 LdC | 80-120 LdC | **20-40 LdC** | | Soporte GPU | Parcial | Parcial | Parcial | **Partial** | | Interfaz multiidioma (ES/EN) | No | No | No | **Sí** | | **Catálogo nativo** | | | | | @@ -85,7 +85,7 @@ La vía oficial de KNIME (Java) requiere programar `NodeDialog` y `NodeView` man ::: :::note Boilerplate por componente -Las 150–250 LdC de KNIME corresponden a la vía Java oficial. La vía Python (Labs) reduce ese número pero añade archivos de configuración propios (`knime.yml`, `pixi.toml`). En dashAI: ≈20–40 LdC para componentes simples; ≈70–110 LdC para modelos complejos con múltiples hiperparámetros, donde la mayor parte del código es la declaración del schema de parámetros. +Las 150-250 LdC de KNIME corresponden a la vía Java oficial. La vía Python (Labs) reduce ese número pero añade archivos de configuración propios (`knime.yml`, `pixi.toml`). En dashAI: ≈20-40 LdC para componentes simples; ≈70-110 LdC para modelos complejos con múltiples hiperparámetros, donde la mayor parte del código es la declaración del schema de parámetros. ::: :::note Solapamiento entre tasks (dashAI) From f919d546c8772f3a2f538a8de3e2a657d14ba670 Mon Sep 17 00:00:00 2001 From: Creylay Date: Thu, 25 Jun 2026 10:09:53 -0400 Subject: [PATCH 02/28] fix: update translation handling for language changes in ModelComparisonTable --- DashAI/front/src/components/models/ModelComparisonTable.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DashAI/front/src/components/models/ModelComparisonTable.jsx b/DashAI/front/src/components/models/ModelComparisonTable.jsx index 571df06b4..7d8eed469 100644 --- a/DashAI/front/src/components/models/ModelComparisonTable.jsx +++ b/DashAI/front/src/components/models/ModelComparisonTable.jsx @@ -44,7 +44,7 @@ function ModelComparisonTable({ const [runs, setRuns] = useState(initialRuns); const [runToDelete, setRunToDelete] = useState(null); - const { t } = useTranslation(["models", "common"]); + const { t, i18n } = useTranslation(["models", "common"]); const theme = useTheme(); const localization = useTableLocalization(); @@ -70,7 +70,7 @@ function ModelComparisonTable({ } }; fetchModels(); - }, []); + }, [i18n.language]); useEffect(() => { const fetchMetrics = async () => { @@ -82,7 +82,7 @@ function ModelComparisonTable({ } }; fetchMetrics(); - }, []); + }, [i18n.language]); // ──────────────────────────────────────────────────────────────────────── // Fetch scoring profiles for this session's task From 784aaf955e16747751cf1bd8c336fc87f69d8af3 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Thu, 25 Jun 2026 11:05:24 -0400 Subject: [PATCH 03/28] docs: add --force-reinstall --no-cache-dir to README pip install commands --- README.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index 140ee58d4..4834973fd 100644 --- a/README.rst +++ b/README.rst @@ -121,10 +121,10 @@ index if you want a smaller CPU only install: .. code:: bash - $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu + $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu --force-reinstall --no-cache-dir # Optional, for GGUF / Llama models (precompiled CPU wheel, no build tools): - $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu + $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu --force-reinstall --no-cache-dir NVIDIA GPU (CUDA 12.8) ~~~~~~~~~~~~~~~~~~~~~~~ @@ -132,10 +132,10 @@ NVIDIA GPU (CUDA 12.8) .. code:: bash # Torch CUDA 12.8 (prebuilt wheels) - $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128 + $ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu128 --force-reinstall --no-cache-dir # Llama compiled with CUDA offload (requires build tools, see below) - $ pip install llama-cpp-python -C cmake.args="-DGGML_CUDA=on" + $ pip install llama-cpp-python -C cmake.args="-DGGML_CUDA=on" --force-reinstall --no-cache-dir --verbose AMD GPU (ROCm 6.4) ~~~~~~~~~~~~~~~~~~ @@ -143,10 +143,10 @@ AMD GPU (ROCm 6.4) .. code:: bash # Torch ROCm (prebuilt wheels) - $ pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.4 + $ pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm6.4 --force-reinstall --no-cache-dir # Llama compiled with HIP/ROCm offload (requires build tools, see below) - $ pip install llama-cpp-python -C cmake.args="-DGGML_HIP=on" + $ pip install llama-cpp-python -C cmake.args="-DGGML_HIP=on" --force-reinstall --no-cache-dir --verbose Build tools for GPU llama-cpp @@ -171,8 +171,8 @@ available for CPU and CUDA: .. code:: bash - $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu - $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/ + $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu --force-reinstall --no-cache-dir + $ pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/ --force-reinstall --no-cache-dir Replace ```` with your CUDA tag. Prebuilt wheels are published for ``cu118``, ``cu121``, ``cu122``, ``cu123``, ``cu124``, ``cu125``, ``cu130`` and @@ -181,7 +181,7 @@ Replace ```` with your CUDA tag. Prebuilt wheels are published for for the available wheels and other backend options. -4. Run dashAI +1. Run dashAI ------------- Start the server and graphical interface with: From c37ec8a36e570c037de0d30a02e5fe0d06ebde85 Mon Sep 17 00:00:00 2001 From: Creylay Date: Thu, 25 Jun 2026 13:57:16 -0400 Subject: [PATCH 04/28] fix: remove original text columns replaced by emb_* counterparts in Embedding class --- DashAI/back/converters/hugging_face/embedding.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DashAI/back/converters/hugging_face/embedding.py b/DashAI/back/converters/hugging_face/embedding.py index 705427b88..cbc726d35 100644 --- a/DashAI/back/converters/hugging_face/embedding.py +++ b/DashAI/back/converters/hugging_face/embedding.py @@ -247,4 +247,9 @@ def _process_batch(self, batch: "DashAIDataset") -> "DashAIDataset": pa.array(embeddings_np[:, i].tolist(), type=pa.float32()), ) + # Remove original text columns — they are replaced by their emb_* counterparts + for column in batch.column_names: + col_idx = result_table.column_names.index(column) + result_table = result_table.remove_column(col_idx) + return DashAIDataset(result_table) From 7a7659c71d9ce1882b067fbe3947ed807474f5db Mon Sep 17 00:00:00 2001 From: Creylay Date: Thu, 25 Jun 2026 13:57:22 -0400 Subject: [PATCH 05/28] fix: remove original text columns replaced by tok_* counterparts in TokenizerConverter --- DashAI/back/converters/hugging_face/tokenizer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/DashAI/back/converters/hugging_face/tokenizer.py b/DashAI/back/converters/hugging_face/tokenizer.py index efe327ccf..79e874e37 100644 --- a/DashAI/back/converters/hugging_face/tokenizer.py +++ b/DashAI/back/converters/hugging_face/tokenizer.py @@ -185,6 +185,11 @@ def _process_batch(self, batch: "DashAIDataset") -> "DashAIDataset": pa.array(input_ids[:, i].tolist(), type=pa.int64()), ) + # Remove original text columns — they are replaced by their tok_* counterparts + for column in batch.column_names: + col_idx = result_table.column_names.index(column) + result_table = result_table.remove_column(col_idx) + return DashAIDataset(result_table) def get_output_type(self, column_name: Optional[str] = None) -> DashAIDataType: From ac64c23d3f281e4106a6ee66a67047931e3d11ab Mon Sep 17 00:00:00 2001 From: Creylay Date: Thu, 25 Jun 2026 15:45:00 -0400 Subject: [PATCH 06/28] =?UTF-8?q?fix:=20update=20Spanish=20display=20names?= =?UTF-8?q?=20for=20Additive=20and=20Skewed=20Chi=C2=B2=20Samplers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py | 2 +- DashAI/back/converters/scikit_learn/skewed_chi_2_sampler.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py b/DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py index e038ba5d2..02c028836 100644 --- a/DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py +++ b/DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py @@ -78,7 +78,7 @@ class AdditiveChi2Sampler( ) DISPLAY_NAME = MultilingualString( en="Additive Chi² Sampler", - es="Muestreador Chi²", + es="Muestreador Chi² Aditivo", pt="Amostrador Qui-2 Aditivo", de="Additiver Chi²-Stichprobennehmer", zh="加性卡方采样器", diff --git a/DashAI/back/converters/scikit_learn/skewed_chi_2_sampler.py b/DashAI/back/converters/scikit_learn/skewed_chi_2_sampler.py index d716a1ac4..5af2a3514 100644 --- a/DashAI/back/converters/scikit_learn/skewed_chi_2_sampler.py +++ b/DashAI/back/converters/scikit_learn/skewed_chi_2_sampler.py @@ -148,7 +148,7 @@ class SkewedChi2Sampler( ) DISPLAY_NAME = MultilingualString( en="Skewed Chi² Sampler", - es="Muestreador Chi²", + es="Muestreador Chi² Sesgado", pt="Amostrador Qui-2 Enviesado", de="Schiefer Chi²-Stichprobennehmer", zh="偏斜卡方采样器", From a787d2db01f2127fc0afe4145994307d1ed5b08a Mon Sep 17 00:00:00 2001 From: Irozuku Date: Thu, 25 Jun 2026 17:28:50 -0400 Subject: [PATCH 07/28] refactor: extract shared categorical encoder mixin Move the categorical feature/target encoding logic out of SklearnLikeModel into a reusable CategoricalEncoderMixin so tabular models share a single implementation instead of duplicating it. Drop the per model CATEGORICAL_ENCODING strategy attribute and its enum: a column whose encoder preference is "label" is label encoded, everything else is one hot encoded. The previous fallback only affected columns with an unrecognized encoder preference, which does not occur in practice. --- .../back/models/categorical_encoder_mixin.py | 208 ++++++++++++++++ .../scikit_learn/bayesian_ridge_regression.py | 4 - .../scikit_learn/elastic_net_regression.py | 4 - .../scikit_learn/k_neighbors_classifier.py | 5 - .../scikit_learn/k_neighbors_regression.py | 4 - .../models/scikit_learn/lasso_regression.py | 4 - DashAI/back/models/scikit_learn/linearSVR.py | 5 - .../models/scikit_learn/linear_regression.py | 5 - .../scikit_learn/linear_svc_classifier.py | 4 - .../scikit_learn/logistic_regression.py | 5 - .../models/scikit_learn/ridge_regression.py | 5 - .../models/scikit_learn/sgd_classifier.py | 4 - .../models/scikit_learn/sklearn_like_model.py | 226 +----------------- DashAI/back/models/scikit_learn/svc.py | 5 - DashAI/back/models/scikit_learn/svr.py | 4 - 15 files changed, 217 insertions(+), 275 deletions(-) create mode 100644 DashAI/back/models/categorical_encoder_mixin.py diff --git a/DashAI/back/models/categorical_encoder_mixin.py b/DashAI/back/models/categorical_encoder_mixin.py new file mode 100644 index 000000000..e760ac180 --- /dev/null +++ b/DashAI/back/models/categorical_encoder_mixin.py @@ -0,0 +1,208 @@ +"""Shared categorical-encoding behavior for tabular DashAI models. + +Models that feed tabular data into an estimator (scikit-learn wrappers, the +PyTorch MLP, etc.) need their ``Categorical`` feature columns turned into a +numeric representation before training/prediction. This mixin centralises that +logic so each model does not reimplement it. + +The mixin provides the ``prepare_dataset`` / ``prepare_output`` hooks expected +by ``BaseModel`` and the fitted encoder state. Persistence of that state is left +to each concrete model (``SklearnLikeModel`` pickles the whole instance via +joblib; the MLP serialises the fields explicitly through ``torch.save``). +""" + +from typing import TYPE_CHECKING, Any, List, Optional + +if TYPE_CHECKING: + from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset + + +class CategoricalEncoderMixin: + """Encode ``Categorical`` feature/target columns into numeric columns. + + When fitting, columns whose ``encoder`` preference is ``"label"`` are label + encoded; every other categorical column is one-hot encoded (the safe default, + since one hot introduces no false ordinal relationship). When applying (not + fitting), the encoders that were actually fitted are applied based on the + stored state (``encodings`` / ``one_hot_encoder``) rather than the dataset's + current ``encoder`` metadata, which can drift between training and prediction + (e.g. a round-trip through Arrow metadata resets the preference to its + default). + """ + + def _setup_categorical_encoders(self) -> None: + """Initialise the fitted-encoder state. + + Concrete models must call this from their ``__init__`` (and ensure the + fields are persisted by their ``save`` / restored by their ``load``). + """ + self.encodings: dict = {} + self.one_hot_encoder: Optional[Any] = None + self.categorical_columns: List[str] = [] + self.output_encodings: dict = {} + + def prepare_dataset( + self, dataset: "DashAIDataset", is_fit: bool = False + ) -> "DashAIDataset": + """Encode categorical feature columns into a numeric representation. + + Parameters + ---------- + dataset : DashAIDataset + The input dataset to preprocess. + is_fit : bool + If True, fit the encoders on the data. If False, apply previously + fitted encoders. Defaults to False. + + Returns + ------- + DashAIDataset + The dataset with categorical columns converted to numeric columns. + """ + from DashAI.back.types.categorical import Categorical + + if not is_fit: + # Apply exactly the encoders fitted during training, regardless of + # the dataset's (possibly drifted) per column encoder preference. + prepared = dataset + if self.encodings: + prepared = self._prepare_label_encoded( + prepared, is_fit, columns=list(self.encodings.keys()) + ) + if self.one_hot_encoder is not None: + prepared = self._prepare_one_hot( + prepared, is_fit, columns=self.categorical_columns + ) + return prepared + + types = dataset.types + if not any(isinstance(t, Categorical) for t in types.values()): + return dataset + + label_cols = [ + c + for c, t in types.items() + if isinstance(t, Categorical) and t.encoder == "label" + ] + # Everything categorical that is not explicitly label-encoded is one-hot + # encoded (the safe default: no false ordinal relationship). + one_hot_cols = [ + c + for c, t in types.items() + if isinstance(t, Categorical) and t.encoder != "label" + ] + + prepared = dataset + if label_cols: + prepared = self._prepare_label_encoded(prepared, is_fit, columns=label_cols) + if one_hot_cols: + prepared = self._prepare_one_hot(prepared, is_fit, columns=one_hot_cols) + return prepared + + def prepare_output( + self, dataset: "DashAIDataset", is_fit: bool = False + ) -> "DashAIDataset": + """Prepare output targets using label encoding. + + Parameters + ---------- + dataset : DashAIDataset + The output dataset to be transformed. + is_fit : bool, optional + If True, fit the encoder. If False, use existing encodings. + + Returns + ------- + DashAIDataset + Dataset with categorical columns converted to integers. + """ + from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( + apply_categorical_label_encoder, + categorical_label_encoder, + ) + + prepared = dataset + if is_fit: + prepared, encodings = categorical_label_encoder(dataset) + self.output_encodings.update(encodings) + elif self.output_encodings: + prepared = apply_categorical_label_encoder(dataset, self.output_encodings) + return prepared + + def _prepare_label_encoded( + self, dataset: "DashAIDataset", is_fit: bool, columns: list = None + ) -> "DashAIDataset": + """Prepare dataset using label encoding for categorical columns. + + This is appropriate for tree based models that don't assume + ordinal relationships between encoded values. + + Parameters + ---------- + dataset : DashAIDataset + The dataset to be transformed. + is_fit : bool + If True, fit the encoder. If False, use existing encodings. + columns : list, optional + If given, only label-encode the specified columns. + + Returns + ------- + DashAIDataset + Dataset with categorical columns converted to integers. + """ + from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( + apply_categorical_label_encoder, + categorical_label_encoder, + ) + + prepared = dataset + if is_fit: + prepared, encodings = categorical_label_encoder(dataset, columns=columns) + self.encodings.update(encodings) + elif self.encodings: + relevant_encodings = { + k: v + for k, v in self.encodings.items() + if columns is None or k in columns + } + prepared = apply_categorical_label_encoder(dataset, relevant_encodings) + return prepared + + def _prepare_one_hot( + self, dataset: "DashAIDataset", is_fit: bool, columns: list = None + ) -> "DashAIDataset": + """Prepare dataset using one hot encoding for categorical columns. + + Parameters + ---------- + dataset : DashAIDataset + The dataset to be transformed. + is_fit : bool + If True, fit the encoder. If False, use existing encoder. + columns : list, optional + If given, only one hot encode the specified columns. + + Returns + ------- + DashAIDataset + Dataset with categorical columns replaced by one hot columns. + """ + from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( + apply_categorical_one_hot_encoder, + categorical_one_hot_encoder, + ) + + if is_fit: + prepared, encoder, cat_cols = categorical_one_hot_encoder( + dataset, columns=columns + ) + self.one_hot_encoder = encoder + self.categorical_columns = cat_cols + elif self.one_hot_encoder is not None: + prepared = apply_categorical_one_hot_encoder( + dataset, self.one_hot_encoder, self.categorical_columns + ) + else: + prepared = dataset + return prepared diff --git a/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py b/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py index 31331174c..b155033d5 100644 --- a/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py +++ b/DashAI/back/models/scikit_learn/bayesian_ridge_regression.py @@ -9,9 +9,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -219,7 +216,6 @@ class BayesianRidgeRegression(RegressionModel, SklearnLikeRegressor, _BayesianRi ) COLOR: str = "#7E57C2" ICON: str = "Psychology" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/elastic_net_regression.py b/DashAI/back/models/scikit_learn/elastic_net_regression.py index 0a3e263ff..6bed797d2 100644 --- a/DashAI/back/models/scikit_learn/elastic_net_regression.py +++ b/DashAI/back/models/scikit_learn/elastic_net_regression.py @@ -10,9 +10,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -241,7 +238,6 @@ class ElasticNetRegression(RegressionModel, SklearnLikeRegressor, _ElasticNet): ) COLOR: str = "#26A69A" ICON: str = "Hub" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/k_neighbors_classifier.py b/DashAI/back/models/scikit_learn/k_neighbors_classifier.py index 00ccdf0af..c77d01cbe 100644 --- a/DashAI/back/models/scikit_learn/k_neighbors_classifier.py +++ b/DashAI/back/models/scikit_learn/k_neighbors_classifier.py @@ -10,9 +10,6 @@ from DashAI.back.models.scikit_learn.sklearn_like_classifier import ( SklearnLikeClassifier, ) -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.tabular_classification_model import TabularClassificationModel @@ -145,8 +142,6 @@ class KNeighborsClassifier( COLOR: str = "#FFD54F" ICON: str = "ScatterPlot" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/k_neighbors_regression.py b/DashAI/back/models/scikit_learn/k_neighbors_regression.py index eeb4b85d7..cad17cc96 100644 --- a/DashAI/back/models/scikit_learn/k_neighbors_regression.py +++ b/DashAI/back/models/scikit_learn/k_neighbors_regression.py @@ -8,9 +8,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -197,7 +194,6 @@ class KNeighborsRegression(RegressionModel, SklearnLikeRegressor, _KNeighborsReg ) COLOR: str = "#FFA726" ICON: str = "ScatterPlot" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/lasso_regression.py b/DashAI/back/models/scikit_learn/lasso_regression.py index f6c478e6a..2b1c37aa7 100644 --- a/DashAI/back/models/scikit_learn/lasso_regression.py +++ b/DashAI/back/models/scikit_learn/lasso_regression.py @@ -10,9 +10,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -206,7 +203,6 @@ class LassoRegression(RegressionModel, SklearnLikeRegressor, _Lasso): ) COLOR: str = "#29B6F6" ICON: str = "SelectAll" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/linearSVR.py b/DashAI/back/models/scikit_learn/linearSVR.py index 859c5b8ec..812599736 100644 --- a/DashAI/back/models/scikit_learn/linearSVR.py +++ b/DashAI/back/models/scikit_learn/linearSVR.py @@ -11,9 +11,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -348,8 +345,6 @@ class LinearSVR(RegressionModel, SklearnLikeRegressor, _LinearSVR): COLOR: str = "#2196F3" ICON: str = "Timeline" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/linear_regression.py b/DashAI/back/models/scikit_learn/linear_regression.py index be2e9a792..c8e5fe7e2 100644 --- a/DashAI/back/models/scikit_learn/linear_regression.py +++ b/DashAI/back/models/scikit_learn/linear_regression.py @@ -9,9 +9,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -161,8 +158,6 @@ class LinearRegression(RegressionModel, SklearnLikeRegressor, _LinearRegression) COLOR: str = "#3F51B5" ICON: str = "ShowChart" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/linear_svc_classifier.py b/DashAI/back/models/scikit_learn/linear_svc_classifier.py index 416d220f2..dcb314d91 100644 --- a/DashAI/back/models/scikit_learn/linear_svc_classifier.py +++ b/DashAI/back/models/scikit_learn/linear_svc_classifier.py @@ -13,9 +13,6 @@ from DashAI.back.models.scikit_learn.sklearn_like_classifier import ( SklearnLikeClassifier, ) -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.tabular_classification_model import TabularClassificationModel @@ -242,7 +239,6 @@ class LinearSVCClassifier( ) COLOR: str = "#FF7043" ICON: str = "LinearScale" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/logistic_regression.py b/DashAI/back/models/scikit_learn/logistic_regression.py index b90add67e..9aa5f4dc8 100644 --- a/DashAI/back/models/scikit_learn/logistic_regression.py +++ b/DashAI/back/models/scikit_learn/logistic_regression.py @@ -11,9 +11,6 @@ from DashAI.back.models.scikit_learn.sklearn_like_classifier import ( SklearnLikeClassifier, ) -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.tabular_classification_model import TabularClassificationModel @@ -159,8 +156,6 @@ class by applying the logistic (sigmoid) function to a linear combination of COLOR: str = "#64B5F6" ICON: str = "TrendingUp" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/ridge_regression.py b/DashAI/back/models/scikit_learn/ridge_regression.py index e59939d9c..01cdfc87a 100644 --- a/DashAI/back/models/scikit_learn/ridge_regression.py +++ b/DashAI/back/models/scikit_learn/ridge_regression.py @@ -11,9 +11,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -280,8 +277,6 @@ class RidgeRegression(RegressionModel, SklearnLikeRegressor, _Ridge): COLOR: str = "#2196F3" ICON: str = "ShowChart" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/sgd_classifier.py b/DashAI/back/models/scikit_learn/sgd_classifier.py index 6fa038ebe..335471f54 100644 --- a/DashAI/back/models/scikit_learn/sgd_classifier.py +++ b/DashAI/back/models/scikit_learn/sgd_classifier.py @@ -12,9 +12,6 @@ from DashAI.back.models.scikit_learn.sklearn_like_classifier import ( SklearnLikeClassifier, ) -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.tabular_classification_model import TabularClassificationModel @@ -272,7 +269,6 @@ class SGDClassifier(TabularClassificationModel, SklearnLikeClassifier, _SGDClass ) COLOR: str = "#78909C" ICON: str = "TrendingDown" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/sklearn_like_model.py b/DashAI/back/models/scikit_learn/sklearn_like_model.py index 9e8409ebf..3547f2b88 100644 --- a/DashAI/back/models/scikit_learn/sklearn_like_model.py +++ b/DashAI/back/models/scikit_learn/sklearn_like_model.py @@ -1,49 +1,26 @@ -from enum import Enum -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING from DashAI.back.models.base_model import BaseModel -from DashAI.back.types.categorical import Categorical +from DashAI.back.models.categorical_encoder_mixin import CategoricalEncoderMixin if TYPE_CHECKING: from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset -class CategoricalEncodingStrategy(str, Enum): - """Strategy for encoding categorical variables. - - LABEL: Use LabelEncoder - - Good for models that don't assume linear relationships between features. - - ONE_HOT: Use OneHotEncoder - creates binary columns for each category. - Required for linear models (Logistic Regression, SVM, KNN) - that would otherwise assume ordinal relationships. - """ - - LABEL = "label" - ONE_HOT = "one_hot" - - -class SklearnLikeModel(BaseModel): +class SklearnLikeModel(CategoricalEncoderMixin, BaseModel): """Abstract base class for scikit-learn-compatible DashAI models. - Provides ``save`` / ``load`` via joblib, categorical encoding helpers - (label or one hot), and the ``prepare_dataset`` / ``prepare_output`` - pipeline expected by the DashAI training loop. Concrete subclasses - (classifiers and regressors) inherit this mixin and supply ``train`` and - ``predict`` implementations backed by scikit-learn estimators. + Provides ``save`` / ``load`` via joblib and inherits the categorical + encoding helpers and ``prepare_dataset`` / ``prepare_output`` pipeline from + ``CategoricalEncoderMixin``. Concrete subclasses (classifiers and + regressors) supply ``train`` and ``predict`` implementations backed by + scikit-learn estimators. """ - CATEGORICAL_ENCODING: CategoricalEncodingStrategy = ( - CategoricalEncodingStrategy.LABEL - ) - def __init__(self, *args, **kwargs): """Initialize the SklearnLikeModel.""" super().__init__(*args, **kwargs) - self.encodings = {} - self.one_hot_encoder: Optional[Any] = None - self.categorical_columns: List[str] = [] - self.output_encodings = {} + self._setup_categorical_encoders() def save(self, filename: str) -> None: """Serialise the model to disk using joblib. @@ -122,188 +99,3 @@ def predict(self, x: "DashAIDataset"): if isinstance(x, DashAIDataset): x = self.prepare_dataset(x, is_fit=False).to_pandas() return super().predict(x) - - def prepare_output( - self, dataset: "DashAIDataset", is_fit: bool = False - ) -> "DashAIDataset": - """Prepare output targets using Label encoding. - - Parameters - ---------- - dataset : DashAIDataset - The output dataset to be transformed. - is_fit : bool, optional - If True, fit the encoder. If False, use existing encodings. - - Returns - ------- - DashAIDataset - Dataset with categorical columns converted to integers. - """ - from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( - apply_categorical_label_encoder, - categorical_label_encoder, - ) - - prepared = dataset - - if is_fit: - prepared, encodings = categorical_label_encoder(dataset) - self.output_encodings.update(encodings) - else: - if self.output_encodings: - prepared = apply_categorical_label_encoder( - dataset, self.output_encodings - ) - - return prepared - - def prepare_dataset( - self, dataset: "DashAIDataset", is_fit: bool = False - ) -> "DashAIDataset": - """Apply the model transformations to the dataset. - - Respects per-column encoder preference stored in each Categorical - column's `encoder` field. Falls back to model's CATEGORICAL_ENCODING - for columns with unrecognized encoder values. - - Parameters - ---------- - dataset : DashAIDataset - The dataset to be transformed. - is_fit : bool, optional - If True, the method will fit encoders on the data. - If False, will apply previously fitted encoders. - - Returns - ------- - DashAIDataset - The prepared dataset ready to be converted to - an accepted format in the model. - """ - import logging - - _logger = logging.getLogger(__name__) - - types = dataset.types - has_categorical = any(isinstance(t, Categorical) for t in types.values()) - - if not has_categorical: - return dataset - - one_hot_cols = [ - c - for c, t in types.items() - if isinstance(t, Categorical) and t.encoder == "one_hot" - ] - label_cols = [ - c - for c, t in types.items() - if isinstance(t, Categorical) and t.encoder == "label" - ] - default_cols = [ - c - for c, t in types.items() - if isinstance(t, Categorical) and t.encoder not in ("one_hot", "label") - ] - if default_cols: - _logger.warning( - "Columns %s have unrecognized encoder preference. " - "Falling back to model strategy %s.", - default_cols, - self.CATEGORICAL_ENCODING, - ) - if self.CATEGORICAL_ENCODING == CategoricalEncodingStrategy.ONE_HOT: - one_hot_cols.extend(default_cols) - else: - label_cols.extend(default_cols) - - prepared = dataset - if label_cols: - prepared = self._prepare_label_encoded(prepared, is_fit, columns=label_cols) - if one_hot_cols: - prepared = self._prepare_one_hot(prepared, is_fit, columns=one_hot_cols) - return prepared - - def _prepare_label_encoded( - self, dataset: "DashAIDataset", is_fit: bool, columns: list = None - ) -> "DashAIDataset": - """Prepare dataset using label encoding for categorical columns. - - This is appropriate for tree based models that don't assume - ordinal relationships between encoded values. - - Parameters - ---------- - dataset : DashAIDataset - The dataset to be transformed. - is_fit : bool - If True, fit the encoder. If False, use existing encodings. - columns : list, optional - If given, only label-encode the specified columns. - - Returns - ------- - DashAIDataset - Dataset with categorical columns converted to integers. - """ - from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( - apply_categorical_label_encoder, - categorical_label_encoder, - ) - - prepared = dataset - - if is_fit: - prepared, encodings = categorical_label_encoder(dataset, columns=columns) - self.encodings.update(encodings) - else: - if self.encodings: - relevant_encodings = { - k: v - for k, v in self.encodings.items() - if columns is None or k in columns - } - prepared = apply_categorical_label_encoder(dataset, relevant_encodings) - - return prepared - - def _prepare_one_hot( - self, dataset: "DashAIDataset", is_fit: bool, columns: list = None - ) -> "DashAIDataset": - """Prepare dataset using one hot encoding for categorical columns. - - Parameters - ---------- - dataset : DashAIDataset - The dataset to be transformed. - is_fit : bool - If True, fit the encoder. If False, use existing encoder. - columns : list, optional - If given, only one hot encode the specified columns. - - Returns - ------- - DashAIDataset - Dataset with categorical columns replaced by one hot columns. - """ - from DashAI.back.dataloaders.classes.dashai_dataset_utils import ( - apply_categorical_one_hot_encoder, - categorical_one_hot_encoder, - ) - - if is_fit: - prepared, encoder, cat_cols = categorical_one_hot_encoder( - dataset, columns=columns - ) - self.one_hot_encoder = encoder - self.categorical_columns = cat_cols - else: - if self.one_hot_encoder is not None: - prepared = apply_categorical_one_hot_encoder( - dataset, self.one_hot_encoder, self.categorical_columns - ) - else: - prepared = dataset - - return prepared diff --git a/DashAI/back/models/scikit_learn/svc.py b/DashAI/back/models/scikit_learn/svc.py index 9f081e2a2..2257c3398 100644 --- a/DashAI/back/models/scikit_learn/svc.py +++ b/DashAI/back/models/scikit_learn/svc.py @@ -12,9 +12,6 @@ from DashAI.back.models.scikit_learn.sklearn_like_classifier import ( SklearnLikeClassifier, ) -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.tabular_classification_model import TabularClassificationModel @@ -307,8 +304,6 @@ class SVC(TabularClassificationModel, SklearnLikeClassifier, _SVC): COLOR: str = "#FF80AB" ICON: str = "Timeline" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT - def __init__(self, **kwargs): """Initialise the model by forwarding all kwargs to the parent class. diff --git a/DashAI/back/models/scikit_learn/svr.py b/DashAI/back/models/scikit_learn/svr.py index 2369320f8..8cbcaac7e 100644 --- a/DashAI/back/models/scikit_learn/svr.py +++ b/DashAI/back/models/scikit_learn/svr.py @@ -9,9 +9,6 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.models.regression_model import RegressionModel -from DashAI.back.models.scikit_learn.sklearn_like_model import ( - CategoricalEncodingStrategy, -) from DashAI.back.models.scikit_learn.sklearn_like_regressor import SklearnLikeRegressor @@ -215,7 +212,6 @@ class SVR(RegressionModel, SklearnLikeRegressor, _SVR): ) COLOR: str = "#EF5350" ICON: str = "ControlPoint" - CATEGORICAL_ENCODING = CategoricalEncodingStrategy.ONE_HOT def __init__(self, **kwargs) -> None: """Initialise the model by forwarding all kwargs to the parent class. From 7beda1dfb66d93ee463ef95f3969fb1e50b76fce Mon Sep 17 00:00:00 2001 From: Irozuku Date: Thu, 25 Jun 2026 17:28:51 -0400 Subject: [PATCH 08/28] fix: encode categorical features in MLP regression MLP regression fed raw Categorical columns straight into torch, crashing with "can't convert np.ndarray of type numpy.object_". It now inherits CategoricalEncoderMixin to encode categorical features (and any categorical target) before tensor conversion. At predict time the fitted encoders are applied from stored state rather than derived again from the dataset's encoder metadata, which can drift between training and prediction. The fitted encoders are persisted in save/load so prediction matches training time preprocessing. --- .../models/scikit_learn/mlp_regression.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/DashAI/back/models/scikit_learn/mlp_regression.py b/DashAI/back/models/scikit_learn/mlp_regression.py index 80d45bf5d..bb0888432 100644 --- a/DashAI/back/models/scikit_learn/mlp_regression.py +++ b/DashAI/back/models/scikit_learn/mlp_regression.py @@ -11,6 +11,7 @@ schema_field, ) from DashAI.back.core.utils import MultilingualString +from DashAI.back.models.categorical_encoder_mixin import CategoricalEncoderMixin from DashAI.back.models.regression_model import RegressionModel from DashAI.back.models.utils import DEVICE_ENUM, DEVICE_PLACEHOLDER, DEVICE_TO_IDX @@ -33,9 +34,9 @@ class MLPRegressorSchema(BaseSchema): optimizer_int_field(ge=1), placeholder={ "optimize": False, - "fixed_value": 5, + "fixed_value": 16, "lower_bound": 1, - "upper_bound": 15, + "upper_bound": 64, }, description=MultilingualString( en="Number of neurons in the hidden layer.", @@ -100,9 +101,9 @@ class MLPRegressorSchema(BaseSchema): optimizer_int_field(ge=1), placeholder={ "optimize": False, - "fixed_value": 5, + "fixed_value": 20, "lower_bound": 1, - "upper_bound": 15, + "upper_bound": 50, }, description=MultilingualString( en="Total number of training passes over the dataset.", @@ -290,7 +291,7 @@ class MLPRegressorSchema(BaseSchema): ) # type: ignore -class MLPRegression(RegressionModel): +class MLPRegression(CategoricalEncoderMixin, RegressionModel): """Single hidden-layer MLP regressor implemented in PyTorch. A Multi-layer Perceptron (MLP) is a feedforward neural network composed of an @@ -404,6 +405,11 @@ def forward(self, x): ) self.model = None + # Initialise the categorical encoder state inherited from + # CategoricalEncoderMixin. These fields are persisted by ``save`` and + # restored by ``load`` so ``predict`` reuses the training-time encoders. + self._setup_categorical_encoders() + def train( self, x_train: "DashAIDataset", @@ -574,6 +580,9 @@ def save(self, filename: str) -> None: "state": self.model.state_dict(), "params": self.params, "input_dim": self.model.model[0].in_features, + "encodings": self.encodings, + "one_hot_encoder": self.one_hot_encoder, + "categorical_columns": self.categorical_columns, }, filename, ) @@ -594,7 +603,10 @@ def load(filename: str) -> "MLPRegression": """ import torch - data = torch.load(filename) + # weights_only=False is required because the checkpoint stores the + # fitted categorical encoders (e.g. a scikit-learn OneHotEncoder), which + # are not part of torch's safe-globals allowlist. + data = torch.load(filename, weights_only=False) instance = MLPRegression(**data["params"]) # Rebuild the model architecture using saved input_dim @@ -607,4 +619,10 @@ def load(filename: str) -> "MLPRegression": # Load the trained weights instance.model.load_state_dict(data["state"]) + # Restore the categorical encoders so predictions match training-time + # preprocessing. + instance.encodings = data.get("encodings", {}) + instance.one_hot_encoder = data.get("one_hot_encoder") + instance.categorical_columns = data.get("categorical_columns", []) + return instance From 4a6853656622c586f864dc45f8e8921a3366bd3d Mon Sep 17 00:00:00 2001 From: Irozuku Date: Thu, 25 Jun 2026 18:01:15 -0400 Subject: [PATCH 09/28] fix: add MAXIMIZE = True to explained variance metric --- DashAI/back/metrics/regression/explained_variance.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DashAI/back/metrics/regression/explained_variance.py b/DashAI/back/metrics/regression/explained_variance.py index 657b8652c..46f4715db 100644 --- a/DashAI/back/metrics/regression/explained_variance.py +++ b/DashAI/back/metrics/regression/explained_variance.py @@ -67,6 +67,8 @@ class ExplainedVariance(RegressionMetric): ), ) + MAXIMIZE = True + @staticmethod def score( true_values: "DashAIDataset", From 1eadfb8ad93b09ebc743c093eef3b77c39877431 Mon Sep 17 00:00:00 2001 From: Felipe Date: Fri, 26 Jun 2026 06:07:48 -0400 Subject: [PATCH 10/28] feat: Solve problem with temp_dir in translation --- .../hugging_face/base_opus_mt_transformer.py | 7 ++-- .../base_text_classification_transformer.py | 8 +++-- .../models/hugging_face/m2m100_transformer.py | 9 +++-- .../models/hugging_face/nllb_transformer.py | 9 +++-- .../hugging_face/t5_small_transformer.py | 9 +++-- DashAI/back/models/utils.py | 34 +++++++++++++++++++ 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/DashAI/back/models/hugging_face/base_opus_mt_transformer.py b/DashAI/back/models/hugging_face/base_opus_mt_transformer.py index fbca91b6b..1b80e1269 100644 --- a/DashAI/back/models/hugging_face/base_opus_mt_transformer.py +++ b/DashAI/back/models/hugging_face/base_opus_mt_transformer.py @@ -8,7 +8,10 @@ from sklearn.exceptions import NotFittedError from DashAI.back.models.translation_model import TranslationModel -from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER +from DashAI.back.models.utils import ( + GPU_OR_CPU_PLACEHOLDER, + resolve_temp_checkpoint_dir, +) if TYPE_CHECKING: from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset @@ -136,7 +139,7 @@ def train( has_validation_data = x_validation is not None and y_validation is not None - output_root = Path(self.TEMP_CHECKPOINT_DIR) + output_root = resolve_temp_checkpoint_dir(self.TEMP_CHECKPOINT_DIR) output_root.mkdir(parents=True, exist_ok=True) run_output_dir = tempfile.mkdtemp( prefix=f"{self.__class__.__name__.lower()}_", diff --git a/DashAI/back/models/hugging_face/base_text_classification_transformer.py b/DashAI/back/models/hugging_face/base_text_classification_transformer.py index 73e9b610f..741207eae 100644 --- a/DashAI/back/models/hugging_face/base_text_classification_transformer.py +++ b/DashAI/back/models/hugging_face/base_text_classification_transformer.py @@ -12,7 +12,10 @@ from sklearn.exceptions import NotFittedError from DashAI.back.models.text_classification_model import TextClassificationModel -from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER +from DashAI.back.models.utils import ( + GPU_OR_CPU_PLACEHOLDER, + resolve_temp_checkpoint_dir, +) from DashAI.back.types.categorical import Categorical if TYPE_CHECKING: @@ -136,7 +139,6 @@ def train(self, x_train, y_train, x_validation=None, y_validation=None): """ import shutil import tempfile - from pathlib import Path import torch from transformers import ( @@ -181,7 +183,7 @@ def train(self, x_train, y_train, x_validation=None, y_validation=None): use_gpu = self.device.lower() == "gpu" can_use_fp16 = torch.cuda.is_available() and use_gpu - base_output_dir = Path(self.TEMP_CHECKPOINT_DIR) + base_output_dir = resolve_temp_checkpoint_dir(self.TEMP_CHECKPOINT_DIR) base_output_dir.mkdir(parents=True, exist_ok=True) run_output_dir = tempfile.mkdtemp( prefix=f"{self.__class__.__name__.lower()}_", diff --git a/DashAI/back/models/hugging_face/m2m100_transformer.py b/DashAI/back/models/hugging_face/m2m100_transformer.py index 8d2df7cce..264e0a49f 100644 --- a/DashAI/back/models/hugging_face/m2m100_transformer.py +++ b/DashAI/back/models/hugging_face/m2m100_transformer.py @@ -13,7 +13,10 @@ OpusMtEnESTransformerSchema, ) from DashAI.back.models.translation_model import TranslationModel -from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER +from DashAI.back.models.utils import ( + GPU_OR_CPU_PLACEHOLDER, + resolve_temp_checkpoint_dir, +) if TYPE_CHECKING: from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset @@ -250,7 +253,9 @@ def train( has_validation_data = x_validation is not None and y_validation is not None - output_root = Path("DashAI/back/user_models/temp_checkpoints_m2m100") + output_root = resolve_temp_checkpoint_dir( + "DashAI/back/user_models/temp_checkpoints_m2m100" + ) output_root.mkdir(parents=True, exist_ok=True) run_output_dir = tempfile.mkdtemp(prefix="m2m100_", dir=str(output_root)) diff --git a/DashAI/back/models/hugging_face/nllb_transformer.py b/DashAI/back/models/hugging_face/nllb_transformer.py index d1b7e2cbe..ab7a5556c 100644 --- a/DashAI/back/models/hugging_face/nllb_transformer.py +++ b/DashAI/back/models/hugging_face/nllb_transformer.py @@ -13,7 +13,10 @@ OpusMtEnESTransformerSchema, ) from DashAI.back.models.translation_model import TranslationModel -from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER +from DashAI.back.models.utils import ( + GPU_OR_CPU_PLACEHOLDER, + resolve_temp_checkpoint_dir, +) if TYPE_CHECKING: from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset @@ -373,7 +376,9 @@ def train( has_validation_data = x_validation is not None and y_validation is not None - output_root = Path("DashAI/back/user_models/temp_checkpoints_nllb") + output_root = resolve_temp_checkpoint_dir( + "DashAI/back/user_models/temp_checkpoints_nllb" + ) output_root.mkdir(parents=True, exist_ok=True) run_output_dir = tempfile.mkdtemp(prefix="nllb_", dir=str(output_root)) diff --git a/DashAI/back/models/hugging_face/t5_small_transformer.py b/DashAI/back/models/hugging_face/t5_small_transformer.py index b61604a29..91373a064 100644 --- a/DashAI/back/models/hugging_face/t5_small_transformer.py +++ b/DashAI/back/models/hugging_face/t5_small_transformer.py @@ -13,7 +13,10 @@ OpusMtEnESTransformerSchema, ) from DashAI.back.models.translation_model import TranslationModel -from DashAI.back.models.utils import GPU_OR_CPU_PLACEHOLDER +from DashAI.back.models.utils import ( + GPU_OR_CPU_PLACEHOLDER, + resolve_temp_checkpoint_dir, +) if TYPE_CHECKING: from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset @@ -221,7 +224,9 @@ def train( has_validation_data = x_validation is not None and y_validation is not None - output_root = Path("DashAI/back/user_models/temp_checkpoints_t5_small") + output_root = resolve_temp_checkpoint_dir( + "DashAI/back/user_models/temp_checkpoints_t5_small" + ) output_root.mkdir(parents=True, exist_ok=True) run_output_dir = tempfile.mkdtemp(prefix="t5_small_", dir=str(output_root)) diff --git a/DashAI/back/models/utils.py b/DashAI/back/models/utils.py index 21db3ac0a..789a8605f 100644 --- a/DashAI/back/models/utils.py +++ b/DashAI/back/models/utils.py @@ -1,3 +1,6 @@ +import os +from pathlib import Path + import torch from DashAI.back.models.hugging_face.llama_utils import ( @@ -5,6 +8,37 @@ is_gpu_available_for_llama_cpp, ) + +def resolve_temp_checkpoint_dir(temp_checkpoint_dir: str) -> Path: + """Resolve a transformer's temp checkpoint dir to an absolute, writable path. + + Historically ``TEMP_CHECKPOINT_DIR`` was a path relative to the repository + root (e.g. ``DashAI/back/user_models/temp_checkpoints_*``). That only works + in development mode, where the current working directory is the repo root. + In a packaged executable (PyInstaller/AppImage) the working directory is the + read-only install location, so creating the directory fails with a + permission/not-found error. + + To work in both cases we anchor the checkpoints under the DashAI local data + directory (``DASHAI_LOCAL_PATH`` env var, falling back to ``~/.DashAI``) and + keep only the final segment of the configured path as the folder name. + + Parameters + ---------- + temp_checkpoint_dir : str + The configured (possibly relative) checkpoint directory. + + Returns + ------- + Path + An absolute path under the DashAI local data directory. + """ + local_path = os.environ.get("DASHAI_LOCAL_PATH") + base = Path(local_path).expanduser() if local_path else Path.home() / ".DashAI" + folder_name = Path(temp_checkpoint_dir).name or "temp_checkpoints" + return base / "user_models" / folder_name + + DEVICE_ENUM: list[str] = ["CPU"] DEVICE_PLACEHOLDER: str = "CPU" DEVICE_TO_IDX: dict[str, int] = {"CPU": -1} From 594a87b541f25355a1520f40ecf53b5228b4c34e Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 09:24:50 -0400 Subject: [PATCH 11/28] fix: clarify Normalizer is row wise in display name and description Rename display name to "Row Wise Normalizer" and rewrite the description to state that normalization is applied per row across the selected columns, warning that a single column collapses to plus or minus 1. Update the preview image to reflect row wise normalization. --- .../converters/scikit_learn/normalizer.py | 38 +++++++++++++----- DashAI/back/static/images/normalizer.png | Bin 129239 -> 136708 bytes 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/DashAI/back/converters/scikit_learn/normalizer.py b/DashAI/back/converters/scikit_learn/normalizer.py index 558254513..88398b5b9 100644 --- a/DashAI/back/converters/scikit_learn/normalizer.py +++ b/DashAI/back/converters/scikit_learn/normalizer.py @@ -65,18 +65,36 @@ class Normalizer(ScalingAndNormalizationConverter, SklearnWrapper, NormalizerOpe SCHEMA = NormalizerSchema DESCRIPTION = MultilingualString( - en="Normalize samples individually to unit norm.", - es="Normaliza muestras individualmente a norma unitaria.", - pt="Normaliza amostras individualmente para norma unitária.", - de="Stichproben individuell auf Einheitsnorm normalisieren.", - zh="将每个样本单独归一化为单位范数。", + en=( + "Normalize each row (sample) to unit norm across the selected columns. " + "Select two or more columns; a single column collapses to plus or minus 1." + ), + es=( + "Normaliza cada fila (muestra) a norma unitaria a lo largo de las columnas " + "seleccionadas. Selecciona dos o más columnas; una sola columna colapsa a " + "más o menos 1." + ), + pt=( + "Normaliza cada linha (amostra) para norma unitária ao longo das colunas " + "selecionadas. Selecione duas ou mais colunas; uma única coluna colapsa " + "para mais ou menos 1." + ), + de=( + "Normalisiert jede Zeile (Stichprobe) über die ausgewählten Spalten auf " + "Einheitsnorm. Mindestens zwei Spalten auswählen; eine einzelne Spalte " + "kollabiert zu plus oder minus 1." + ), + zh=( + "在所选列上将每一行(样本)归一化为单位范数。" + "请选择两列或以上;单列会收敛为正负 1。" + ), ) DISPLAY_NAME = MultilingualString( - en="Normalizer", - es="Normalizador", - pt="Normalizador", - de="Normalisierer", - zh="归一化器", + en="Row Wise Normalizer", + es="Normalizador por filas", + pt="Normalizador por linhas", + de="Zeilenweiser Normalisierer", + zh="按行归一化器", ) IMAGE_PREVIEW = "normalizer.png" diff --git a/DashAI/back/static/images/normalizer.png b/DashAI/back/static/images/normalizer.png index 168730dd7018d714fca42493e1f5f321ceab55cc..8815ad74de2d32b2eb32c6773b4eb34b87f0a76c 100644 GIT binary patch literal 136708 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 From 95f5694ca7ef1674909878075b3702dddd8971c0 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 10:20:37 -0400 Subject: [PATCH 12/28] fix: cap select all to column cardinality limit --- .../components/notebooks/ColumnSelector.jsx | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/DashAI/front/src/components/notebooks/ColumnSelector.jsx b/DashAI/front/src/components/notebooks/ColumnSelector.jsx index 4adbcaf78..ac3c62876 100644 --- a/DashAI/front/src/components/notebooks/ColumnSelector.jsx +++ b/DashAI/front/src/components/notebooks/ColumnSelector.jsx @@ -206,13 +206,15 @@ function ColumnSelector({ ); const handleSelectAllRows = useCallback(() => { + const limit = inputCardinality.exact || inputCardinality.max; const validIds = getValidColumnIds(); - const allValidSelected = - validIds.length > 0 && - validIds.every((id) => rowSelectionModel.includes(id)); + const selectableIds = limit ? validIds.slice(0, limit) : validIds; + const allSelectableSelected = + selectableIds.length > 0 && + selectableIds.every((id) => rowSelectionModel.includes(id)); - handleSelection(allValidSelected ? {} : toMRT(validIds)); - }, [getValidColumnIds, rowSelectionModel]); + handleSelection(allSelectableSelected ? {} : toMRT(selectableIds)); + }, [getValidColumnIds, rowSelectionModel, inputCardinality]); // Effect to update selection data and validation whenever rowSelectionModel changes useEffect(() => { @@ -313,15 +315,20 @@ function ColumnSelector({ } : {}, }), - muiSelectAllCheckboxProps: () => ({ - checked: - getValidColumnIds().length > 0 && - getValidColumnIds().every((id) => rowSelectionModel.includes(id)), - indeterminate: - rowSelectionModel.length > 0 && - rowSelectionModel.length < getValidColumnIds().length, - onChange: handleSelectAllRows, - }), + muiSelectAllCheckboxProps: () => { + const limit = inputCardinality.exact || inputCardinality.max; + const validIds = getValidColumnIds(); + const selectableIds = limit ? validIds.slice(0, limit) : validIds; + return { + checked: + selectableIds.length > 0 && + selectableIds.every((id) => rowSelectionModel.includes(id)), + indeterminate: + rowSelectionModel.length > 0 && + rowSelectionModel.length < selectableIds.length, + onChange: handleSelectAllRows, + }; + }, localization, }); From 4801d555cc4b960182e15f4c83822640a109ce35 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 10:20:58 -0400 Subject: [PATCH 13/28] refactor: replace per-type dtype restrictions with global dtype blacklist --- DashAI/back/exploration/base_explorer.py | 19 +++++++----- .../back/exploration/explorers/corr_matrix.py | 7 +++-- .../back/exploration/explorers/cov_matrix.py | 7 +++-- .../back/exploration/explorers/ecdf_plot.py | 7 +++-- .../exploration/explorers/multibox_plot.py | 7 +++-- .../explorers/parallel_cordinates.py | 7 +++-- .../exploration/explorers/scatter_matrix.py | 7 +++-- .../exploration/explorers/scatter_plot.py | 7 +++-- .../test_base_explorer_metadata.py | 30 +++++++++---------- 9 files changed, 62 insertions(+), 36 deletions(-) diff --git a/DashAI/back/exploration/base_explorer.py b/DashAI/back/exploration/base_explorer.py index a77dadef7..ed3e9c926 100644 --- a/DashAI/back/exploration/base_explorer.py +++ b/DashAI/back/exploration/base_explorer.py @@ -12,6 +12,12 @@ from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset +# Dtypes that cannot be plotted on a numeric axis. Shared default for plot +# explorers that accept the Categorical semantic type but only when it is +# numerically encoded (an empty dtype means the dtype is unknown). +NON_NUMERIC_DTYPES: Final[List[str]] = ["string", "bool", ""] + + class BaseExplorerSchema(BaseSchema): """ Base schema for explorers, it defines the parameters to be used in each explorer. @@ -103,9 +109,9 @@ def get_metadata(cls) -> Dict[str, Any]: meta.pop("restricted_dtypes", None) meta.pop("numeric_categorical_only", None) - # Ensure type_dtype_restrictions is always present for the frontend - if "type_dtype_restrictions" not in meta: - meta["type_dtype_restrictions"] = {} + # Ensure non_allowed_dtypes is always present for the frontend + if "non_allowed_dtypes" not in meta: + meta["non_allowed_dtypes"] = [] return meta @@ -172,8 +178,8 @@ def validate_columns( if "max" in input_cardinality and n > input_cardinality["max"]: return False - # Per-type dtype exclusions: maps semantic type name → list of forbidden dtypes. - type_dtype_restrictions = metadata.get("type_dtype_restrictions", {}) + # Global dtype blacklist: dtypes that are never valid for this explorer. + non_allowed_dtypes = metadata.get("non_allowed_dtypes", []) for column in selected_columns: column_name = column["columnName"] col_info = column_spec.get(column_name, {}) @@ -182,8 +188,7 @@ def validate_columns( if allowed_types and col_type not in allowed_types: return False - forbidden_dtypes = type_dtype_restrictions.get(col_type, []) - if forbidden_dtypes and col_dtype in forbidden_dtypes: + if non_allowed_dtypes and col_dtype in non_allowed_dtypes: return False if allowed_dtypes and col_dtype not in allowed_dtypes: return False diff --git a/DashAI/back/exploration/explorers/corr_matrix.py b/DashAI/back/exploration/explorers/corr_matrix.py index 4a0e7e4b4..2e9b00b33 100644 --- a/DashAI/back/exploration/explorers/corr_matrix.py +++ b/DashAI/back/exploration/explorers/corr_matrix.py @@ -9,7 +9,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.statistical_explorer import StatisticalExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -197,7 +200,7 @@ class CorrelationMatrixExplorer(StatisticalExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 2}, } diff --git a/DashAI/back/exploration/explorers/cov_matrix.py b/DashAI/back/exploration/explorers/cov_matrix.py index ec478c312..69282628e 100644 --- a/DashAI/back/exploration/explorers/cov_matrix.py +++ b/DashAI/back/exploration/explorers/cov_matrix.py @@ -3,7 +3,10 @@ from DashAI.back.core.schema_fields import bool_field, int_field, schema_field from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.statistical_explorer import StatisticalExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -192,7 +195,7 @@ class CovarianceMatrixExplorer(StatisticalExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 2}, } diff --git a/DashAI/back/exploration/explorers/ecdf_plot.py b/DashAI/back/exploration/explorers/ecdf_plot.py index ad03bb6f4..4f4d6941d 100644 --- a/DashAI/back/exploration/explorers/ecdf_plot.py +++ b/DashAI/back/exploration/explorers/ecdf_plot.py @@ -11,7 +11,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.distribution_explorer import DistributionExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -170,7 +173,7 @@ class ECDFPlotExplorer(DistributionExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 1}, } diff --git a/DashAI/back/exploration/explorers/multibox_plot.py b/DashAI/back/exploration/explorers/multibox_plot.py index 60febc426..0bbe310fb 100644 --- a/DashAI/back/exploration/explorers/multibox_plot.py +++ b/DashAI/back/exploration/explorers/multibox_plot.py @@ -11,7 +11,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.multidimensional_explorer import MultidimensionalExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -165,7 +168,7 @@ class MultiColumnBoxPlotExplorer(MultidimensionalExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 1}, } diff --git a/DashAI/back/exploration/explorers/parallel_cordinates.py b/DashAI/back/exploration/explorers/parallel_cordinates.py index 2491157d6..9d93a783e 100644 --- a/DashAI/back/exploration/explorers/parallel_cordinates.py +++ b/DashAI/back/exploration/explorers/parallel_cordinates.py @@ -9,7 +9,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.multidimensional_explorer import MultidimensionalExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -94,7 +97,7 @@ class ParallelCordinatesExplorer(MultidimensionalExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 2}, } diff --git a/DashAI/back/exploration/explorers/scatter_matrix.py b/DashAI/back/exploration/explorers/scatter_matrix.py index c5e9c73c1..1569abc21 100644 --- a/DashAI/back/exploration/explorers/scatter_matrix.py +++ b/DashAI/back/exploration/explorers/scatter_matrix.py @@ -9,7 +9,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.relationship_explorer import RelationshipExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -123,7 +126,7 @@ class ScatterMatrixExplorer(RelationshipExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"min": 2}, } diff --git a/DashAI/back/exploration/explorers/scatter_plot.py b/DashAI/back/exploration/explorers/scatter_plot.py index 481b3d675..4c79e5240 100644 --- a/DashAI/back/exploration/explorers/scatter_plot.py +++ b/DashAI/back/exploration/explorers/scatter_plot.py @@ -9,7 +9,10 @@ ) from DashAI.back.core.utils import MultilingualString from DashAI.back.dependencies.database.models import Explorer, Notebook -from DashAI.back.exploration.base_explorer import BaseExplorerSchema +from DashAI.back.exploration.base_explorer import ( + NON_NUMERIC_DTYPES, + BaseExplorerSchema, +) from DashAI.back.exploration.relationship_explorer import RelationshipExplorer from DashAI.back.types.categorical import Categorical from DashAI.back.types.value_types import Float, Integer @@ -129,7 +132,7 @@ class ScatterPlotExplorer(RelationshipExplorer): metadata: Dict[str, Any] = { "allowed_types": [Float, Integer, Categorical], "allowed_dtypes": [], - "type_dtype_restrictions": {"Categorical": ["string", "bool", ""]}, + "non_allowed_dtypes": NON_NUMERIC_DTYPES, "input_cardinality": {"exact": 2}, } diff --git a/tests/back/exploration/test_base_explorer_metadata.py b/tests/back/exploration/test_base_explorer_metadata.py index 71756c8de..9eebb5901 100644 --- a/tests/back/exploration/test_base_explorer_metadata.py +++ b/tests/back/exploration/test_base_explorer_metadata.py @@ -203,14 +203,14 @@ def test_validate_columns_missing_column_in_spec_passes_when_unrestricted(): assert cls.validate_columns(explorer_info, column_spec) is True -# --- type_dtype_restrictions tests --- +# --- non_allowed_dtypes tests --- -def test_type_dtype_restrictions_passes_allowed_dtype(): +def test_non_allowed_dtypes_passes_int_dtype(): cls = _make_explorer( allowed_types=[Float, Integer, Categorical], allowed_dtypes=[], - type_dtype_restrictions={"Categorical": ["string", "bool", ""]}, + non_allowed_dtypes=["string", "bool", ""], input_cardinality={"min": 1}, ) explorer_info = _MockExplorerInfo([{"columnName": "cat_num"}]) @@ -218,11 +218,11 @@ def test_type_dtype_restrictions_passes_allowed_dtype(): assert cls.validate_columns(explorer_info, column_spec) is True -def test_type_dtype_restrictions_passes_float_dtype(): +def test_non_allowed_dtypes_passes_float_dtype(): cls = _make_explorer( allowed_types=[Float, Integer, Categorical], allowed_dtypes=[], - type_dtype_restrictions={"Categorical": ["string", "bool", ""]}, + non_allowed_dtypes=["string", "bool", ""], input_cardinality={"min": 1}, ) explorer_info = _MockExplorerInfo([{"columnName": "cat_num"}]) @@ -230,11 +230,11 @@ def test_type_dtype_restrictions_passes_float_dtype(): assert cls.validate_columns(explorer_info, column_spec) is True -def test_type_dtype_restrictions_blocks_string_dtype(): +def test_non_allowed_dtypes_blocks_string_dtype(): cls = _make_explorer( allowed_types=[Float, Integer, Categorical], allowed_dtypes=[], - type_dtype_restrictions={"Categorical": ["string", "bool", ""]}, + non_allowed_dtypes=["string", "bool", ""], input_cardinality={"min": 1}, ) explorer_info = _MockExplorerInfo([{"columnName": "cat_str"}]) @@ -242,11 +242,11 @@ def test_type_dtype_restrictions_blocks_string_dtype(): assert cls.validate_columns(explorer_info, column_spec) is False -def test_type_dtype_restrictions_blocks_bool_dtype(): +def test_non_allowed_dtypes_blocks_bool_dtype(): cls = _make_explorer( allowed_types=[Float, Integer, Categorical], allowed_dtypes=[], - type_dtype_restrictions={"Categorical": ["string", "bool", ""]}, + non_allowed_dtypes=["string", "bool", ""], input_cardinality={"min": 1}, ) explorer_info = _MockExplorerInfo([{"columnName": "cat_bool"}]) @@ -254,7 +254,7 @@ def test_type_dtype_restrictions_blocks_bool_dtype(): assert cls.validate_columns(explorer_info, column_spec) is False -def test_type_dtype_restrictions_absent_allows_string_categorical(): +def test_non_allowed_dtypes_absent_allows_string_categorical(): cls = _make_explorer( allowed_types=[Float, Integer, Categorical], allowed_dtypes=[], @@ -265,20 +265,20 @@ def test_type_dtype_restrictions_absent_allows_string_categorical(): assert cls.validate_columns(explorer_info, column_spec) is True -def test_get_metadata_includes_type_dtype_restrictions(): +def test_get_metadata_includes_non_allowed_dtypes(): cls = _make_explorer( allowed_types=[], allowed_dtypes=[], - type_dtype_restrictions={"Categorical": ["string", "bool", ""]}, + non_allowed_dtypes=["string", "bool", ""], ) meta = cls.get_metadata() - assert meta["type_dtype_restrictions"] == {"Categorical": ["string", "bool", ""]} + assert meta["non_allowed_dtypes"] == ["string", "bool", ""] -def test_get_metadata_type_dtype_restrictions_defaults_to_empty(): +def test_get_metadata_non_allowed_dtypes_defaults_to_empty(): cls = _make_explorer(allowed_types=[], allowed_dtypes=[]) meta = cls.get_metadata() - assert meta["type_dtype_restrictions"] == {} + assert meta["non_allowed_dtypes"] == [] def test_get_metadata_drops_numeric_categorical_only(): From 601e4231391b3c6a51b838362f76daf332f81262 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 10:21:19 -0400 Subject: [PATCH 14/28] feat: show excluded data types in column selector --- .../components/notebooks/ColumnSelector.jsx | 26 ++++++++++++++----- .../src/components/notebooks/RightBar.jsx | 11 +++----- .../explorerCreation/ScopeStepExplorer.jsx | 4 +-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/DashAI/front/src/components/notebooks/ColumnSelector.jsx b/DashAI/front/src/components/notebooks/ColumnSelector.jsx index ac3c62876..7cee84cda 100644 --- a/DashAI/front/src/components/notebooks/ColumnSelector.jsx +++ b/DashAI/front/src/components/notebooks/ColumnSelector.jsx @@ -29,6 +29,7 @@ import { useTableLocalization } from "../../utils/useTableLocalization"; * @param {Object} props.inputCardinality - Cardinality requirements {min, max, exact} (optional) * @param {Array} props.allowedDtypes - Array of allowed dtype strings (optional) * @param {Array} props.allowedTypes - Array of allowed semantic type names (optional) + * @param {Array} props.nonAllowedDtypes - Array of forbidden dtype strings (optional) * @param {Function} props.onSelectionChange - Callback when selection changes (selectedColumns) (optional) * @param {Function} props.onValidationChange - Callback when validation status changes (isValid) (optional) @@ -39,7 +40,7 @@ function ColumnSelector({ inputCardinality = {}, allowedDtypes = [], allowedTypes = [], - typesDtypeRestrictions = {}, + nonAllowedDtypes = [], onSelectionChange = () => {}, onValidationChange = () => {}, columnTypes = null, @@ -161,16 +162,15 @@ function ColumnSelector({ if (allowedDtypes.length > 0 && !allowedDtypes.includes(row.dataType)) { return false; } - const forbiddenDtypes = typesDtypeRestrictions[row.valueType]; - if (forbiddenDtypes) { + if (nonAllowedDtypes.length > 0) { const dtypeKey = row.dataType === t("common:unknown") ? "" : row.dataType; - if (forbiddenDtypes.includes(dtypeKey)) return false; + if (nonAllowedDtypes.includes(dtypeKey)) return false; } return true; }) .map((row) => row.id); - }, [rows, allowedDtypes, allowedTypes, typesDtypeRestrictions]); + }, [rows, allowedDtypes, allowedTypes, nonAllowedDtypes]); // Check if row is selectable - using useCallback for stability const isRowSelectable = useCallback( @@ -413,6 +413,20 @@ function ColumnSelector({ )} + + {/* Excluded data types */} + {nonAllowedDtypes.length > 0 && ( + + {t("datasets:label.excludedDataTypes", { + dtypes: nonAllowedDtypes + .map((d) => (d === "" ? t("common:unknown") : d)) + .join(", "), + })} + + )} {tool?.metadata?.changes_row_count && ( @@ -455,7 +469,7 @@ ColumnSelector.propTypes = { }), allowedDtypes: PropTypes.array, allowedTypes: PropTypes.array, - typesDtypeRestrictions: PropTypes.object, + nonAllowedDtypes: PropTypes.array, onSelectionChange: PropTypes.func, onValidationChange: PropTypes.func, }; diff --git a/DashAI/front/src/components/notebooks/RightBar.jsx b/DashAI/front/src/components/notebooks/RightBar.jsx index 08b936edd..f63a0c91c 100644 --- a/DashAI/front/src/components/notebooks/RightBar.jsx +++ b/DashAI/front/src/components/notebooks/RightBar.jsx @@ -147,8 +147,7 @@ export default function RightBar({ notebook, onToggle }) { const allowedTypes = explorer?.metadata?.allowed_types || []; const allowedDtypes = explorer?.metadata?.allowed_dtypes || []; const inputCardinality = explorer?.metadata?.input_cardinality || {}; - const typesDtypeRestrictions = - explorer?.metadata?.type_dtype_restrictions || {}; + const nonAllowedDtypes = explorer?.metadata?.non_allowed_dtypes || []; let validColumns = datasetColumns; let disabled = false; @@ -169,14 +168,12 @@ export default function RightBar({ notebook, onToggle }) { ); } - // Apply per-type dtype exclusions declared by the backend - if (Object.keys(typesDtypeRestrictions).length > 0) { + // Apply global dtype blacklist declared by the backend + if (nonAllowedDtypes.length > 0) { validColumns = validColumns.filter((col) => { - const forbidden = typesDtypeRestrictions[col.valueType]; - if (!forbidden) return true; const dtypeKey = col.dataType === t("common:unknown") ? "" : col.dataType; - return !forbidden.includes(dtypeKey); + return !nonAllowedDtypes.includes(dtypeKey); }); } diff --git a/DashAI/front/src/components/notebooks/explorerCreation/ScopeStepExplorer.jsx b/DashAI/front/src/components/notebooks/explorerCreation/ScopeStepExplorer.jsx index c6025b13b..e5eae85e9 100644 --- a/DashAI/front/src/components/notebooks/explorerCreation/ScopeStepExplorer.jsx +++ b/DashAI/front/src/components/notebooks/explorerCreation/ScopeStepExplorer.jsx @@ -18,7 +18,7 @@ export default function ScopeStepExplorer({ const allowedTypes = tool?.metadata?.allowed_types || []; const allowedDtypes = tool?.metadata?.allowed_dtypes || []; const inputCardinality = tool?.metadata?.input_cardinality || {}; - const typesDtypeRestrictions = tool?.metadata?.type_dtype_restrictions || {}; + const nonAllowedDtypes = tool?.metadata?.non_allowed_dtypes || []; const tourContext = useTourContext(); const { t } = useTranslation(["datasets", "common"]); @@ -52,7 +52,7 @@ export default function ScopeStepExplorer({ inputCardinality={inputCardinality} allowedTypes={allowedTypes} allowedDtypes={allowedDtypes} - typesDtypeRestrictions={typesDtypeRestrictions} + nonAllowedDtypes={nonAllowedDtypes} onSelectionChange={(selected) => setScopeColumns(selected)} onValidationChange={(isValid) => setIsSelectionValid(isValid)} /> From 6866edf9b31493f0bd6caa17b827e0879b07a490 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 10:21:32 -0400 Subject: [PATCH 15/28] feat: add excluded data types translations --- DashAI/front/src/utils/i18n/locales/de/datasets.json | 1 + DashAI/front/src/utils/i18n/locales/en/datasets.json | 1 + DashAI/front/src/utils/i18n/locales/es/datasets.json | 1 + DashAI/front/src/utils/i18n/locales/pt/datasets.json | 1 + DashAI/front/src/utils/i18n/locales/zh/datasets.json | 1 + 5 files changed, 5 insertions(+) diff --git a/DashAI/front/src/utils/i18n/locales/de/datasets.json b/DashAI/front/src/utils/i18n/locales/de/datasets.json index 927630823..abc790ea6 100644 --- a/DashAI/front/src/utils/i18n/locales/de/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/de/datasets.json @@ -100,6 +100,7 @@ "all": "alle", "allowedDataTypes": "Zulässige Datentypen: <1><0>", "allowedValueTypes": "Zulässige Wertetypen: <1><0>", + "excludedDataTypes": "Ausgeschlossene Datentypen: {{dtypes}}", "allTypeChangesValid": "Alle Typänderungen sind gültig und können sicher angewendet werden.", "analysisTools": "Analysewerkzeuge", "appearance": "Erscheinungsbild", diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json index b9e95c740..c0e9aa1bd 100644 --- a/DashAI/front/src/utils/i18n/locales/en/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json @@ -98,6 +98,7 @@ "all": "all", "allowedDataTypes": "Allowed data types: <1><0>", "allowedValueTypes": "Allowed value types: <1><0>", + "excludedDataTypes": "Excluded data types: {{dtypes}}", "allTypeChangesValid": "All type changes are valid and can be applied safely.", "analysisTools": "Analysis Tools", "appearance": "Appearance", diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json index 2ed5a6efb..4abc3b59c 100644 --- a/DashAI/front/src/utils/i18n/locales/es/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json @@ -103,6 +103,7 @@ "all": "todos", "allowedDataTypes": "Tipos de datos permitidos: <1><0>", "allowedValueTypes": "Tipos de valores permitidos: <1><0>", + "excludedDataTypes": "Tipos de datos excluidos: {{dtypes}}", "allTypeChangesValid": "Todos los cambios de tipo son válidos y pueden aplicarse de forma segura.", "analysisTools": "Herramientas de Análisis", "appearance": "Apariencia", diff --git a/DashAI/front/src/utils/i18n/locales/pt/datasets.json b/DashAI/front/src/utils/i18n/locales/pt/datasets.json index b3f4caed7..68df32154 100644 --- a/DashAI/front/src/utils/i18n/locales/pt/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/pt/datasets.json @@ -103,6 +103,7 @@ "all": "todos", "allowedDataTypes": "Tipos de dados permitidos: <1><0>", "allowedValueTypes": "Tipos de valores permitidos: <1><0>", + "excludedDataTypes": "Tipos de dados excluídos: {{dtypes}}", "allTypeChangesValid": "Todas as alterações de tipo são válidas e podem ser aplicadas com segurança.", "analysisTools": "Ferramentas de Análise", "appearance": "Aparência", diff --git a/DashAI/front/src/utils/i18n/locales/zh/datasets.json b/DashAI/front/src/utils/i18n/locales/zh/datasets.json index def52be26..8cbfcb0c0 100644 --- a/DashAI/front/src/utils/i18n/locales/zh/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/zh/datasets.json @@ -99,6 +99,7 @@ "all": "全部", "allowedDataTypes": "允许的数据类型:<1><0>", "allowedValueTypes": "允许的值类型:<1><0>", + "excludedDataTypes": "排除的数据类型:{{dtypes}}", "allTypeChangesValid": "所有类型更改均有效,可以安全应用。", "analysisTools": "分析工具", "appearance": "外观", From 5f96b6fab0de8c6cc47901d3ba654aa98c70d874 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 11:06:25 -0400 Subject: [PATCH 16/28] fix: ship diffusers source and avoid AutoPipeline so frozen exe can run image models The packaged .exe strips .py source, but torch.jit.script runs at import time in diffusers (kolors) and needs original source via inspect.getsource, crashing with 'could not get source code'. Ship diffusers source as a data dir in the spec (same as transformers) so TorchScript can read it, and load sdxl-turbo via StableDiffusionXLPipeline directly to avoid importing every pipeline class through AutoPipeline. --- DashAI/back/models/hugging_face/sdxl_turbo_model.py | 6 +++--- dashai.spec | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/DashAI/back/models/hugging_face/sdxl_turbo_model.py b/DashAI/back/models/hugging_face/sdxl_turbo_model.py index a0e66db10..918d90aed 100644 --- a/DashAI/back/models/hugging_face/sdxl_turbo_model.py +++ b/DashAI/back/models/hugging_face/sdxl_turbo_model.py @@ -384,7 +384,7 @@ def __init__(self, **kwargs): """Download and initialise the SDXL Turbo pipeline. Downloads ``stabilityai/sdxl-turbo`` from HuggingFace Hub via - ``AutoPipelineForText2Image.from_pretrained`` and moves the pipeline + ``StableDiffusionXLPipeline.from_pretrained`` and moves the pipeline to the requested device. When a GPU is available, the ``fp16`` variant is loaded to halve memory usage; CPU inference uses ``float32``. @@ -414,7 +414,7 @@ def __init__(self, **kwargs): Number of images to generate per prompt call. """ import torch - from diffusers import AutoPipelineForText2Image + from diffusers import StableDiffusionXLPipeline kwargs = self.validate_and_transform(kwargs) use_gpu = DEVICE_TO_IDX.get(kwargs.get("device")) >= 0 @@ -422,7 +422,7 @@ def __init__(self, **kwargs): f"cuda:{DEVICE_TO_IDX.get(kwargs.get('device'))}" if use_gpu else "cpu" ) - self.model = AutoPipelineForText2Image.from_pretrained( + self.model = StableDiffusionXLPipeline.from_pretrained( "stabilityai/sdxl-turbo", torch_dtype=torch.float16 if use_gpu else torch.float32, variant="fp16" if use_gpu else None, diff --git a/dashai.spec b/dashai.spec index 0c0e27d87..6bbf94822 100644 --- a/dashai.spec +++ b/dashai.spec @@ -34,6 +34,11 @@ a = Analysis( ), ("DashAI/back/seeds", "DashAI/back/seeds"), (f"{SITEPKG}/transformers", "transformers"), + # Ship source .py for packages that run torch.jit.script at import time. + # PyInstaller bundles only .pyc, but TorchScript needs original source + # via inspect.getsource, so these must be shipped as data dirs. + (f"{SITEPKG}/diffusers", "diffusers"), + (f"{SITEPKG}/controlnet_aux", "controlnet_aux"), ] + webview_datas, hiddenimports=webview_hiddenimports, hookspath=["hooks"], From 21953a132cfbbf300aa44cf4a5eb61c5fab478e1 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 11:32:49 -0400 Subject: [PATCH 17/28] fix: preserve source resolution in SD1.5 depth ControlNet The depth map was hardcoded to 512x512, forcing every output to that size regardless of the input image. Interpolate the depth map back to the source resolution instead, rounding each side down to a multiple of 8 as required by the SD 1.5 UNet latent downsampling. --- .../sd15_depth_controlnet_model.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/DashAI/back/models/hugging_face/sd15_depth_controlnet_model.py b/DashAI/back/models/hugging_face/sd15_depth_controlnet_model.py index 6dc06e3f8..900bfb44f 100644 --- a/DashAI/back/models/hugging_face/sd15_depth_controlnet_model.py +++ b/DashAI/back/models/hugging_face/sd15_depth_controlnet_model.py @@ -181,8 +181,9 @@ def get_depth_map_sd15(image, device): """Convert an input image to a normalised depth map for SD 1.5 ControlNet. Uses Intel's DPT-Hybrid-MiDaS model to estimate per-pixel depth, then - bilinearly interpolates the result to 512x512 and normalises values to the - [0, 1] range before returning a three-channel PIL image. + interpolates the result back to the source image resolution (rounded down + to a multiple of 8) and normalises values to the [0, 1] range before + returning a three-channel PIL image. Parameters ---------- @@ -195,8 +196,9 @@ def get_depth_map_sd15(image, device): Returns ------- PIL.Image.Image - A 512x512 RGB image where each channel encodes the normalised depth - value, ready to be used as a ControlNet conditioning signal. + An RGB image at the source resolution (each side rounded down to a + multiple of 8) where each channel encodes the normalised depth value, + ready to be used as a ControlNet conditioning signal. """ import numpy as np import torch @@ -215,9 +217,16 @@ def get_depth_map_sd15(image, device): with torch.no_grad(), torch.autocast(device, dtype=torch.float16): depth_map = depth_estimator(pixel_values).predicted_depth + # Preserve the source resolution. SD 1.5's UNet downsamples by 8 in latent + # space, so both dimensions must be divisible by 8; round down to the + # nearest multiple to avoid a pipeline shape error. + width, height = image.size + width = max(8, (width // 8) * 8) + height = max(8, (height // 8) * 8) + depth_map = torch.nn.functional.interpolate( depth_map.unsqueeze(1), - size=(512, 512), + size=(height, width), mode="bicubic", align_corners=False, ) From af95e2075a94f3b234f7745cac9a12da5a44c0c4 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 11:40:04 -0400 Subject: [PATCH 18/28] fix: make optuna CmaEsSampler work and drop incompatible GridSampler Add the cmaes dependency so CmaEsSampler no longer fails with ModuleNotFoundError, and remove GridSampler from the sampler enum since it requires an explicit search_space and cannot run with the bounds-based suggest_int/suggest_float optimization flow. --- DashAI/back/optimizers/optuna_optimizer.py | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/DashAI/back/optimizers/optuna_optimizer.py b/DashAI/back/optimizers/optuna_optimizer.py index 3e57b05df..df551733a 100644 --- a/DashAI/back/optimizers/optuna_optimizer.py +++ b/DashAI/back/optimizers/optuna_optimizer.py @@ -38,7 +38,6 @@ class OptunaSchema(BaseSchema): enum=[ "TPESampler", "CmaEsSampler", - "GridSampler", "GPSampler", "NSGAIISampler", "QMCSampler", diff --git a/requirements.txt b/requirements.txt index 2fc5b6b99..165d4afe6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ controlnet_aux sacrebleu sentencepiece optuna +cmaes hyperopt nvidia-ml-py openpyxl From 51bc8127a34a503d2a4f801927cdb967979e3342 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 12:01:33 -0400 Subject: [PATCH 19/28] fix: match GGUF filename case so llama.cpp models load on Linux Llama.from_pretrained matches the filename pattern with fnmatch, which is case insensitive on Windows but case sensitive on Linux. The SmolLM patterns used uppercase quant tags (Q4_K_M, Q8_0) while HuggingFaceTB ships lowercase files, and the Mixtral default fallback used a lowercase repo prefix. Both failed only in the packaged Linux AppImage. Align the patterns and default with the real repo filenames. --- DashAI/back/models/hugging_face/mixtral_model.py | 2 +- DashAI/back/models/hugging_face/smol_lm_model.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DashAI/back/models/hugging_face/mixtral_model.py b/DashAI/back/models/hugging_face/mixtral_model.py index 3b4b8254d..402eb281a 100644 --- a/DashAI/back/models/hugging_face/mixtral_model.py +++ b/DashAI/back/models/hugging_face/mixtral_model.py @@ -487,7 +487,7 @@ def __init__(self, **kwargs): self.frequency_penalty = kwargs.pop("frequency_penalty", 0.1) self.n_ctx = kwargs.pop("context_window", 512) - self.filename = kwargs.get("filename", "mixtral-8x7b-instruct-v0.1.Q2_K.gguf") + self.filename = kwargs.get("filename", "Mixtral-8x7B-Instruct-v0.1.Q2_K.gguf") use_gpu = LLAMA_DEVICE_TO_IDX.get(kwargs.get("device")) >= 0 self.model = Llama.from_pretrained( diff --git a/DashAI/back/models/hugging_face/smol_lm_model.py b/DashAI/back/models/hugging_face/smol_lm_model.py index 864c02819..a5f56bdd9 100644 --- a/DashAI/back/models/hugging_face/smol_lm_model.py +++ b/DashAI/back/models/hugging_face/smol_lm_model.py @@ -18,8 +18,8 @@ ) SMOLLM_FILENAME_MAP = { - "HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF": "*Q4_K_M.gguf", - "HuggingFaceTB/SmolLM2-360M-Instruct-GGUF": "*Q8_0.gguf", + "HuggingFaceTB/SmolLM2-1.7B-Instruct-GGUF": "*q4_k_m.gguf", + "HuggingFaceTB/SmolLM2-360M-Instruct-GGUF": "*q8_0.gguf", } @@ -417,7 +417,7 @@ def __init__(self, **kwargs): self.frequency_penalty = kwargs.pop("frequency_penalty", 0.1) self.n_ctx = kwargs.pop("context_window", 512) - self.filename = SMOLLM_FILENAME_MAP.get(self.model_name, "*Q4_K_M.gguf") + self.filename = SMOLLM_FILENAME_MAP.get(self.model_name, "*q4_k_m.gguf") use_gpu = LLAMA_DEVICE_TO_IDX.get(kwargs.get("device")) >= 0 self.model = Llama.from_pretrained( From c13a778e1d931c9a44bc86a2931bb5510edbdb3e Mon Sep 17 00:00:00 2001 From: Creylay Date: Fri, 26 Jun 2026 12:55:57 -0400 Subject: [PATCH 20/28] fix: allow empty output in VarianceThreshold by handling ValueError during fit --- .../scikit_learn/variance_threshold.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/DashAI/back/converters/scikit_learn/variance_threshold.py b/DashAI/back/converters/scikit_learn/variance_threshold.py index ca741f530..98aff4e01 100644 --- a/DashAI/back/converters/scikit_learn/variance_threshold.py +++ b/DashAI/back/converters/scikit_learn/variance_threshold.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, Union + from sklearn.feature_selection import VarianceThreshold as VarianceThresholdOperation from DashAI.back.converters.category.dimensionality_reduction import ( @@ -10,6 +12,9 @@ from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer +if TYPE_CHECKING: + from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset + class VarianceThresholdSchema(BaseSchema): """Schema for VarianceThreshold hyperparameters. @@ -88,6 +93,37 @@ class VarianceThreshold( zh="方差阈值", ) + def fit( + self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None + ) -> "VarianceThreshold": + """Fit the transformer, allowing all features to be removed if none pass. + + sklearn raises a ValueError when no feature meets the threshold; we catch + it and return self instead so that ``transform`` can legitimately return a + dataset with zero columns. ``self.variances_`` is already populated by + sklearn before it raises, so the internal state is correct. + + Parameters + ---------- + x : DashAIDataset + Input dataset. + y : DashAIDataset, optional + Ignored; present for API consistency. + + Returns + ------- + VarianceThreshold + The fitted instance. + """ + try: + return super().fit(x, y) + except ValueError as e: + if "meets the variance threshold" not in str(e): + raise + # self.variances_ is already set by sklearn before it raises, + # so transform will correctly produce a zero-column result. + return self + def get_output_type(self, column_name: str = None) -> DashAIDataType: """Return the DashAI data type produced by this converter for a column. From bb9568bbbfa45b6f2551c5e362cb4202ab162854 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 14:01:43 -0400 Subject: [PATCH 21/28] fix: render AppImage icon from SVG isotype at high resolution The AppImage recipe extracted frame 0 of dashAI.ico, a 16x16 entry, so python-appimage installed the icon under hicolor/16x16 and desktops showed a generic icon. Rasterize the scalable dashai-isotype.svg to a 512x512 PNG via rsvg-convert instead, giving desktops a high-resolution icon. --- .github/workflows/publish.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4e5530c1e..a81b38615 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -251,7 +251,7 @@ jobs: python -m pip install --upgrade pip pip install build python-appimage sudo apt-get update - sudo apt-get install -y libfuse2 imagemagick + sudo apt-get install -y libfuse2 imagemagick librsvg2-bin - name: Build wheel (frontend bundled) run: python -m build --wheel - name: Verify frontend is included in wheel @@ -262,8 +262,11 @@ jobs: fi - name: Prepare AppImage recipe run: | - # Icon referenced by dashai.desktop (Icon=dashai); take the first frame of the .ico - convert installer/dashAI.ico[0] appimage/dashai.png + # Icon referenced by dashai.desktop (Icon=dashai). Rasterize the + # scalable SVG isotype to a large square PNG so desktops have a + # high-resolution icon (the .ico only carried small frames). + rsvg-convert -h 512 DashAI/front/public/dashai-isotype.svg -o /tmp/dashai-isotype.png + convert /tmp/dashai-isotype.png -background none -gravity center -extent 512x512 appimage/dashai.png # Append the freshly built wheel (with bundled frontend) to the recipe requirements WHEEL=$(ls "$PWD"/dist/*.whl | head -n1) echo "$WHEEL" >> appimage/requirements.txt From 8a782b4a12660c7fb293ac84e391a7fae81ec500 Mon Sep 17 00:00:00 2001 From: Irozuku Date: Fri, 26 Jun 2026 15:41:38 -0400 Subject: [PATCH 22/28] style: center chat title and adjust session info layout --- .../components/generative/GenerativeChat.jsx | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/DashAI/front/src/components/generative/GenerativeChat.jsx b/DashAI/front/src/components/generative/GenerativeChat.jsx index 85b38d76a..5f12cd2d3 100644 --- a/DashAI/front/src/components/generative/GenerativeChat.jsx +++ b/DashAI/front/src/components/generative/GenerativeChat.jsx @@ -241,40 +241,30 @@ export default function GenerativeChat() { sx={{ width: "100%", display: "flex", - flexDirection: "column", + flexDirection: "row", justifyContent: "space-between", alignItems: "center", borderRadius: 1, - opacity: 0.5, + my: 2, + px: 1, }} > - - - {sessionInfo?.name ? sessionInfo.name : "Untitled Session"}{" "} - {sessionInfo?.description ? ":" : null} {sessionInfo?.description} - - - - setSessionInfoVisible(true)}> - - - + + {sessionInfo?.name ? sessionInfo.name : "Untitled Session"}{" "} + {sessionInfo?.description ? ":" : null} {sessionInfo?.description} + + + + setSessionInfoVisible(true)}> + + From 9e432679532793ea24facefa972414e35d43b6f3 Mon Sep 17 00:00:00 2001 From: Creylay Date: Fri, 26 Jun 2026 16:12:31 -0400 Subject: [PATCH 23/28] fix: add validation for n_components in dimensionality reduction converters and update UI to reflect input cardinality requirements --- DashAI/back/converters/base_converter.py | 3 ++ .../category/dimensionality_reduction.py | 1 + .../back/converters/scikit_learn/fast_ica.py | 1 + .../back/converters/scikit_learn/nystroem.py | 1 + DashAI/back/converters/scikit_learn/pca.py | 13 ++++++++ .../converters/scikit_learn/truncated_svd.py | 1 + .../src/components/notebooks/RightBar.jsx | 22 +++++++++++++ .../FormConverterSection.jsx | 2 ++ .../ParameterStepConverter.jsx | 33 +++++++++++++++++-- .../converterCreation/ScopeStepConverter.jsx | 2 ++ .../src/utils/i18n/locales/de/datasets.json | 1 + .../src/utils/i18n/locales/en/datasets.json | 1 + .../src/utils/i18n/locales/es/datasets.json | 1 + .../src/utils/i18n/locales/pt/datasets.json | 1 + .../src/utils/i18n/locales/zh/datasets.json | 1 + 15 files changed, 82 insertions(+), 2 deletions(-) diff --git a/DashAI/back/converters/base_converter.py b/DashAI/back/converters/base_converter.py index a29a74956..9acf87e0c 100644 --- a/DashAI/back/converters/base_converter.py +++ b/DashAI/back/converters/base_converter.py @@ -72,6 +72,9 @@ def get_metadata(cls) -> Dict[str, Any]: meta["color"] = cls.COLOR if cls.COLOR else "rgb(255, 255, 255)" meta["supervised"] = cls.SUPERVISED meta["changes_row_count"] = cls.CHANGES_ROW_COUNT + meta["n_components_features_bounded"] = getattr( + cls, "N_COMPONENTS_FEATURES_BOUNDED", False + ) # Serialize allowed_types class references → class name strings for the frontend raw_types = meta.get("allowed_types", []) diff --git a/DashAI/back/converters/category/dimensionality_reduction.py b/DashAI/back/converters/category/dimensionality_reduction.py index 25c2619f1..378296343 100644 --- a/DashAI/back/converters/category/dimensionality_reduction.py +++ b/DashAI/back/converters/category/dimensionality_reduction.py @@ -26,3 +26,4 @@ class DimensionalityReductionConverter(BaseConverter): ) ICON: Final[str] = Icon.Layers.value COLOR: Final[str] = "rgb(255, 99, 132)" + N_COMPONENTS_FEATURES_BOUNDED: bool = True diff --git a/DashAI/back/converters/scikit_learn/fast_ica.py b/DashAI/back/converters/scikit_learn/fast_ica.py index 095c2157d..0e0ab35dd 100644 --- a/DashAI/back/converters/scikit_learn/fast_ica.py +++ b/DashAI/back/converters/scikit_learn/fast_ica.py @@ -208,6 +208,7 @@ class FastICA(DimensionalityReductionConverter, SklearnWrapper, FastICAOperation """ SCHEMA = FastICASchema + N_COMPONENTS_FEATURES_BOUNDED: bool = False DESCRIPTION = MultilingualString( en="FastICA: a fast algorithm for Independent Component Analysis.", es=( diff --git a/DashAI/back/converters/scikit_learn/nystroem.py b/DashAI/back/converters/scikit_learn/nystroem.py index 2f4d7eea8..7aef4b009 100644 --- a/DashAI/back/converters/scikit_learn/nystroem.py +++ b/DashAI/back/converters/scikit_learn/nystroem.py @@ -170,6 +170,7 @@ class Nystroem(DimensionalityReductionConverter, SklearnWrapper, NystroemOperati """ SCHEMA = NystroemSchema + N_COMPONENTS_FEATURES_BOUNDED: bool = False DESCRIPTION = MultilingualString( en=( "Approximate a kernel map using a subset of the training data. " diff --git a/DashAI/back/converters/scikit_learn/pca.py b/DashAI/back/converters/scikit_learn/pca.py index 2276a4957..a55042624 100644 --- a/DashAI/back/converters/scikit_learn/pca.py +++ b/DashAI/back/converters/scikit_learn/pca.py @@ -306,6 +306,19 @@ def __init__(self, **kwargs): super().__init__(**kwargs) + def fit(self, x, y=None): + x_pandas = x.to_pandas() if hasattr(x, "to_pandas") else x + n_samples, n_features = x_pandas.shape + max_components = min(n_samples, n_features) + if isinstance(self.n_components, int) and self.n_components > max_components: + raise ValueError( + f"n_components={self.n_components} exceeds " + f"min(n_samples, n_features)={max_components}. " + f"Reduce n_components to at most {max_components}, " + "or select more columns in the converter scope." + ) + return super().fit(x, y) + def get_output_type(self, column_name: str = None) -> DashAIDataType: """Return the DashAI data type produced by this converter for a column. diff --git a/DashAI/back/converters/scikit_learn/truncated_svd.py b/DashAI/back/converters/scikit_learn/truncated_svd.py index d0361a99a..6d6d53d90 100644 --- a/DashAI/back/converters/scikit_learn/truncated_svd.py +++ b/DashAI/back/converters/scikit_learn/truncated_svd.py @@ -211,6 +211,7 @@ class TruncatedSVD( metadata = { "allowed_types": [Float, Integer], "allowed_dtypes": [], + "input_cardinality": {"min": 2}, } def __init__(self, **kwargs): diff --git a/DashAI/front/src/components/notebooks/RightBar.jsx b/DashAI/front/src/components/notebooks/RightBar.jsx index 08b936edd..829cfcc77 100644 --- a/DashAI/front/src/components/notebooks/RightBar.jsx +++ b/DashAI/front/src/components/notebooks/RightBar.jsx @@ -227,6 +227,7 @@ export default function RightBar({ notebook, onToggle }) { const allowedTypes = converter?.metadata?.allowed_types || []; const allowedDtypes = converter?.metadata?.allowed_dtypes || []; + const inputCardinality = converter?.metadata?.input_cardinality || {}; let validColumns = datasetColumns; let disabled = false; @@ -247,6 +248,27 @@ export default function RightBar({ notebook, onToggle }) { ); } + // Check cardinality requirements + if (inputCardinality.exact != null) { + if (validColumns.length < inputCardinality.exact) { + disabled = true; + tooltip += `\n\n${t("datasets:error.requiresExactColumns", { + required: inputCardinality.exact, + available: validColumns.length, + count: inputCardinality.exact, + })}`; + } + } else if (inputCardinality.min != null) { + if (validColumns.length < inputCardinality.min) { + disabled = true; + tooltip += `\n\n${t("datasets:error.requiresMinColumns", { + required: inputCardinality.min, + available: validColumns.length, + count: inputCardinality.min, + })}`; + } + } + // Check if there are no valid columns at all (some restriction was applied) if ( validColumns.length === 0 && diff --git a/DashAI/front/src/components/notebooks/converterCreation/FormConverterSection.jsx b/DashAI/front/src/components/notebooks/converterCreation/FormConverterSection.jsx index abb8d38f2..9505fd7d7 100644 --- a/DashAI/front/src/components/notebooks/converterCreation/FormConverterSection.jsx +++ b/DashAI/front/src/components/notebooks/converterCreation/FormConverterSection.jsx @@ -160,6 +160,8 @@ export default function FormConverterSection({ {step === 1 && ( 0 && + currentNComponents > nColumnsSelected; + return ( {t("datasets:label.configureParameters")} + {showNComponentsWarning && ( + + {t("datasets:label.nComponentsColumnInfo", { + count: nColumnsSelected, + })} + + )} setStep(0)} saveButtonText={t("datasets:button.createConverter")} hideButtons={hideButtons} + onValuesChange={(values) => { + if (values?.n_components !== undefined) { + setCurrentNComponents(values.n_components); + } + }} /> diff --git a/DashAI/front/src/components/notebooks/converterCreation/ScopeStepConverter.jsx b/DashAI/front/src/components/notebooks/converterCreation/ScopeStepConverter.jsx index 27bca2b66..36332b30b 100644 --- a/DashAI/front/src/components/notebooks/converterCreation/ScopeStepConverter.jsx +++ b/DashAI/front/src/components/notebooks/converterCreation/ScopeStepConverter.jsx @@ -29,6 +29,7 @@ export default function ScopeStepConverter({ const tourContext = useTourContext(); const allowedTypes = tool?.metadata?.allowed_types || []; const allowedDtypes = tool?.metadata?.allowed_dtypes || []; + const inputCardinality = tool?.metadata?.input_cardinality || {}; const { t } = useTranslation(["common", "datasets"]); const { columnTypes } = useExplorersAndConverters(); @@ -97,6 +98,7 @@ export default function ScopeStepConverter({ tool={tool} allowedTypes={allowedTypes} allowedDtypes={allowedDtypes} + inputCardinality={inputCardinality} columnTypes={columnTypes} onSelectionChange={(columnsInfo) => { const processedColumns = columnsInfo.map((col) => ({ diff --git a/DashAI/front/src/utils/i18n/locales/de/datasets.json b/DashAI/front/src/utils/i18n/locales/de/datasets.json index 927630823..17d9064db 100644 --- a/DashAI/front/src/utils/i18n/locales/de/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/de/datasets.json @@ -267,6 +267,7 @@ "missingValuesOverview": "Übersicht fehlender Werte", "mostFrequent": "Am häufigsten", "nameYourNotebook": "Notizbuch benennen", + "nComponentsColumnInfo": "Sie haben {{count}} Spalte(n) ausgewählt. n_components muss kleiner oder gleich {{count}} sein. Reduzieren Sie n_components oder wählen Sie mehr Spalten aus, um Fehler zu vermeiden.", "newDatasetCreatedWithTransformations": "Ein neuer Datensatz wird mit diesen Transformationen erstellt. Er kann mit anderen Modulen verwendet werden, ohne das Original zu beeinflussen.", "noDataQualityIssuesDetected": "Keine Datenqualitätsprobleme erkannt", "noDuplicateRows": "Keine doppelten Zeilen im Datensatz gefunden.", diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json index b9e95c740..d92ed6bae 100644 --- a/DashAI/front/src/utils/i18n/locales/en/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json @@ -267,6 +267,7 @@ "missingValuesOverview": "Missing Values Overview", "mostFrequent": "Most Frequent", "nameYourNotebook": "Name your Notebook", + "nComponentsColumnInfo": "You selected {{count}} column(s). n_components must be less than or equal to {{count}}. Reduce n_components or add more columns to avoid errors.", "newDatasetCreatedWithTransformations": "A new dataset will be created with these transformations. It can be used with other modules without affecting the original.", "noDataQualityIssuesDetected": "No data quality issues detected", "noDuplicateRows": "No duplicate rows detected in the dataset.", diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json index 2ed5a6efb..dad8b9668 100644 --- a/DashAI/front/src/utils/i18n/locales/es/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json @@ -273,6 +273,7 @@ "missingValuesOverview": "Resumen de Valores Faltantes", "mostFrequent": "Más Frecuente", "nameYourNotebook": "Nombre su Cuaderno", + "nComponentsColumnInfo": "Seleccionaste {{count}} columna(s). n_components debe ser menor o igual a {{count}}. Reduce n_components o agrega más columnas para evitar errores.", "newDatasetCreatedWithTransformations": "Se creará un nuevo dataset con estas transformaciones. Puede usarse con otros módulos sin afectar el original.", "noDataQualityIssuesDetected": "No se detectaron problemas de calidad de datos", "noDuplicateRows": "No se detectaron filas duplicadas en el dataset.", diff --git a/DashAI/front/src/utils/i18n/locales/pt/datasets.json b/DashAI/front/src/utils/i18n/locales/pt/datasets.json index b3f4caed7..456f7be66 100644 --- a/DashAI/front/src/utils/i18n/locales/pt/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/pt/datasets.json @@ -273,6 +273,7 @@ "missingValuesOverview": "Resumo de Valores Ausentes", "mostFrequent": "Mais Frequente", "nameYourNotebook": "Nomeie seu Caderno", + "nComponentsColumnInfo": "Você selecionou {{count}} coluna(s). n_components deve ser menor ou igual a {{count}}. Reduza n_components ou adicione mais colunas para evitar erros.", "newDatasetCreatedWithTransformations": "Um novo conjunto de dados será criado com estas transformações. Pode ser usado com outros módulos sem afetar o original.", "noDataQualityIssuesDetected": "Nenhum problema de qualidade de dados detectado", "noDuplicateRows": "Nenhuma linha duplicada detectada no conjunto de dados.", diff --git a/DashAI/front/src/utils/i18n/locales/zh/datasets.json b/DashAI/front/src/utils/i18n/locales/zh/datasets.json index def52be26..375530318 100644 --- a/DashAI/front/src/utils/i18n/locales/zh/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/zh/datasets.json @@ -268,6 +268,7 @@ "missingValuesOverview": "缺失值概览", "mostFrequent": "最频繁", "nameYourNotebook": "为笔记本命名", + "nComponentsColumnInfo": "您选择了 {{count}} 列。n_components 必须小于或等于 {{count}}。请减少 n_components 或选择更多列以避免错误。", "newDatasetCreatedWithTransformations": "将使用这些变换创建新数据集。可在其他模块中使用,不影响原始数据。", "noDataQualityIssuesDetected": "未检测到数据质量问题", "noDuplicateRows": "数据集中未检测到重复行。", From 4ebe18bb169ac76ed223e82247a7e4fbc21c8172 Mon Sep 17 00:00:00 2001 From: Creylay Date: Fri, 26 Jun 2026 17:06:47 -0400 Subject: [PATCH 24/28] fix: update n_components warning message to use consistent variable naming in translations --- .../notebooks/converterCreation/ParameterStepConverter.jsx | 2 +- DashAI/front/src/utils/i18n/locales/de/datasets.json | 2 +- DashAI/front/src/utils/i18n/locales/en/datasets.json | 2 +- DashAI/front/src/utils/i18n/locales/es/datasets.json | 2 +- DashAI/front/src/utils/i18n/locales/pt/datasets.json | 2 +- DashAI/front/src/utils/i18n/locales/zh/datasets.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx b/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx index 9e836c195..745ecb4a1 100644 --- a/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx +++ b/DashAI/front/src/components/notebooks/converterCreation/ParameterStepConverter.jsx @@ -91,7 +91,7 @@ export default function ParameterStepConverter({ {showNComponentsWarning && ( {t("datasets:label.nComponentsColumnInfo", { - count: nColumnsSelected, + n: nColumnsSelected, })} )} diff --git a/DashAI/front/src/utils/i18n/locales/de/datasets.json b/DashAI/front/src/utils/i18n/locales/de/datasets.json index 17d9064db..a17c481a4 100644 --- a/DashAI/front/src/utils/i18n/locales/de/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/de/datasets.json @@ -267,7 +267,7 @@ "missingValuesOverview": "Übersicht fehlender Werte", "mostFrequent": "Am häufigsten", "nameYourNotebook": "Notizbuch benennen", - "nComponentsColumnInfo": "Sie haben {{count}} Spalte(n) ausgewählt. n_components muss kleiner oder gleich {{count}} sein. Reduzieren Sie n_components oder wählen Sie mehr Spalten aus, um Fehler zu vermeiden.", + "nComponentsColumnInfo": "Sie haben {{n}} Spalte(n) ausgewählt. n_components muss kleiner oder gleich {{n}} sein. Reduzieren Sie n_components oder wählen Sie mehr Spalten aus, um Fehler zu vermeiden.", "newDatasetCreatedWithTransformations": "Ein neuer Datensatz wird mit diesen Transformationen erstellt. Er kann mit anderen Modulen verwendet werden, ohne das Original zu beeinflussen.", "noDataQualityIssuesDetected": "Keine Datenqualitätsprobleme erkannt", "noDuplicateRows": "Keine doppelten Zeilen im Datensatz gefunden.", diff --git a/DashAI/front/src/utils/i18n/locales/en/datasets.json b/DashAI/front/src/utils/i18n/locales/en/datasets.json index d92ed6bae..3ff08896f 100644 --- a/DashAI/front/src/utils/i18n/locales/en/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/en/datasets.json @@ -267,7 +267,7 @@ "missingValuesOverview": "Missing Values Overview", "mostFrequent": "Most Frequent", "nameYourNotebook": "Name your Notebook", - "nComponentsColumnInfo": "You selected {{count}} column(s). n_components must be less than or equal to {{count}}. Reduce n_components or add more columns to avoid errors.", + "nComponentsColumnInfo": "You selected {{n}} column(s). n_components must be less than or equal to {{n}}. Reduce n_components or add more columns to avoid errors.", "newDatasetCreatedWithTransformations": "A new dataset will be created with these transformations. It can be used with other modules without affecting the original.", "noDataQualityIssuesDetected": "No data quality issues detected", "noDuplicateRows": "No duplicate rows detected in the dataset.", diff --git a/DashAI/front/src/utils/i18n/locales/es/datasets.json b/DashAI/front/src/utils/i18n/locales/es/datasets.json index dad8b9668..285c76fff 100644 --- a/DashAI/front/src/utils/i18n/locales/es/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/es/datasets.json @@ -273,7 +273,7 @@ "missingValuesOverview": "Resumen de Valores Faltantes", "mostFrequent": "Más Frecuente", "nameYourNotebook": "Nombre su Cuaderno", - "nComponentsColumnInfo": "Seleccionaste {{count}} columna(s). n_components debe ser menor o igual a {{count}}. Reduce n_components o agrega más columnas para evitar errores.", + "nComponentsColumnInfo": "Seleccionaste {{n}} columna(s). n_components debe ser menor o igual a {{n}}. Reduce n_components o agrega más columnas para evitar errores.", "newDatasetCreatedWithTransformations": "Se creará un nuevo dataset con estas transformaciones. Puede usarse con otros módulos sin afectar el original.", "noDataQualityIssuesDetected": "No se detectaron problemas de calidad de datos", "noDuplicateRows": "No se detectaron filas duplicadas en el dataset.", diff --git a/DashAI/front/src/utils/i18n/locales/pt/datasets.json b/DashAI/front/src/utils/i18n/locales/pt/datasets.json index 456f7be66..959e787b4 100644 --- a/DashAI/front/src/utils/i18n/locales/pt/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/pt/datasets.json @@ -273,7 +273,7 @@ "missingValuesOverview": "Resumo de Valores Ausentes", "mostFrequent": "Mais Frequente", "nameYourNotebook": "Nomeie seu Caderno", - "nComponentsColumnInfo": "Você selecionou {{count}} coluna(s). n_components deve ser menor ou igual a {{count}}. Reduza n_components ou adicione mais colunas para evitar erros.", + "nComponentsColumnInfo": "Você selecionou {{n}} coluna(s). n_components deve ser menor ou igual a {{n}}. Reduza n_components ou adicione mais colunas para evitar erros.", "newDatasetCreatedWithTransformations": "Um novo conjunto de dados será criado com estas transformações. Pode ser usado com outros módulos sem afetar o original.", "noDataQualityIssuesDetected": "Nenhum problema de qualidade de dados detectado", "noDuplicateRows": "Nenhuma linha duplicada detectada no conjunto de dados.", diff --git a/DashAI/front/src/utils/i18n/locales/zh/datasets.json b/DashAI/front/src/utils/i18n/locales/zh/datasets.json index 375530318..ecb9852d7 100644 --- a/DashAI/front/src/utils/i18n/locales/zh/datasets.json +++ b/DashAI/front/src/utils/i18n/locales/zh/datasets.json @@ -268,7 +268,7 @@ "missingValuesOverview": "缺失值概览", "mostFrequent": "最频繁", "nameYourNotebook": "为笔记本命名", - "nComponentsColumnInfo": "您选择了 {{count}} 列。n_components 必须小于或等于 {{count}}。请减少 n_components 或选择更多列以避免错误。", + "nComponentsColumnInfo": "您选择了 {{n}} 列。n_components 必须小于或等于 {{n}}。请减少 n_components 或选择更多列以避免错误。", "newDatasetCreatedWithTransformations": "将使用这些变换创建新数据集。可在其他模块中使用,不影响原始数据。", "noDataQualityIssuesDetected": "未检测到数据质量问题", "noDuplicateRows": "数据集中未检测到重复行。", From c40808c02b6bfa86268a18db53af2eeac1ddedc5 Mon Sep 17 00:00:00 2001 From: Creylay Date: Fri, 26 Jun 2026 18:22:09 -0400 Subject: [PATCH 25/28] feat: enhance MissingIndicator converter with missing value normalization and indicator columns --- .../scikit_learn/missing_indicator.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/DashAI/back/converters/scikit_learn/missing_indicator.py b/DashAI/back/converters/scikit_learn/missing_indicator.py index 2be11954a..8b1297201 100644 --- a/DashAI/back/converters/scikit_learn/missing_indicator.py +++ b/DashAI/back/converters/scikit_learn/missing_indicator.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING, Union + from sklearn.impute import MissingIndicator as MissingIndicatorOperation from DashAI.back.converters.category.basic_preprocessing import ( @@ -9,6 +11,11 @@ from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Integer +if TYPE_CHECKING: + import pandas as pd + + from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset + class MissingIndicatorSchema(BaseSchema): """Schema for configuring the MissingIndicator converter. @@ -75,7 +82,122 @@ def __init__(self, **kwargs): Configuration keyword arguments matching the converter's schema fields. Forwarded to the underlying scikit-learn class. """ + # Force indicators for all selected features so the user always sees the + # new column, even when a feature has no missing values (all-False indicator). + kwargs.setdefault("features", "all") super().__init__(**kwargs) + # SklearnWrapper.__init__ sets set_output(transform="pandas"), which causes + # sklearn's __init_subclass__ wrapper to intercept our custom transform and + # attempt to rename its output using get_feature_names_out() (which returns + # only the indicator column count, not the combined output count). + # Reset to "default" so the wrapper returns our DashAIDataset as-is. + if hasattr(self, "set_output"): + self.set_output(transform="default") + + @staticmethod + def _normalize_missing(frame: "pd.DataFrame") -> "pd.DataFrame": + """Return a copy of *frame* where object-column missing values are float NaN. + + HuggingFace/PyArrow stores missing strings as ``None`` (Python), but + sklearn's ``_get_mask`` uses ``x != x`` which is ``False`` for ``None`` + (only ``float('nan') != float('nan')`` is ``True``). We also treat + empty strings as missing to match the dataset-filter behaviour. + """ + import numpy as np + + frame = frame.copy() + for col in frame.select_dtypes(include="object").columns: + frame[col] = frame[col].replace("", np.nan) + frame[col] = frame[col].where(frame[col].notna(), np.nan) + return frame + + def fit( + self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None + ) -> "MissingIndicator": + """Fit after normalising missing values so sklearn detects them.""" + from DashAI.back.dataloaders.classes.dashai_dataset import to_dashai_dataset + + x_pandas = x.to_pandas() if hasattr(x, "to_pandas") else x + x_clean_ds = to_dashai_dataset(self._normalize_missing(x_pandas)) + if hasattr(x, "types"): + x_clean_ds.types = x.types.copy() + return super().fit(x_clean_ds, y) + + def transform( + self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None + ) -> "DashAIDataset": + """Transform x by appending missing-value indicator columns. + + Keeps the original columns intact and appends one boolean indicator + column per feature that had missing values during fit. Indicator + columns are named ``missingindicator_`` so that + ``_rebuild_dataset_with_transformed_columns`` treats them as *new* + columns rather than replacements, preserving the original data. + """ + import numpy as np + import pandas as pd + + from DashAI.back.dataloaders.classes.dashai_dataset import to_dashai_dataset + + x_pandas = x.to_pandas() if hasattr(x, "to_pandas") else x + + # Normalise missing values before sklearn sees the data (None and "" + # are both treated as missing, matching dataset-filter behaviour). + x_for_sklearn = self._normalize_missing(x_pandas) + + sklearn_cls = next( + ( + cls + for cls in type(self).__mro__ + if "sklearn" in cls.__module__ + and "DashAI" not in cls.__module__ + and "transform" in cls.__dict__ + ), + None, + ) + if sklearn_cls is None: + raise RuntimeError( + "No sklearn class with a 'transform' method found in the MRO." + ) + + indicators = sklearn_cls.__dict__["transform"](self, x_for_sklearn) + + # features_ contains the column indices for which indicators are produced. + # With features='all' (default), this always equals all input column indices. + if hasattr(self, "features_") and len(self.features_) > 0: + indicator_col_names = [ + f"missingindicator_{x_pandas.columns[i]}" for i in self.features_ + ] + else: + indicator_col_names = [ + f"missingindicator_{col}" for col in x_pandas.columns + ] + + if isinstance(indicators, np.ndarray): + indicators_df = pd.DataFrame( + indicators, + columns=indicator_col_names, + index=x_pandas.index, + ) + else: + indicators_df = indicators.copy() + indicators_df.columns = indicator_col_names + + combined_df = pd.concat([x_pandas, indicators_df], axis=1) + converted_dataset = to_dashai_dataset(combined_df) + + output_type = self.get_output_type() + for col in indicator_col_names: + if col in converted_dataset.column_names: + converted_dataset.types[col] = output_type + + # Preserve original column types from the input dataset + if hasattr(x, "types"): + for col in x_pandas.columns: + if col in x.types and col in converted_dataset.column_names: + converted_dataset.types[col] = x.types[col] + + return converted_dataset def get_output_type(self, column_name: str = None) -> DashAIDataType: """Return the DashAI data type produced by this converter for a column. From 995ef58f6663124c603dac6cf294b978de9f831e Mon Sep 17 00:00:00 2001 From: Felipe Date: Sun, 28 Jun 2026 19:42:28 -0400 Subject: [PATCH 26/28] feat: Change colors and style of the modal target column --- .../ConverterTargetColumnModal.jsx | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/DashAI/front/src/components/notebooks/converterCreation/ConverterTargetColumnModal.jsx b/DashAI/front/src/components/notebooks/converterCreation/ConverterTargetColumnModal.jsx index 6c372e5c1..021a2261c 100644 --- a/DashAI/front/src/components/notebooks/converterCreation/ConverterTargetColumnModal.jsx +++ b/DashAI/front/src/components/notebooks/converterCreation/ConverterTargetColumnModal.jsx @@ -12,9 +12,9 @@ import { Dialog, DialogContent, DialogTitle, - Stack, + DialogActions, } from "@mui/material"; -import { ArrowBackOutlined, ViewColumn } from "@mui/icons-material"; +import { Close as CloseIcon, ViewColumn } from "@mui/icons-material"; import { getDatasetTypesByFilePath } from "../../../api/datasets"; import { useSnackbar } from "notistack"; import { useTranslation } from "react-i18next"; @@ -184,37 +184,39 @@ const ConverterTargetColumnModal = ({ }, }} > - - - setOpen(false)}> - + + + {t("datasets:button.setColumn")} + setOpen(false)} + size="small" + sx={{ color: "text.secondary" }} + > + - - {t("datasets:button.setColumn")} - - - - - - - {t("datasets:label.classTargetColumn")} - - - {t("datasets:label.selectTargetColumnDescription")} - - - - + + + + + {t("datasets:label.classTargetColumn")} + + + {t("datasets:label.selectTargetColumnDescription")} + + - - - + )} @@ -234,6 +236,9 @@ const ConverterTargetColumnModal = ({ ConverterTargetColumnModal.propTypes = { updateClassColumn: PropTypes.func.isRequired, classColumnInitialValue: PropTypes.number, + notebook: PropTypes.shape({ + file_path: PropTypes.string, + }).isRequired, }; export default ConverterTargetColumnModal; From da23123e9ddddb799b310e5c53f6b53ad24806ea Mon Sep 17 00:00:00 2001 From: Felipe Date: Sun, 28 Jun 2026 20:00:00 -0400 Subject: [PATCH 27/28] fix type in converters --- .../converters/category/feature_selection.py | 64 ++++++++++++++++++- .../scikit_learn/generic_univariate_select.py | 19 ------ .../converters/scikit_learn/select_fdr.py | 19 ------ .../converters/scikit_learn/select_fpr.py | 19 ------ .../converters/scikit_learn/select_fwe.py | 19 ------ .../converters/scikit_learn/select_k_best.py | 19 ------ .../scikit_learn/select_percentile.py | 19 ------ .../scikit_learn/variance_threshold.py | 41 ++++++++++-- 8 files changed, 100 insertions(+), 119 deletions(-) diff --git a/DashAI/back/converters/category/feature_selection.py b/DashAI/back/converters/category/feature_selection.py index 9b7171433..f090ae69a 100644 --- a/DashAI/back/converters/category/feature_selection.py +++ b/DashAI/back/converters/category/feature_selection.py @@ -1,8 +1,12 @@ -from typing import Final +from typing import TYPE_CHECKING, Final, Union from DashAI.back.converters.base_converter import BaseConverter from DashAI.back.core.utils import MultilingualString from DashAI.back.static.icons import Icon +from DashAI.back.types.dashai_data_type import DashAIDataType + +if TYPE_CHECKING: + from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset class FeatureSelectionConverter(BaseConverter): @@ -15,6 +19,10 @@ class FeatureSelectionConverter(BaseConverter): Use these converters to reduce overfitting, speed up training, and improve model interpretability by retaining only the most informative features. + + These converters only drop columns; the retained columns keep their + original values untouched, so their data types must be preserved instead of + being coerced to float. """ CATEGORY = MultilingualString( @@ -26,3 +34,57 @@ class FeatureSelectionConverter(BaseConverter): ) ICON: Final[str] = Icon.FilterList.value COLOR: Final[str] = "rgb(255, 206, 86)" + + def fit( + self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None + ) -> "FeatureSelectionConverter": + """Fit the selector while remembering the input column types. + + Feature selection only keeps a subset of the input columns without + modifying their values, so the original types are captured here to be + returned later by ``get_output_type``. Types are recorded during ``fit`` + (rather than ``transform``) because scikit-learn auto-wraps ``transform`` + on subclasses and would coerce its output back to a pandas DataFrame. + + Parameters + ---------- + x : DashAIDataset + The input dataset to fit the selector on. + y : DashAIDataset, optional + Target values for the supervised selectors. Defaults to None. + + Returns + ------- + FeatureSelectionConverter + The fitted selector instance (self). + """ + if hasattr(x, "types") and x.types is not None: + self._input_types = dict(x.types) + return super().fit(x, y) + + def get_output_type(self, column_name: str = None) -> DashAIDataType: + """Return the original DashAI data type of a retained column. + + Since feature selection leaves the retained columns' values unchanged, + the output type matches the input type of that column. + + Parameters + ---------- + column_name : str, optional + The name of the retained column. Defaults to None. + + Returns + ------- + DashAIDataType + The original type of the column. Falls back to ``float64`` when the + input type is unknown (feature selectors only operate on numbers). + """ + input_types = getattr(self, "_input_types", None) + if input_types is not None and column_name in input_types: + return input_types[column_name] + + import pyarrow as pa + + from DashAI.back.types.value_types import Float + + return Float(arrow_type=pa.float64()) diff --git a/DashAI/back/converters/scikit_learn/generic_univariate_select.py b/DashAI/back/converters/scikit_learn/generic_univariate_select.py index 7799d344e..4ce7f2324 100644 --- a/DashAI/back/converters/scikit_learn/generic_univariate_select.py +++ b/DashAI/back/converters/scikit_learn/generic_univariate_select.py @@ -14,7 +14,6 @@ ) from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -83,21 +82,3 @@ class GenericUnivariateSelect( ) IMAGE_PREVIEW = "generic_univariate_select.png" metadata = {"allowed_types": [Float, Integer], "allowed_dtypes": []} - - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) diff --git a/DashAI/back/converters/scikit_learn/select_fdr.py b/DashAI/back/converters/scikit_learn/select_fdr.py index a37762be2..0dc935952 100644 --- a/DashAI/back/converters/scikit_learn/select_fdr.py +++ b/DashAI/back/converters/scikit_learn/select_fdr.py @@ -5,7 +5,6 @@ from DashAI.back.core.schema_fields import float_field, schema_field from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -104,21 +103,3 @@ def __init__(self, **kwargs): schema fields. Forwarded to the underlying scikit-learn class. """ super().__init__(**kwargs) - - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) diff --git a/DashAI/back/converters/scikit_learn/select_fpr.py b/DashAI/back/converters/scikit_learn/select_fpr.py index 84d966955..8097b8d52 100644 --- a/DashAI/back/converters/scikit_learn/select_fpr.py +++ b/DashAI/back/converters/scikit_learn/select_fpr.py @@ -5,7 +5,6 @@ from DashAI.back.core.schema_fields import float_field, schema_field from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -95,21 +94,3 @@ def __init__(self, **kwargs): schema fields. Forwarded to the underlying scikit-learn class. """ super().__init__(**kwargs) - - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) diff --git a/DashAI/back/converters/scikit_learn/select_fwe.py b/DashAI/back/converters/scikit_learn/select_fwe.py index 5f187b516..4efd800eb 100644 --- a/DashAI/back/converters/scikit_learn/select_fwe.py +++ b/DashAI/back/converters/scikit_learn/select_fwe.py @@ -5,7 +5,6 @@ from DashAI.back.core.schema_fields import float_field, schema_field from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -96,24 +95,6 @@ class SelectFwe(FeatureSelectionConverter, SklearnWrapper, SelectFweOperation): IMAGE_PREVIEW = "select_fwe.png" metadata = {"allowed_types": [Float, Integer], "allowed_dtypes": []} - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) - def __init__(self, **kwargs): """Initialize the SelectFwe converter. diff --git a/DashAI/back/converters/scikit_learn/select_k_best.py b/DashAI/back/converters/scikit_learn/select_k_best.py index dc78135a8..1bffc79f7 100644 --- a/DashAI/back/converters/scikit_learn/select_k_best.py +++ b/DashAI/back/converters/scikit_learn/select_k_best.py @@ -10,7 +10,6 @@ ) from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -83,24 +82,6 @@ class SelectKBest(FeatureSelectionConverter, SklearnWrapper, SelectKBestOperatio IMAGE_PREVIEW = "select_k_best.png" metadata = {"allowed_types": [Float, Integer], "allowed_dtypes": []} - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) - def __init__(self, **kwargs): """Initialize the SelectKBest converter. diff --git a/DashAI/back/converters/scikit_learn/select_percentile.py b/DashAI/back/converters/scikit_learn/select_percentile.py index c5346129d..7054e237e 100644 --- a/DashAI/back/converters/scikit_learn/select_percentile.py +++ b/DashAI/back/converters/scikit_learn/select_percentile.py @@ -5,7 +5,6 @@ from DashAI.back.core.schema_fields import int_field, schema_field from DashAI.back.core.schema_fields.base_schema import BaseSchema from DashAI.back.core.utils import MultilingualString -from DashAI.back.types.dashai_data_type import DashAIDataType from DashAI.back.types.value_types import Float, Integer @@ -87,24 +86,6 @@ class SelectPercentile( IMAGE_PREVIEW = "select_percentile.png" metadata = {"allowed_types": [Float, Integer], "allowed_dtypes": []} - def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. - - Parameters - ---------- - column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. - - Returns - ------- - DashAIDataType - A Float type backed by ``pyarrow.float64()``. - """ - import pyarrow as pa - - return Float(arrow_type=pa.float64()) - def __init__(self, **kwargs): """Initialize the SelectPercentile converter. diff --git a/DashAI/back/converters/scikit_learn/variance_threshold.py b/DashAI/back/converters/scikit_learn/variance_threshold.py index ca741f530..6ad002e5c 100644 --- a/DashAI/back/converters/scikit_learn/variance_threshold.py +++ b/DashAI/back/converters/scikit_learn/variance_threshold.py @@ -88,20 +88,53 @@ class VarianceThreshold( zh="方差阈值", ) + def fit(self, x, y=None): + """Fit the selector while remembering the input column types. + + VarianceThreshold only drops low-variance columns without modifying the + retained columns' values, so their original types are captured here to + be returned later by ``get_output_type`` instead of coercing to float. + Types are recorded during ``fit`` (rather than ``transform``) because + scikit-learn auto-wraps ``transform`` on subclasses and would coerce its + output back to a pandas DataFrame. + + Parameters + ---------- + x : DashAIDataset + The input dataset to fit the selector on. + y : DashAIDataset, optional + Not used by this unsupervised selector. Defaults to None. + + Returns + ------- + VarianceThreshold + The fitted selector instance (self). + """ + if hasattr(x, "types") and x.types is not None: + self._input_types = dict(x.types) + return super().fit(x, y) + def get_output_type(self, column_name: str = None) -> DashAIDataType: - """Return the DashAI data type produced by this converter for a column. + """Return the original DashAI data type of a retained column. + + Since the selection leaves the retained columns' values unchanged, the + output type matches the input type of that column. Parameters ---------- column_name : str, optional - Not used; all output columns share the - same type. Defaults to None. + The name of the retained column. Defaults to None. Returns ------- DashAIDataType - A Float type backed by ``pyarrow.float64()``. + The original type of the column. Falls back to ``float64`` when the + input type is unknown (the selector only operates on numbers). """ + input_types = getattr(self, "_input_types", None) + if input_types is not None and column_name in input_types: + return input_types[column_name] + import pyarrow as pa return Float(arrow_type=pa.float64()) From 43fa7bf9cb8078d91202ef73244ce09ff7676a6e Mon Sep 17 00:00:00 2001 From: Cristian Tamblay Date: Sun, 28 Jun 2026 21:21:21 -0400 Subject: [PATCH 28/28] Bump to 0.9.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8db3f6f7e..1688e7825 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def load_requirements(filename): setup( name="DashAI", - version="0.9.5", + version="0.9.6", license="MIT", description=( "DashAI: a graphical toolbox for training, evaluating and deploying "