Analyse PLS-SEM
Comprendre, exĂ©cuter et interprĂ©ter votre modĂšle d’Ă©quations structurelles par moindres carrĂ©s partiels.
Qu’est-ce que le PLS-SEM ?
Le PLS-SEM (Partial Least Squares â Structural Equation Modeling), ou « modĂšle d’Ă©quations structurelles par moindres carrĂ©s partiels », est une mĂ©thode statistique qui permet de tester simultanĂ©ment des liens de cause Ă effet entre des concepts abstraits â appelĂ©s variables latentes â qui ne peuvent pas se mesurer directement.
Vous ne pouvez pas mesurer l’« UtilitĂ© perçue » d’un robot avec un thermomĂštre. En revanche, vous pouvez poser 4 ou 5 questions sur une Ă©chelle de 1 Ă 5 :
« Ce robot m’aide Ă accomplir mes tĂąches plus efficacement », « Utiliser ce robot amĂ©liore ma productivitĂ© »âŠ
Le PLS transforme ces rĂ©ponses en un score synthĂ©tique (« variable latente »), puis teste si ce score influence d’autres concepts comme l’Intention d’utilisation. C’est ça, un modĂšle PLS : des flĂšches de causalitĂ© entre des concepts abstraits, tous construits Ă partir de vos questions de terrain.
Comment ça fonctionne, étape par étape
Les 4 critÚres-clés que le code calcule pour vous
Alpha de Cronbach
Mesure si les questions d’un mĂȘme bloc sont cohĂ©rentes entre elles. Si le score est faible, vos rĂ©pondants n’ont pas compris les questions de la mĂȘme façon.
Seuil â„ 0,70AVE
Average Variance Extracted : vĂ©rifie que vos questions capturent bien le concept qu’elles sont censĂ©es mesurer, et non du bruit alĂ©atoire.
Seuil â„ 0,50VIF
Variance Inflation Factor : s’assure que deux variables ne font pas « double emploi ». Un VIF Ă©levĂ© rend les coefficients instables et peu fiables.
Seuil < 5,00Matrice HTMT
Test le plus sĂ©vĂšre : prouve que deux concepts sont bien distincts dans l’esprit du rĂ©pondant. Si le score est trop proche de 1, les concepts se confondent.
Seuil < 0,85 â 0,90Avant de lancer le code : Ă©tape cruciale
Le script Python lit vos données depuis un fichier Excel. Vous devez impérativement modifier le nom du fichier à la ligne 10 du code selon votre groupe :
| Votre groupe | Nom du fichier Ă inscrire dans le code |
|---|---|
| L3 MRC et IHR | BASE-MRC-IHR.xlsx |
| L3 MHR Hébergement | BASE-Hbgt.xlsx |
| L3 MHR Restauration | BASE-MHR-Rest.xlsx |
Attention : Si le nom ne correspond pas exactement (majuscules, tirets, extensionâŠ), le programme affichera une erreur “File Not Found” et ne pourra pas dĂ©marrer.
Téléchargement des bases de données
Téléchargez le fichier Excel correspondant à votre groupe et importez-le dans votre session Google Colab :
Le code Python (Version Expert)
Ce code calcule automatiquement les Coefficients, le Bootstrap, le VIF, l’AVE et la matrice HTMT. Copiez ce script dans Google Colab :
import pandas as pd
import numpy as np
from sklearn.model_selection import cross_val_predict
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import statsmodels.api as sm
from statsmodels.stats.outliers_influence import variance_inflation_factor
from tqdm import tqdm
# ==========================================
# === 1) CONFIGURATION ET LECTURE DU FICHIER
# ==========================================
FILE_PATH = "/content/BASE-MRC-IHR.xlsx"
SHEET_NAME = "BASE"
df = pd.read_excel(FILE_PATH, sheet_name=SHEET_NAME)
print(f"Fichier chargé : {FILE_PATH} | Taille : {df.shape}")
# Blocs de mesure â construits impliquĂ©s dans H1âH6 + H_Dehum + H_Emploi
blocks = {
"Anthropomorphisme": ["ANT1","ANT2","ANT3","ANT4","ANT5"],
"Utilite": ["UTIL1","UTIL2","UTIL3","UTIL4","UTIL5","UTIL6","UTIL7"],
"Plaisir": ["PLAIS1","PLAIS2","PLAIS3","PLAIS4"],
"RisqueViePrivée": ["RCONTETH1","RCONTETH2","RCONTETH3","RCONTETH4"],
"RisquePsy": ["RPSY1","RPSY2","RPSY3"],
"RisqueDehum": ["RDEHUM1","RDEHUM2","RDEHUM3","RDEHUM4","RDEHUM5"], # NOUVEAU
"RisqueEmploi": ["RPEMP1","RPEMP2","RPEMP3","RPEMP4","RPEMP5"], # NOUVEAU
"Intention": ["INT1","INT2","INT3","INT4"]
}
# Topographie du réseau (Inner Model)
# Ajout de RisqueDehum â Intention et RisqueEmploi â Intention
base_relations = [
("Anthropomorphisme", "Utilite"), # H1
("Anthropomorphisme", "Plaisir"), # H2
("Plaisir", "Intention"), # H3
("Utilite", "Intention"), # H4
("RisqueViePrivée", "Intention"), # H5
("RisquePsy", "Intention"), # H6
("RisqueDehum", "Intention"), # H_Dehum â NOUVEAU
("RisqueEmploi", "Intention") # H_Emploi â NOUVEAU
]
# ModĂšle Structurel (Ăquations)
# Intention intÚgre désormais les deux nouveaux prédicteurs de risque
equations = {
"Utilite": ["Anthropomorphisme"], # H1
"Plaisir": ["Anthropomorphisme"], # H2
"Intention": ["Plaisir", "Utilite", "RisqueViePrivée", "RisquePsy",
"RisqueDehum", "RisqueEmploi"] # H3âH6 + Dehum + Emploi
}
all_items = [item for sublist in blocks.values() for item in sublist]
df_items = df[all_items].dropna().copy()
# ==========================================
# === 2) L'ALGORITHME ITERATIF PLS-SEM
# ==========================================
def run_pls_sem(data_raw, blks, relations, max_iter=100, tol=1e-5):
X = (data_raw - data_raw.mean()) / data_raw.std()
lv_names = list(blks.keys())
C = pd.DataFrame(0.0, index=lv_names, columns=lv_names)
for s, t in relations:
if s in lv_names and t in lv_names:
C.loc[s, t] = 1.0
C.loc[t, s] = 1.0
Y = pd.DataFrame({lv: X[items[0]] for lv, items in blks.items()})
for _ in range(max_iter):
Y_old = Y.copy()
E = Y.corr() * C
Z = Y.dot(E)
Z = (Z - Z.mean()) / Z.std()
W = {}
for lv, items in blks.items():
w = X[items].apply(lambda col: col.cov(Z[lv]))
w = w / np.linalg.norm(w)
W[lv] = w
Y[lv] = X[items].dot(w)
Y = (Y - Y.mean()) / Y.std()
if np.abs(Y - Y_old).max().max() < tol:
break
Loadings = {lv: X[items].apply(lambda col: col.corr(Y[lv])) for lv, items in blks.items()}
return Y, W, Loadings
Y_final, W_final, L_final = run_pls_sem(df_items, blocks, base_relations)
# La fonction est conservée mais ne crée plus de variables d'interaction
def calculate_inner_variables(Y_df):
return Y_df.copy()
Y_complet = calculate_inner_variables(Y_final)
# ==========================================
# === 3) CALCUL DES CRITĂRES DE QUALITĂ
# ==========================================
qualite_list = []
loadings_df_list = []
for lv, loadings in L_final.items():
for item, l_val in loadings.items():
loadings_df_list.append({"Construit": lv, "Item": item, "Loading_Wold": l_val})
n = len(blocks[lv])
corr_matrix = df_items[blocks[lv]].corr()
avg_corr = corr_matrix.values[np.triu_indices(n, k=1)].mean() if n > 1 else 1.0
alpha = (n * avg_corr) / (1 + (n - 1) * avg_corr) if n > 1 else 1.0
ave = np.mean(loadings**2)
sum_l = np.sum(loadings)
sum_e = np.sum(1 - loadings**2)
cr = (sum_l**2) / (sum_l**2 + sum_e)
qualite_list.append({"Variable": lv, "Cronbach_Alpha": alpha, "Composite_Reliability": cr, "AVE": ave})
df_loadings = pd.DataFrame(loadings_df_list)
df_qualite = pd.DataFrame(qualite_list)
X_std = (df_items - df_items.mean()) / df_items.std()
latent_names = list(blocks.keys())
htmt_df = pd.DataFrame(index=latent_names, columns=latent_names, dtype=float)
for i, b1 in enumerate(latent_names):
for j, b2 in enumerate(latent_names):
if i == j:
htmt_df.iloc[i,j] = 1.0
elif i < j:
items1, items2 = blocks[b1], blocks[b2]
hetero_mean = X_std[items1 + items2].corr().loc[items1, items2].values.mean()
mono1 = X_std[items1].corr().values[np.triu_indices(len(items1), k=1)].mean() if len(items1)>1 else 1.0
mono2 = X_std[items2].corr().values[np.triu_indices(len(items2), k=1)].mean() if len(items2)>1 else 1.0
htmt_val = abs(hetero_mean / np.sqrt(max(0.0001, abs(mono1 * mono2))))
htmt_df.iloc[i,j] = htmt_val
htmt_df.iloc[j,i] = htmt_val
fl_matrix = Y_final.corr()
for b in latent_names:
fl_matrix.loc[b, b] = np.sqrt(df_qualite.loc[df_qualite['Variable'] == b, 'AVE'].values[0])
# ==========================================
# === 4) MODELE STRUCTUREL (RÂČ, fÂČ, VIF)
# ==========================================
results_inner = {y: sm.OLS(Y_complet[y], sm.add_constant(Y_complet[xs])).fit() for y, xs in equations.items()}
structurel_data = []
for dep, preds in equations.items():
X_mat = sm.add_constant(Y_complet[preds])
R2_incl = results_inner[dep].rsquared
for i, p in enumerate(preds, start=1):
vif = variance_inflation_factor(X_mat.values, i)
preds_excl = [x for x in preds if x != p]
R2_excl = 0 if not preds_excl else sm.OLS(Y_complet[dep], sm.add_constant(Y_complet[preds_excl])).fit().rsquared
f2 = (R2_incl - R2_excl) / (1 - R2_incl)
structurel_data.append({
"Variable_Dep": dep, "Predictor": p,
"Coefficient": results_inner[dep].params[p],
"f2": f2, "VIF": vif
})
df_structurel = pd.DataFrame(structurel_data)
# ==========================================
# === 5) BOOTSTRAPPING (t-values)
# ==========================================
def run_bootstrap(data_raw, n_boot=500):
rng = np.random.default_rng(123)
boot_coefs = []
for _ in tqdm(range(n_boot), desc="Bootstrapping PLS"):
sample = data_raw.sample(len(data_raw), replace=True, random_state=rng.integers(0, 100000))
Y_boot, _, _ = run_pls_sem(sample, blocks, base_relations, max_iter=30)
Y_boot_comp = calculate_inner_variables(Y_boot)
row = {}
for dep, preds in equations.items():
res = sm.OLS(Y_boot_comp[dep], sm.add_constant(Y_boot_comp[preds])).fit()
for p in preds:
row[(dep, p)] = res.params[p]
boot_coefs.append(row)
return pd.DataFrame(boot_coefs)
boot_df = run_bootstrap(df_items, n_boot=500)
boot_std = boot_df.std()
boot_ci = boot_df.quantile([0.025, 0.975]).T
boot_ci.columns = ["CI_lower", "CI_upper"]
df_structurel["t-value"] = df_structurel.apply(
lambda r: abs(r["Coefficient"] / boot_std[(r["Variable_Dep"], r["Predictor"])]), axis=1
)
df_structurel["p-value"] = [results_inner[r["Variable_Dep"]].pvalues[r["Predictor"]] for _, r in df_structurel.iterrows()]
df_structurel["Significatif_t"] = df_structurel["t-value"] > 1.96
# ==========================================
# === 6) FIT GLOBAL (SRMR) & MACHINE LEARNING (QÂČ / PLSpredict)
# ==========================================
# --- SRMR ---
R_obs = df_items.corr()
R_implied = pd.DataFrame(np.eye(len(all_items)), index=all_items, columns=all_items)
lv_corr = Y_complet[latent_names].corr()
item_lv_map = {item: lv for lv, items in blocks.items() for item in items}
item_loading_map = {item: L_final[lv][item] for lv, items in blocks.items() for item in items}
for i in range(len(all_items)):
for j in range(i+1, len(all_items)):
it1, it2 = all_items[i], all_items[j]
lv1, lv2 = item_lv_map[it1], item_lv_map[it2]
r_imp = item_loading_map[it1] * item_loading_map[it2] * lv_corr.loc[lv1, lv2]
R_implied.loc[it1, it2] = r_imp
R_implied.loc[it2, it1] = r_imp
diff = R_obs.values - R_implied.values
mask = np.triu(np.ones(diff.shape), k=1).astype(bool)
srmr_value = np.sqrt(np.mean(diff[mask]**2))
df_srmr = pd.DataFrame([{
"Metric": "SRMR Global", "Valeur": srmr_value,
"Seuil": "< 0.08 (Idéal) / < 0.10 (Acceptable)",
"Statut": "Excellent" if srmr_value < 0.08 else ("Acceptable" if srmr_value < 0.10 else "Ă revoir")
}])
# --- QÂČ (10-Fold CV sur modĂšle structurel) ---
lr = LinearRegression()
q2_list = []
for dep, preds in equations.items():
y_pred_cv = cross_val_predict(lr, Y_complet[preds], Y_complet[dep], cv=10)
q2_list.append({
"Variable EndogĂšne": dep,
"R2_In_Sample": results_inner[dep].rsquared,
"Q2_Predictive_Relevance": r2_score(Y_complet[dep], y_pred_cv)
})
df_q2 = pd.DataFrame(q2_list)
# --- PLSpredict (Items endogĂšnes - Uniquement sur l'Intention) ---
plspredict_list = []
endog_targets = ["Intention"]
for target_lv in endog_targets:
for item in blocks[target_lv]:
y_item = df_items[item]
exog_lvs = equations[target_lv]
exog_items = list(set([it for ex in exog_lvs if ex in blocks for it in blocks[ex]]))
X_item_lm = df_items[exog_items] if exog_items else df_items.drop(columns=[item])
y_pred_lm = cross_val_predict(lr, X_item_lm, y_item, cv=10)
y_pred_naive = np.full_like(y_item, y_item.mean())
rmse_lm = np.sqrt(mean_squared_error(y_item, y_pred_lm))
rmse_naive = np.sqrt(mean_squared_error(y_item, y_pred_naive))
q2_predict_item = 1 - (mean_squared_error(y_item, y_pred_lm) / mean_squared_error(y_item, y_pred_naive))
plspredict_list.append({
"Cible": target_lv, "Indicateur": item,
"RMSE_Machine_Learning (LM)": rmse_lm, "RMSE_Moyenne_Naive": rmse_naive,
"Q2_predict": q2_predict_item,
"Pouvoir_Predictif": "Fort" if rmse_lm < rmse_naive else "Faible"
})
df_plspredict = pd.DataFrame(plspredict_list)
# ==========================================
# === 7) EXPORT EXCEL EXHAUSTIF
# ==========================================
filename = "Resultats_Analyse_Expert_PLS.xlsx"
with pd.ExcelWriter(filename) as writer:
df_structurel.to_excel(writer, sheet_name='Paths_tvalue_f2_VIF', index=False)
df_srmr.to_excel(writer, sheet_name='Fit_SRMR', index=False)
df_q2.to_excel(writer, sheet_name='Q2_Relevance_Structurelle', index=False)
df_plspredict.to_excel(writer, sheet_name='PLSpredict_Items', index=False)
df_loadings.to_excel(writer, sheet_name='Loadings_Items', index=False)
df_qualite.to_excel(writer, sheet_name='Fiabilite_Mesure', index=False)
fl_matrix.to_excel(writer, sheet_name='Fornell_Larcker')
htmt_df.to_excel(writer, sheet_name='Validite_HTMT')
print(f"ModÚle étendu exécuté avec succÚs. Fichier généré : {filename}")
Comment lire votre fichier Excel ?
Le fichier « Resultats_Analyse_Expert_PLS.xlsx » contient plusieurs onglets. Voici comment interpréter chacun d'eux :
Pour valider vos hypothÚses. Si la colonne « Significatif » affiche True, l'impact causal est réel et statistiquement prouvé.
Vérifiez que l'AVE > 0,50 et l'Alpha de Cronbach > 0,70 pour chaque variable latente.
Tous les VIF doivent ĂȘtre infĂ©rieurs Ă 5. Au-delĂ , les rĂ©sultats sont instables et peu interprĂ©tables.
Chaque score doit ĂȘtre < 0,85 ou 0,90 pour prouver que vos concepts sont bien distincts les uns des autres.
Indique la part de variance expliquée par votre modÚle. Plus proche de 1, plus votre modÚle est puissant.
Mesure la pertinence prédictive : votre modÚle fait-il mieux que de simplement prédire la moyenne ?
AccÚs aux résultats par groupe
Sélectionnez votre groupe ci-dessous pour accéder directement à la page de consultation de vos résultats :