lecrapaud 2.3.0__tar.gz → 2.3.2__tar.gz

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 (83) hide show
  1. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/PKG-INFO +1 -1
  2. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/feature_selection.py +24 -5
  3. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/model_selection.py +61 -15
  4. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/experiment.py +1 -1
  5. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/feature_selection.py +15 -1
  6. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/pyproject.toml +2 -1
  7. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/LICENSE +0 -0
  8. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/README.md +0 -0
  9. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/__init__.py +0 -0
  10. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/base.py +0 -0
  11. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/business_objective.py +0 -0
  12. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/column_suggester.py +0 -0
  13. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/config.py +0 -0
  14. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/context_search_space.py +0 -0
  15. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/__init__.py +0 -0
  16. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/README +0 -0
  17. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/env.py +0 -0
  18. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/script.py.mako +0 -0
  19. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_06_23_1748-f089dfb7e3ba_.py +0 -0
  20. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_06_24_1216-c62251b129ed_.py +0 -0
  21. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_06_24_1711-86457e2f333f_.py +0 -0
  22. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_06_25_1759-72aa496ca65b_.py +0 -0
  23. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_08_25_1434-7ed9963e732f_add_best_score_to_model_selection.py +0 -0
  24. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_08_28_1516-c36e9fee22b9_add_avg_precision_to_score.py +0 -0
  25. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_08_28_1622-8b11c1ba982e_change_name_column.py +0 -0
  26. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_10_25_0635-07e303521594_add_unique_constraint_to_score.py +0 -0
  27. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_10_26_1727-033e0f7eca4f_merge_score_and_model_trainings_into_.py +0 -0
  28. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_10_28_2006-0a8fb7826e9b_add_number_of_targets_and_remove_other_.py +0 -0
  29. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_12_16_1644-9a191ae1c27d_add_bias_to_score.py +0 -0
  30. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2025_12_20_1915-99108bd42b68_add_score_to_experiment.py +0 -0
  31. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_21_1814-a1b2c3d4e5f6_add_artifact_storage.py +0 -0
  32. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_21_1830-b2c3d4e5f6g7_migrate_files_to_db.py +0 -0
  33. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_26_1942-f723ecc52b89_rename_model_to_model_type.py +0 -0
  34. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_26_2120-080475ab231a_rename_model_selection_to_best_model.py +0 -0
  35. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_28_1427-dc9b28c3e796_fix_wrong_model_type_for_classification.py +0 -0
  36. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_28_1616-721327520692_remove_best_prefix_from_best_model.py +0 -0
  37. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_01_28_1721-0e47cb122c58_fix_method_names.py +0 -0
  38. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_03_31_1600-a1b2c3d4e5f7_add_state_to_experiments.py +0 -0
  39. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_05_29_2100-c1d2e3f4a5b6_add_resume_keys_to_experiments.py +0 -0
  40. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_05_29_2200-e3f4a5b6c7d8_add_studies_table.py +0 -0
  41. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_05_29_2210-f4a5b6c7d8e9_add_headline_score_to_models.py +0 -0
  42. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_05_30_1000-c3d4e5f6a7b8_add_production_experiment_to_studies.py +0 -0
  43. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_05_30_1030-d4e5f6a7b8c9_add_experiment_version_and_study_promotions.py +0 -0
  44. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_06_03_1300-p1q2r3s4t5u6_add_phase_step_to_experiments.py +0 -0
  45. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic/versions/2026_06_03_1400-q1r2s3t4u5v6_size_nullable.py +0 -0
  46. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/alembic.ini +0 -0
  47. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/db/session.py +0 -0
  48. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/directories.py +0 -0
  49. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/experiment.py +0 -0
  50. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/feature_engineering.py +0 -0
  51. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/feature_preprocessing.py +0 -0
  52. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/integrations/ollama_integration.py +0 -0
  53. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/integrations/openai_integration.py +0 -0
  54. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/integrations/sentry_integration.py +0 -0
  55. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/metrics.py +0 -0
  56. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/misc/tabpfn_tests.ipynb +0 -0
  57. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/misc/test-gpu-bilstm.ipynb +0 -0
  58. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/misc/test-gpu-resnet.ipynb +0 -0
  59. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/misc/test-gpu-transformers.ipynb +0 -0
  60. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/mixins.py +0 -0
  61. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/model.py +0 -0
  62. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/model_preprocessing.py +0 -0
  63. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/__init__.py +0 -0
  64. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/base.py +0 -0
  65. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/best_model.py +0 -0
  66. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/experiment_artifact.py +0 -0
  67. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/experiment_data.py +0 -0
  68. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/feature.py +0 -0
  69. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/feature_selection_rank.py +0 -0
  70. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/model.py +0 -0
  71. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/model_type.py +0 -0
  72. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/study.py +0 -0
  73. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/study_promotion.py +0 -0
  74. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/target.py +0 -0
  75. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/models/utils.py +0 -0
  76. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/pipeline.py +0 -0
  77. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/search_space.py +0 -0
  78. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/services/__init__.py +0 -0
  79. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/services/artifact_service.py +0 -0
  80. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/services/embedding_service.py +0 -0
  81. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/tabular_dl.py +0 -0
  82. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/torch_models.py +0 -0
  83. {lecrapaud-2.3.0 → lecrapaud-2.3.2}/lecrapaud/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lecrapaud
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Framework for machine and deep learning, with regression, classification and time series analysis
5
5
  License: Apache License
