Spaces:
Sleeping
Sleeping
Update src/module/Forecasting.py
Browse files- src/module/Forecasting.py +40 -40
src/module/Forecasting.py
CHANGED
|
@@ -163,10 +163,10 @@ class HospitalForecaster:
|
|
| 163 |
Returns:
|
| 164 |
Dictionnaire des métriques
|
| 165 |
"""
|
| 166 |
-
progress_bar = st.progress(0, text="
|
| 167 |
|
| 168 |
# ===== 1. LightGBM =====
|
| 169 |
-
progress_bar.progress(0.1, text="
|
| 170 |
lgb_model = lgb.LGBMRegressor(
|
| 171 |
n_estimators=500,
|
| 172 |
learning_rate=0.05,
|
|
@@ -190,7 +190,7 @@ class HospitalForecaster:
|
|
| 190 |
self._evaluate_model('LightGBM', lgb_model)
|
| 191 |
|
| 192 |
# ===== 2. XGBoost =====
|
| 193 |
-
progress_bar.progress(0.4, text="
|
| 194 |
xgb_model = xgb.XGBRegressor(
|
| 195 |
n_estimators=500,
|
| 196 |
learning_rate=0.05,
|
|
@@ -214,7 +214,7 @@ class HospitalForecaster:
|
|
| 214 |
self._evaluate_model('XGBoost', xgb_model)
|
| 215 |
|
| 216 |
# ===== 3. Random Forest =====
|
| 217 |
-
progress_bar.progress(0.7, text="
|
| 218 |
rf_model = RandomForestRegressor(
|
| 219 |
n_estimators=300,
|
| 220 |
max_depth=15,
|
|
@@ -485,7 +485,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 485 |
if 'ml_features' not in st.session_state:
|
| 486 |
st.error("❌ **Données ML absentes.** Veuillez d'abord exécuter le module **Feature Engineering**.")
|
| 487 |
|
| 488 |
-
if st.button("
|
| 489 |
st.switch_page("pages/03_Features.py")
|
| 490 |
return
|
| 491 |
|
|
@@ -496,7 +496,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 496 |
# ===== CONFIGURATION =====
|
| 497 |
st.divider()
|
| 498 |
|
| 499 |
-
with st.expander("
|
| 500 |
col1, col2, col3 = st.columns(3)
|
| 501 |
|
| 502 |
with col1:
|
|
@@ -534,7 +534,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 534 |
|
| 535 |
try:
|
| 536 |
# Préparation des données
|
| 537 |
-
with st.spinner("
|
| 538 |
split_info = forecaster.prepare_data(
|
| 539 |
target_col=target,
|
| 540 |
val_days=val_days,
|
|
@@ -542,7 +542,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 542 |
)
|
| 543 |
|
| 544 |
# Affichage des plages de dates
|
| 545 |
-
st.markdown("###
|
| 546 |
|
| 547 |
col1, col2, col3 = st.columns(3)
|
| 548 |
|
|
@@ -583,7 +583,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 583 |
except Exception as e:
|
| 584 |
st.error(f"❌ **Erreur lors de l'entraînement :** {str(e)}")
|
| 585 |
import traceback
|
| 586 |
-
with st.expander("
|
| 587 |
st.code(traceback.format_exc())
|
| 588 |
return
|
| 589 |
|
|
@@ -595,16 +595,16 @@ def show_forecasting_module(client, sheet_name):
|
|
| 595 |
|
| 596 |
# ===== ONGLETS PRINCIPAUX =====
|
| 597 |
tab_future, tab_audit, tab_export = st.tabs([
|
| 598 |
-
"
|
| 599 |
-
"
|
| 600 |
-
"
|
| 601 |
])
|
| 602 |
|
| 603 |
# ================================================================
|
| 604 |
# ONGLET 1 : PRÉDICTIONS FUTURES
|
| 605 |
# ================================================================
|
| 606 |
with tab_future:
|
| 607 |
-
st.markdown("###
|
| 608 |
|
| 609 |
col_h1, col_h2 = st.columns([3, 1])
|
| 610 |
|
|
@@ -622,7 +622,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 622 |
months_map = {"1 Mois": 1, "3 Mois": 3, "6 Mois": 6, "12 Mois": 12}
|
| 623 |
months = months_map[horizon_label]
|
| 624 |
|
| 625 |
-
with st.spinner("
|
| 626 |
f_dates, f_values, conf_intervals = forecaster.generate_future_forecast_recursive(months)
|
| 627 |
|
| 628 |
# Graphique des prédictions futures
|
|
@@ -633,7 +633,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 633 |
fig.add_trace(go.Scatter(
|
| 634 |
x=hist['date'],
|
| 635 |
y=hist[target],
|
| 636 |
-
name="
|
| 637 |
line=dict(color='gray', width=2),
|
| 638 |
opacity=0.6
|
| 639 |
))
|
|
@@ -642,7 +642,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 642 |
fig.add_trace(go.Scatter(
|
| 643 |
x=f_dates,
|
| 644 |
y=f_values,
|
| 645 |
-
name=f"
|
| 646 |
line=dict(color='#00E676', width=3),
|
| 647 |
mode='lines+markers',
|
| 648 |
marker=dict(size=5)
|
|
@@ -697,7 +697,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 697 |
col2.metric("Maximum Prévu", f"{np.max(f_values):.1f}")
|
| 698 |
col3.metric("Minimum Prévu", f"{np.min(f_values):.1f}")
|
| 699 |
col4.metric("Tendance",
|
| 700 |
-
"
|
| 701 |
f"{((f_values[-1] / f_values[0]) - 1) * 100:+.1f}%")
|
| 702 |
|
| 703 |
# Détection d'anomalies futures
|
|
@@ -715,20 +715,20 @@ def show_forecasting_module(client, sheet_name):
|
|
| 715 |
else:
|
| 716 |
st.success("✅ Aucun pic anormal détecté dans la période")
|
| 717 |
|
| 718 |
-
st.caption(f"
|
| 719 |
|
| 720 |
# ================================================================
|
| 721 |
# ONGLET 2 : AUDIT & COMPARAISON
|
| 722 |
# ================================================================
|
| 723 |
with tab_audit:
|
| 724 |
-
st.markdown("###
|
| 725 |
|
| 726 |
# Annonce du vainqueur
|
| 727 |
col_winner, col_reset = st.columns([4, 1])
|
| 728 |
|
| 729 |
with col_winner:
|
| 730 |
st.success(f"""
|
| 731 |
-
###
|
| 732 |
|
| 733 |
Sur les **{split_info['test']['n_samples']} derniers jours** de test, ce modèle a montré
|
| 734 |
la meilleure précision avec une erreur moyenne de seulement **{metrics[best]['MAE']:.2f}** unités par jour.
|
|
@@ -738,7 +738,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 738 |
""")
|
| 739 |
|
| 740 |
with col_reset:
|
| 741 |
-
if st.button("
|
| 742 |
for key in ['forecaster', 'metrics', 'split_info', 'forecast_target']:
|
| 743 |
if key in st.session_state:
|
| 744 |
del st.session_state[key]
|
|
@@ -747,7 +747,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 747 |
st.divider()
|
| 748 |
|
| 749 |
# ===== TABLEAU COMPARATIF =====
|
| 750 |
-
st.markdown("###
|
| 751 |
|
| 752 |
comparison_data = []
|
| 753 |
for model_name in ['XGBoost', 'LightGBM', 'Random Forest']:
|
|
@@ -769,7 +769,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 769 |
)
|
| 770 |
|
| 771 |
# Explications des métriques
|
| 772 |
-
with st.expander("
|
| 773 |
st.markdown("""
|
| 774 |
**R² (Coefficient de Détermination) :**
|
| 775 |
- Mesure de 0% à 100% : plus c'est proche de 100%, mieux c'est
|
|
@@ -797,7 +797,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 797 |
st.divider()
|
| 798 |
|
| 799 |
# ===== GRAPHIQUE COMPARATIF SUR TEST SET =====
|
| 800 |
-
st.markdown("###
|
| 801 |
|
| 802 |
fig_comp = go.Figure()
|
| 803 |
|
|
@@ -805,7 +805,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 805 |
fig_comp.add_trace(go.Scatter(
|
| 806 |
x=forecaster.dates_test,
|
| 807 |
y=forecaster.y_test,
|
| 808 |
-
name="
|
| 809 |
line=dict(color='white', width=4, dash='dot'),
|
| 810 |
mode='lines+markers',
|
| 811 |
marker=dict(size=6)
|
|
@@ -845,22 +845,22 @@ def show_forecasting_module(client, sheet_name):
|
|
| 845 |
|
| 846 |
st.plotly_chart(fig_comp, use_container_width=True)
|
| 847 |
|
| 848 |
-
st.caption("
|
| 849 |
|
| 850 |
st.divider()
|
| 851 |
|
| 852 |
# ===== SHAP : INTERPRÉTABILITÉ =====
|
| 853 |
-
st.markdown("###
|
| 854 |
|
| 855 |
st.info("""
|
| 856 |
**SHAP** (SHapley Additive exPlanations) révèle **quelles variables influencent le plus**
|
| 857 |
les prédictions du modèle.
|
| 858 |
|
| 859 |
-
|
| 860 |
""")
|
| 861 |
|
| 862 |
if SHAP_AVAILABLE:
|
| 863 |
-
with st.spinner("
|
| 864 |
explainer, shap_values, X_sample = forecaster.compute_shap_values(n_samples=200)
|
| 865 |
|
| 866 |
if shap_values is not None:
|
|
@@ -890,7 +890,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 890 |
plt.close()
|
| 891 |
|
| 892 |
# Explications textuelles
|
| 893 |
-
st.markdown("####
|
| 894 |
|
| 895 |
# Top 5 features
|
| 896 |
feature_importance = pd.DataFrame({
|
|
@@ -905,19 +905,19 @@ def show_forecasting_module(client, sheet_name):
|
|
| 905 |
|
| 906 |
# Traduction en français
|
| 907 |
if 'lag' in feature:
|
| 908 |
-
explanation = f"
|
| 909 |
elif 'mean' in feature or 'rolling' in feature:
|
| 910 |
-
explanation = f"
|
| 911 |
elif 'taux' in feature:
|
| 912 |
-
explanation = f"
|
| 913 |
elif 'personnel' in feature:
|
| 914 |
-
explanation = f"
|
| 915 |
elif 'scenario' in feature or 'intensity' in feature:
|
| 916 |
-
explanation = f"
|
| 917 |
elif 'mois' in feature or 'jour' in feature:
|
| 918 |
-
explanation = f"
|
| 919 |
else:
|
| 920 |
-
explanation = f"
|
| 921 |
|
| 922 |
st.write(f"- {explanation}")
|
| 923 |
|
|
@@ -937,7 +937,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 937 |
|
| 938 |
with col1:
|
| 939 |
# Export des métriques
|
| 940 |
-
st.markdown("####
|
| 941 |
|
| 942 |
metrics_df = pd.DataFrame(metrics).T
|
| 943 |
metrics_csv = metrics_df.to_csv(index=True).encode('utf-8')
|
|
@@ -953,7 +953,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 953 |
|
| 954 |
with col2:
|
| 955 |
# Export des prédictions sur test
|
| 956 |
-
st.markdown("####
|
| 957 |
|
| 958 |
predictions_df = pd.DataFrame({
|
| 959 |
'date': forecaster.dates_test,
|
|
@@ -975,7 +975,7 @@ def show_forecasting_module(client, sheet_name):
|
|
| 975 |
st.dataframe(predictions_df.head(10), use_container_width=True)
|
| 976 |
|
| 977 |
# Export des prédictions futures
|
| 978 |
-
st.markdown("####
|
| 979 |
|
| 980 |
if 'f_dates' in locals() and 'f_values' in locals():
|
| 981 |
future_df = pd.DataFrame({
|
|
|
|
| 163 |
Returns:
|
| 164 |
Dictionnaire des métriques
|
| 165 |
"""
|
| 166 |
+
progress_bar = st.progress(0, text=" Entraînement en cours...")
|
| 167 |
|
| 168 |
# ===== 1. LightGBM =====
|
| 169 |
+
progress_bar.progress(0.1, text=" LightGBM...")
|
| 170 |
lgb_model = lgb.LGBMRegressor(
|
| 171 |
n_estimators=500,
|
| 172 |
learning_rate=0.05,
|
|
|
|
| 190 |
self._evaluate_model('LightGBM', lgb_model)
|
| 191 |
|
| 192 |
# ===== 2. XGBoost =====
|
| 193 |
+
progress_bar.progress(0.4, text=" XGBoost...")
|
| 194 |
xgb_model = xgb.XGBRegressor(
|
| 195 |
n_estimators=500,
|
| 196 |
learning_rate=0.05,
|
|
|
|
| 214 |
self._evaluate_model('XGBoost', xgb_model)
|
| 215 |
|
| 216 |
# ===== 3. Random Forest =====
|
| 217 |
+
progress_bar.progress(0.7, text=" Random Forest...")
|
| 218 |
rf_model = RandomForestRegressor(
|
| 219 |
n_estimators=300,
|
| 220 |
max_depth=15,
|
|
|
|
| 485 |
if 'ml_features' not in st.session_state:
|
| 486 |
st.error("❌ **Données ML absentes.** Veuillez d'abord exécuter le module **Feature Engineering**.")
|
| 487 |
|
| 488 |
+
if st.button(" Aller au Feature Engineering"):
|
| 489 |
st.switch_page("pages/03_Features.py")
|
| 490 |
return
|
| 491 |
|
|
|
|
| 496 |
# ===== CONFIGURATION =====
|
| 497 |
st.divider()
|
| 498 |
|
| 499 |
+
with st.expander(" Configuration de l'Entraînement", expanded=False):
|
| 500 |
col1, col2, col3 = st.columns(3)
|
| 501 |
|
| 502 |
with col1:
|
|
|
|
| 534 |
|
| 535 |
try:
|
| 536 |
# Préparation des données
|
| 537 |
+
with st.spinner(" Préparation des données (Train/Val/Test)..."):
|
| 538 |
split_info = forecaster.prepare_data(
|
| 539 |
target_col=target,
|
| 540 |
val_days=val_days,
|
|
|
|
| 542 |
)
|
| 543 |
|
| 544 |
# Affichage des plages de dates
|
| 545 |
+
st.markdown("### Plages de Dates des Sets")
|
| 546 |
|
| 547 |
col1, col2, col3 = st.columns(3)
|
| 548 |
|
|
|
|
| 583 |
except Exception as e:
|
| 584 |
st.error(f"❌ **Erreur lors de l'entraînement :** {str(e)}")
|
| 585 |
import traceback
|
| 586 |
+
with st.expander(" Détails de l'erreur"):
|
| 587 |
st.code(traceback.format_exc())
|
| 588 |
return
|
| 589 |
|
|
|
|
| 595 |
|
| 596 |
# ===== ONGLETS PRINCIPAUX =====
|
| 597 |
tab_future, tab_audit, tab_export = st.tabs([
|
| 598 |
+
" Prédictions Futures",
|
| 599 |
+
" Audit & Comparaison",
|
| 600 |
+
" Export & Données"
|
| 601 |
])
|
| 602 |
|
| 603 |
# ================================================================
|
| 604 |
# ONGLET 1 : PRÉDICTIONS FUTURES
|
| 605 |
# ================================================================
|
| 606 |
with tab_future:
|
| 607 |
+
st.markdown("### Projections Récursives avec Saisonnalité")
|
| 608 |
|
| 609 |
col_h1, col_h2 = st.columns([3, 1])
|
| 610 |
|
|
|
|
| 622 |
months_map = {"1 Mois": 1, "3 Mois": 3, "6 Mois": 6, "12 Mois": 12}
|
| 623 |
months = months_map[horizon_label]
|
| 624 |
|
| 625 |
+
with st.spinner(" Calcul de la trajectoire récursive avec saisonnalité..."):
|
| 626 |
f_dates, f_values, conf_intervals = forecaster.generate_future_forecast_recursive(months)
|
| 627 |
|
| 628 |
# Graphique des prédictions futures
|
|
|
|
| 633 |
fig.add_trace(go.Scatter(
|
| 634 |
x=hist['date'],
|
| 635 |
y=hist[target],
|
| 636 |
+
name=" Historique Réel",
|
| 637 |
line=dict(color='gray', width=2),
|
| 638 |
opacity=0.6
|
| 639 |
))
|
|
|
|
| 642 |
fig.add_trace(go.Scatter(
|
| 643 |
x=f_dates,
|
| 644 |
y=f_values,
|
| 645 |
+
name=f" Projection {best}",
|
| 646 |
line=dict(color='#00E676', width=3),
|
| 647 |
mode='lines+markers',
|
| 648 |
marker=dict(size=5)
|
|
|
|
| 697 |
col2.metric("Maximum Prévu", f"{np.max(f_values):.1f}")
|
| 698 |
col3.metric("Minimum Prévu", f"{np.min(f_values):.1f}")
|
| 699 |
col4.metric("Tendance",
|
| 700 |
+
" Hausse" if f_values[-1] > f_values[0] else " Baisse",
|
| 701 |
f"{((f_values[-1] / f_values[0]) - 1) * 100:+.1f}%")
|
| 702 |
|
| 703 |
# Détection d'anomalies futures
|
|
|
|
| 715 |
else:
|
| 716 |
st.success("✅ Aucun pic anormal détecté dans la période")
|
| 717 |
|
| 718 |
+
st.caption(f" Prédiction générée par **{best}** | Fiabilité : **{metrics[best]['R²']*100:.0f}%**")
|
| 719 |
|
| 720 |
# ================================================================
|
| 721 |
# ONGLET 2 : AUDIT & COMPARAISON
|
| 722 |
# ================================================================
|
| 723 |
with tab_audit:
|
| 724 |
+
st.markdown("### Le Champion de la Prédiction")
|
| 725 |
|
| 726 |
# Annonce du vainqueur
|
| 727 |
col_winner, col_reset = st.columns([4, 1])
|
| 728 |
|
| 729 |
with col_winner:
|
| 730 |
st.success(f"""
|
| 731 |
+
### Gagnant : **{best}**
|
| 732 |
|
| 733 |
Sur les **{split_info['test']['n_samples']} derniers jours** de test, ce modèle a montré
|
| 734 |
la meilleure précision avec une erreur moyenne de seulement **{metrics[best]['MAE']:.2f}** unités par jour.
|
|
|
|
| 738 |
""")
|
| 739 |
|
| 740 |
with col_reset:
|
| 741 |
+
if st.button(" Réinitialiser", help="Relancer l'entraînement"):
|
| 742 |
for key in ['forecaster', 'metrics', 'split_info', 'forecast_target']:
|
| 743 |
if key in st.session_state:
|
| 744 |
del st.session_state[key]
|
|
|
|
| 747 |
st.divider()
|
| 748 |
|
| 749 |
# ===== TABLEAU COMPARATIF =====
|
| 750 |
+
st.markdown("### Tableau Comparatif Détaillé")
|
| 751 |
|
| 752 |
comparison_data = []
|
| 753 |
for model_name in ['XGBoost', 'LightGBM', 'Random Forest']:
|
|
|
|
| 769 |
)
|
| 770 |
|
| 771 |
# Explications des métriques
|
| 772 |
+
with st.expander(" Comprendre les Métriques"):
|
| 773 |
st.markdown("""
|
| 774 |
**R² (Coefficient de Détermination) :**
|
| 775 |
- Mesure de 0% à 100% : plus c'est proche de 100%, mieux c'est
|
|
|
|
| 797 |
st.divider()
|
| 798 |
|
| 799 |
# ===== GRAPHIQUE COMPARATIF SUR TEST SET =====
|
| 800 |
+
st.markdown("### Comparaison Visuelle sur le Test Set")
|
| 801 |
|
| 802 |
fig_comp = go.Figure()
|
| 803 |
|
|
|
|
| 805 |
fig_comp.add_trace(go.Scatter(
|
| 806 |
x=forecaster.dates_test,
|
| 807 |
y=forecaster.y_test,
|
| 808 |
+
name=" RÉALITÉ",
|
| 809 |
line=dict(color='white', width=4, dash='dot'),
|
| 810 |
mode='lines+markers',
|
| 811 |
marker=dict(size=6)
|
|
|
|
| 845 |
|
| 846 |
st.plotly_chart(fig_comp, use_container_width=True)
|
| 847 |
|
| 848 |
+
st.caption(" **Plus une ligne colorée est proche de la ligne blanche, meilleur est le modèle**")
|
| 849 |
|
| 850 |
st.divider()
|
| 851 |
|
| 852 |
# ===== SHAP : INTERPRÉTABILITÉ =====
|
| 853 |
+
st.markdown("### Comprendre les Décisions de l'IA (SHAP)")
|
| 854 |
|
| 855 |
st.info("""
|
| 856 |
**SHAP** (SHapley Additive exPlanations) révèle **quelles variables influencent le plus**
|
| 857 |
les prédictions du modèle.
|
| 858 |
|
| 859 |
+
Plus la barre est longue, plus cette feature est importante pour les décisions de l'IA.
|
| 860 |
""")
|
| 861 |
|
| 862 |
if SHAP_AVAILABLE:
|
| 863 |
+
with st.spinner(" Calcul des valeurs SHAP (analyse d'importance)..."):
|
| 864 |
explainer, shap_values, X_sample = forecaster.compute_shap_values(n_samples=200)
|
| 865 |
|
| 866 |
if shap_values is not None:
|
|
|
|
| 890 |
plt.close()
|
| 891 |
|
| 892 |
# Explications textuelles
|
| 893 |
+
st.markdown("#### Interprétation")
|
| 894 |
|
| 895 |
# Top 5 features
|
| 896 |
feature_importance = pd.DataFrame({
|
|
|
|
| 905 |
|
| 906 |
# Traduction en français
|
| 907 |
if 'lag' in feature:
|
| 908 |
+
explanation = f" Historique récent ({feature})"
|
| 909 |
elif 'mean' in feature or 'rolling' in feature:
|
| 910 |
+
explanation = f" Tendance sur plusieurs jours ({feature})"
|
| 911 |
elif 'taux' in feature:
|
| 912 |
+
explanation = f" Taux ou ratio ({feature})"
|
| 913 |
elif 'personnel' in feature:
|
| 914 |
+
explanation = f" Ressources humaines ({feature})"
|
| 915 |
elif 'scenario' in feature or 'intensity' in feature:
|
| 916 |
+
explanation = f" Scénario/événement ({feature})"
|
| 917 |
elif 'mois' in feature or 'jour' in feature:
|
| 918 |
+
explanation = f" Facteur temporel ({feature})"
|
| 919 |
else:
|
| 920 |
+
explanation = f" {feature}"
|
| 921 |
|
| 922 |
st.write(f"- {explanation}")
|
| 923 |
|
|
|
|
| 937 |
|
| 938 |
with col1:
|
| 939 |
# Export des métriques
|
| 940 |
+
st.markdown("#### Métriques des Modèles")
|
| 941 |
|
| 942 |
metrics_df = pd.DataFrame(metrics).T
|
| 943 |
metrics_csv = metrics_df.to_csv(index=True).encode('utf-8')
|
|
|
|
| 953 |
|
| 954 |
with col2:
|
| 955 |
# Export des prédictions sur test
|
| 956 |
+
st.markdown("#### Prédictions Test Set")
|
| 957 |
|
| 958 |
predictions_df = pd.DataFrame({
|
| 959 |
'date': forecaster.dates_test,
|
|
|
|
| 975 |
st.dataframe(predictions_df.head(10), use_container_width=True)
|
| 976 |
|
| 977 |
# Export des prédictions futures
|
| 978 |
+
st.markdown("#### Prédictions Futures")
|
| 979 |
|
| 980 |
if 'f_dates' in locals() and 'f_values' in locals():
|
| 981 |
future_df = pd.DataFrame({
|