easy_ml 0.1.3 → 0.2.0.pre.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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 -4
- 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
|
+
}
|