6
6
  License-File: LICENSE
@@ -1634,11 +1634,30 @@ class FeatureSelector(LeCrapaudEstimatorMixin):
1634
1634
  # utils
1635
1635
  # TODO : can we use this to select the ideal number of features ?
1636
1636
  def feature_selection_analysis(feature_selection_id: int, n_components: int = 5):
1637
-
1638
- feature_selection = FeatureSelection.get(feature_selection_id)
1639
- experiment_dir = feature_selection.experiment.path
1640
- features = [f.name for f in feature_selection.features]
1641
- target = feature_selection.target.name
1637
+ # `FS.features` is `lazy="select"` (changed from selectin to drop
1638
+ # the cascading explosion on API endpoints). Wrap the load + the
1639
+ # M2M materialisation inside one session so the access at line
1640
+ # below doesn't hit DetachedInstanceError. Same goes for
1641
+ # `.experiment.path` and `.target.name` — selectin'd at parent
1642
+ # level, so post-session access is fine for those.
1643
+ from lecrapaud.db.session import get_db
1644
+ from sqlalchemy.orm import selectinload as _selectin
1645
+
1646
+ with get_db() as _db:
1647
+ feature_selection = (
1648
+ _db.query(FeatureSelection)
1649
+ .options(_selectin(FeatureSelection.features))
1650
+ .filter(FeatureSelection.id == feature_selection_id)
1651
+ .first()
1652
+ )
1653
+ if feature_selection is None:
1654
+ raise ValueError(f"FeatureSelection {feature_selection_id} not found")
1655
+ # Materialise everything we need from the session BEFORE
1656
+ # the context manager exits (post-exit attribute access is
1657
+ # OK for selectin-loaded relationships).
1658
+ features = [f.name for f in feature_selection.features]
1659
+ experiment_dir = feature_selection.experiment.path
1660
+ target = feature_selection.target.name
1642
1661
  target_number = target.split("_")[1]
1643
1662
 
