Skip to content

Commit af8ec3c

Browse files
Merge pull request #745 from DashAISoftware/develop
0.9.6
2 parents d8af22f + 9965e96 commit af8ec3c

79 files changed

Lines changed: 897 additions & 589 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/publish.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ jobs:
251251
python -m pip install --upgrade pip
252252
pip install build python-appimage
253253
sudo apt-get update
254-
sudo apt-get install -y libfuse2 imagemagick
254+
sudo apt-get install -y libfuse2 imagemagick librsvg2-bin
255255
- name: Build wheel (frontend bundled)
256256
run: python -m build --wheel
257257
- name: Verify frontend is included in wheel
@@ -262,8 +262,11 @@ jobs:
262262
fi
263263
- name: Prepare AppImage recipe
264264
run: |
265-
# Icon referenced by dashai.desktop (Icon=dashai); take the first frame of the .ico
266-
convert installer/dashAI.ico[0] appimage/dashai.png
265+
# Icon referenced by dashai.desktop (Icon=dashai). Rasterize the
266+
# scalable SVG isotype to a large square PNG so desktops have a
267+
# high-resolution icon (the .ico only carried small frames).
268+
rsvg-convert -h 512 DashAI/front/public/dashai-isotype.svg -o /tmp/dashai-isotype.png
269+
convert /tmp/dashai-isotype.png -background none -gravity center -extent 512x512 appimage/dashai.png
267270
# Append the freshly built wheel (with bundled frontend) to the recipe requirements
268271
WHEEL=$(ls "$PWD"/dist/*.whl | head -n1)
269272
echo "$WHEEL" >> appimage/requirements.txt

DashAI/back/converters/base_converter.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ def get_metadata(cls) -> Dict[str, Any]:
7272
meta["color"] = cls.COLOR if cls.COLOR else "rgb(255, 255, 255)"
7373
meta["supervised"] = cls.SUPERVISED
7474
meta["changes_row_count"] = cls.CHANGES_ROW_COUNT
75+
meta["n_components_features_bounded"] = getattr(
76+
cls, "N_COMPONENTS_FEATURES_BOUNDED", False
77+
)
7578

7679
# Serialize allowed_types class references → class name strings for the frontend
7780
raw_types = meta.get("allowed_types", [])

DashAI/back/converters/category/dimensionality_reduction.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ class DimensionalityReductionConverter(BaseConverter):
2626
)
2727
ICON: Final[str] = Icon.Layers.value
2828
COLOR: Final[str] = "rgb(255, 99, 132)"
29+
N_COMPONENTS_FEATURES_BOUNDED: bool = True

DashAI/back/converters/category/feature_selection.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
from typing import Final
1+
from typing import TYPE_CHECKING, Final, Union
22

33
from DashAI.back.converters.base_converter import BaseConverter
44
from DashAI.back.core.utils import MultilingualString
55
from DashAI.back.static.icons import Icon
6+
from DashAI.back.types.dashai_data_type import DashAIDataType
7+
8+
if TYPE_CHECKING:
9+
from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
610

711

812
class FeatureSelectionConverter(BaseConverter):
@@ -15,6 +19,10 @@ class FeatureSelectionConverter(BaseConverter):
1519
1620
Use these converters to reduce overfitting, speed up training, and improve
1721
model interpretability by retaining only the most informative features.
22+
23+
These converters only drop columns; the retained columns keep their
24+
original values untouched, so their data types must be preserved instead of
25+
being coerced to float.
1826
"""
1927

2028
CATEGORY = MultilingualString(
@@ -26,3 +34,57 @@ class FeatureSelectionConverter(BaseConverter):
2634
)
2735
ICON: Final[str] = Icon.FilterList.value
2836
COLOR: Final[str] = "rgb(255, 206, 86)"
37+
38+
def fit(
39+
self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None
40+
) -> "FeatureSelectionConverter":
41+
"""Fit the selector while remembering the input column types.
42+
43+
Feature selection only keeps a subset of the input columns without
44+
modifying their values, so the original types are captured here to be
45+
returned later by ``get_output_type``. Types are recorded during ``fit``
46+
(rather than ``transform``) because scikit-learn auto-wraps ``transform``
47+
on subclasses and would coerce its output back to a pandas DataFrame.
48+
49+
Parameters
50+
----------
51+
x : DashAIDataset
52+
The input dataset to fit the selector on.
53+
y : DashAIDataset, optional
54+
Target values for the supervised selectors. Defaults to None.
55+
56+
Returns
57+
-------
58+
FeatureSelectionConverter
59+
The fitted selector instance (self).
60+
"""
61+
if hasattr(x, "types") and x.types is not None:
62+
self._input_types = dict(x.types)
63+
return super().fit(x, y)
64+
65+
def get_output_type(self, column_name: str = None) -> DashAIDataType:
66+
"""Return the original DashAI data type of a retained column.
67+
68+
Since feature selection leaves the retained columns' values unchanged,
69+
the output type matches the input type of that column.
70+
71+
Parameters
72+
----------
73+
column_name : str, optional
74+
The name of the retained column. Defaults to None.
75+
76+
Returns
77+
-------
78+
DashAIDataType
79+
The original type of the column. Falls back to ``float64`` when the
80+
input type is unknown (feature selectors only operate on numbers).
81+
"""
82+
input_types = getattr(self, "_input_types", None)
83+
if input_types is not None and column_name in input_types:
84+
return input_types[column_name]
85+
86+
import pyarrow as pa
87+
88+
from DashAI.back.types.value_types import Float
89+
90+
return Float(arrow_type=pa.float64())

DashAI/back/converters/hugging_face/embedding.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,9 @@ def _process_batch(self, batch: "DashAIDataset") -> "DashAIDataset":
247247
pa.array(embeddings_np[:, i].tolist(), type=pa.float32()),
248248
)
249249

250+
# Remove original text columns — they are replaced by their emb_* counterparts
251+
for column in batch.column_names:
252+
col_idx = result_table.column_names.index(column)
253+
result_table = result_table.remove_column(col_idx)
254+
250255
return DashAIDataset(result_table)

DashAI/back/converters/hugging_face/tokenizer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ def _process_batch(self, batch: "DashAIDataset") -> "DashAIDataset":
185185
pa.array(input_ids[:, i].tolist(), type=pa.int64()),
186186
)
187187

188+
# Remove original text columns — they are replaced by their tok_* counterparts
189+
for column in batch.column_names:
190+
col_idx = result_table.column_names.index(column)
191+
result_table = result_table.remove_column(col_idx)
192+
188193
return DashAIDataset(result_table)
189194

190195
def get_output_type(self, column_name: Optional[str] = None) -> DashAIDataType:

DashAI/back/converters/scikit_learn/additive_chi_2_sampler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class AdditiveChi2Sampler(
7878
)
7979
DISPLAY_NAME = MultilingualString(
8080
en="Additive Chi² Sampler",
81-
es="Muestreador Chi²",
81+
es="Muestreador Chi² Aditivo",
8282
pt="Amostrador Qui-2 Aditivo",
8383
de="Additiver Chi²-Stichprobennehmer",
8484
zh="加性卡方采样器",

DashAI/back/converters/scikit_learn/fast_ica.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class FastICA(DimensionalityReductionConverter, SklearnWrapper, FastICAOperation
208208
"""
209209

210210
SCHEMA = FastICASchema
211+
N_COMPONENTS_FEATURES_BOUNDED: bool = False
211212
DESCRIPTION = MultilingualString(
212213
en="FastICA: a fast algorithm for Independent Component Analysis.",
213214
es=(

DashAI/back/converters/scikit_learn/generic_univariate_select.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
)
1515
from DashAI.back.core.schema_fields.base_schema import BaseSchema
1616
from DashAI.back.core.utils import MultilingualString
17-
from DashAI.back.types.dashai_data_type import DashAIDataType
1817
from DashAI.back.types.value_types import Float, Integer
1918

2019

@@ -83,21 +82,3 @@ class GenericUnivariateSelect(
8382
)
8483
IMAGE_PREVIEW = "generic_univariate_select.png"
8584
metadata = {"allowed_types": [Float, Integer], "allowed_dtypes": []}
86-
87-
def get_output_type(self, column_name: str = None) -> DashAIDataType:
88-
"""Return the DashAI data type produced by this converter for a column.
89-
90-
Parameters
91-
----------
92-
column_name : str, optional
93-
Not used; all output columns share the
94-
same type. Defaults to None.
95-
96-
Returns
97-
-------
98-
DashAIDataType
99-
A Float type backed by ``pyarrow.float64()``.
100-
"""
101-
import pyarrow as pa
102-
103-
return Float(arrow_type=pa.float64())

DashAI/back/converters/scikit_learn/missing_indicator.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import TYPE_CHECKING, Union
2+
13
from sklearn.impute import MissingIndicator as MissingIndicatorOperation
24

35
from DashAI.back.converters.category.basic_preprocessing import (
@@ -9,6 +11,11 @@
911
from DashAI.back.types.dashai_data_type import DashAIDataType
1012
from DashAI.back.types.value_types import Integer
1113

14+
if TYPE_CHECKING:
15+
import pandas as pd
16+
17+
from DashAI.back.dataloaders.classes.dashai_dataset import DashAIDataset
18+
1219

1320
class MissingIndicatorSchema(BaseSchema):
1421
"""Schema for configuring the MissingIndicator converter.
@@ -75,7 +82,122 @@ def __init__(self, **kwargs):
7582
Configuration keyword arguments matching the converter's
7683
schema fields. Forwarded to the underlying scikit-learn class.
7784
"""
85+
# Force indicators for all selected features so the user always sees the
86+
# new column, even when a feature has no missing values (all-False indicator).
87+
kwargs.setdefault("features", "all")
7888
super().__init__(**kwargs)
89+
# SklearnWrapper.__init__ sets set_output(transform="pandas"), which causes
90+
# sklearn's __init_subclass__ wrapper to intercept our custom transform and
91+
# attempt to rename its output using get_feature_names_out() (which returns
92+
# only the indicator column count, not the combined output count).
93+
# Reset to "default" so the wrapper returns our DashAIDataset as-is.
94+
if hasattr(self, "set_output"):
95+
self.set_output(transform="default")
96+
97+
@staticmethod
98+
def _normalize_missing(frame: "pd.DataFrame") -> "pd.DataFrame":
99+
"""Return a copy of *frame* where object-column missing values are float NaN.
100+
101+
HuggingFace/PyArrow stores missing strings as ``None`` (Python), but
102+
sklearn's ``_get_mask`` uses ``x != x`` which is ``False`` for ``None``
103+
(only ``float('nan') != float('nan')`` is ``True``). We also treat
104+
empty strings as missing to match the dataset-filter behaviour.
105+
"""
106+
import numpy as np
107+
108+
frame = frame.copy()
109+
for col in frame.select_dtypes(include="object").columns:
110+
frame[col] = frame[col].replace("", np.nan)
111+
frame[col] = frame[col].where(frame[col].notna(), np.nan)
112+
return frame
113+
114+
def fit(
115+
self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None
116+
) -> "MissingIndicator":
117+
"""Fit after normalising missing values so sklearn detects them."""
118+
from DashAI.back.dataloaders.classes.dashai_dataset import to_dashai_dataset
119+
120+
x_pandas = x.to_pandas() if hasattr(x, "to_pandas") else x
121+
x_clean_ds = to_dashai_dataset(self._normalize_missing(x_pandas))
122+
if hasattr(x, "types"):
123+
x_clean_ds.types = x.types.copy()
124+
return super().fit(x_clean_ds, y)
125+
126+
def transform(
127+
self, x: "DashAIDataset", y: Union["DashAIDataset", None] = None
128+
) -> "DashAIDataset":
129+
"""Transform x by appending missing-value indicator columns.
130+
131+
Keeps the original columns intact and appends one boolean indicator
132+
column per feature that had missing values during fit. Indicator
133+
columns are named ``missingindicator_<original_col_name>`` so that
134+
``_rebuild_dataset_with_transformed_columns`` treats them as *new*
135+
columns rather than replacements, preserving the original data.
136+
"""
137+
import numpy as np
138+
import pandas as pd
139+
140+
from DashAI.back.dataloaders.classes.dashai_dataset import to_dashai_dataset
141+
142+
x_pandas = x.to_pandas() if hasattr(x, "to_pandas") else x
143+
144+
# Normalise missing values before sklearn sees the data (None and ""
145+
# are both treated as missing, matching dataset-filter behaviour).
146+
x_for_sklearn = self._normalize_missing(x_pandas)
147+
148+
sklearn_cls = next(
149+
(
150+
cls
151+
for cls in type(self).__mro__
152+
if "sklearn" in cls.__module__
153+
and "DashAI" not in cls.__module__
154+
and "transform" in cls.__dict__
155+
),
156+
None,
157+
)
158+
if sklearn_cls is None:
159+
raise RuntimeError(
160+
"No sklearn class with a 'transform' method found in the MRO."
161+
)
162+
163+
indicators = sklearn_cls.__dict__["transform"](self, x_for_sklearn)
164+
165+
# features_ contains the column indices for which indicators are produced.
166+
# With features='all' (default), this always equals all input column indices.
167+
if hasattr(self, "features_") and len(self.features_) > 0:
168+
indicator_col_names = [
169+
f"missingindicator_{x_pandas.columns[i]}" for i in self.features_
170+
]
171+
else:
172+
indicator_col_names = [
173+
f"missingindicator_{col}" for col in x_pandas.columns
174+
]
175+
176+
if isinstance(indicators, np.ndarray):
177+
indicators_df = pd.DataFrame(
178+
indicators,
179+
columns=indicator_col_names,
180+
index=x_pandas.index,
181+
)
182+
else:
183+
indicators_df = indicators.copy()
184+
indicators_df.columns = indicator_col_names
185+
186+
combined_df = pd.concat([x_pandas, indicators_df], axis=1)
187+
converted_dataset = to_dashai_dataset(combined_df)
188+
189+
output_type = self.get_output_type()
190+
for col in indicator_col_names:
191+
if col in converted_dataset.column_names:
192+
converted_dataset.types[col] = output_type
193+
194+
# Preserve original column types from the input dataset
195+
if hasattr(x, "types"):
196+
for col in x_pandas.columns:
197+
if col in x.types and col in converted_dataset.column_names:
198+
converted_dataset.types[col] = x.types[col]
199+
200+
return converted_dataset
79201

80202
def get_output_type(self, column_name: str = None) -> DashAIDataType:
81203
"""Return the DashAI data type produced by this converter for a column.

0 commit comments

Comments
 (0)