Spaces:
Sleeping
Sleeping
CI Bot commited on
Commit ·
7de1562
1
Parent(s): bc6e002
CI deploy Fri Nov 21 11:21:49 UTC 2025
Browse files- Dockerfile +18 -3
- README.md +21 -0
- coverage.xml +1 -1
- src/data/models/__init__.py +1 -1
- src/drift/__init__.py +0 -0
- src/drift/monitoring.py +115 -0
- src/scripts/api_simulation.py +161 -0
Dockerfile
CHANGED
|
@@ -2,9 +2,9 @@ FROM python:3.13-slim
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
# Installer dépendances système
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
-
gcc build-essential libpq-dev libssl-dev libffi-dev \
|
| 8 |
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
|
| 10 |
# Installer Poetry
|
|
@@ -23,6 +23,21 @@ COPY ./ ./
|
|
| 23 |
# Installer le projet
|
| 24 |
RUN poetry install --only-root --no-interaction --no-ansi
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
EXPOSE 7860
|
| 27 |
|
| 28 |
-
CMD ["
|
|
|
|
| 2 |
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
+
# Installer dépendances système + cron
|
| 6 |
RUN apt-get update && apt-get install -y \
|
| 7 |
+
gcc build-essential libpq-dev libssl-dev libffi-dev cron \
|
| 8 |
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
|
| 10 |
# Installer Poetry
|
|
|
|
| 23 |
# Installer le projet
|
| 24 |
RUN poetry install --only-root --no-interaction --no-ansi
|
| 25 |
|
| 26 |
+
# Créer le fichier cron : exécution le 1er de chaque mois à 02:00
|
| 27 |
+
RUN echo "0 2 1 * * cd /app && /usr/local/bin/poetry run python -m src.drift.monitoring >> /var/log/drift_cron.log 2>&1" > /etc/cron.d/drift_cron
|
| 28 |
+
|
| 29 |
+
# Donner les bons droits au fichier cron
|
| 30 |
+
RUN chmod 0644 /etc/cron.d/drift_cron && crontab /etc/cron.d/drift_cron
|
| 31 |
+
|
| 32 |
+
# Créer le fichier de log
|
| 33 |
+
RUN touch /var/log/drift_cron.log
|
| 34 |
+
|
| 35 |
+
# Script de démarrage qui lance à la fois l'API et cron
|
| 36 |
+
RUN echo '#!/bin/bash\n\
|
| 37 |
+
cron\n\
|
| 38 |
+
exec uvicorn src.api.main:app --host 0.0.0.0 --port 7860' > /start.sh && \
|
| 39 |
+
chmod +x /start.sh
|
| 40 |
+
|
| 41 |
EXPOSE 7860
|
| 42 |
|
| 43 |
+
CMD ["/start.sh"]
|
README.md
CHANGED
|
@@ -124,4 +124,25 @@ Il est nécessaire de préconfigurer cet environnement avec les variables suivan
|
|
| 124 |
* `HF_PIPELINE` : pré-rempli dans `.env.dist`
|
| 125 |
* `XDG_CACHE_HOME` : `/tmp/.cache`
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
|
|
|
| 124 |
* `HF_PIPELINE` : pré-rempli dans `.env.dist`
|
| 125 |
* `XDG_CACHE_HOME` : `/tmp/.cache`
|
| 126 |
|
| 127 |
+
## Détection automatique du Datadrift
|
| 128 |
+
|
| 129 |
+
Le code permettant de gérer le rapport HTML du datadrift est inclus dans `src/drift/monitoring.py`
|
| 130 |
+
Voici son fonctionnement :
|
| 131 |
+
|
| 132 |
+
* Récupère les données de logs de prédictions depuis la base de données (`predict_logs`)
|
| 133 |
+
* Compare ces données de production au jeu d'entrainement local avec **Evidently**
|
| 134 |
+
* Génère le rapport au format HTML dans `.data/drift/report.html`
|
| 135 |
+
|
| 136 |
+
Ce script de monitoring s'exécute en production comme en local une fois par mois
|
| 137 |
+
La configuration est faite dans le `Dockerfile`, par l'utilisation de `crontab`
|
| 138 |
+
|
| 139 |
+
## Profiling automatique du code de prédiction du modèle
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
Le profiling du code s'effectue automatiquement, il lancé lors de la création de la stack locale avec `make start` (il n'est pas utile de l'exporter en production), il permet :
|
| 143 |
+
* de simuler un appel à `/predict` et analyse la stack d'appels avec `cProfile`
|
| 144 |
+
* de générer un rapport d'analyse dans `.data/profiling/`, en html et en `.prof`
|
| 145 |
+
* de lancer le container docker `p8-snakeviz` pour visualiser les résultats du rapports à l'adresse suivante :
|
| 146 |
+
* http://localhost:8082/snakeviz/%2Fapp%2Fprofiling%2Fpredict_inference.prof (**port à adapter à votre configuration**)
|
| 147 |
+
|
| 148 |
|
coverage.xml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
<?xml version="1.0" ?>
|
| 2 |
-
<coverage version="7.12.0" timestamp="
|
| 3 |
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.12.0 -->
|
| 4 |
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
| 5 |
<sources>
|
|
|
|
| 1 |
<?xml version="1.0" ?>
|
| 2 |
+
<coverage version="7.12.0" timestamp="1763724058110" lines-valid="290" lines-covered="242" line-rate="0.8345" branches-valid="16" branches-covered="7" branch-rate="0.4375" complexity="0">
|
| 3 |
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.12.0 -->
|
| 4 |
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
|
| 5 |
<sources>
|
src/data/models/__init__.py
CHANGED
|
@@ -3,5 +3,5 @@ from .predict_logs import PredictLogs
|
|
| 3 |
|
| 4 |
__all__ = [
|
| 5 |
"Base",
|
| 6 |
-
"PredictLogs"
|
| 7 |
]
|
|
|
|
| 3 |
|
| 4 |
__all__ = [
|
| 5 |
"Base",
|
| 6 |
+
"PredictLogs"
|
| 7 |
]
|
src/drift/__init__.py
ADDED
|
File without changes
|
src/drift/monitoring.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from sqlalchemy import text
|
| 6 |
+
from evidently import Report
|
| 7 |
+
from evidently.presets import DataDriftPreset
|
| 8 |
+
|
| 9 |
+
# Ajuste ce chemin à ton projet si besoin
|
| 10 |
+
ROOT_DIR = Path(__file__).resolve().parents[2]
|
| 11 |
+
sys.path.insert(0, str(ROOT_DIR))
|
| 12 |
+
|
| 13 |
+
from src.data.database import get_db
|
| 14 |
+
|
| 15 |
+
# Config
|
| 16 |
+
DATA_DIR = ROOT_DIR / ".data"
|
| 17 |
+
TRAIN_PATH = DATA_DIR / "application_train.csv"
|
| 18 |
+
WINDOW_DAYS = 100
|
| 19 |
+
REPORT_OUTPUT = DATA_DIR / "drift" / "report.html"
|
| 20 |
+
|
| 21 |
+
def extract_prod_data() -> pd.DataFrame:
|
| 22 |
+
"""Extrait les données prod depuis la table predict_logs."""
|
| 23 |
+
db = next(get_db())
|
| 24 |
+
|
| 25 |
+
query = f"""
|
| 26 |
+
SELECT input_payload->'application_data' AS data
|
| 27 |
+
FROM public.predict_logs
|
| 28 |
+
WHERE status = 'success'
|
| 29 |
+
AND date >= NOW() - INTERVAL '{WINDOW_DAYS} days'
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
rows = db.execute(text(query)).fetchall()
|
| 33 |
+
db.close()
|
| 34 |
+
|
| 35 |
+
if not rows:
|
| 36 |
+
return pd.DataFrame()
|
| 37 |
+
|
| 38 |
+
data = [row[0] for row in rows]
|
| 39 |
+
return pd.DataFrame(data)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def load_reference_data() -> pd.DataFrame:
|
| 43 |
+
"""Charge les données d'entraînement comme référence."""
|
| 44 |
+
print(f"Chargement des données de référence depuis {TRAIN_PATH}")
|
| 45 |
+
return pd.read_csv(TRAIN_PATH)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def generate_drift_report(reference_data: pd.DataFrame, current_data: pd.DataFrame) -> None:
|
| 49 |
+
"""Génère un rapport HTML de drift avec Evidently."""
|
| 50 |
+
|
| 51 |
+
# Créer le dossier reports s'il n'existe pas
|
| 52 |
+
REPORT_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
| 53 |
+
|
| 54 |
+
# Aligner les colonnes entre référence et production
|
| 55 |
+
common_cols = list(set(reference_data.columns) & set(current_data.columns))
|
| 56 |
+
|
| 57 |
+
if not common_cols:
|
| 58 |
+
print("Aucune colonne commune entre les données de référence et de production!")
|
| 59 |
+
return
|
| 60 |
+
|
| 61 |
+
print(f"Colonnes communes détectées: {len(common_cols)}")
|
| 62 |
+
|
| 63 |
+
reference_subset = reference_data[common_cols]
|
| 64 |
+
current_subset = current_data[common_cols]
|
| 65 |
+
|
| 66 |
+
print("Génération du rapport de drift...")
|
| 67 |
+
|
| 68 |
+
# Ici : API Evidently "nouvelle" avec Report + metric_preset
|
| 69 |
+
report = Report(
|
| 70 |
+
metrics=[
|
| 71 |
+
DataDriftPreset(),
|
| 72 |
+
]
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
eval = report.run(
|
| 76 |
+
reference_data=reference_subset,
|
| 77 |
+
current_data=current_subset,
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
# Cette version‑là de Report a bien save_html
|
| 81 |
+
eval.save_html(str(REPORT_OUTPUT))
|
| 82 |
+
|
| 83 |
+
print(f"Rapport de drift généré: {REPORT_OUTPUT}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def main():
|
| 87 |
+
"""Point d'entrée principal."""
|
| 88 |
+
print("=" * 60)
|
| 89 |
+
print("ANALYSE DE DRIFT DU MODÈLE")
|
| 90 |
+
print("=" * 60)
|
| 91 |
+
|
| 92 |
+
# 1. Charger les données de référence (train)
|
| 93 |
+
reference_data = load_reference_data()
|
| 94 |
+
print(f"Données de référence: {reference_data.shape}")
|
| 95 |
+
|
| 96 |
+
# 2. Extraire les données de production
|
| 97 |
+
print(f"\nExtraction des données de production ({WINDOW_DAYS} derniers jours)...")
|
| 98 |
+
current_data = extract_prod_data()
|
| 99 |
+
|
| 100 |
+
if current_data.empty:
|
| 101 |
+
print("Aucune donnée de production trouvée!")
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
print(f"Données de production: {current_data.shape}")
|
| 105 |
+
|
| 106 |
+
# 3. Générer le rapport de drift
|
| 107 |
+
generate_drift_report(reference_data, current_data)
|
| 108 |
+
|
| 109 |
+
print("\n" + "=" * 60)
|
| 110 |
+
print("ANALYSE TERMINÉE")
|
| 111 |
+
print("=" * 60)
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
if __name__ == "__main__":
|
| 115 |
+
main()
|
src/scripts/api_simulation.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import requests
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import time
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# Configuration
|
| 9 |
+
API_URL = os.getenv("API_URL")
|
| 10 |
+
API_TOKEN = os.getenv("API_TOKEN")
|
| 11 |
+
DATA_DIR = Path(".data")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_data():
|
| 15 |
+
"""Charge les données application_test et bureau"""
|
| 16 |
+
app_test = pd.read_csv(DATA_DIR / "application_test.csv")
|
| 17 |
+
bureau = pd.read_csv(DATA_DIR / "bureau.csv")
|
| 18 |
+
return app_test, bureau
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def get_bureau_records(sk_id_curr, bureau_df):
|
| 22 |
+
"""Récupère tous les enregistrements bureau pour un SK_ID_CURR donné"""
|
| 23 |
+
bureau_records = bureau_df[bureau_df['SK_ID_CURR'] == sk_id_curr]
|
| 24 |
+
|
| 25 |
+
if bureau_records.empty:
|
| 26 |
+
return []
|
| 27 |
+
|
| 28 |
+
# Convertir en liste de dictionnaires
|
| 29 |
+
bureau_list = bureau_records.to_dict('records')
|
| 30 |
+
|
| 31 |
+
# Remplacer les NaN par None pour le JSON
|
| 32 |
+
for record in bureau_list:
|
| 33 |
+
for key, value in record.items():
|
| 34 |
+
if pd.isna(value):
|
| 35 |
+
record[key] = None
|
| 36 |
+
|
| 37 |
+
return bureau_list
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def create_api_payload(app_row, bureau_records):
|
| 41 |
+
"""Crée le payload JSON pour l'API"""
|
| 42 |
+
# Convertir la ligne application en dict
|
| 43 |
+
app_dict = app_row.to_dict()
|
| 44 |
+
|
| 45 |
+
# Remplacer les NaN par None
|
| 46 |
+
for key, value in app_dict.items():
|
| 47 |
+
if pd.isna(value):
|
| 48 |
+
app_dict[key] = None
|
| 49 |
+
|
| 50 |
+
payload = {
|
| 51 |
+
"application_data": app_dict,
|
| 52 |
+
"bureau_data": bureau_records
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
return payload
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def call_api(payload):
|
| 59 |
+
"""Appelle l'API et retourne la réponse"""
|
| 60 |
+
try:
|
| 61 |
+
start_time = time.time()
|
| 62 |
+
response = requests.post(
|
| 63 |
+
API_URL+'/predict',
|
| 64 |
+
json=payload,headers={
|
| 65 |
+
"Content-Type": "application/json",
|
| 66 |
+
"Authorization": f"Bearer {API_TOKEN}"
|
| 67 |
+
},
|
| 68 |
+
timeout=30
|
| 69 |
+
)
|
| 70 |
+
inference_time = time.time() - start_time
|
| 71 |
+
|
| 72 |
+
if response.status_code == 200:
|
| 73 |
+
result = response.json()
|
| 74 |
+
result['inference_time_ms'] = round(inference_time * 1000, 2)
|
| 75 |
+
result['status'] = 'success'
|
| 76 |
+
return result
|
| 77 |
+
else:
|
| 78 |
+
return {
|
| 79 |
+
'status': 'error',
|
| 80 |
+
'status_code': response.status_code,
|
| 81 |
+
'error_message': response.text,
|
| 82 |
+
'inference_time_ms': round(inference_time * 1000, 2)
|
| 83 |
+
}
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return {
|
| 86 |
+
'status': 'error',
|
| 87 |
+
'error_message': str(e),
|
| 88 |
+
'inference_time_ms': None
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def generate_production_data(start_index=0, num_records=10):
|
| 93 |
+
"""
|
| 94 |
+
Génère des données de production en appelant l'API
|
| 95 |
+
|
| 96 |
+
Args:
|
| 97 |
+
start_index: Index de départ dans application_test.csv
|
| 98 |
+
num_records: Nombre d'enregistrements à traiter
|
| 99 |
+
api_url: URL de l'API
|
| 100 |
+
"""
|
| 101 |
+
print(f"🚀 Génération de {num_records} prédictions à partir de l'index {start_index}")
|
| 102 |
+
|
| 103 |
+
# Charger les données
|
| 104 |
+
print("Chargement des données...")
|
| 105 |
+
app_test, bureau = load_data()
|
| 106 |
+
|
| 107 |
+
# Sélectionner les lignes
|
| 108 |
+
end_index = start_index + num_records
|
| 109 |
+
selected_rows = app_test.iloc[start_index:end_index]
|
| 110 |
+
|
| 111 |
+
print(f"{len(selected_rows)} lignes sélectionnées (index {start_index} à {end_index - 1})")
|
| 112 |
+
|
| 113 |
+
# Préparer les résultats
|
| 114 |
+
results = []
|
| 115 |
+
payloads = []
|
| 116 |
+
|
| 117 |
+
# Traiter chaque ligne
|
| 118 |
+
for idx, (_, row) in enumerate(selected_rows.iterrows(), 1):
|
| 119 |
+
sk_id_curr = row['SK_ID_CURR']
|
| 120 |
+
print(f"\n[{idx}/{num_records}] Traitement de SK_ID_CURR: {sk_id_curr}")
|
| 121 |
+
|
| 122 |
+
# Récupérer les données bureau
|
| 123 |
+
bureau_records = get_bureau_records(sk_id_curr, bureau)
|
| 124 |
+
print(f" 📋 {len(bureau_records)} enregistrements bureau trouvés")
|
| 125 |
+
|
| 126 |
+
# Créer le payload
|
| 127 |
+
payload = create_api_payload(row, bureau_records)
|
| 128 |
+
payloads.append(payload)
|
| 129 |
+
|
| 130 |
+
# Appeler l'API
|
| 131 |
+
print(f"Appel de l'API...")
|
| 132 |
+
api_response = call_api(payload)
|
| 133 |
+
|
| 134 |
+
# Ajouter les métadonnées
|
| 135 |
+
result = {
|
| 136 |
+
'timestamp': datetime.now().isoformat(),
|
| 137 |
+
'sk_id_curr': int(sk_id_curr),
|
| 138 |
+
'start_index': start_index,
|
| 139 |
+
'record_index': start_index + idx - 1,
|
| 140 |
+
'api_response': api_response
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
results.append(result)
|
| 144 |
+
|
| 145 |
+
if api_response['status'] == 'success':
|
| 146 |
+
print(f" ✅ Succès - Temps: {api_response.get('inference_time_ms', 'N/A')} ms")
|
| 147 |
+
else:
|
| 148 |
+
print(f" ❌ Erreur: {api_response.get('error_message', 'Unknown')}")
|
| 149 |
+
|
| 150 |
+
# Pause pour ne pas surcharger l'API
|
| 151 |
+
#time.sleep(0.1)
|
| 152 |
+
return results
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
if __name__ == "__main__":
|
| 156 |
+
|
| 157 |
+
# Génère 100 prédictions à partir de l'index 0
|
| 158 |
+
results = generate_production_data(
|
| 159 |
+
start_index=0,
|
| 160 |
+
num_records=500
|
| 161 |
+
)
|