1644
1663
  train, val, train_scaled, val_scaled, _scaler_y = load_train_data(
@@ -119,6 +119,44 @@ def test_hardware():
119
119
  warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
120
120
 
121
121
 
122
+ def _safe_concatenate(data_list, axis=0):
123
+ """
124
+ Safely concatenate data regardless of whether it's numpy arrays or pandas objects.
125
+
126
+ This function detects the data type and uses the appropriate concatenation method:
127
+ - np.concatenate for numpy arrays
128
+ - pd.concat for pandas Series/DataFrames
129
+
130
+ Args:
131
+ data_list: List of data objects to concatenate
132
+ axis: Axis along which to concatenate
133
+
134
+ Returns:
135
+ Concatenated data using the appropriate method
136
+ """
137
+ if not data_list:
138
+ raise ValueError("Cannot concatenate empty list")
139
+
140
+ # Check the type of the first non-None element
141
+ first_data = next((d for d in data_list if d is not None), None)
142
+ if first_data is None:
143
+ raise ValueError("All data elements are None")
144
+
145
+ # Determine the data type and use appropriate concatenation method
146
+ if isinstance(first_data, np.ndarray):
147
+ # Use numpy concatenation for numpy arrays
148
+ return np.concatenate(data_list, axis=axis)
149
+ elif isinstance(first_data, (pd.DataFrame, pd.Series)):
150
+ # Use pandas concatenation for pandas objects
151
+ return pd.concat(data_list, axis=axis)
152
+ else:
153
+ # For other types, try pandas first, fallback to numpy
154
+ try:
155
+ return pd.concat(data_list, axis=axis)
156
+ except (TypeError, ValueError):
157
+ return np.concatenate(data_list, axis=axis)
158
+
159
+
122
160
  def trainable_cv(
123
161
  params,
124
162
  x_train,
@@ -152,13 +190,13 @@ def trainable_cv(
152
190
  in `LeCrapaudModel.fit_boosting._sort_for_ranking`.
153
191
  """
154
192
  # Combine train and validation data for cross-validation
155
- if recurrent:
156
- x_train_val = np.concatenate([x_train, x_val], axis=0)
157
- y_train_val = np.concatenate([y_train, y_val], axis=0)
158
- else:
159
- x_train_val = pd.concat([x_train, x_val], axis=0)
160
- y_train_val = pd.concat([y_train, y_val], axis=0)
161
- # Store original index for later use if needed
193
+ # Use safe concatenation to handle mixed data types properly
194
+ x_train_val = _safe_concatenate([x_train, x_val], axis=0)
195
+ y_train_val = _safe_concatenate([y_train, y_val], axis=0)
196
+
197
+ # Store original index for later use if needed (only for pandas objects)
198
+ original_index = None
199
+ if hasattr(x_train_val, 'index'):
162
200
  original_index = x_train_val.index.copy()
163
201
  # Reset index for proper iloc indexing with CV splits
164
202
  x_train_val = x_train_val.reset_index(drop=True)
@@ -557,6 +595,19 @@ class ModelSelector(LeCrapaudEstimatorMixin):
557
595
 
558
596
  if self.target_number is not None:
559
597
  self.features = self.experiment.get_features(self.target_number)
598
+ # Feature selection can legitimately end up keeping nothing
599
+ # (over-strict thresholds, too few samples, all-constant
600
+ # columns). Training on a (N, 0) matrix raises the opaque
601
+ # sklearn "Found array with 0 feature(s)" deep in the fit
602
+ # call — fail fast here with an actionable message instead.
603
+ if not self.features:
604
+ raise ValueError(
605
+ f"Feature selection produced 0 features for "
606
+ f"TARGET_{self.target_number} (experiment "
607
+ f"{self.experiment_id}). Cannot train on an empty "
608
+ f"feature matrix — check the input data or relax the "
609
+ f"feature-selection thresholds."
610
+ )
560
611
 
561
612
  self.all_features = self.experiment.get_all_features(
562
613
  date_column=self.date_column, group_column=self.group_column
@@ -865,14 +916,9 @@ class ModelSelector(LeCrapaudEstimatorMixin):
865
916
 
866
917
  # Always evaluate on test set (no cross-validation here)
867
918
  # The hyperopt already did CV if needed to find best params.
868
- # Recurrent models carry 3D numpy arrays (no pandas index), so
869
- # concatenate with numpy — pd.concat rejects ndarrays (pandas 3).
870
- if model.recurrent:
871
- x_train_full = np.concatenate([x_train, x_val], axis=0)
872
- y_train_full = np.concatenate([y_train, y_val], axis=0)
873
- else:
874
- x_train_full = pd.concat([x_train, x_val], axis=0)
875
- y_train_full = pd.concat([y_train, y_val], axis=0)
919
+ # Use safe concatenation to handle mixed data types properly
920
+ x_train_full = _safe_concatenate([x_train, x_val], axis=0)
921
+ y_train_full = _safe_concatenate([y_train, y_val], axis=0)
876
922
  # Combine the per-row ranking groups the same way for the final
877
923
  # fit (train+val together, test on its own). `None` when not ranking.
878
924
  train_full_groups = None
@@ -444,7 +444,7 @@ class Experiment(Base):
444
444
  own_contribs = self._headline_contributions()
445
445
  if not own_contribs:
446
446
  if self.state == self.STATE_COMPLETED:
447
- logger.error(
447
+ logger.warning(
448
448
  f"Completed experiment {self.id} has no usable scores "
449
449
  f"(no headline / rmse / logloss on any Best Model)"
450
450
  )
@@ -97,7 +97,21 @@ class FeatureSelection(Base):
97
97
  "Feature",
98
98
  secondary=lecrapaud_feature_selection_association,
99
99
  back_populates="feature_selections",
100
- lazy="selectin",
100
+ # `lazy="select"` (default) instead of `selectin`. The M2M
101
+ # association explodes the query cost when an Experiment is
102
+ # loaded (via the cascading selectin from
103
+ # Experiment.feature_selections → each FS → features here).
104
+ # For a 12-target / ~200-feature run, this is ~2400 Feature
105
+ # rows + the join table — pure cost for any API endpoint
106
+ # that doesn't render features.
107
+ # Trade-off: accessing `fs.features` AFTER the session closes
108
+ # raises DetachedInstanceError. All internal lecrapaud
109
+ # accesses already happen inside `@with_db` (get_features,
110
+ # get_all_features); the one utility that didn't is now
111
+ # wrapped explicitly. External consumers that need the M2M
112
+ # outside a session should pass `selectinload(FS.features)`
113
+ # in their query.
114
+ lazy="select",
101
115
  )
102
116
 
103
117
  __table_args__ = (
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lecrapaud"
3
- version = "2.3.0"
3
+ version = "2.3.2"
4
4
  description = "Framework for machine and deep learning, with regression, classification and time series analysis"
5
5
  authors = [
6
6
  {name = "Pierre H. Gallet"}
@@ -94,4 +94,5 @@ requires = ["poetry-core"]
94
94
  [tool.pytest.ini_options]
95
95
  markers = [
96
96
  "order: reorder test execution",
97
+ "slow: trains real models end-to-end; deselect for a fast loop with -m 'not slow'",
97
98
  ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes