# -*- coding: utf-8 -*-
"""
MCR-ALS pour analyse UV-Visible (Co3O4 + RB5)
Avec séparation du colorant libre et adsorbé - VERSION CORRIGÉE
"""
# Installation des packages nécessaires
!pip install pandas numpy scikit-learn matplotlib openpyxl scipy
# Import des bibliothèques
import pandas as pd
import numpy as np
import [Link] as plt
from [Link] import lstsq
from [Link] import nnls, minimize, curve_fit
from [Link] import PCA
from [Link] import savgol_filter
from [Link] import UnivariateSpline
from [Link] import files
import warnings
[Link]('ignore')
print("=" * 80)
print("MCR-ALS pour analyse UV-Visible (Nanoparticules Co3O4 + Colorant
RB5)")
print("Avec séparation colorant libre vs adsorbé")
print("=" * 80)
#
=======================================================================
=====
# 1. UPLOAD DES FICHIERS EXCEL
#
=======================================================================
=====
print("\n" + "=" * 60)
print("ÉTAPE 1: TÉLÉVERSEMENT DES FICHIERS")
print("=" * 60)
print("\nVeuillez sélectionner vos 3 fichiers Excel:")
print("1. Spectre des nanoparticules seules (Co3O4)")
print("2. Spectre du colorant RB5 seul")
print("3. Spectre du mélange (Nanoparticules + RB5)")
uploaded_files = {}
for i, nom_fichier in enumerate(["Nanoparticules", "RB5", "Melange"],
1):
print(f"\n--- Téléversement fichier {i}/3: {nom_fichier} ---")
uploaded = [Link]()
filename = list([Link]())[0]
uploaded_files[nom_fichier] = filename
print(f"✓ Fichier chargé: {filename}")
print("\n✓ TOUS LES FICHIERS ONT ÉTÉ CHARGÉS")
#
=======================================================================
=====
# 2. LECTURE DES DONNÉES
#
=======================================================================
=====
def load_uv_data(filename):
try:
df = pd.read_excel(filename, header=None)
wavelength = [Link][:, 0].[Link](float)
absorbance = [Link][:, 1].[Link](float)
return wavelength, absorbance
except:
df = pd.read_excel(filename)
wavelength = [Link][:, 0].[Link](float)
absorbance = [Link][:, 1].[Link](float)
return wavelength, absorbance
wl_np, abs_np = load_uv_data(uploaded_files['Nanoparticules'])
wl_rb5, abs_rb5 = load_uv_data(uploaded_files['RB5'])
wl_mix, abs_mix = load_uv_data(uploaded_files['Melange'])
# Interpolation
wl_min = max(wl_np[0], wl_rb5[0], wl_mix[0])
wl_max = min(wl_np[-1], wl_rb5[-1], wl_mix[-1])
wl_common = [Link](wl_min, wl_max, 500)
abs_np_interp = [Link](wl_common, wl_np, abs_np)
abs_rb5_interp = [Link](wl_common, wl_rb5, abs_rb5)
abs_mix_interp = [Link](wl_common, wl_mix, abs_mix)
#
=======================================================================
=====
# 3. PRÉTRAITEMENT
#
=======================================================================
=====
def normalize_spectrum(absorbance):
return (absorbance - [Link](absorbance)) / ([Link](absorbance) -
[Link](absorbance) + 1e-10)
abs_np_norm = normalize_spectrum(abs_np_interp)
abs_rb5_norm = normalize_spectrum(abs_rb5_interp)
abs_mix_norm = normalize_spectrum(abs_mix_interp)
print("\n✓ Prétraitement appliqué")
#
=======================================================================
=====
# 4. CLASSE MCR-ALS CORRIGÉE
#
=======================================================================
=====
class MCRALS_Adsorption:
"""MCR-ALS avec séparation du colorant libre et adsorbé - VERSION
CORRIGÉE"""
def __init__(self, D, n_components=3, max_iter=500, tol=1e-8):
self.D = D
self.n_components = n_components
self.max_iter = max_iter
[Link] = tol
self.C = None
[Link] = None
self.loss_history = []
def initialize_with_pure_spectra(self, pure_spectra_list):
"""Initialisation avec spectres purs connus"""
n_samples, n_wavelengths = [Link]
[Link] = [Link]((self.n_components, n_wavelengths))
for i in range(min(len(pure_spectra_list), self.n_components)):
[Link][i, :] = pure_spectra_list[i]
for i in range(len(pure_spectra_list), self.n_components):
[Link][i, :] = [Link](n_wavelengths)
self.C = [Link]([Link].T, self.D.T, rcond=None)[0].T
self._apply_constraints()
def _apply_constraints(self):
"""Application des contraintes de non-négativité"""
self.C = [Link](self.C, 0)
[Link] = [Link]([Link], 0)
# Normalisation
norms = [Link]([Link], axis=1, keepdims=True)
norms[norms == 0] = 1
[Link] = [Link] / norms
self.C = self.C * norms.T
def fit(self):
"""Optimisation ALS"""
print(f"Optimisation avec {self.n_components} composants...")
for iteration in range(self.max_iter):
# Mise à jour des concentrations
self.C = [Link]([Link].T, self.D.T,
rcond=None)[0].T
self._apply_constraints()
# Mise à jour des spectres
[Link] = [Link](self.C, self.D, rcond=None)[0]
self._apply_constraints()
# Calcul de la perte
D_reconstructed = [Link](self.C, [Link])
loss = [Link]((self.D - D_reconstructed)**2) /
[Link](self.D**2)
self.loss_history.append(loss)
if len(self.loss_history) > 1:
if abs(self.loss_history[-2] - loss) < [Link]:
print(f"✓ Convergence à l'itération {iteration}")
break
if iteration % 50 == 0 and iteration > 0:
print(f" Itération {iteration}: Loss = {loss:.2e}")
return self.C, [Link]
def reconstruct(self):
"""Reconstruction des données"""
return [Link](self.C, [Link])
#
=======================================================================
=====
# 5. DÉCONVOLUTION POUR OBTENIR LE SPECTRE DU COLORANT ADSORBÉ
#
=======================================================================
=====
print("\n" + "=" * 60)
print("ÉTAPE: DÉCONVOLUTION LIBRE/ADSORBÉ")
print("=" * 60)
# Matrice des données
D = [Link]([abs_np_norm, abs_rb5_norm, abs_mix_norm])
# Première estimation avec 2 composants pour trouver le résidu
mcr2 = MCRALS_Adsorption(D, n_components=2)
mcr2.initialize_with_pure_spectra([abs_np_norm, abs_rb5_norm])
C2, ST2 = [Link]()
D_reconstructed2 = [Link]()
# Le résidu (3ème composant) est attribué au colorant adsorbé
residual = D[2, :] - D_reconstructed2[2, :]
spectrum_adsorbed = [Link](residual, 0)
if [Link](spectrum_adsorbed) > 0:
spectrum_adsorbed = spectrum_adsorbed / [Link](spectrum_adsorbed)
print("✓ Spectre du colorant adsorbé estimé")
#
=======================================================================
=====
# 6. MCR-ALS FINAL AVEC 3 COMPOSANTS
#
=======================================================================
=====
print("\n" + "=" * 60)
print("ÉTAPE: MCR-ALS AVEC 3 COMPOSANTS")
print("=" * 60)
mcr3 = MCRALS_Adsorption(D, n_components=3)
mcr3.initialize_with_pure_spectra([abs_np_norm, abs_rb5_norm,
spectrum_adsorbed])
C3, ST3 = [Link]()
D_reconstructed3 = [Link]()
#
=======================================================================
=====
# 7. ANALYSE DES RÉSULTATS
#
=======================================================================
=====
# Calcul du shift spectral
peak_free_idx = [Link](ST3[1, :])
peak_ads_idx = [Link](ST3[2, :])
peak_free = wl_common[peak_free_idx]
peak_ads = wl_common[peak_ads_idx]
shift = peak_ads - peak_free
# Distribution du colorant dans le mélange
total_rb5 = C3[2, 1] + C3[2, 2]
if total_rb5 > 0:
pct_free = (C3[2, 1] / total_rb5) * 100
pct_ads = (C3[2, 2] / total_rb5) * 100
else:
pct_free = pct_ads = 50
print(f"\n📊 RÉSULTATS QUANTITATIFS:")
print(f" • Pic RB5 libre: {peak_free:.1f} nm")
print(f" • Pic RB5 adsorbé: {peak_ads:.1f} nm")
print(f" • Shift spectral: {shift:+.1f} nm")
print(f" • RB5 libre dans mélange: {pct_free:.1f}%")
print(f" • RB5 adsorbé dans mélange: {pct_ads:.1f}%")
#
=======================================================================
=====
# 8. VISUALISATIONS
#
=======================================================================
=====
# Figure 1: Spectres purs (NP, RB5 libre, RB5 adsorbé)
fig, ax = [Link](figsize=(12, 6))
[Link](wl_common, ST3[0, :], 'b-', linewidth=2, label='Nanoparticules
Co₃O₄')
[Link](wl_common, ST3[1, :], 'g-', linewidth=2, label='Colorant RB5
(libre)')
[Link](wl_common, ST3[2, :], 'r-', linewidth=2, label='Colorant RB5
(adsorbé)')
ax.set_xlabel('Longueur d\'onde (nm)', fontsize=12)
ax.set_ylabel('Absorbance normalisée', fontsize=12)
ax.set_title('Spectres purs résolus par MCR-ALS', fontsize=14)
[Link]()
[Link](True, alpha=0.3)
plt.tight_layout()
[Link]()
# Figure 2: Comparaison libre vs adsorbé
fig, ax = [Link](figsize=(12, 6))
[Link](wl_common, ST3[1, :], 'g-', linewidth=2, label='RB5 libre',
alpha=0.8)
[Link](wl_common, ST3[2, :], 'r-', linewidth=2, label='RB5 adsorbé',
alpha=0.8)
if abs(shift) > 2:
[Link](peak_free-10, peak_ads+10, alpha=0.2, color='yellow',
label=f'Shift = {shift:.1f} nm')
ax.set_xlabel('Longueur d\'onde (nm)', fontsize=12)
ax.set_ylabel('Absorbance normalisée', fontsize=12)
ax.set_title('Comparaison: Colorant RB5 libre vs adsorbé', fontsize=14)
[Link]()
[Link](True, alpha=0.3)
plt.tight_layout()
[Link]()
# Figure 3: Déconvolution du mélange
fig, axes = [Link](1, 3, figsize=(15, 5))
# Contribution NP
axes[0].plot(wl_common, C3[2, 0] * ST3[0, :], 'b-', linewidth=2)
axes[0].set_title(f'Nanoparticules (conc. = {C3[2, 0]:.3f})',
fontsize=12)
axes[0].set_xlabel('Longueur d\'onde (nm)')
axes[0].grid(True, alpha=0.3)
# Contribution RB5 libre
axes[1].plot(wl_common, C3[2, 1] * ST3[1, :], 'g-', linewidth=2)
axes[1].set_title(f'RB5 libre (conc. = {C3[2, 1]:.3f})', fontsize=12)
axes[1].set_xlabel('Longueur d\'onde (nm)')
axes[1].grid(True, alpha=0.3)
# Contribution RB5 adsorbé
axes[2].plot(wl_common, C3[2, 2] * ST3[2, :], 'r-', linewidth=2)
axes[2].set_title(f'RB5 adsorbé (conc. = {C3[2, 2]:.3f})', fontsize=12)
axes[2].set_xlabel('Longueur d\'onde (nm)')
axes[2].grid(True, alpha=0.3)
[Link]('Contributions individuelles au spectre du mélange',
fontsize=14)
plt.tight_layout()
[Link]()
# Figure 4: Qualité de reconstruction
fig, ax = [Link](figsize=(12, 6))
[Link](wl_common, D[2, :], 'b-', linewidth=2, label='Mélange
expérimental')
[Link](wl_common, D_reconstructed3[2, :], 'r--', linewidth=2,
label='Reconstruit MCR-ALS')
ax.set_xlabel('Longueur d\'onde (nm)', fontsize=12)
ax.set_ylabel('Absorbance normalisée', fontsize=12)
ax.set_title('Qualité de la reconstruction du mélange', fontsize=14)
[Link]()
[Link](True, alpha=0.3)
plt.tight_layout()
[Link]()
# Figure 5: Résidus
fig, ax = [Link](figsize=(12, 5))
residuals = D[2, :] - D_reconstructed3[2, :]
[Link](wl_common, residuals, 'purple', linewidth=1.5)
[Link](y=0, color='k', linestyle='-', linewidth=0.5)
ax.fill_between(wl_common, 0, residuals, alpha=0.3, color='purple')
ax.set_xlabel('Longueur d\'onde (nm)', fontsize=12)
ax.set_ylabel('Résidus', fontsize=12)
ax.set_title(f'Résidus de reconstruction (RMSD =
{[Link]([Link](residuals**2)):.4f})', fontsize=14)
[Link](True, alpha=0.3)
plt.tight_layout()
[Link]()
# Figure 6: Courbe de convergence
fig, ax = [Link](figsize=(10, 5))
[Link](mcr3.loss_history, 'b-', linewidth=2)
ax.set_xlabel('Itération', fontsize=12)
ax.set_ylabel('Erreur relative (RSS)', fontsize=12)
ax.set_title('Convergence de MCR-ALS', fontsize=14)
ax.set_yscale('log')
[Link](True, alpha=0.3)
plt.tight_layout()
[Link]()
#
=======================================================================
=====
# 9. EXPORT DES RÉSULTATS
#
=======================================================================
=====
print("\n" + "=" * 60)
print("EXPORT DES RÉSULTATS")
print("=" * 60)
results_df = [Link]()
results_df['Longueur_donde_nm'] = wl_common
results_df['Nanoparticules'] = ST3[0, :]
results_df['RB5_libre'] = ST3[1, :]
results_df['RB5_adsorbe'] = ST3[2, :]
results_df['Melange_experimental'] = D[2, :]
results_df['Melange_reconstruit'] = D_reconstructed3[2, :]
results_df['Residus'] = residuals
csv_filename = 'resultats_MCR_ALS_complet.csv'
results_df.to_csv(csv_filename, index=False)
print(f"✓ Résultats sauvegardés dans '{csv_filename}'")
[Link](csv_filename)
# Rapport texte
report = f"""
=======================================================================
=========
RAPPORT MCR-ALS - ADSORPTION RB5 SUR NANOPARTICULES Co₃O₄
=======================================================================
=========
📊 RÉSULTATS SPECTROSCOPIQUES:
• Longueur d'onde max RB5 libre: {peak_free:.1f} nm
• Longueur d'onde max RB5 adsorbé: {peak_ads:.1f} nm
• Shift spectral: {shift:+.1f} nm
📈 DISTRIBUTION DU COLORANT DANS LE MÉLANGE:
• RB5 libre: {pct_free:.1f}%
• RB5 adsorbé: {pct_ads:.1f}%
• Rapport adsorbé/libre: {pct_ads/pct_free:.2f}
🎯 QUALITÉ DU MODÈLE:
• R² de reconstruction: {1 -
[Link](residuals**2)/[Link](D[2,:]**2):.4f}
• RMSD: {[Link]([Link](residuals**2)):.4f}
• Itérations MCR-ALS: {len(mcr3.loss_history)}
=======================================================================
=========
"""
with open('rapport_adsorption.txt', 'w') as f:
[Link](report)
print("✓ Rapport sauvegardé")
[Link]('rapport_adsorption.txt')
print("\n" + "=" * 60)
print("✓ ANALYSE TERMINÉE AVEC SUCCÈS")
print("=" * 60)
print("\n📁 FICHIERS PRODUITS:")
print(" • resultats_MCR_ALS_complet.csv (données complètes)")
print(" • rapport_adsorption.txt (résumé)")
print("\n✅ Vous avez maintenant les spectres du colorant LIBRE et
ADSORBÉ!")
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-
packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-
packages (2.0.2)
Requirement already satisfied: scikit-learn in
/usr/local/lib/python3.12/dist-packages (1.6.1)
Requirement already satisfied: matplotlib in
/usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: openpyxl in /usr/local/lib/python3.12/dist-
packages (3.1.5)
Requirement already satisfied: scipy in /usr/local/lib/python3.12/dist-
packages (1.16.3)
Requirement already satisfied: python-dateutil>=2.8.2 in
/usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in
/usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in
/usr/local/lib/python3.12/dist-packages (from pandas) (2026.1)
Requirement already satisfied: joblib>=1.2.0 in
/usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.3)
Requirement already satisfied: threadpoolctl>=3.1.0 in
/usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)
Requirement already satisfied: contourpy>=1.0.1 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (4.62.1)
Requirement already satisfied: kiwisolver>=1.3.1 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (26.1)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-
packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in
/usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)
Requirement already satisfied: et-xmlfile in
/usr/local/lib/python3.12/dist-packages (from openpyxl) (2.0.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-
packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
===========================================================================
=====
MCR-ALS pour analyse UV-Visible (Nanoparticules Co3O4 + Colorant RB5)
Avec séparation colorant libre vs adsorbé
===========================================================================
=====
============================================================
ÉTAPE 1: TÉLÉVERSEMENT DES FICHIERS
============================================================
Veuillez sélectionner vos 3 fichiers Excel:
1. Spectre des nanoparticules seules (Co3O4)
2. Spectre du colorant RB5 seul
3. Spectre du mélange (Nanoparticules + RB5)
--- Téléversement fichier 1/3: Nanoparticules ---
• nanoparticule [Link](application/[Link]-
[Link]) - 41012 bytes, last modified: n/a - 100% done
Saving nanoparticule [Link] to nanoparticule seul (1).xlsx
✓ Fichier chargé: nanoparticule seul (1).xlsx
--- Téléversement fichier 2/3: RB5 ---
• colorant [Link](application/[Link])
- 39422 bytes, last modified: n/a - 100% done
Saving colorant [Link] to colorant seule (1).xlsx
✓ Fichier chargé: colorant seule (1).xlsx
--- Téléversement fichier 3/3: Melange ---
• mélange nanoparticule + [Link](application/[Link]-
[Link]) - 46346 bytes, last modified: n/a - 100% done
Saving mélange nanoparticule + [Link] to mélange nanoparticule +
colorant (1).xlsx
✓ Fichier chargé: mélange nanoparticule + colorant (1).xlsx
✓ TOUS LES FICHIERS ONT ÉTÉ CHARGÉS
✓ Prétraitement appliqué
============================================================
ÉTAPE: DÉCONVOLUTION LIBRE/ADSORBÉ
============================================================
Optimisation avec 2 composants...
✓ Convergence à l'itération 2
✓ Spectre du colorant adsorbé estimé
============================================================
ÉTAPE: MCR-ALS AVEC 3 COMPOSANTS
============================================================
Optimisation avec 3 composants...
✓ Convergence à l'itération 16
RÉSULTATS QUANTITATIFS:
• Pic RB5 libre: 601.6 nm
• Pic RB5 adsorbé: 225.2 nm
• Shift spectral: -376.4 nm
• RB5 libre dans mélange: 82.4%
• RB5 adsorbé dans mélange: 17.6%
============================================================
EXPORT DES RÉSULTATS
============================================================
✓ Résultats sauvegardés dans 'resultats_MCR_ALS_complet.csv'
✓ Rapport sauvegardé
============================================================
✓ ANALYSE TERMINÉE AVEC SUCCÈS
============================================================
FICHIERS PRODUITS:
• resultats_MCR_ALS_complet.csv (données complètes)
• rapport_adsorption.txt (résumé)
✅ Vous avez maintenant les spectres du colorant LIBRE et ADSORBÉ!