Metadata-Version: 2.4
Name: RationalML
Version: 1.1.0
Summary: Optuna model comparison for prepared classification and regression data with optional leakage-safe preprocessing
Author: Arthur D.
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/arthur-diaz/RationalML
Project-URL: Source, https://github.com/arthur-diaz/RationalML
Keywords: automl,model-comparison,optuna,classification,regression,tabular
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy<3,>=1.24
Requires-Dist: pandas<4,>=2
Requires-Dist: scikit-learn<2,>=1.5
Requires-Dist: optuna<6,>=4
Requires-Dist: threadpoolctl<4,>=3.5
Provides-Extra: boosting
Requires-Dist: lightgbm<5,>=4; extra == "boosting"
Requires-Dist: xgboost<4,>=2; extra == "boosting"
Provides-Extra: legacy
Requires-Dist: tqdm>=4; extra == "legacy"
Provides-Extra: excel
Requires-Dist: openpyxl<4,>=3.0.10; extra == "excel"
Provides-Extra: mlflow
Requires-Dist: mlflow<4,>=3.1; extra == "mlflow"
Provides-Extra: shap
Requires-Dist: shap<0.50,>=0.49.1; extra == "shap"
Provides-Extra: test
Requires-Dist: pytest<10,>=8; extra == "test"
Dynamic: license-file

# RationalML — V1.0.0

RationalML compare et optimise des modèles de classification binaire,
multiclass et de régression sur les
variables choisies par le Data Scientist. **RationalML n'est pas un outil de
feature engineering automatique. Il suppose par défaut que le Data Scientist
fournit un DataFrame prêt pour la modélisation.**

Le Data Scientist garde la main sur le nettoyage, le choix des variables,
le feature engineering et les transformations métier. RationalML prend en
charge le split train/test, la cross-validation, Optuna, la comparaison des
modèles, les métriques et les résultats.

**License: Apache-2.0.** RationalML is distributed under the Apache License 2.0.

| `preprocessing` | Contrat |
| --- | --- |
| `None` **(défaut)** | Contrôle total du Data Scientist : aucune transformation des features |
| `"basic"` | Commodité minimale : imputation, OneHotEncoding et scaling selon le modèle |
| Transformer sklearn | Preprocessing utilisateur, cloné et ajusté à l'intérieur de chaque fold CV |

## Démarrer en une minute

Python 3.10 ou plus. Pour travailler depuis le dépôt :

```sh
python -m pip install -e .
python examples/quickstart.py
```

Le cœur requiert numpy, pandas, scikit-learn, Optuna et threadpoolctl. LightGBM
et XGBoost sont optionnels via l'extra `boosting`. `models="auto"` sélectionne
les modèles enregistrés dont les dépendances sont installées ; une dépendance
explicitement demandée mais absente produit `MissingDependencyError`.
L'extra `legacy` fournit tqdm pour `local_optimizer`. Les modèles sont filtrés
par tâche : les régresseurs ne sont jamais proposés à une classification.

```python
import pandas as pd
from sklearn.datasets import make_classification
from rationalml import AutoML

# Données locales, sans téléchargement ; features déjà prêtes pour le modèle.
X, y = make_classification(
    n_samples=80, n_features=4, n_informative=3,
    n_redundant=0, random_state=7,
)
df_model = pd.DataFrame(X, columns=[f"feature_{i}" for i in range(4)])
df_model["target"] = y
result = AutoML(
    target="target",
    positive_class=1,
    preprocessing=None,  # défaut
    models=["logistic_regression"],  # modèle core, aucun extra requis
    metric="roc_auc",
    cv=2, n_trials=1, random_state=42, verbose=0,
).fit(df_model)

print(result.leaderboard)
print(result.test_metrics)
new_df_model = df_model.drop(columns="target").head()
predictions = result.predict(new_df_model)
probabilities = result.predict_proba(new_df_model)
p_positive = result.predict_positive_proba(new_df_model)
```

Puis, sans réentraîner ni rappeler le modèle :

```python
result.ranking_table()
result.calibration_summary()
```

Le core ne demande aucun des cinq extras d'exécution. Installer uniquement
ceux nécessaires : `pip install -e ".[boosting]"`, `".[excel]"`, `".[mlflow]"`
ou `".[shap]"`. Les exemples de ces fonctions sont présentés séparément plus bas.
La version publiée pourra s'installer avec `pip install RationalML` **après
publication** ; cette préparation n'effectue aucune publication.

Les autres tâches utilisent également un DataFrame préparé, sans preprocessing
par défaut :

```python
multiclass_result = AutoML(
    target="segment", task="multiclass",
).fit(df_multiclass)
segments = multiclass_result.predict(new_df_multiclass)
segment_probabilities = multiclass_result.predict_proba(new_df_multiclass)
# Colonne j = P(segment == multiclass_result.classes_[j]).

regression_result = AutoML(
    target="revenue", task="regression",
).fit(df_regression)
revenues = regression_result.predict(new_df_regression)
```

| Tâche | `metric="auto"` (défaut) | CV |
| --- | --- | --- |
| `binary` | `roc_auc` | `StratifiedKFold` |
| `multiclass` | `f1_macro` | `StratifiedKFold` |
| `regression` | `rmse` | `KFold` |

Les deux splitters utilisent `shuffle=True` et `random_state`. Le split
train/test est stratifié seulement pour la classification.
Avec `task="auto"`, exactement deux valeurs donnent binary ; plus de deux
labels non numériques donnent multiclass. **Numeric targets with more than
two values require an explicit task.** L'ambiguïté produit
`UnsupportedTaskError`, sans heuristique de cardinalité. Une cible à une seule
classe exige notamment `task="regression"` si c'est une mesure constante.

| Tâche | Modèles | Métriques disponibles |
| --- | --- | --- |
| binary | `logistic_regression`, `lightgbm`, `xgboost` | roc_auc, average_precision, accuracy, balanced_accuracy, precision, recall, f1, log_loss |
| multiclass | `logistic_regression`, `lightgbm`, `xgboost` | accuracy, balanced_accuracy, f1_macro, f1_weighted, log_loss |
| regression | `ridge`, `lightgbm_regressor`, `xgboost_regressor` | rmse, mae, r2 |

