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.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -4
  119. data/app/models/easy_ml/prediction.rb +44 -0
  120. data/app/models/easy_ml/retraining_job.rb +278 -0
  121. data/app/models/easy_ml/retraining_run.rb +184 -0
  122. data/app/models/easy_ml/settings.rb +37 -0
  123. data/app/models/easy_ml/splitter.rb +90 -0
  124. data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
  125. data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
  126. data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
  127. data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
  128. data/app/models/easy_ml/tuner_job.rb +56 -0
  129. data/app/models/easy_ml/tuner_run.rb +31 -0
  130. data/app/models/splitter_history.rb +6 -0
  131. data/app/serializers/easy_ml/column_serializer.rb +27 -0
  132. data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
  133. data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
  134. data/app/serializers/easy_ml/feature_serializer.rb +27 -0
  135. data/app/serializers/easy_ml/model_serializer.rb +90 -0
  136. data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
  137. data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
  138. data/app/serializers/easy_ml/settings_serializer.rb +9 -0
  139. data/app/views/layouts/easy_ml/application.html.erb +15 -0
  140. data/config/initializers/resque.rb +3 -0
  141. data/config/resque-pool.yml +6 -0
  142. data/config/routes.rb +39 -0
  143. data/config/spring.rb +1 -0
  144. data/config/vite.json +15 -0
  145. data/lib/easy_ml/configuration.rb +64 -0
  146. data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
  147. data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
  148. data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
  149. data/lib/easy_ml/core/model_evaluator.rb +161 -89
  150. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
  151. data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
  152. data/lib/easy_ml/core/tuner.rb +123 -62
  153. data/lib/easy_ml/core.rb +0 -3
  154. data/lib/easy_ml/core_ext/hash.rb +24 -0
  155. data/lib/easy_ml/core_ext/pathname.rb +11 -5
  156. data/lib/easy_ml/data/date_converter.rb +90 -0
  157. data/lib/easy_ml/data/filter_extensions.rb +31 -0
  158. data/lib/easy_ml/data/polars_column.rb +126 -0
  159. data/lib/easy_ml/data/polars_reader.rb +297 -0
  160. data/lib/easy_ml/data/preprocessor.rb +280 -142
  161. data/lib/easy_ml/data/simple_imputer.rb +255 -0
  162. data/lib/easy_ml/data/splits/file_split.rb +252 -0
  163. data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
  164. data/lib/easy_ml/data/splits/split.rb +95 -0
  165. data/lib/easy_ml/data/splits.rb +9 -0
  166. data/lib/easy_ml/data/statistics_learner.rb +93 -0
  167. data/lib/easy_ml/data/synced_directory.rb +341 -0
  168. data/lib/easy_ml/data.rb +6 -2
  169. data/lib/easy_ml/engine.rb +105 -6
  170. data/lib/easy_ml/feature_store.rb +227 -0
  171. data/lib/easy_ml/features.rb +61 -0
  172. data/lib/easy_ml/initializers/inflections.rb +17 -3
  173. data/lib/easy_ml/logging.rb +2 -2
  174. data/lib/easy_ml/predict.rb +74 -0
  175. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
  176. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
  177. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
  178. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
  179. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
  180. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
  181. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
  182. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
  183. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
  184. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
  185. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
  186. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
  187. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
  188. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
  189. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
  190. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
  191. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
  192. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
  193. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
  194. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
  195. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
  196. data/lib/easy_ml/support/est.rb +5 -1
  197. data/lib/easy_ml/support/file_rotate.rb +79 -15
  198. data/lib/easy_ml/support/file_support.rb +9 -0
  199. data/lib/easy_ml/support/local_file.rb +24 -0
  200. data/lib/easy_ml/support/lockable.rb +62 -0
  201. data/lib/easy_ml/support/synced_file.rb +103 -0
  202. data/lib/easy_ml/support/utc.rb +5 -1
  203. data/lib/easy_ml/support.rb +6 -3
  204. data/lib/easy_ml/version.rb +4 -1
  205. data/lib/easy_ml.rb +7 -2
  206. metadata +355 -72
  207. data/app/models/easy_ml/models.rb +0 -5
  208. data/lib/easy_ml/core/model.rb +0 -30
  209. data/lib/easy_ml/core/model_core.rb +0 -181
  210. data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
  211. data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
  212. data/lib/easy_ml/core/models/xgboost.rb +0 -10
  213. data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
  214. data/lib/easy_ml/core/models.rb +0 -10
  215. data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
  216. data/lib/easy_ml/core/uploaders.rb +0 -7
  217. data/lib/easy_ml/data/dataloader.rb +0 -6
  218. data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
  219. data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
  220. data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
  221. data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
  222. data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
  223. data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
  224. data/lib/easy_ml/data/dataset/splits.rb +0 -11
  225. data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
  226. data/lib/easy_ml/data/dataset/splitters.rb +0 -9
  227. data/lib/easy_ml/data/dataset.rb +0 -430
  228. data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
  229. data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
  230. data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
  231. data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
  232. data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
  233. data/lib/easy_ml/data/datasource.rb +0 -33
  234. data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
  235. data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
  236. data/lib/easy_ml/deployment.rb +0 -5
  237. data/lib/easy_ml/support/synced_directory.rb +0 -134
  238. data/lib/easy_ml/transforms.rb +0 -29
  239. /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
+ }