240 lines
8.5 KiB
Python
240 lines
8.5 KiB
Python
import sys
|
|
sys.stdout.reconfigure(encoding='utf-8')
|
|
import pandas as pd
|
|
import numpy as np
|
|
import scipy.stats as stats
|
|
from scipy.cluster.hierarchy import linkage, fcluster
|
|
import matplotlib.pyplot as plt
|
|
import seaborn as sns
|
|
from factor_analyzer.factor_analyzer import calculate_kmo, calculate_bartlett_sphericity
|
|
import os
|
|
|
|
# --- Helper function for Varimax Rotation ---
|
|
def varimax(loadings, max_iter=500, tolerance=1e-6):
|
|
X = loadings.copy()
|
|
n_rows, n_cols = X.shape
|
|
if n_cols < 2:
|
|
return X, np.eye(n_cols)
|
|
|
|
R = np.eye(n_cols)
|
|
d = 0
|
|
for i in range(max_iter):
|
|
d_old = d
|
|
Lambda = np.dot(X, R)
|
|
grad = np.dot(X.T, Lambda**3 - (1.0 / n_rows) * np.dot(Lambda, np.diag(np.sum(Lambda**2, axis=0))))
|
|
u, s, vh = np.linalg.svd(grad)
|
|
R = np.dot(u, vh)
|
|
d = np.sum(s)
|
|
if d_old != 0 and (d - d_old) / d_old < tolerance:
|
|
break
|
|
|
|
rotated_loadings = np.dot(X, R)
|
|
return rotated_loadings, R
|
|
|
|
# --- Setup Paths ---
|
|
base_dir = r"c:\Users\NASPC\Documents\Du án tại SG tháng 8"
|
|
output_dir = os.path.join(base_dir, "PCA_HCA_Results")
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
|
|
file_path = os.path.join(base_dir, 'Data_VN_filter_v5.xlsx')
|
|
df = pd.read_excel(file_path)
|
|
|
|
# --- 1. Variables Definition ---
|
|
pca_vars = [
|
|
'Temperature', 'Noise', 'Stormwind', 'Respiratory', # RES
|
|
'Exercises', 'Culture', 'Beauty', 'Education', 'Society', 'Spirit', # CES
|
|
'Dirty', 'Unsafe', 'Danger' # DES
|
|
]
|
|
demographic_vars = ['Gender', 'Career', 'Literacy']
|
|
habit_vars = ['Distance', 'Frequency', 'Time', 'Transportation']
|
|
|
|
# Ensure variables exist and drop NaNs for PCA
|
|
df_pca = df[pca_vars].dropna()
|
|
n_samples = len(df_pca)
|
|
print(f"--- Bước 2: Phân tích thành phần chính (PCA) ---")
|
|
print(f"Cỡ mẫu (N): {n_samples}")
|
|
|
|
# --- 2. KMO & Bartlett's Test ---
|
|
kmo_all, kmo_model = calculate_kmo(df_pca)
|
|
bartlett_stat, bartlett_p = calculate_bartlett_sphericity(df_pca)
|
|
|
|
print("\nBảng 1: Kaiser-Meyer-Olkin Test")
|
|
print(f"Overall MSA: {kmo_model:.5f}")
|
|
for var, kmo_val in zip(pca_vars, kmo_all):
|
|
print(f" {var}: {kmo_val:.5f}")
|
|
|
|
print("\nBảng 2: Bartlett's Test of Sphericity")
|
|
df_bartlett = len(pca_vars) * (len(pca_vars) - 1) / 2
|
|
print(f"X^2: {bartlett_stat:.5f}, df: {int(df_bartlett)}, p: {bartlett_p}")
|
|
|
|
# --- 3. PCA with Varimax Rotation ---
|
|
# Standardize with ddof=1 to match JASP
|
|
X_mean = df_pca.mean()
|
|
X_std = df_pca.std(ddof=1)
|
|
X_scaled = (df_pca - X_mean) / X_std
|
|
|
|
# Correlation matrix
|
|
R_corr = np.corrcoef(df_pca.T)
|
|
eigenvalues, eigenvectors = np.linalg.eigh(R_corr)
|
|
|
|
# Sort descending
|
|
idx = np.argsort(eigenvalues)[::-1]
|
|
eigenvalues = eigenvalues[idx]
|
|
eigenvectors = eigenvectors[:, idx]
|
|
|
|
print("\nBảng 6: Component Characteristics (Unrotated)")
|
|
for i, ev in enumerate(eigenvalues[:5]): # Print top 5 for illustration
|
|
print(f"Component {i+1}: Eigenvalue = {ev:.5f}, Proportion = {ev/sum(eigenvalues):.5f}")
|
|
|
|
# Extract 2 components
|
|
unrotated_loadings = eigenvectors[:, :2] * np.sqrt(eigenvalues[:2])
|
|
rotated_loadings, R = varimax(unrotated_loadings)
|
|
|
|
# Align signs with JASP: PC1 positive for Beauty, PC2 positive for Dirty
|
|
idx_beauty = pca_vars.index('Beauty')
|
|
idx_dirty = pca_vars.index('Dirty')
|
|
|
|
if rotated_loadings[idx_beauty, 0] < 0:
|
|
rotated_loadings[:, 0] = -rotated_loadings[:, 0]
|
|
R[:, 0] = -R[:, 0]
|
|
if rotated_loadings[idx_dirty, 1] < 0:
|
|
rotated_loadings[:, 1] = -rotated_loadings[:, 1]
|
|
R[:, 1] = -R[:, 1]
|
|
|
|
uniqueness = 1 - np.sum(rotated_loadings**2, axis=1)
|
|
|
|
print("\nBảng 5: Component Loadings (Varimax Rotated)")
|
|
loadings_df = pd.DataFrame(rotated_loadings, index=pca_vars, columns=['PC1', 'PC2'])
|
|
loadings_df['Uniqueness'] = uniqueness
|
|
print(loadings_df.round(5).to_string())
|
|
|
|
# Save Loadings to CSV
|
|
loadings_df.to_csv(os.path.join(output_dir, 'PCA_Component_Loadings.csv'))
|
|
|
|
# --- 4. Plot PCA Loading Biplot ---
|
|
plt.figure(figsize=(10, 8))
|
|
plt.scatter(rotated_loadings[:, 0], rotated_loadings[:, 1], color='blue', alpha=0.5)
|
|
for i, txt in enumerate(pca_vars):
|
|
plt.annotate(txt, (rotated_loadings[i, 0], rotated_loadings[i, 1]), xytext=(5,5), textcoords='offset points')
|
|
plt.axhline(0, color='black',linewidth=1, ls='--')
|
|
plt.axvline(0, color='black',linewidth=1, ls='--')
|
|
plt.xlabel('Component 1 (ESS)')
|
|
plt.ylabel('Component 2 (DES)')
|
|
plt.title('PCA Loading Plot (Varimax Rotated)')
|
|
plt.grid(True, linestyle=':', alpha=0.6)
|
|
plt.savefig(os.path.join(output_dir, 'PCA_Loading_Plot.png'), dpi=300)
|
|
plt.close()
|
|
|
|
# --- 5. Component Scores for HCA ---
|
|
# Calculate Standardized Component Scores
|
|
scores_std = (np.dot(X_scaled, eigenvectors[:, :2]) / np.sqrt(eigenvalues[:2])).dot(R)
|
|
|
|
print("\n--- Bước 3: Phân tích cụm phân cấp (HCA) & Chi-square ---")
|
|
# --- 6. HCA (Ward's Method) ---
|
|
Z = linkage(scores_std, method='ward')
|
|
clusters = fcluster(Z, 3, criterion='maxclust')
|
|
|
|
# Map clusters to original dataframe
|
|
# Note: Since we dropped NaNs for PCA, we need to carefully assign back
|
|
# Assuming original df has no NaNs in these columns based on previous logs (N=307 valid).
|
|
# Just to be safe, we assign via index.
|
|
df.loc[df_pca.index, 'Cluster'] = clusters
|
|
df.loc[df_pca.index, 'PC1_Score'] = scores_std[:, 0]
|
|
df.loc[df_pca.index, 'PC2_Score'] = scores_std[:, 1]
|
|
|
|
# Align cluster labels with JASP based on sizes (199, 61, 47) and Means
|
|
cluster_sizes = df['Cluster'].value_counts()
|
|
print(f"\nGiai đoạn 3.1 - Gom cụm (HCA)")
|
|
print("Cluster Sizes (Before Label Alignment):")
|
|
print(cluster_sizes)
|
|
|
|
# Map our generic cluster IDs (1, 2, 3) to JASP's cluster IDs
|
|
# From our previous test:
|
|
# Our Cluster with size 199 -> JASP Cluster 3 (Hài hòa & Thụ hưởng)
|
|
# Our Cluster with size 61 -> JASP Cluster 2 (Thờ ơ)
|
|
# Our Cluster with size 47 -> JASP Cluster 1 (Thực dụng & Lo ngại)
|
|
mapping = {}
|
|
for clst, size in cluster_sizes.items():
|
|
if size == 199:
|
|
mapping[clst] = 3
|
|
elif size == 61:
|
|
mapping[clst] = 2
|
|
elif size == 47:
|
|
mapping[clst] = 1
|
|
else:
|
|
mapping[clst] = clst # fallback
|
|
|
|
df['Cluster'] = df['Cluster'].map(mapping)
|
|
print("\nCluster Sizes (Aligned with JASP):")
|
|
print(df['Cluster'].value_counts())
|
|
|
|
cluster_means = df.groupby('Cluster')[['PC1_Score', 'PC2_Score']].mean()
|
|
print("\nBảng 3: Cluster Means")
|
|
print(cluster_means.round(5))
|
|
|
|
# Plot Cluster Means
|
|
cluster_means.plot(kind='bar', figsize=(10, 6))
|
|
plt.title('Cluster Means for PC1 and PC2')
|
|
plt.ylabel('Mean Standardized Score')
|
|
plt.xlabel('Cluster')
|
|
plt.axhline(0, color='black', linewidth=0.8, ls='--')
|
|
plt.xticks(rotation=0)
|
|
plt.savefig(os.path.join(output_dir, 'HCA_Cluster_Means_Plot.png'), dpi=300)
|
|
plt.close()
|
|
|
|
# Save DataFrame with Clusters
|
|
output_dataset_path = os.path.join(base_dir, 'Data_VN_filter_v5_with_clusters.xlsx')
|
|
df.to_excel(output_dataset_path, index=False)
|
|
print(f"\nĐã lưu dataset mới kèm nhãn Cụm và Điểm nhân tố tại: {output_dataset_path}")
|
|
|
|
# --- 7. Profiling (Chi-Square) ---
|
|
def compute_cramer_v(chi2, n, shape):
|
|
return np.sqrt(chi2 / (n * (min(shape) - 1)))
|
|
|
|
def profile_clusters(df, variables, title):
|
|
print(f"\n{title}")
|
|
results = []
|
|
|
|
for var in variables:
|
|
if var not in df.columns:
|
|
continue
|
|
# Contingency table (cross-tabulation)
|
|
ct = pd.crosstab(df['Cluster'], df[var])
|
|
|
|
# Chi-square test
|
|
chi2, p, dof, expected = stats.chi2_contingency(ct)
|
|
n = ct.sum().sum()
|
|
|
|
# Cramer's V and Phi
|
|
v = compute_cramer_v(chi2, n, ct.shape)
|
|
|
|
print(f"\n--- {var} ---")
|
|
print("Contingency Table (Counts):")
|
|
print(ct)
|
|
print(f"Chi-Square: {chi2:.5f}, df: {dof}, p: {p:.5f}")
|
|
print(f"Cramer's V (Contingency coefficient approx): {v:.5f}")
|
|
|
|
results.append({
|
|
'Variable': var,
|
|
'Chi_Square': chi2,
|
|
'df': dof,
|
|
'p_value': p,
|
|
'Cramers_V': v
|
|
})
|
|
|
|
# Detailed Table (like JASP) with row percentages
|
|
ct_pct = ct.div(ct.sum(axis=1), axis=0) * 100
|
|
# save detailed to csv just in case
|
|
ct_detailed = pd.concat([ct, ct_pct.add_suffix('_Pct')], axis=1)
|
|
ct_detailed.to_csv(os.path.join(output_dir, f'Profiling_{var}_Contingency.csv'))
|
|
|
|
res_df = pd.DataFrame(results)
|
|
res_df.to_csv(os.path.join(output_dir, f'Profiling_{title.replace(" ", "_")}_Summary.csv'), index=False)
|
|
|
|
profile_clusters(df, demographic_vars, "Lần so sánh 1: Nhân khẩu học (Demographics)")
|
|
profile_clusters(df, habit_vars, "Lần so sánh 2: Thói quen tương tác (Habits)")
|
|
|
|
print("\n--- HOÀN THÀNH ---")
|
|
print(f"Các bảng kết quả và biểu đồ đã được lưu tại: {output_dir}")
|