LightGBM/XGBoost restent optionnels. `log_loss`, `rmse` et `mae` sont minimisées ;
les autres métriques sont maximisées. RMSE utilise
[`root_mean_squared_error`](https://scikit-learn.org/1.5/modules/generated/sklearn.metrics.root_mean_squared_error.html),
disponible dans les versions sklearn supportées. Une métrique incompatible
échoue avant Optuna. `result.primary_metric` et `result.config.metric` exposent
le nom effectif ; `result.config.task` expose la tâche résolue.

Avec `None`, les features doivent être numériques ou booléennes, sans NaN
ni infini. RationalML n'impute, n'encode, ne scale et ne supprime aucune
colonne, même pour LogisticRegression. Des catégories ou NaN produisent
`DataValidationError`, avec les trois choix : préparer les données avant
RationalML, utiliser `"basic"` ou fournir un transformer sklearn.

## Option de convenance : basic

Le mode `"basic"` est **« basic preprocessing, not feature engineering »**.
Il reprend uniquement les transformations de V0.2.0, sur demande explicite.
Par exemple, `df_raw` peut contenir age, income, country, is_customer et churn,
avec des catégories et des valeurs manquantes :

```python
basic_result = AutoML(
    target="churn", positive_class=1, preprocessing="basic",
    models="auto", metric="roc_auc",
).fit(df_raw)

basic_result.predict(new_df_raw)
basic_result.predict_positive_proba(new_df_raw)
```

| Type brut | Transformation basic |
| --- | --- |
| Entiers et flottants, y compris dtypes nullable | `SimpleImputer(strategy="median")`, puis scaling selon le modèle |
| `bool` / `boolean` nullable | Représentation 0/1 et imputation par le mode, sans scaling |
| `object`, `category`, `string` | Normalisation des manquants, imputation par le mode et `OneHotEncoder(handle_unknown="ignore", sparse_output=False)` |
| Datetime, datetime avec timezone, timedelta | `DataValidationError` explicite |

En mode basic, `ModelSpec.requires_scaling=True` active `StandardScaler` pour
LogisticRegression et Ridge ; LightGBM/XGBoost déclarent False et ne sont pas scalés.
Ce champ n'a aucun effet avec `None` ou un transformer utilisateur.

`FeatureSchema` est inféré uniquement sur les lignes d'entraînement du fold,
puis sur le train complet du pipeline final. Les constantes sont conservées
avec `UserWarning`. Une colonne entièrement manquante dans un train, y
compris un fold, produit `DataValidationError` : aucun fold n'emprunte de
statistique aux autres lignes. Les infinis et structures imbriquées sont
rejetés ; aucune ligne ou colonne n'est supprimée silencieusement.

Les catégories doivent être des scalaires homogènes : chaînes ou nombres,
sans mélange des deux familles. Les catégories pandas non observées dans
le train ne sont pas apprises depuis les métadonnées du dtype. Une catégorie
inconnue à l'inférence devient un bloc OneHot de zéros. Aucun `LabelEncoder`
n'est appliqué aux features.

`PreprocessingConfig` reste un réglage avancé facultatif du mode basic,
avec les mêmes cinq options qu'en V0.2.0, sans ajout :

```python
from rationalml import AutoMLConfig, PreprocessingConfig

config = AutoMLConfig(
    target="churn", preprocessing=PreprocessingConfig(scale_numeric=False),
)
```

Ses défauts sont median / most_frequent / onehot / scale_numeric="auto" /
handle_unknown="ignore". Le numérique accepte aussi mean et most_frequent,
le scaling accepte True/False, et handle_unknown accepte "error".

## Option expert : transformer sklearn utilisateur

Le Data Scientist fournit un transformer compatible sklearn, avec `fit`,
`transform`, `get_params`, et clonable par `sklearn.base.clone`.
Un `Pipeline`, un `ColumnTransformer` ou un transformer personnalisé convient.
RationalML n'ajoute aucune transformation autour de cet objet.

```python
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

# Choix explicites du Data Scientist. country contient des chaînes ou None.
my_column_transformer = ColumnTransformer([
    ("numeric", Pipeline([
        ("imputer", SimpleImputer(strategy="median")),
        ("scaler", StandardScaler()),
    ]), ["age", "income"]),
    ("country", Pipeline([
        ("imputer", SimpleImputer(strategy="most_frequent", missing_values=None)),
        ("encoder", OneHotEncoder(handle_unknown="ignore", sparse_output=False)),
    ]), ["country"]),
])

expert_result = AutoML(
    target="churn", positive_class=1, preprocessing=my_column_transformer,
    models="logistic_regression", metric="roc_auc",
).fit(df_raw)

expert_result.predict(new_df_raw)
expert_result.predict_proba(new_df_raw)
```

Adapter l'imputer aux données fournies : l'exemple utilise None pour les
catégories, plutôt que NaN, défaut sklearn. Le transformer est responsable
de ses dtypes, valeurs et transformations. RationalML vérifie seulement la
structure et les colonnes brutes dans ce mode.

**À chaque fold**, RationalML appelle `clone(user_transformer)`, construit un
Pipeline neuf avec un estimateur neuf, puis ajuste le tout sur le fold train.
Le fold validation est seulement transformé. Pour le fit final, RationalML
clone encore le transformer et l'ajuste sur le train complet, jamais sur le
holdout. L'objet fourni n'est pas ajusté ni modifié par RationalML ; le
clonage sklearn standard ne reprend pas son éventuel état déjà appris.
Un transformer dont le clonage renvoie la même instance est rejeté.
Voir le [contrat de clone](https://scikit-learn.org/1.5/modules/generated/sklearn.base.clone.html).

Les transformations réalisées en amont restent sous la responsabilité du
Data Scientist. Pour couvrir par la CV les transformations apprenant des
statistiques, fournir leur transformer dans `preprocessing`.

## Validation et inférence

`fit` exige un DataFrame avec des noms de colonnes uniques, non vides et de
type chaîne, et une cible sans valeur manquante. Binary exige exactement deux
classes, multiclass au moins trois, et regression une cible numérique finie.
Chaque classe doit conserver au moins `cv` observations dans le train ; toutes
les classes doivent être présentes dans le test. Les catégories pandas non
observées de la cible ne comptent pas comme classes. En régression, au moins
deux lignes par fold validation et dans le test sont exigées pour que R2 soit
défini. Aucune cible n'est imputée.

Les noms de modèles demandés doivent être uniques, y compris après normalisation
des espaces et de la casse. Les doublons produisent `ConfigurationError` qui
les nomme, sans déduplication silencieuse. En cas d'égalité exacte de CV,
le premier modèle demandé conserve la priorité. Les options numériques telles
que `cv`, `n_trials`, `random_state` et `n_jobs` refusent les booléens.

Dans les trois modes, un DataFrame d'inférence doit contenir exactement les
mêmes noms de features, chacun une fois. Leur ordre peut changer : RationalML
les réordonne selon `feature_names`. Les colonnes manquantes, supplémentaires
ou dupliquées provoquent `DataValidationError`. L'entrée est copiée avant son
utilisation par le pipeline.

- `None` : données prêtes, numériques/booléennes, sans NaN ni infini ; aucune
  inférence de schéma, recherche de constantes ou transformation automatique.
- `"basic"` : contrôles de types V0.2 ; int/float compatibles, booléens ou 0/1,
  catégories de même famille. Les NaN et catégories inconnues sont acceptés,
  y compris un batch entièrement manquant, grâce au preprocessing ajusté.
- Transformer : données brutes transmises sans les règles de `FeatureSchema`
  basic ; le propre contrat de dtypes du transformer fait foi.

Une matrice numpy de même largeur est reconstruite avec les noms dans l'ordre
d'origine. Le mode basic exige un DataFrame si des catégories sont présentes.
Le DataFrame reste recommandé pour le mode expert et ses sélections par nom.

## Targets et résultats

Le contrat V0.1.1 reste inchangé. `positive_class` explicite est vérifiée
parmi les deux labels puis encodée en 1 ; l'autre classe devient 0.
Sans choix explicite : booléens → True, cible exactement {0, 1} → 1, autres
labels comparables → plus grand label selon l'ordre naturel avec `UserWarning`
précisant le choix et recommandant `positive_class`.
Le choix est déterministe, indépendant des fréquences et de l'ordre des lignes.
Une classe positive absente produit `DataValidationError` ; 0, False et une
chaîne vide restent des choix explicites valides lorsqu'ils sont présents.

`AutoMLResult` expose `positive_class`, `negative_class` et `classes_` dans
l'ordre **[negative_class, positive_class]**. `predict` restitue les labels
originaux. `predict_proba` retourne P(negative_class) en colonne 0 et
P(positive_class) en colonne 1 ; `predict_positive_proba` retourne ce dernier
vecteur 1D. Precision, recall, f1, roc_auc et average_precision utilisent la
classe interne 1, y compris si le label original positif est 0 ou False.

En multiclass, un `MulticlassLabelEncoder` léger encapsule sklearn LabelEncoder,
ajusté uniquement sur le train. Il encode les labels en 0…n_classes-1 dans
l'ordre déterministe de `classes_`, rejette les labels inconnus et restaure
les labels originaux dans `predict`. `predict_proba` renvoie la matrice complète,
colonne j correspondant à `classes_[j]`, également utilisée par log_loss.

En régression, aucun encodeur : y conserve son dtype numérique et `predict`
renvoie les valeurs numériques. `classes_` et `predict_proba` lèvent
`UnsupportedTaskError`. `positive_class`, `negative_class` et
`predict_positive_proba` lèvent cette erreur hors binary. Renseigner
`positive_class` hors binary produit `ConfigurationError` après résolution
de la tâche.

- `task`, `target`, `config`, `primary_metric` : tâche résolue, cible, copie
  validée des options et métrique effective.
- `leaderboard` : scores et diagnostics CV, triés selon la direction de la métrique.
- `cv_results` : modèle, essai, score moyen et scores des folds Optuna.
- `best_model_name`, `best_params` : gagnant CV et paramètres de son estimateur.
- `best_model` : **`sklearn.pipeline.Pipeline`**. Avec None : étape `estimator`
  seule. Avec basic ou transformer : `preprocessing`, puis `estimator`.
  Les attributs natifs sont dans `best_model.named_steps["estimator"]` ;
  ses labels de classification restent internes. Utiliser `AutoMLResult` pour les labels
  originaux et les contrôles de colonnes.
- `metrics`, `test_metrics` : toutes les métriques de la tâche du seul gagnant,
  calculées lors d'une évaluation unique sur le test.
- `feature_names` : noms bruts dans l'ordre d'entraînement.
- `feature_schema` : schéma du train final en mode basic ; None ailleurs.
- `transformed_feature_names` : noms fournis au modèle, ou None si indisponibles.
- `feature_importance` : DataFrame brut, colonnes `feature`, `importance`
  numérique et `source_feature` en binary/régression. Coefficients logistiques
  et Ridge signés, importances natives des arbres, triés par valeur absolue.
  LogisticRegression multiclass ajoute `class` : une ligne par classe et feature,
  sans moyenne ni perte de lignes de coef_. Les arbres multiclass conservent
  leur importance native globale par feature. Aucun Styler.
- `train_indices`, `test_indices` : positions des lignes, même avec index dupliqué.

Avec None, les noms et source_feature sont ceux d'origine. Avec basic,
`ColumnTransformer.get_feature_names_out()` fournit par exemple
`categorical__country_France`. Le mapping source utilise les slices de sortie
et les catégories apprises, sans découper les noms sur des underscores.
Les caractères spéciaux sont échappés pour LightGBM/XGBoost.

Avec un transformer utilisateur, RationalML essaie `get_feature_names_out`
et vérifie la cohérence des noms. Si les noms sont indisponibles ou non fiables,
`transformed_feature_names=None` et `feature_importance` est un DataFrame vide
avec `UserWarning`. Le fit et l'inférence restent utilisables. Quand les noms
sont disponibles, `source_feature` reste manquant : aucune filiation n'est
inventée pour les transformations utilisateur.

## Baseline et stabilité CV

Une baseline naïve est évaluée sur **TRAIN uniquement**, avec la même métrique
principale et exactement les mêmes folds que les modèles :
[`DummyClassifier(strategy="prior")`](https://scikit-learn.org/1.5/modules/generated/sklearn.dummy.DummyClassifier.html)
en binary/multiclass, et
[`DummyRegressor(strategy="mean")`](https://scikit-learn.org/1.5/modules/generated/sklearn.dummy.DummyRegressor.html)
en regression. Elle apprend seulement la cible, sans preprocessing des features
ni Optuna. Elle reste un diagnostic, hors candidats à `best_model`, sans refit
sur le train complet ni métrique calculée sur TEST.

```python
result.baseline_name         # "dummy_classifier" ou "dummy_regressor"
result.baseline_score        # moyenne des scores des folds
result.baseline_cv_std       # np.std(...), ddof=0
result.baseline_fold_scores  # tuple des scores, dans l'ordre des folds
result.leaderboard
# rank, cv_score, cv_std, cv_min, cv_max, improvement_vs_baseline, n_trials_completed
```

`cv_std`, `cv_min` et `cv_max` décrivent les folds du meilleur trial de chaque
modèle, sans score ni seuil arbitraire de stabilité. La convention de l'écart
type reste `ddof=0`. `improvement_vs_baseline` vaut modèle − baseline pour
maximize, baseline − modèle pour minimize : ROC-AUC 0,8 contre 0,5 donne +0,3 ;
RMSE 8 contre 10 donne +2. Une valeur négative indique une performance inférieure
à la référence. Le classement et le gagnant restent fondés uniquement sur
`cv_score`. `n_trials_completed` compte seulement les essais Optuna `COMPLETE`,
utile avec timeout ; FAIL et PRUNED sont exclus. Aucun timing n'est ajouté.

## Business ranking

**All ranking diagnostics are computed on the untouched holdout test set.**
Les analyses lisent les prédictions de l'évaluation initiale, après sélection
et refit du gagnant. Aucun nouvel appel au modèle ; aucun retour vers Optuna,
la baseline ou le preprocessing. `result.test_predictions` retourne une copie
indépendante avec l'index original, y compris ses doublons :

| Tâche | Colonnes de `test_predictions` |
| --- | --- |
| binary | `y_true`, `y_pred` en labels originaux ; `score` = P(positive_class) |
| multiclass | `y_true`, `y_pred` en labels originaux ; `proba_0`, `proba_1`… ; colonne `proba_j` = P(classes_[j]) |
| regression | `y_true`, `y_pred`, `residual` = y_true − y_pred |

Les noms de probabilités utilisent leur position, sans dépendre du texte des
labels. L'inférence ultérieure ne modifie pas cet artefact.

```python
# Binary : score de la classe positive déjà définie.
ranking = result.ranking_table(n_bins=10)
ranking[["segment", "count", "positive_rate", "cumulative_positive_capture", "lift"]]
top10 = result.top_segment(0.10)
top10.score.mean()

# Multiclass : analyse one-vs-rest explicite, class_label obligatoire.
multiclass_result.ranking_table(class_label="gold", n_bins=10)
multiclass_result.top_segment(0.10, class_label="gold")

# Regression : niveaux de prédiction, sans notion de classe positive ou lift.
regression_result.ranking_table(n_bins=10)
regression_result.top_segment(0.10)
```

Le segment 1 contient les scores/prédictions les plus élevés. Le tri décroissant
est stable : les égalités gardent l'ordre stocké du holdout. Le découpage utilise
la position, avec `effective_bins=min(n_bins, n_rows)` : groupes non vides,
tailles différant d'au plus une ligne, sans qcut. `n_bins` doit être un entier
>= 2, hors bool. Le top contient exactement `ceil(n_rows*fraction)` lignes,
au moins une, sans extension pour les égalités ; `0 < fraction <= 1`.

Binary/one-vs-rest retourne `segment`, `count`, `score_min`, `score_max`,
`score_mean`, `positives`, `positive_rate`, `population_share`, `positive_capture`,
`cumulative_positive_capture`, `lift`, `cumulative_lift`. La capture rapporte les
positifs aux positifs totaux ; le lift rapporte le taux positif au taux global,
et sa version cumulative utilise les lignes depuis le segment 1. Zéro positif
produit une erreur explicite. `class_label` est réservé au multiclass et vérifié
parmi `classes_`.

Regression retourne `segment`, `count`, `pred_min`, `pred_max`, `pred_mean`,
`actual_mean`, `bias`, `mae`, `rmse`. Le biais vaut pred_mean − actual_mean,
donc l'opposé du résidu moyen. Tous les tableaux contiennent des valeurs brutes,
sans Styler, formatage ou seuil interprétatif.

## Probability calibration diagnostics

Ces diagnostics utilisent exclusivement les prédictions initiales du **holdout**
stockées dans le résultat, sans fit, predict, transform, CV ou Optuna. Ils restent
disponibles même sans modèle et ne modifient ni les probabilités ni la sélection
du gagnant. **Ils ne recalibrent pas le modèle.**

```python
calibration = result.calibration_table(n_bins=10)
summary = result.calibration_summary(n_bins=10)

# Multiclass : one-vs-rest, classe choisie obligatoirement parmi classes_.
multiclass_result.calibration_table(class_label="gold")
multiclass_result.calibration_summary(class_label="gold")
```

Binary utilise `score` et `y_true == result.positive_class`, avec les labels
originaux ; `class_label` y est interdit. Multiclass utilise `proba_j`, où
`classes_[j]` est la classe demandée (y compris `class_label=0`). Une classe
absente/inconnue produit `ConfigurationError`. Regression produit
`UnsupportedTaskError` pour les deux méthodes.

Le tri des probabilités est **croissant et stable** : calibration bin 1 contient
les probabilités les plus faibles, tandis que ranking segment 1 contient les
plus élevées. Avec `effective_bins=min(n_bins, n_rows)`, chaque position indexée à partir de zéro
reçoit `floor(position * effective_bins / n_rows) + 1`. Les bins sont non vides,
leurs effectifs diffèrent d'au plus une ligne et les égalités gardent l'ordre du
holdout. `n_bins` est un entier >= 2, hors bool. Les index dupliqués ou composites
ne servent jamais d'identifiant unique. Les probabilités doivent être numériques,
finies et dans [0, 1] ; une valeur invalide produit `DataValidationError`, sans clipping.

Le tableau numérique contient exactement `bin`, `count`, `proba_min`, `proba_max`,
`proba_mean`, `observed_rate`, `calibration_gap`, `absolute_calibration_gap`,
`population_share`. **calibration_gap = observed_rate − proba_mean** : un gap
positif signifie une sous-estimation de la fréquence observée, un gap négatif
une surestimation. Les objets retournés sont indépendants du résultat.

Le dictionnaire de synthèse contient trois floats :

- `brier_score = mean((y_binary - probability) ** 2)`, dans [0, 1], indépendant
  du binning. Plus faible signifie un meilleur score probabiliste. Le Brier
  **n'est pas une mesure pure de calibration** : il dépend aussi de la qualité
  et de la discrimination des probabilités.
- `expected_calibration_error` (ECE) = somme des `population_share * absolute_calibration_gap`.
- `max_calibration_error` (MCE) = maximum des `absolute_calibration_gap`.

ECE et MCE utilisent exactement les bins du tableau demandé et **dépendent du
nombre de bins**, sans être des constantes intrinsèques du modèle. La V0.8
retourne uniquement des valeurs numériques et tables, sans graphique.

## Excel export

L'extra Excel est optionnel ; le cœur s'importe et fonctionne sans openpyxl :

```shell
pip install "rationalml[excel]"
# Depuis ce dépôt : python -m pip install -e ".[excel]"
```

```python
result.to_excel("model_report.xlsx")  # retourne un pathlib.Path

from rationalml import ExcelReportConfig

config = ExcelReportConfig(include_predictions=True, top_fraction=0.10)
result.to_excel("model_report.xlsx", config=config)

# Multiclass : classe métier choisie explicitement.
multiclass_result.to_excel("segment_report.xlsx", class_label="gold")
```

Les feuilles communes sont **Summary**, **Leaderboard**, **Metrics**,
**Feature Importance**, **Hyperparameters**. Binary et regression ajoutent
**Ranking** et **Top Segment**. En multiclass, ces deux feuilles sont présentes
uniquement si `class_label` est fourni ; sans classe, le reste du rapport est
exporté normalement. **Calibration** est ajoutée en binary, et en multiclass
uniquement avec `class_label`, avec le diagnostic canonique à **10 bins**.
Summary ajoute alors les valeurs numériques **Brier score**, **Expected calibration
error**, **Max calibration error**, au format décimal normal ; regression n'ajoute
aucun diagnostic de calibration. Aucune option d'ExcelReportConfig n'est ajoutée.
**Predictions** est créée seulement avec
`include_predictions=True` (False par défaut), à partir du holdout stocké.
Top Segment et Predictions conservent l'index dans leur première colonne,
sans exporter les features d'origine. Les index composites sont représentés
en JSON et les dates avec fuseau en ISO, pour conserver leurs identifiants.

`ExcelReportConfig` est une dataclass frozen : `decimal_places=3`,
`percentage_places=2` (entiers entre 0 et 15), `include_predictions=False`,
`top_fraction=0.10` avec `0 < top_fraction <= 1`. Les nombres restent numériques :
formats `0.000` pour les décimales, `0.00%` pour les taux/captures, `0.00` pour
les lifts et `0` pour les entiers. La locale est gérée par Excel. Le style
reste sobre : en-têtes gras, filtres, première ligne figée et largeurs bornées.

L'export lit uniquement AutoMLResult et appelle ses méthodes ranking/top
existantes ; aucun fit, predict, predict_proba, recalcul des métriques ou
appel Optuna. Une importance indisponible n'empêche pas l'export.
Openpyxl est importé à la demande ; s'il manque, `MissingDependencyError`
indique la commande d'installation. L'extra requiert `openpyxl>=3.0.10,<4` ;
l'écriture directe ne dépend pas des exigences du writer Excel de pandas.

Le chemin doit avoir l'extension `.xlsx` et un parent existant. Les dossiers
ne sont pas créés. La sauvegarde complète dans un temporaire du même dossier
précède le remplacement atomique via `os.replace` ; une erreur de sauvegarde
ou de remplacement laisse l'ancien fichier intact et nettoie le temporaire.
Sous Windows, la destination doit être disponible au remplacement, donc
fermée par les autres lecteurs. Les limites de
1 048 576 lignes **en-tête compris** et 16 384 colonnes sont vérifiées avant
sauvegarde : aucune troncature silencieuse. Les textes trop longs ou les
nombres infinis provoquent aussi une erreur explicite.

## MLflow tracking

MLflow est une couche optionnelle de traçabilité **après fit**, sans connexion
au moteur d'optimisation :

```shell
pip install "rationalml[mlflow]"
# Depuis ce dépôt : python -m pip install -e ".[mlflow]"
```

```python
from rationalml import MLflowConfig

run_id = result.log_mlflow(
    config=MLflowConfig(
        experiment_name="customer-churn",
        run_name="churn-model",
        tracking_uri="http://127.0.0.1:5000",
    )
)

# Multiclass : ranking et calibration pour la classe explicitement choisie.
multiclass_result.log_mlflow(
    config=MLflowConfig(experiment_name="segments", log_model=False),
    class_label="gold",
)
```

Chaque appel crée **un run RationalML et un enfant par modèle du leaderboard**,
jamais un run par trial Optuna. Le parent contient les métadonnées explicites
du fit, la baseline, le meilleur score CV et les métriques test préfixées
`test_`. Chaque enfant conserve ses meilleurs paramètres dans
`result.model_best_params`, ses diagnostics CV et le tag `rationalml.selected`.

Les tables sont `leaderboard.json` (modèle en colonne), `cv_results.json`,
`feature_importance.json` si disponible et `ranking.json` en binary/regression.
En multiclass, ranking nécessite `class_label` ; une classe invalide est
rejetée avant création des runs et la classe choisie est tracée dans le
paramètre parent `ranking_class`. Aucun Top Segment n'est envoyé.
En binary, `calibration.json` est ajouté avec **10 bins**, ainsi que les métriques
parent `calibration_brier_score`, `calibration_expected_error` et
`calibration_max_error`. Multiclass les ajoute uniquement avec `class_label` ;
regression les omet. Cette table est agrégée, sans index client, cible ou prédiction
individuelle ni features. Elle peut donc être loggée par défaut ; aucune option
de MLflowConfig n'est ajoutée et les enfants restent des diagnostics CV.
`log_predictions=False` par défaut : aucune observation individuelle du
holdout. L'opt-in ajoute `predictions.json` avec une première colonne d'index
original, renommée par suffixe si son nom entre en conflit. Les features
originales et datasets train/test ne sont jamais envoyés.

`MLflowConfig` est frozen : `experiment_name="RationalML"`, `run_name=None`,
`tracking_uri=None`, `log_model=True`, `log_predictions=False`, `nested=False`,
`tags=None`. Les tags sont copiés et leurs valeurs scalaires converties en
texte. Les clés `rationalml.*` et `mlflow.parentRunId` sont réservées ; un
conflit produit `ConfigurationError`. Les paramètres structurés utilisent
du JSON trié ; un objet non sérialisable est représenté par `<module.Class>`
avec warning, sans adresse mémoire.

`log_model=True` sérialise uniquement le Pipeline gagnant complet avec l'API
MLflow 3 `name="model"` et cloudpickle, y compris le preprocessing utilisateur.
Aucun input_example, signature inférée, fit, predict, predict_proba, autologging
ou enregistrement au Model Registry. Le résultat reste inchangé ; seul le
`run_id` est retourné.

Une URI fournie est temporaire et restaurée même en cas d'échec ; None respecte
la configuration MLflow existante. Aucun serveur n'est lancé. Un run utilisateur
actif exige `nested=True` et la même URI : il reste actif, tandis que les runs
RationalML sont fermés. L'expérience est sélectionnée/créée via son ID sans
changer l'expérience active de l'utilisateur. Un échec du backend remonte.
MLflow est importé uniquement à la demande ; s'il manque, `MissingDependencyError`
indique l'installation de l'extra `mlflow` (`mlflow>=3.1,<4`).

## SHAP explainability

SHAP est optionnel et importé uniquement lors de `result.explain(...)` :

```shell
pip install "rationalml[shap]"
# SHAP >=0.49.1,<0.50 ; RationalML conserve Python >=3.10.
```

```python
import shap

# Le Data Scientist choisit les observations et la distribution de référence.
background = X_reference.iloc[:100]
explanation = result.explain(X_clients, background=background)

shap.plots.bar(explanation)
shap.plots.waterfall(explanation[0])

# Multiclass : une seule classe métier, class_label obligatoire (0 accepté).
explanation = multiclass_result.explain(
    X_clients, background=background, class_label="gold",
)
```

`explain(X, *, background, class_label=None)` retourne une **shap.Explanation**
avec `values.shape == (n_samples, n_transformed_features)` et une base par ligne.
Binary explique la classe interne **1**, donc `result.positive_class`, y compris
avec labels strings/bool ou `positive_class=0`. Multiclass sélectionne exactement
`classes_[j]` et refuse une classe absente/inconnue. Regression explique la
prédiction numérique. `class_label` est interdit hors multiclass.

**X et background sont obligatoirement fournis par l'utilisateur.** Les colonnes
sont validées/réordonnées comme pour predict ; ndarray suit le même contrat.
Un background de 50 à 200 observations représentatives est souvent pratique,
mais aucune taille n'est imposée et **aucun échantillonnage n'est effectué**,
y compris au-delà des 100 lignes par défaut du masker SHAP. Le background définit
la distribution de référence ; aucune donnée du fit ou de test_predictions
n'est récupérée. AutoMLResult ne conserve ni données fournies, ni explainer,
ni explication ou SHAP values. AutoML.fit n'importe et ne calcule jamais SHAP.

Le preprocessing None transmet les features préparées ; basic et custom utilisent
uniquement **transform avec les objets fitted du Pipeline**, sans fit, fit_transform,
clone ou nouvelles statistiques. L'estimateur est expliqué dans l'espace transformé :
**LinearExplainer** pour LogisticRegression/Ridge ; **TreeExplainer** pour
LightGBM/XGBoost, avec background explicite, `model_output="raw"` et
`feature_perturbation="interventional"`. Aucun Kernel/Permutation/DeepExplainer
ni fallback automatique n'est utilisé pour un modèle non pris en charge.

**base_value + somme(SHAP values) ≈ sortie brute du modèle.** En classification,
cela signifie marge/decision score (log-odds pour la logistique binaire), sans
unité « points de probabilité ». Regression utilise la prédiction numérique.
Les noms viennent de transformed_feature_names, ou des noms d'origine sans
preprocessing. Les colonnes OHE restent distinctes, sans agrégation vers leur
source. Un custom sans noms fiables utilise `feature_0`, `feature_1`… avec
UserWarning explicite. `Explanation.data` contient les valeurs transformées
réellement reçues par l'estimateur.

Les sorties sparse sont conservées en **CSR pour LinearExplainer** ; les valeurs
SHAP sont numériques denses, mais la matrice d'entrée n'est pas densifiée par
RationalML. TreeExplainer interventional exige ici des matrices transformées
denses : une combinaison sparse est rejetée explicitement, sans conversion
automatique. Les plots SHAP restent à la charge de l'utilisateur.

**Compatibilité XGBoost :** SHAP 0.49.1 ne lit pas les intercepts vectoriels de
XGBoost >=3.1. Cette combinaison lève ConfigurationError pour explain, tout en
conservant le support entraînement/inférence existant. Pour cette intégration,
utiliser XGBoost >=2,<3.1 ; les tests d'additivité utilisent **3.0.5** dans une
installation isolée. La contrainte boosting du projet reste inchangée. Le
[correctif SHAP est introduit en 0.50](https://github.com/shap/shap/releases/tag/v0.50.0),
qui abandonne Python 3.10 ; RationalML conserve donc la série 0.49.x demandée.

`feature_importance` garde ses coefficients/importances natives rapides ; explain
calcule des contributions SHAP sur les observations explicitement choisies.
**SHAP explique la prédiction du modèle, sans démontrer une relation causale.**
Aucun plot RationalML, envoi automatique vers Excel/MLflow ou stockage SHAP
n'est ajouté. Une dépendance absente produit MissingDependencyError avec la
commande d'installation de l'extra.

## Protocole et responsabilités

```text
FULL DATA
    ├── TEST réservé
    └── TRAIN
          ├── encodage cible si classification, création unique des folds
          ├── baseline CV sur ces folds, diagnostic cible uniquement
          ├── CV / Optuna
          │     ├── fold TRAIN : Pipeline neuf, fit sur ces lignes uniquement
          │     └── fold VALIDATION : transform et évaluation
          ├── choix du modèle et des paramètres par score CV
          └── Pipeline final neuf, fit sur TRAIN complet
                    └── transform et évaluation unique sur TEST
                          └── prédictions conservées → diagnostics/restitution sur demande
```

Le holdout ne participe à aucun fit, à Optuna, ni à la sélection des modèles.
**TEST n'intervient jamais dans le choix du modèle.** Il n'apprend ni catégories,
ni imputation/scaling, ni hyperparamètres. SHAP reçoit un background explicite
sur demande après fit ; RationalML ne le choisit pas dans TEST. Les diagnostics
de calibration ne recalibrent jamais le modèle.
Le mode basic construit un preprocessor neuf ; le mode expert clone celui
fourni. Aucun transformer fitted n'est partagé entre folds. Early stopping
et pruning restent désactivés, et les métriques utilisent les prédictions
natives des estimateurs.

Optuna utilise des essais séquentiels et un sampler seedé. `n_jobs` contrôle
les threads des modèles et le calcul numérique. `n_trials` et `timeout`
s'appliquent par modèle ; un timeout réel peut modifier le nombre d'essais
terminés malgré un seed identique. Le contrat d'affichage est décrit ci-dessous.

Les composants existants conservent leurs responsabilités. V0.3 ajoute
seulement un encodeur multiclass, des entrées de registries et des branches
explicites pour le split/CV et l'inférence. `ModelSpec.task_params` déclare les
objectifs multiclass des arbres ; AutoML ne connaît pas les noms des modèles.
Le sous-package `preprocessing` est inchangé par rapport à V0.2.1, sans
transformation supplémentaire.

Un modèle s'ajoute via `ModelRegistry.register(ModelSpec(...))`, une métrique
via `MetricRegistry.register(MetricSpec(...))`. Le modèle doit accepter
random_state, déclarer `n_jobs_parameter` (ou None), supporter predict_proba en classification
et définir `requires_scaling` pour basic. Aucune modification du moteur n'est
nécessaire pour l'enregistrer.

## CPU, affichage et durées — préparation V1.1

Ces ajouts sont présents sur la branche de préparation V1.1 ; la version du
projet reste **1.0.0** jusqu'à une décision de release séparée. Ils ne sont pas
encore inclus dans la distribution PyPI 1.0.0.

```python
result = AutoML(
    target="churn", positive_class=1, preprocessing=None,
    models="auto", n_jobs=2, verbose=1,
).fit(df_model)

print(result.summary())  # str ; aucune nouvelle prédiction ni aucun fit
print(result.fit_time)   # secondes
print(result.model_fit_times)  # dict[str, float], dans l'ordre des modèles demandés
```

| n_jobs | Contrat |
|---|---|
| `1` (défaut) | Un thread de calcul par backend contrôlé. |
| `N > 0` | Au maximum N threads par backend contrôlé. |
| `-1` | CPU disponibles selon les conventions des backends ; aucune limite supplémentaire imposée aux pools natifs. |

Les trials Optuna et les folds CV restent **séquentiels** : n_jobs ne crée
aucun worker Optuna, aucun parallélisme de folds, ni nouvelle couche joblib.
LightGBM et XGBoost reçoivent n_jobs, en classification comme en régression.
LogisticRegression/lbfgs et Ridge utilisent les limites BLAS/OpenMP de
threadpoolctl ; aucun paramètre n_jobs artificiel n'est transmis à Ridge.
Ces limites sont temporaires pendant CV, refit, évaluation et les méthodes
`result.predict*`, puis restaurées même en cas d'erreur. Avec -1, une limite
native imposée extérieurement reste respectée. n_jobs refuse 0, les entiers
inférieurs à -1, les booléens et les valeurs non entières.

Il s'agit d'un budget par backend contrôlable, sans affinité CPU imposée au
processus. Le parallélisme propre à un transformer utilisateur reste sous
sa responsabilité. Un modèle enregistré déclare `n_jobs_parameter` dans
ModelSpec. Les appels directs à best_model utilisent le contrat natif sklearn.

| verbose | Affichage RationalML |
|---|---|
| `0` | Aucune sortie normale, aucun INFO Optuna et aucune barre. |
| `1` (défaut) | Présentation du run, progression réelle, résumé final ; aucun détail de trial Optuna. |
| `>= 2` | Présentation, détails INFO des trials Optuna, résumé final ; aucune barre. |

Comme en V1.0, verbose accepte tout entier >= 0, sans borne supérieure :
2, 3 et 10 utilisent le même mode détaillé. Les négatifs, booléens et
valeurs non entières sont refusés. Seul l'affichage dépend de verbose.

Les modèles intégrés conservent leurs réglages informatifs silencieux.
Les niveaux Optuna sont appliqués temporairement et restaurés dans un finally,
y compris les niveaux explicites des loggers enfants existants. Les handlers,
formatters, filtres et la propagation configurés par l'utilisateur sont conservés.
Le contrôle du logging Optuna et des pools natifs agit au niveau du processus :
éviter de lancer simultanément des fits indépendants dans des threads partageant
ces configurations globales ; aucun parallélisme de ce type n'est créé ici.

La barre utilise uniquement la bibliothèque standard. Son total est
`nombre de modèles × n_trials`. Chaque trial terminé compte une fois,
y compris FAIL/PRUNED ; un trial encore RUNNING ne compte pas. Le moteur
conserve NopPruner et son comportement d'arrêt sur une exception non gérée.
Une interruption ou un timeout affiche le compteur réellement atteint, sans
compléter artificiellement la barre. Dans un terminal, la ligne est réécrite ;
dans un fichier/notebook, une ligne est émise par modèle et à la fermeture.
n_trials et timeout restent des budgets **par modèle**. Un timeout est contrôlé
entre trials et n'interrompt pas un fold en cours.

Les warnings Python conservent les filtres standard ou ceux définis par
l'utilisateur à tous les niveaux verbose. Aucune agrégation ni suppression
globale des warnings sklearn n'est ajoutée. Un vrai problème peut donc être
signalé même avec verbose=0. Les erreurs/warnings Optuna restent accessibles.

Les durées utilisent `time.perf_counter()` et sont disponibles à tous les
niveaux verbose, sans ajout de colonne aux tableaux V1 :

- `fit_time: float` : secondes depuis l'entrée de fit, incluant validation,
  copies, split, baseline, CV/Optuna, refit, évaluation unique TEST, importance
  et construction du résultat. Inclut la présentation/progression éventuelle,
  mais exclut l'impression du résumé final et toute opération post-fit.
- `model_fit_times: dict[str, float]` : temps d'optimisation de chaque modèle,
  incluant ses espaces, construction de pipelines, fits et évaluations CV.
  Le gagnant inclut en plus la construction et le fit de son pipeline final
  sur TRAIN complet. Exclut split, baseline, holdout et feature importance.
  Un non-gagnant n'est pas refitted après sélection.
- `summary() -> str` : classement CV, gagnant, métrique primaire, score TEST
  déjà stocké et durées. Ne fait aucun print, fit, predict, ranking ou export,
  et ne modifie pas les résultats. Utiliser `print(result.summary())` pour
  réafficher le résumé terminal.

L'affichage ne participe à aucune décision ML. Les durées ne sont pas
reproductibles ; un budget timeout peut déjà faire varier le nombre de trials
à cause des temps d'exécution, y compris du coût de l'affichage.

L'espace LightGBM moderne est borné pour limiter les configurations
structurellement dégénérées : profondeur 2–12, feuilles 4–128 limitées par
2**max_depth, learning_rate 0.01–0.2 logarithmique, L1/L2 1e-8–10
logarithmiques, min_split_gain 0–0.1, min_child_samples 5–50,
colsample_bytree 0.5–1, subsample 0.6–1 et subsample_freq 1–5.
Les trois tâches partagent cet espace ; les objectifs restent distincts.
`min_child_samples` utilise `suggest_int` linéaire : 20 est la valeur par défaut
LightGBM, 5 apporte de la flexibilité et 50 conserve une régularisation importante.
Cette borne réduit les configurations limitant fortement les splits sur de petits
datasets, sans dépendre de la tâche ni du dataset Credit Card Fraud.
Les domaines explicitement fournis, notamment legacy, restent conservés.
Aucun class weight, rééquilibrage ni seuil automatique n'est ajouté.

## Reproducibility

`random_state` contrôle le split train/test, les folds CV, le sampler Optuna
et le seed des modèles construits par RationalML. À données, configuration
et environnement identiques, les tests comparent sélection, paramètres,
prédictions et diagnostics avec des tolérances numériques adaptées. Un
transformer utilisateur reste responsable de ses propres sources d'aléa.
Cela ne promet pas une égalité bit-à-bit universelle entre bibliothèques,
versions, plateformes ou niveaux de parallélisme. Un timeout peut aussi
changer le nombre de trials terminés. Le roundtrip pickle des pipelines
built-in est testé dans une même version, sans garantie inter-version.

## Contrat public V1 et stabilité

La V1 stabilise exactement **11 exports racine** de `rationalml.__all__` :

| Groupe | Exports |
| --- | --- |
| Core | AutoML, AutoMLConfig, AutoMLResult, TaskType |
| Extension | ModelRegistry, ModelSpec, MetricRegistry, MetricSpec |
| Configuration | PreprocessingConfig, ExcelReportConfig, MLflowConfig |

Les helpers avancés restent accessibles depuis leurs sous-modules :

```python
from rationalml.tasks import normalize_task
from rationalml.preprocessing import FeatureSchema, infer_schema, build_preprocessor
```

Ils ne font pas partie du contrat SemVer stable de l'API racine. Leurs objets
et comportements restent conservés ; `AutoMLResult.feature_schema` peut
toujours contenir un `FeatureSchema` en mode basic.

Les signatures de
`AutoML`, `fit`, `predict`, `predict_proba`, `predict_positive_proba`,
`ranking_table`, `top_segment`, `calibration_table`, `calibration_summary`,
`to_excel`, `log_mlflow` et `explain` restent celles de V0.10.

Les attributs publics du résultat sont ceux documentés ici : `task`, `target`,
`config`, `primary_metric`, `leaderboard`, `cv_results`, `best_model`,
`best_model_name`, `best_params`, `model_best_params`, `metrics`, `test_metrics`,
`feature_importance`, `feature_names`, `feature_schema`,
`transformed_feature_names`, `train_indices`, `test_indices`, `test_predictions`,
`baseline_name`, `baseline_score`, `baseline_cv_std`, `baseline_fold_scores`,
`classes_`, `positive_class`, `negative_class` (ces trois derniers respectent
leurs restrictions de tâche). Le constructeur dataclass du résultat, son
encodeur et ses champs privés ne constituent pas un contrat de construction
ou de sérialisation. `best_model` est un Pipeline sklearn fitted complet,
directement utilisable ; ses détails `named_steps` ne sont pas une API stable
de RationalML. Pour restaurer les labels originaux et contrôler les colonnes,
utiliser les méthodes du résultat.

Les noms **et l'ordre** des colonnes ci-dessous sont stables :

| Tableau | Colonnes dans l'ordre |
| --- | --- |
| leaderboard, index nommé `model` | rank, cv_score, cv_std, cv_min, cv_max, improvement_vs_baseline, n_trials_completed |
| cv_results | model, trial, value, fold_0 … fold_(cv-1) |
| test_predictions binary | y_true, y_pred, score |
| test_predictions multiclass | y_true, y_pred, proba_0 … proba_(n_classes-1) |
| test_predictions regression | y_true, y_pred, residual |
| importance native binary/regression et arbres multiclass | feature, importance, source_feature |
| importance LogisticRegression multiclass | feature, class, importance, source_feature |
| ranking classification | segment, count, score_min, score_max, score_mean, positives, positive_rate, population_share, positive_capture, cumulative_positive_capture, lift, cumulative_lift |
| ranking regression | segment, count, pred_min, pred_max, pred_mean, actual_mean, bias, mae, rmse |
| calibration classification | bin, count, proba_min, proba_max, proba_mean, observed_rate, calibration_gap, absolute_calibration_gap, population_share |

Le leaderboard est trié par score CV ; les égalités conservent l'ordre des
modèles demandé. `cv_results` suit l'ordre demandé puis les essais terminés.
La baseline reste hors leaderboard et hors sélection. Les diagnostics test
ne déterminent jamais le gagnant. `test_predictions` conserve l'index original
du holdout, renvoie une copie et ne contient aucune feature brute.
Une importance indisponible reste un tableau vide à trois colonnes avec warning.

Politique SemVer : patch = correction compatible ; minor = ajout compatible ;
major = changement incompatible du contrat public. Une suppression publique
sera normalement précédée d'une dépréciation documentée et d'un warning,
puis effectuée dans une version majeure. Les modules non documentés et les
helpers préfixés `_` sont privés. Les structures internes Optuna, attributs
privés sklearn et formats pickle entre versions ne sont pas garantis.

Les erreurs documentées de `rationalml.exceptions` dérivent d'`AutoMLError` :
`ConfigurationError`, `DataValidationError`, `UnsupportedTaskError`,
`MissingDependencyError`, `OptimizationError` et `LegacyAPIError`.
Le type et la signification sont stables ; le texte complet peut évoluer.
Les tests vérifient le type et un fragment utile du message.

## Migration V0.10.0 → V1.0.0

Aucune signature ni logique ML ne change. La version provient désormais des
métadonnées installées, avec `pyproject.toml` comme source unique : installer
le package, y compris en editable pour développer. Les six modules historiques
restent distribués volontairement. Les tests de schémas, le quickstart et les
audits wheel/sdist fixent les contrats déjà existants. Les quatre helpers
`normalize_task`, `FeatureSchema`, `infer_schema`, `build_preprocessor` quittent
la racine avant la première publication stable : adapter leurs imports vers
les sous-modules ci-dessus. Les tests adaptent ces imports, la liste des exports,
les attentes de version et le contrat de licence Apache-2.0.

## Migration V0.9.0 → V0.10.0

Aucune signature ni fonctionnalité ML nouvelle. Les doublons de modèles
étaient déjà refusés ; l'erreur identifie maintenant les noms concernés.
L'export Excel remplace la destination seulement après sauvegarde complète
dans un temporaire du même dossier, fermé avant écriture sur Windows et
nettoyé en cas d'échec. L'ancien fichier reste intact si save/replace échoue.
Les erreurs RationalML explicites traversent SHAP sans être réemballées ;
les autres erreurs conservent leur cause dans `ConfigurationError`.

## Migration V0.8.0 → V0.9.0

`explain` et l'extra optionnel shap sont ajoutés sans modifier les champs du
résultat, l'entraînement, l'inférence ou les exports. Aucune nouvelle dépendance
obligatoire. Les modèles sans explainer pris en charge et les combinaisons
TreeExplainer/sparse sont rejetés explicitement. L'incompatibilité entre
SHAP 0.49.x et les intercepts XGBoost >=3.1 concerne uniquement explain ; les
versions XGBoost installées et les API existantes restent conservées.

## Migration V0.7.0 → V0.8.0

Les API et contrats d'entraînement/inférence restent inchangés. Les deux méthodes
de calibration sont ajoutées à AutoMLResult sans nouveau champ ni dépendance.
Les rapports classification concernés ajoutent une feuille Calibration et trois
lignes Summary ; MLflow ajoute un artefact agrégé et trois métriques parent.
Adapter les consommateurs qui vérifient une liste exacte de feuilles, artefacts
ou métriques. `test_metrics` et le leaderboard ne changent pas. Aucune calibration
réelle du modèle n'est introduite.

## Migration V0.6.0 → V0.7.0

Les contrats ML/Excel sont conservés. `model_best_params` est ajouté avec
`default_factory=dict`, sans casser les constructions manuelles existantes.
Pour un ancien résultat sans ce champ, le gagnant utilise `best_params` ;
les autres candidats gardent leurs diagnostics CV avec un warning indiquant
que leurs paramètres sont indisponibles. Aucun paramètre n'est inventé.
Installer l'extra `mlflow` uniquement pour utiliser `log_mlflow`.

## Migration V0.5.0 → V0.6.0

Aucun contrat ML existant ne change. `to_excel` et `ExcelReportConfig` sont
ajoutés ; installer l'extra `excel` uniquement pour exporter. Les helpers Excel
legacy restent désactivés : utiliser `AutoMLResult.to_excel`.

## Migration V0.4.0 → V0.5.0

L'API fit/inférence et le leaderboard gardent leurs contrats. Le résultat conserve
désormais les prédictions du holdout : mémoire proportionnelle à ses lignes
et, en multiclass, au nombre de classes. Les constructions manuelles doivent
renseigner `_test_predictions` ; les anciens résultats sérialisés doivent être
réentraînés pour disposer des nouveaux diagnostics. `test_predictions` est une
propriété sans setter : modifier la copie retournée ne modifie pas le résultat.

## Migration V0.3.0 → V0.4.0

Le leaderboard conserve ses trois colonnes et en ajoute quatre. Les consommateurs
exigeant une liste exacte de colonnes doivent adapter leur sélection.
`AutoMLResult` ajoute les quatre champs baseline ; une construction manuelle
doit les renseigner. `cv_results`, les métriques, les search spaces, la sélection
et les trois modes preprocessing gardent leurs contrats V0.3.

## Migration V0.2.1 → V0.3.0

- `metric` passe de `"roc_auc"` à `"auto"` ; le comportement binaire effectif
  reste roc_auc. Les configurations retournées exposent les noms résolus.
- Les labels textuels à plus de deux classes peuvent désormais résoudre
  multiclass ; les cibles numériques à plus de deux valeurs restent une
  erreur avec auto et nécessitent un choix explicite.
- `positive_class` et les méthodes binaires ne s'étendent pas aux nouvelles
  tâches ; aucune classe positive multiclass n'est inventée.
- `ModelRegistry.available()` / `MetricRegistry.available()` sans filtre
  incluent les nouvelles entrées : passer une tâche pour filtrer.
- L'importance logistique multiclass est un format long avec colonne `class`.
  Les formats binaires, les trois modes preprocessing et les erreurs legacy
  restent conservés.

## Migration V0.2.0 → V0.2.1

- Le défaut est désormais `preprocessing=None` : le DataFrame doit être prêt.
  Utiliser explicitement `"basic"` pour retrouver les transformations V0.2.0.
- `preprocessing="auto"` est retiré avec `ConfigurationError` et indication
  de migration. `models="auto"`, `task="auto"` et le réglage avancé
  `scale_numeric="auto"` restent valides.
- Le pipeline None ne contient plus d'étape `preprocessing`. Les accès directs
  à `best_model.named_steps["preprocessing"]` doivent être conditionnels.
- `feature_schema` est None hors basic. Les résultats numériques peuvent
  changer si le scaling automatique V0.2.0 n'est plus demandé.
- Un transformer sans noms exploitables produit une importance indisponible
  avec warning, sans empêcher l'entraînement.

## Compatibilité legacy

Le package s'importe avec `from rationalml import AutoML`. Les six modules
historiques `strategy`, `sco_mod`, `scoring`, `report`, `metrics`, `second_step`
restent distribués parce que leur compatibilité est annoncée et testée.
Ils sont **legacy, hors contrat stable V1**, binaires et séparés du cœur.
Leur retrait futur exigera une migration annoncée ; les chemins dangereux
restent désactivés. `local_optimizer` → `sco_mod` et
`second_step` utilisent des DataFrames numériques/booléens sans preprocessing
automatique. Leurs résultats sont des DataFrames bruts ; les AutoMLResult
sont disponibles dans `init_model.results_` / `result_`.

Les seuils, pruning et early stopping legacy restent retirés avec warning.
Excel, model_save, model_select et class_weight="auto" du chemin historique
lèvent `LegacyAPIError`. Les poids fixes restent utilisables.
`scoring.opti` exige `training_only=True` et des labels train 0/1 ; sa seconde
sortie contient cv_std. Les anciens helpers `scoring.objective` et ceux de
`report.py` conservent leurs erreurs de migration explicites.

`Metrics.sub_area` et `Metrics.sub_area_pr` restent retirées avec
`LegacyAPIError` depuis V0.1.1 : les anciens calculs ROC/PR partiels étaient
incorrects et leur contrat métier ambigu. `roc_auc` et `average_precision`
évaluent les courbes globales sans prétendre remplacer ces aires partielles.

## Données conservées et confidentialité

`test_predictions` contient les labels réels et les prédictions du holdout ;
le résultat occupe donc une mémoire proportionnelle au nombre de lignes et de
classes. Excel et MLflow exportent les prédictions individuelles seulement sur
opt-in. La calibration est agrégée ; elle n'ajuste jamais les probabilités.
SHAP reçoit uniquement les X et background fournis explicitement. Il n'y a
aucun autologging, envoi automatique de données ou promesse juridique de conformité.

## Vérification et limites

La V1 conserve les **1133 cas V0.10** et ajoute **117 cas** (25 fonctions de test
avec paramétrisation). Les nouvelles validations verrouillent les schémas,
les classes à travers toutes les sorties, le quickstart et les garde-fous des
distributions/releases. Les tests existants continuent de couvrir :

- holdout features/targets modifiés avec split explicitement fixé, deux candidats,
  trois tâches et trois modes preprocessing ;
- statistiques finales basic inspectées et indices CV identiques pour baseline
  et trois modèles, avec priorité utilisateur en cas d'égalité ;
- reproductibilité des six modèles built-in, diagnostics et roundtrip pickle ;
- DataFrame/config/transformer utilisateur inchangés, indices atypiques et copies
  indépendantes des tableaux, absence d'état caché post-fit ;
- configurations ambiguës, cibles/features invalides et petits effectifs avant Optuna ;
- signatures publiques, exports racine, versions, metadata et import sans extras ;
- échecs de sauvegarde/remplacement Excel et transparence des exceptions SHAP.

La CI `.github/workflows/ci.yml` comprend le core sans
extras sous Python 3.10/3.11/3.12, la suite complète avec extras sous 3.12,
les minima SHAP/XGBoost, MLflow et Excel, puis le build sdist/wheel. Chaque
job vérifie pip check. Le smoke test `tools/check_wheel.py`
réinstalle le wheel core dans un nouveau venv, importe hors du dépôt avec -I,
vérifie la version installée, les exports et l'absence des cinq dépendances
optionnelles, puis exécute réellement le quickstart. L'installation utilise
`--no-index` et un wheelhouse préparé séparément. `tools/check_sdist.py`
reconstruit un wheel hors dépôt, sans réseau, depuis le sdist extrait et
compare les sources et métadonnées. Le sdist inclut tous les tests et helpers,
les exemples et documents de release, sans `.github` ni caches/données.

Validations locales Windows V1 :

| Environnement | Tests réussis | Warnings | Skips |
| --- | ---: | ---: | ---: |
| Tous les extras, Python 3.12.6 | 1250 | 2 | 0 |
| Core sans extras, Python 3.10.19 | 847 | 10 | 90 |
| SHAP 0.49.1 + XGBoost 3.0.5, Python 3.10.19 | 211 | 7 | 0 |
| MLflow 3.1.4 + SQLAlchemy 2.0.54, Python 3.12.6 | 110 | 28 | 0 |
| Openpyxl 3.0.10, Python 3.12.6 | 128 | 0 | 0 |

Les skips core proviennent exclusivement de pytest.importorskip pour les
extras absents. Les warnings sont conservés : classe positive implicite ;
SettingWithCopyWarning dans les anciens transformers de test qui écrivent
volontairement sur leurs entrées (pandas 2) ; dépréciations internes MLflow
(noload SQLAlchemy 2.1, utcnow, Pydantic et Query.get). Aucun filtre global de
warnings n'est ajouté. La suite complète passe en 324,70 secondes, sans échec.

**Compatibilité constatée :** MLflow 3.1.4 importe
FallbackAsyncAdaptedQueuePool, supprimé en SQLAlchemy 2.1. Le job minimum
et sa validation locale utilisent donc sqlalchemy<2.1. Pour reproduire :
`pip install "rationalml[mlflow]" "mlflow==3.1.*" "sqlalchemy<2.1"`.
La borne mlflow>=3.1,<4 reste inchangée. SHAP 0.49.x conserve la restriction
sur XGBoost à base_score vectoriel (>=3.1) : fit/predict restent fonctionnels,
explain exige pour ce chemin XGBoost <3.1. La destination Excel doit être
fermée par ses lecteurs lors du remplacement atomique sous Windows.
Les jobs GitHub Actions sont configurés ; Python 3.11 n'est pas exécuté localement.

La mémoire du OneHot dense basic reste à surveiller. Les colonnes entièrement
manquantes dans un fold basic restent une erreur ; les constantes peuvent
avertir par fold/essai. Aucun preprocessing, modèle, métrique, explainer,
export ou API de persistence supplémentaire n'est ajouté en V1. Le protocole
ML de V0.10 reste inchangé : split d'abord, folds partagés sur train uniquement,
preprocessing neuf par fold, choix CV, refit sur train complet et évaluation
unique du gagnant sur test.

## Préparation de publication

**License: Apache-2.0.** `LICENSE` contient le texte officiel intégral.
Le backend `setuptools>=77.0.3` génère les métadonnées SPDX ; le workflow
`release.yml` exige `License-Expression: Apache-2.0` et un unique `License-File: LICENSE`.
Le dépôt officiel [RationalML](https://github.com/arthur-diaz/RationalML) est
actuellement privé : accès public aux liens à vérifier avant publication.
Le nom PyPI et le Trusted Publisher restent à vérifier/configurer par le
mainteneur. `RELEASE.md` décrit la checklist locale et les paramètres distants ;
`CHANGELOG.md` retrace les versions à partir de l'historique réel.
Aucune publication, création de tag ni push n'est effectué par cette préparation.
