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.
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 -5
  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
+ }