easy_ml 0.1.4 → 0.2.0.pre.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +234 -26
- data/Rakefile +45 -0
- data/app/controllers/easy_ml/application_controller.rb +67 -0
- data/app/controllers/easy_ml/columns_controller.rb +38 -0
- data/app/controllers/easy_ml/datasets_controller.rb +156 -0
- data/app/controllers/easy_ml/datasources_controller.rb +88 -0
- data/app/controllers/easy_ml/deploys_controller.rb +20 -0
- data/app/controllers/easy_ml/models_controller.rb +151 -0
- data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
- data/app/controllers/easy_ml/settings_controller.rb +59 -0
- data/app/frontend/components/AlertProvider.tsx +108 -0
- data/app/frontend/components/DatasetPreview.tsx +161 -0
- data/app/frontend/components/EmptyState.tsx +28 -0
- data/app/frontend/components/ModelCard.tsx +255 -0
- data/app/frontend/components/ModelDetails.tsx +334 -0
- data/app/frontend/components/ModelForm.tsx +384 -0
- data/app/frontend/components/Navigation.tsx +300 -0
- data/app/frontend/components/Pagination.tsx +72 -0
- data/app/frontend/components/Popover.tsx +55 -0
- data/app/frontend/components/PredictionStream.tsx +105 -0
- data/app/frontend/components/ScheduleModal.tsx +726 -0
- data/app/frontend/components/SearchInput.tsx +23 -0
- data/app/frontend/components/SearchableSelect.tsx +132 -0
- data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
- data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
- data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
- data/app/frontend/components/dataset/ColumnList.tsx +101 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
- data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
- data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
- data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
- data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
- data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
- data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
- data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
- data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
- data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
- data/app/frontend/components/dataset/splitters/constants.ts +77 -0
- data/app/frontend/components/dataset/splitters/types.ts +168 -0
- data/app/frontend/components/dataset/splitters/utils.ts +53 -0
- data/app/frontend/components/features/CodeEditor.tsx +46 -0
- data/app/frontend/components/features/DataPreview.tsx +150 -0
- data/app/frontend/components/features/FeatureCard.tsx +88 -0
- data/app/frontend/components/features/FeatureForm.tsx +235 -0
- data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
- data/app/frontend/components/settings/PluginSettings.tsx +81 -0
- data/app/frontend/components/ui/badge.tsx +44 -0
- data/app/frontend/components/ui/collapsible.tsx +9 -0
- data/app/frontend/components/ui/scroll-area.tsx +46 -0
- data/app/frontend/components/ui/separator.tsx +29 -0
- data/app/frontend/entrypoints/App.tsx +40 -0
- data/app/frontend/entrypoints/Application.tsx +24 -0
- data/app/frontend/hooks/useAutosave.ts +61 -0
- data/app/frontend/layouts/Layout.tsx +38 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/mockData.ts +272 -0
- data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
- data/app/frontend/pages/DatasetsPage.tsx +261 -0
- data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
- data/app/frontend/pages/DatasourcesPage.tsx +261 -0
- data/app/frontend/pages/EditModelPage.tsx +45 -0
- data/app/frontend/pages/EditTransformationPage.tsx +56 -0
- data/app/frontend/pages/ModelsPage.tsx +115 -0
- data/app/frontend/pages/NewDatasetPage.tsx +366 -0
- data/app/frontend/pages/NewModelPage.tsx +45 -0
- data/app/frontend/pages/NewTransformationPage.tsx +43 -0
- data/app/frontend/pages/SettingsPage.tsx +272 -0
- data/app/frontend/pages/ShowModelPage.tsx +30 -0
- data/app/frontend/pages/TransformationsPage.tsx +95 -0
- data/app/frontend/styles/application.css +100 -0
- data/app/frontend/types/dataset.ts +146 -0
- data/app/frontend/types/datasource.ts +33 -0
- data/app/frontend/types/preprocessing.ts +1 -0
- data/app/frontend/types.ts +113 -0
- data/app/helpers/easy_ml/application_helper.rb +10 -0
- data/app/jobs/easy_ml/application_job.rb +21 -0
- data/app/jobs/easy_ml/batch_job.rb +46 -0
- data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
- data/app/jobs/easy_ml/deploy_job.rb +13 -0
- data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
- data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
- data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
- data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
- data/app/jobs/easy_ml/training_job.rb +62 -0
- data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
- data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
- data/app/models/easy_ml/cleaner.rb +82 -0
- data/app/models/easy_ml/column.rb +124 -0
- data/app/models/easy_ml/column_history.rb +30 -0
- data/app/models/easy_ml/column_list.rb +122 -0
- data/app/models/easy_ml/concerns/configurable.rb +61 -0
- data/app/models/easy_ml/concerns/versionable.rb +19 -0
- data/app/models/easy_ml/dataset.rb +767 -0
- data/app/models/easy_ml/dataset_history.rb +56 -0
- data/app/models/easy_ml/datasource.rb +182 -0
- data/app/models/easy_ml/datasource_history.rb +24 -0
- data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
- data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
- data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
- data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
- data/app/models/easy_ml/deploy.rb +114 -0
- data/app/models/easy_ml/event.rb +79 -0
- data/app/models/easy_ml/feature.rb +437 -0
- data/app/models/easy_ml/feature_history.rb +38 -0
- data/app/models/easy_ml/model.rb +575 -41
- data/app/models/easy_ml/model_file.rb +133 -0
- data/app/models/easy_ml/model_file_history.rb +24 -0
- data/app/models/easy_ml/model_history.rb +51 -0
- data/app/models/easy_ml/models/base_model.rb +58 -0
- data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
- data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
- data/app/models/easy_ml/models/xgboost.rb +544 -5
- data/app/models/easy_ml/prediction.rb +44 -0
- data/app/models/easy_ml/retraining_job.rb +278 -0
- data/app/models/easy_ml/retraining_run.rb +184 -0
- data/app/models/easy_ml/settings.rb +37 -0
- data/app/models/easy_ml/splitter.rb +90 -0
- data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
- data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
- data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
- data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
- data/app/models/easy_ml/tuner_job.rb +56 -0
- data/app/models/easy_ml/tuner_run.rb +31 -0
- data/app/models/splitter_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +27 -0
- data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
- data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
- data/app/serializers/easy_ml/feature_serializer.rb +27 -0
- data/app/serializers/easy_ml/model_serializer.rb +90 -0
- data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
- data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
- data/app/serializers/easy_ml/settings_serializer.rb +9 -0
- data/app/views/layouts/easy_ml/application.html.erb +15 -0
- data/config/initializers/resque.rb +3 -0
- data/config/resque-pool.yml +6 -0
- data/config/routes.rb +39 -0
- data/config/spring.rb +1 -0
- data/config/vite.json +15 -0
- data/lib/easy_ml/configuration.rb +64 -0
- data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
- data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
- data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
- data/lib/easy_ml/core/model_evaluator.rb +161 -89
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
- data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
- data/lib/easy_ml/core/tuner.rb +123 -62
- data/lib/easy_ml/core.rb +0 -3
- data/lib/easy_ml/core_ext/hash.rb +24 -0
- data/lib/easy_ml/core_ext/pathname.rb +11 -5
- data/lib/easy_ml/data/date_converter.rb +90 -0
- data/lib/easy_ml/data/filter_extensions.rb +31 -0
- data/lib/easy_ml/data/polars_column.rb +126 -0
- data/lib/easy_ml/data/polars_reader.rb +297 -0
- data/lib/easy_ml/data/preprocessor.rb +280 -142
- data/lib/easy_ml/data/simple_imputer.rb +255 -0
- data/lib/easy_ml/data/splits/file_split.rb +252 -0
- data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
- data/lib/easy_ml/data/splits/split.rb +95 -0
- data/lib/easy_ml/data/splits.rb +9 -0
- data/lib/easy_ml/data/statistics_learner.rb +93 -0
- data/lib/easy_ml/data/synced_directory.rb +341 -0
- data/lib/easy_ml/data.rb +6 -2
- data/lib/easy_ml/engine.rb +105 -6
- data/lib/easy_ml/feature_store.rb +227 -0
- data/lib/easy_ml/features.rb +61 -0
- data/lib/easy_ml/initializers/inflections.rb +17 -3
- data/lib/easy_ml/logging.rb +2 -2
- data/lib/easy_ml/predict.rb +74 -0
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
- data/lib/easy_ml/support/est.rb +5 -1
- data/lib/easy_ml/support/file_rotate.rb +79 -15
- data/lib/easy_ml/support/file_support.rb +9 -0
- data/lib/easy_ml/support/local_file.rb +24 -0
- data/lib/easy_ml/support/lockable.rb +62 -0
- data/lib/easy_ml/support/synced_file.rb +103 -0
- data/lib/easy_ml/support/utc.rb +5 -1
- data/lib/easy_ml/support.rb +6 -3
- data/lib/easy_ml/version.rb +4 -1
- data/lib/easy_ml.rb +7 -2
- metadata +355 -72
- data/app/models/easy_ml/models.rb +0 -5
- data/lib/easy_ml/core/model.rb +0 -30
- data/lib/easy_ml/core/model_core.rb +0 -181
- data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
- data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
- data/lib/easy_ml/core/models/xgboost.rb +0 -10
- data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
- data/lib/easy_ml/core/models.rb +0 -10
- data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
- data/lib/easy_ml/core/uploaders.rb +0 -7
- data/lib/easy_ml/data/dataloader.rb +0 -6
- data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
- data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
- data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
- data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
- data/lib/easy_ml/data/dataset/splits.rb +0 -11
- data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
- data/lib/easy_ml/data/dataset/splitters.rb +0 -9
- data/lib/easy_ml/data/dataset.rb +0 -430
- data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
- data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
- data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
- data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
- data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
- data/lib/easy_ml/data/datasource.rb +0 -33
- data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
- data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
- data/lib/easy_ml/deployment.rb +0 -5
- data/lib/easy_ml/support/synced_directory.rb +0 -134
- data/lib/easy_ml/transforms.rb +0 -29
- /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,256 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { Filter, Database, Wrench, Eye, EyeOff, AlertTriangle, ChevronLeft, ChevronRight } from 'lucide-react';
|
3
|
+
import type { Column } from '../../types';
|
4
|
+
|
5
|
+
const ITEMS_PER_PAGE = 5;
|
6
|
+
interface ColumnFiltersProps {
|
7
|
+
types: string[];
|
8
|
+
activeFilters: {
|
9
|
+
view: 'all' | 'training' | 'hidden' | 'preprocessed' | 'nulls';
|
10
|
+
types: string[];
|
11
|
+
};
|
12
|
+
onFilterChange: (filters: {
|
13
|
+
view: 'all' | 'training' | 'hidden' | 'preprocessed' | 'nulls';
|
14
|
+
types: string[];
|
15
|
+
}) => void;
|
16
|
+
columnStats: {
|
17
|
+
total: number;
|
18
|
+
filtered: number;
|
19
|
+
training: number;
|
20
|
+
hidden: number;
|
21
|
+
withPreprocessing: number;
|
22
|
+
withNulls: number;
|
23
|
+
};
|
24
|
+
colHasPreprocessingSteps: (col: Column) => boolean;
|
25
|
+
columns: Column[];
|
26
|
+
}
|
27
|
+
|
28
|
+
export function ColumnFilters({
|
29
|
+
types,
|
30
|
+
activeFilters,
|
31
|
+
onFilterChange,
|
32
|
+
columnStats,
|
33
|
+
colHasPreprocessingSteps,
|
34
|
+
columns
|
35
|
+
}: ColumnFiltersProps) {
|
36
|
+
const getViewStats = (view: typeof activeFilters.view) => {
|
37
|
+
switch (view) {
|
38
|
+
case 'training':
|
39
|
+
return `${columnStats.training} columns`;
|
40
|
+
case 'hidden':
|
41
|
+
return `${columnStats.hidden} columns`;
|
42
|
+
case 'preprocessed':
|
43
|
+
return `${columnStats.withPreprocessing} columns`;
|
44
|
+
case 'nulls':
|
45
|
+
return `${columnStats.withNulls} columns`;
|
46
|
+
default:
|
47
|
+
return `${columnStats.total} columns`;
|
48
|
+
}
|
49
|
+
};
|
50
|
+
|
51
|
+
const calculateNullPercentage = (column: Column) => {
|
52
|
+
if (!column.statistics?.processed?.null_count || !column.statistics?.processed?.num_rows) return 0;
|
53
|
+
return (column.statistics.processed.null_count / column.statistics.processed.num_rows) * 100;
|
54
|
+
};
|
55
|
+
|
56
|
+
const columnsWithNulls = columns
|
57
|
+
.filter(col => col.statistics?.processed.null_count && col.statistics.processed.null_count > 0)
|
58
|
+
.sort((a, b) => calculateNullPercentage(b) - calculateNullPercentage(a));
|
59
|
+
|
60
|
+
const [currentPage, setCurrentPage] = useState(1);
|
61
|
+
const totalPages = Math.ceil(columnsWithNulls.length / ITEMS_PER_PAGE);
|
62
|
+
const paginatedColumns = columnsWithNulls.slice(
|
63
|
+
(currentPage - 1) * ITEMS_PER_PAGE,
|
64
|
+
currentPage * ITEMS_PER_PAGE
|
65
|
+
);
|
66
|
+
|
67
|
+
const toggleType = (type: string) => {
|
68
|
+
onFilterChange({
|
69
|
+
...activeFilters,
|
70
|
+
types: activeFilters.types.includes(type)
|
71
|
+
? activeFilters.types.filter(t => t !== type)
|
72
|
+
: [...activeFilters.types, type]
|
73
|
+
});
|
74
|
+
};
|
75
|
+
|
76
|
+
return (
|
77
|
+
<div className="p-4 border-b space-y-4">
|
78
|
+
<div className="flex items-center justify-between">
|
79
|
+
<h3 className="text-sm font-medium text-gray-900 flex items-center gap-2">
|
80
|
+
<Filter className="w-4 h-4" />
|
81
|
+
Column Views
|
82
|
+
</h3>
|
83
|
+
<div className="text-sm text-gray-500">
|
84
|
+
Showing {columnStats.filtered} of {columnStats.total} columns
|
85
|
+
</div>
|
86
|
+
</div>
|
87
|
+
|
88
|
+
<div className="space-y-4">
|
89
|
+
{/* View Selector */}
|
90
|
+
<div className="flex flex-wrap gap-2">
|
91
|
+
<button
|
92
|
+
onClick={() => onFilterChange({ ...activeFilters, view: 'all' })}
|
93
|
+
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium ${
|
94
|
+
activeFilters.view === 'all'
|
95
|
+
? 'bg-gray-100 text-gray-900'
|
96
|
+
: 'text-gray-600 hover:bg-gray-50'
|
97
|
+
}`}
|
98
|
+
>
|
99
|
+
<Database className="w-4 h-4" />
|
100
|
+
All
|
101
|
+
<span className="text-xs text-gray-500 ml-1">
|
102
|
+
({getViewStats('all')})
|
103
|
+
</span>
|
104
|
+
</button>
|
105
|
+
<button
|
106
|
+
onClick={() => onFilterChange({ ...activeFilters, view: 'training' })}
|
107
|
+
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium ${
|
108
|
+
activeFilters.view === 'training'
|
109
|
+
? 'bg-green-100 text-green-900'
|
110
|
+
: 'text-gray-600 hover:bg-gray-50'
|
111
|
+
}`}
|
112
|
+
>
|
113
|
+
<Eye className="w-4 h-4" />
|
114
|
+
Training
|
115
|
+
<span className="text-xs text-gray-500 ml-1">
|
116
|
+
({getViewStats('training')})
|
117
|
+
</span>
|
118
|
+
</button>
|
119
|
+
<button
|
120
|
+
onClick={() => onFilterChange({ ...activeFilters, view: 'hidden' })}
|
121
|
+
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium ${
|
122
|
+
activeFilters.view === 'hidden'
|
123
|
+
? 'bg-gray-100 text-gray-900'
|
124
|
+
: 'text-gray-600 hover:bg-gray-50'
|
125
|
+
}`}
|
126
|
+
>
|
127
|
+
<EyeOff className="w-4 h-4" />
|
128
|
+
Hidden
|
129
|
+
<span className="text-xs text-gray-500 ml-1">
|
130
|
+
({getViewStats('hidden')})
|
131
|
+
</span>
|
132
|
+
</button>
|
133
|
+
<button
|
134
|
+
onClick={() => onFilterChange({ ...activeFilters, view: 'preprocessed' })}
|
135
|
+
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium ${
|
136
|
+
activeFilters.view === 'preprocessed'
|
137
|
+
? 'bg-blue-100 text-blue-900'
|
138
|
+
: 'text-gray-600 hover:bg-gray-50'
|
139
|
+
}`}
|
140
|
+
>
|
141
|
+
<Wrench className="w-4 h-4" />
|
142
|
+
Preprocessed
|
143
|
+
<span className="text-xs text-gray-500 ml-1">
|
144
|
+
({getViewStats('preprocessed')})
|
145
|
+
</span>
|
146
|
+
</button>
|
147
|
+
<button
|
148
|
+
onClick={() => onFilterChange({ ...activeFilters, view: 'nulls' })}
|
149
|
+
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-md text-sm font-medium ${
|
150
|
+
activeFilters.view === 'nulls'
|
151
|
+
? 'bg-yellow-100 text-yellow-900'
|
152
|
+
: 'text-gray-600 hover:bg-gray-50'
|
153
|
+
}`}
|
154
|
+
>
|
155
|
+
<AlertTriangle className="w-4 h-4" />
|
156
|
+
Has Nulls
|
157
|
+
<span className="text-xs text-gray-500 ml-1">
|
158
|
+
({getViewStats('nulls')})
|
159
|
+
</span>
|
160
|
+
</button>
|
161
|
+
</div>
|
162
|
+
|
163
|
+
{/* Column Types */}
|
164
|
+
<div>
|
165
|
+
<label className="text-xs font-medium text-gray-700 mb-2 block">
|
166
|
+
Column Types
|
167
|
+
</label>
|
168
|
+
<div className="flex flex-wrap gap-2">
|
169
|
+
{types.map(type => (
|
170
|
+
<button
|
171
|
+
key={type}
|
172
|
+
onClick={() => toggleType(type)}
|
173
|
+
className={`px-2 py-1 rounded-md text-xs font-medium ${
|
174
|
+
activeFilters.types.includes(type)
|
175
|
+
? 'bg-blue-100 text-blue-700'
|
176
|
+
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
177
|
+
}`}
|
178
|
+
>
|
179
|
+
{type}
|
180
|
+
</button>
|
181
|
+
))}
|
182
|
+
</div>
|
183
|
+
</div>
|
184
|
+
|
185
|
+
{activeFilters.view === 'preprocessed' && columnStats.withPreprocessing > 0 && (
|
186
|
+
<div className="bg-blue-50 rounded-lg p-3">
|
187
|
+
<h4 className="text-sm font-medium text-blue-900 mb-2">Preprocessing Overview</h4>
|
188
|
+
<div className="space-y-2">
|
189
|
+
{columns
|
190
|
+
.filter(colHasPreprocessingSteps)
|
191
|
+
.map(col => (
|
192
|
+
<div key={col.name} className="flex items-center justify-between text-sm">
|
193
|
+
<span className="text-blue-800">{col.name}</span>
|
194
|
+
<span className="text-blue-600">
|
195
|
+
{col.preprocessing_steps?.training.method}
|
196
|
+
</span>
|
197
|
+
</div>
|
198
|
+
))}
|
199
|
+
</div>
|
200
|
+
</div>
|
201
|
+
)}
|
202
|
+
|
203
|
+
{activeFilters.view === 'nulls' && columnsWithNulls.length > 0 && (
|
204
|
+
<div className="bg-yellow-50 rounded-lg p-3">
|
205
|
+
<div className="flex items-center justify-between mb-3">
|
206
|
+
<h4 className="text-sm font-medium text-yellow-900">Null Value Distribution</h4>
|
207
|
+
<div className="flex items-center gap-2 text-sm text-yellow-700">
|
208
|
+
<button
|
209
|
+
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
210
|
+
disabled={currentPage === 1}
|
211
|
+
className="p-1 rounded hover:bg-yellow-100 disabled:opacity-50"
|
212
|
+
>
|
213
|
+
<ChevronLeft className="w-4 h-4" />
|
214
|
+
</button>
|
215
|
+
<span>
|
216
|
+
Page {currentPage} of {totalPages}
|
217
|
+
</span>
|
218
|
+
<button
|
219
|
+
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
220
|
+
disabled={currentPage === totalPages}
|
221
|
+
className="p-1 rounded hover:bg-yellow-100 disabled:opacity-50"
|
222
|
+
>
|
223
|
+
<ChevronRight className="w-4 h-4" />
|
224
|
+
</button>
|
225
|
+
</div>
|
226
|
+
</div>
|
227
|
+
<div className="space-y-2">
|
228
|
+
{paginatedColumns.map(col => (
|
229
|
+
<div key={col.name} className="flex items-center gap-2">
|
230
|
+
<span className="text-yellow-800 text-sm min-w-[120px]">{col.name}</span>
|
231
|
+
<div className="flex-1 h-2 bg-yellow-100 rounded-full overflow-hidden">
|
232
|
+
<div
|
233
|
+
className="h-full bg-yellow-400 rounded-full"
|
234
|
+
style={{ width: `${calculateNullPercentage(col)}%` }}
|
235
|
+
/>
|
236
|
+
</div>
|
237
|
+
<div className="flex items-center gap-2">
|
238
|
+
<span className="text-yellow-800 text-xs">
|
239
|
+
{calculateNullPercentage(col).toFixed(1)}% null
|
240
|
+
</span>
|
241
|
+
<span className="text-yellow-600 text-xs">
|
242
|
+
({col.statistics?.nullCount?.toLocaleString()} / {col.statistics?.rowCount?.toLocaleString()})
|
243
|
+
</span>
|
244
|
+
</div>
|
245
|
+
</div>
|
246
|
+
))}
|
247
|
+
</div>
|
248
|
+
<div className="mt-3 text-sm text-yellow-700">
|
249
|
+
{columnsWithNulls.length} columns contain null values
|
250
|
+
</div>
|
251
|
+
</div>
|
252
|
+
)}
|
253
|
+
</div>
|
254
|
+
</div>
|
255
|
+
);
|
256
|
+
}
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Settings2, AlertCircle, Target, EyeOff, Eye } from 'lucide-react';
|
3
|
+
import type { Column } from '../../types';
|
4
|
+
import { usePage } from "@inertiajs/react";
|
5
|
+
|
6
|
+
interface ColumnListProps {
|
7
|
+
columns: Column[];
|
8
|
+
selectedColumn: string | null;
|
9
|
+
onColumnSelect: (columnName: string) => void;
|
10
|
+
onToggleHidden: (columnName: string) => void;
|
11
|
+
}
|
12
|
+
|
13
|
+
export function ColumnList({
|
14
|
+
columns,
|
15
|
+
selectedColumn,
|
16
|
+
onColumnSelect,
|
17
|
+
onToggleHidden
|
18
|
+
}: ColumnListProps) {
|
19
|
+
const { rootPath } = usePage().props;
|
20
|
+
|
21
|
+
return (
|
22
|
+
<div className="space-y-2 pb-2">
|
23
|
+
{columns.map(column => (
|
24
|
+
<div
|
25
|
+
key={column.name}
|
26
|
+
className={`p-3 rounded-lg border ${
|
27
|
+
selectedColumn === column.name
|
28
|
+
? 'border-blue-500 bg-blue-50'
|
29
|
+
: column.is_target
|
30
|
+
? 'border-purple-500 bg-purple-50'
|
31
|
+
: column.hidden
|
32
|
+
? 'border-gray-200 bg-gray-50'
|
33
|
+
: 'border-gray-200 hover:border-gray-300'
|
34
|
+
} transition-colors duration-150`}
|
35
|
+
>
|
36
|
+
<div className="flex items-center justify-between mb-2">
|
37
|
+
<div className="flex items-center gap-2">
|
38
|
+
{column.is_target && (
|
39
|
+
<Target className="w-4 h-4 text-purple-500" />
|
40
|
+
)}
|
41
|
+
<span className={`font-medium ${column.hidden ? 'text-gray-500' : 'text-gray-900'}`}>
|
42
|
+
{column.name}
|
43
|
+
</span>
|
44
|
+
<span className="text-xs px-2 py-0.5 bg-gray-100 text-gray-600 rounded-full">
|
45
|
+
{column.datatype}
|
46
|
+
</span>
|
47
|
+
</div>
|
48
|
+
<div className="flex items-center gap-2">
|
49
|
+
{!column.is_target && (
|
50
|
+
<button
|
51
|
+
onClick={() => onToggleHidden(column.name)}
|
52
|
+
className={`p-1 rounded hover:bg-gray-100 ${
|
53
|
+
column.hidden
|
54
|
+
? 'text-gray-500'
|
55
|
+
: 'text-gray-400 hover:text-gray-600'
|
56
|
+
}`}
|
57
|
+
title={column.hidden ? 'Show column' : 'Hide column'}
|
58
|
+
>
|
59
|
+
{column.hidden ? (
|
60
|
+
<EyeOff className="w-4 h-4" />
|
61
|
+
) : (
|
62
|
+
<Eye className="w-4 h-4" />
|
63
|
+
)}
|
64
|
+
</button>
|
65
|
+
)}
|
66
|
+
<button
|
67
|
+
onClick={() => onColumnSelect(column.name)}
|
68
|
+
className="p-1 rounded text-gray-400 hover:text-gray-600 hover:bg-gray-100"
|
69
|
+
title="Configure preprocessing"
|
70
|
+
>
|
71
|
+
<Settings2 className="w-4 h-4" />
|
72
|
+
</button>
|
73
|
+
</div>
|
74
|
+
</div>
|
75
|
+
<div className="text-sm text-gray-500">
|
76
|
+
{column.description && (
|
77
|
+
<p className={`mb-1 line-clamp-1 ${column.drop_if_null ? 'text-gray-400' : ''}`}>
|
78
|
+
{column.description}
|
79
|
+
</p>
|
80
|
+
)}
|
81
|
+
<div className="flex flex-wrap gap-2">
|
82
|
+
{column.preprocessing_steps && column.preprocessing_steps?.training &&
|
83
|
+
column.preprocessing_steps?.training?.method !== 'none' && (
|
84
|
+
<div className="flex items-center gap-1 text-blue-600">
|
85
|
+
<AlertCircle className="w-3 h-3" />
|
86
|
+
<span className="text-xs">Preprocessing configured</span>
|
87
|
+
</div>
|
88
|
+
)}
|
89
|
+
{column.hidden && (
|
90
|
+
<div className="flex items-center gap-1 text-gray-400">
|
91
|
+
<EyeOff className="w-3 h-3" />
|
92
|
+
<span className="text-xs">Hidden from training</span>
|
93
|
+
</div>
|
94
|
+
)}
|
95
|
+
</div>
|
96
|
+
</div>
|
97
|
+
</div>
|
98
|
+
))}
|
99
|
+
</div>
|
100
|
+
);
|
101
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import React from "react";
|
2
|
+
import { Settings2 } from "lucide-react";
|
3
|
+
import { Popover } from "../Popover";
|
4
|
+
|
5
|
+
export function FeatureConfigPopover() {
|
6
|
+
return (
|
7
|
+
<Popover
|
8
|
+
trigger={
|
9
|
+
<button
|
10
|
+
type="button"
|
11
|
+
className="p-2 text-gray-400 hover:text-gray-600"
|
12
|
+
title="Configure features"
|
13
|
+
>
|
14
|
+
<Settings2 className="w-5 h-5" />
|
15
|
+
</button>
|
16
|
+
}
|
17
|
+
className="w-96"
|
18
|
+
>
|
19
|
+
<div className="space-y-4">
|
20
|
+
<p className="text-sm text-gray-600">
|
21
|
+
Feature options can be configured in the codebase, and loaded in
|
22
|
+
initializers:
|
23
|
+
</p>
|
24
|
+
|
25
|
+
<div className="bg-gray-50 p-3 rounded-md">
|
26
|
+
<code className="text-sm text-gray-800">
|
27
|
+
config/initializers/features.rb
|
28
|
+
</code>
|
29
|
+
</div>
|
30
|
+
|
31
|
+
<p className="text-sm text-gray-600">Example feature implementation:</p>
|
32
|
+
|
33
|
+
<pre className="bg-gray-50 p-3 rounded-md overflow-x-auto">
|
34
|
+
<code className="text-xs text-gray-800">
|
35
|
+
{`# lib/features/did_convert.rb
|
36
|
+
module Features
|
37
|
+
class DidConvert
|
38
|
+
include EasyML::Features
|
39
|
+
|
40
|
+
def did_convert(df)
|
41
|
+
df.with_column(
|
42
|
+
(Polars.col("rev") > 0)
|
43
|
+
.alias("did_convert")
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
feature :did_convert,
|
48
|
+
name: "Did Convert",
|
49
|
+
description: "Boolean true/false..."
|
50
|
+
end
|
51
|
+
end`}
|
52
|
+
</code>
|
53
|
+
</pre>
|
54
|
+
</div>
|
55
|
+
</Popover>
|
56
|
+
);
|
57
|
+
}
|
@@ -0,0 +1,205 @@
|
|
1
|
+
import React, { useState } from "react";
|
2
|
+
import {
|
3
|
+
GripVertical,
|
4
|
+
X,
|
5
|
+
Plus,
|
6
|
+
ArrowDown,
|
7
|
+
ArrowUp,
|
8
|
+
Settings2,
|
9
|
+
} from "lucide-react";
|
10
|
+
import { SearchableSelect } from "../SearchableSelect";
|
11
|
+
import { FeatureConfigPopover } from "./FeatureConfigPopover";
|
12
|
+
import { Feature } from "../../types/dataset";
|
13
|
+
|
14
|
+
interface FeaturePickerProps {
|
15
|
+
options: Feature[];
|
16
|
+
initialFeatures?: Feature[];
|
17
|
+
onFeaturesChange: (features: Feature[]) => void;
|
18
|
+
}
|
19
|
+
|
20
|
+
export function FeaturePicker({
|
21
|
+
options,
|
22
|
+
initialFeatures = [],
|
23
|
+
onFeaturesChange,
|
24
|
+
}: FeaturePickerProps) {
|
25
|
+
const [selectedFeatures, setSelectedFeatures] =
|
26
|
+
useState<Feature[]>(initialFeatures);
|
27
|
+
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
28
|
+
|
29
|
+
console.log(selectedFeatures);
|
30
|
+
const availableFeatures = options.filter(
|
31
|
+
(feature) => !selectedFeatures.find((t) => t.name === feature.name)
|
32
|
+
);
|
33
|
+
|
34
|
+
const updateFeatures = (newFeatures: Feature[]) => {
|
35
|
+
const featuresWithPosition = newFeatures.map((feature, index) => ({
|
36
|
+
...feature,
|
37
|
+
feature_position: index,
|
38
|
+
}));
|
39
|
+
|
40
|
+
setSelectedFeatures(featuresWithPosition);
|
41
|
+
onFeaturesChange(featuresWithPosition);
|
42
|
+
};
|
43
|
+
|
44
|
+
const handleAddFeature = (transformName: string) => {
|
45
|
+
const feature = options.find((t) => t.name === transformName);
|
46
|
+
if (feature) {
|
47
|
+
const newFeature = {
|
48
|
+
...feature,
|
49
|
+
feature_position: selectedFeatures.length,
|
50
|
+
};
|
51
|
+
updateFeatures([...selectedFeatures, newFeature]);
|
52
|
+
}
|
53
|
+
};
|
54
|
+
|
55
|
+
const handleRemove = (index: number) => {
|
56
|
+
const newFeatures = [...selectedFeatures];
|
57
|
+
newFeatures.splice(index, 1);
|
58
|
+
updateFeatures(newFeatures);
|
59
|
+
};
|
60
|
+
|
61
|
+
const handleMoveUp = (index: number) => {
|
62
|
+
if (index === 0) return;
|
63
|
+
const newFeatures = [...selectedFeatures];
|
64
|
+
[newFeatures[index - 1], newFeatures[index]] = [
|
65
|
+
newFeatures[index],
|
66
|
+
newFeatures[index - 1],
|
67
|
+
];
|
68
|
+
updateFeatures(newFeatures);
|
69
|
+
};
|
70
|
+
|
71
|
+
const handleMoveDown = (index: number) => {
|
72
|
+
if (index === selectedFeatures.length - 1) return;
|
73
|
+
const newFeatures = [...selectedFeatures];
|
74
|
+
[newFeatures[index], newFeatures[index + 1]] = [
|
75
|
+
newFeatures[index + 1],
|
76
|
+
newFeatures[index],
|
77
|
+
];
|
78
|
+
updateFeatures(newFeatures);
|
79
|
+
};
|
80
|
+
|
81
|
+
const handleDragStart = (e: React.DragEvent, index: number) => {
|
82
|
+
setDraggedIndex(index);
|
83
|
+
};
|
84
|
+
|
85
|
+
const handleDragOver = (e: React.DragEvent, index: number) => {
|
86
|
+
e.preventDefault();
|
87
|
+
if (draggedIndex === null || draggedIndex === index) return;
|
88
|
+
|
89
|
+
const newFeatures = [...selectedFeatures];
|
90
|
+
const [draggedFeature] = newFeatures.splice(draggedIndex, 1);
|
91
|
+
newFeatures.splice(index, 0, draggedFeature);
|
92
|
+
updateFeatures(newFeatures);
|
93
|
+
setDraggedIndex(index);
|
94
|
+
};
|
95
|
+
|
96
|
+
const handleDragEnd = () => {
|
97
|
+
setDraggedIndex(null);
|
98
|
+
};
|
99
|
+
|
100
|
+
return (
|
101
|
+
<div className="space-y-4">
|
102
|
+
{/* Add Feature */}
|
103
|
+
<div className="flex items-center gap-4">
|
104
|
+
<div className="flex-1">
|
105
|
+
<SearchableSelect
|
106
|
+
options={availableFeatures.map((feature) => ({
|
107
|
+
value: feature.name,
|
108
|
+
label: feature.name,
|
109
|
+
description: feature.description,
|
110
|
+
}))}
|
111
|
+
value=""
|
112
|
+
onChange={(value) => handleAddFeature(value as string)}
|
113
|
+
placeholder="Add a transform..."
|
114
|
+
/>
|
115
|
+
</div>
|
116
|
+
<FeatureConfigPopover />
|
117
|
+
</div>
|
118
|
+
|
119
|
+
{/* Selected Features */}
|
120
|
+
<div className="space-y-2">
|
121
|
+
{selectedFeatures.map((feature, index) => (
|
122
|
+
<div
|
123
|
+
key={feature.name}
|
124
|
+
draggable
|
125
|
+
onDragStart={(e) => handleDragStart(e, index)}
|
126
|
+
onDragOver={(e) => handleDragOver(e, index)}
|
127
|
+
onDragEnd={handleDragEnd}
|
128
|
+
className={`flex items-center gap-3 p-3 bg-white border rounded-lg ${
|
129
|
+
draggedIndex === index
|
130
|
+
? "border-blue-500 shadow-lg"
|
131
|
+
: "border-gray-200"
|
132
|
+
} ${draggedIndex !== null ? "cursor-grabbing" : ""}`}
|
133
|
+
>
|
134
|
+
<button
|
135
|
+
type="button"
|
136
|
+
className="p-1 text-gray-400 hover:text-gray-600 cursor-grab active:cursor-grabbing"
|
137
|
+
>
|
138
|
+
<GripVertical className="w-4 h-4" />
|
139
|
+
</button>
|
140
|
+
|
141
|
+
<div className="flex-1 min-w-0">
|
142
|
+
<div className="flex items-center gap-2">
|
143
|
+
<span className="font-medium text-gray-900">
|
144
|
+
{feature.name}
|
145
|
+
</span>
|
146
|
+
<span
|
147
|
+
className={`text-xs px-2 py-0.5 rounded-full ${
|
148
|
+
feature.feature_type === "calculation"
|
149
|
+
? "bg-blue-100 text-blue-800"
|
150
|
+
: feature.feature_type === "lookup"
|
151
|
+
? "bg-purple-100 text-purple-800"
|
152
|
+
: "bg-green-100 text-green-800"
|
153
|
+
}`}
|
154
|
+
>
|
155
|
+
{"feature"}
|
156
|
+
</span>
|
157
|
+
</div>
|
158
|
+
<p className="text-sm text-gray-500 truncate">
|
159
|
+
{feature.description}
|
160
|
+
</p>
|
161
|
+
</div>
|
162
|
+
|
163
|
+
<div className="flex items-center gap-1">
|
164
|
+
<button
|
165
|
+
type="button"
|
166
|
+
onClick={() => handleMoveUp(index)}
|
167
|
+
disabled={index === 0}
|
168
|
+
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
169
|
+
title="Move up"
|
170
|
+
>
|
171
|
+
<ArrowUp className="w-4 h-4" />
|
172
|
+
</button>
|
173
|
+
<button
|
174
|
+
type="button"
|
175
|
+
onClick={() => handleMoveDown(index)}
|
176
|
+
disabled={index === selectedFeatures.length - 1}
|
177
|
+
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-50"
|
178
|
+
title="Move down"
|
179
|
+
>
|
180
|
+
<ArrowDown className="w-4 h-4" />
|
181
|
+
</button>
|
182
|
+
<button
|
183
|
+
type="button"
|
184
|
+
onClick={() => handleRemove(index)}
|
185
|
+
className="p-1 text-gray-400 hover:text-red-600"
|
186
|
+
title="Remove transform"
|
187
|
+
>
|
188
|
+
<X className="w-4 h-4" />
|
189
|
+
</button>
|
190
|
+
</div>
|
191
|
+
</div>
|
192
|
+
))}
|
193
|
+
|
194
|
+
{selectedFeatures.length === 0 && (
|
195
|
+
<div className="text-center py-8 bg-gray-50 border-2 border-dashed border-gray-200 rounded-lg">
|
196
|
+
<Plus className="w-8 h-8 text-gray-400 mx-auto mb-2" />
|
197
|
+
<p className="text-sm text-gray-500">
|
198
|
+
Add features to enrich your dataset
|
199
|
+
</p>
|
200
|
+
</div>
|
201
|
+
)}
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
);
|
205
|
+
}
|