-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathblind_clustering.py
More file actions
112 lines (97 loc) · 4.6 KB
/
Copy pathblind_clustering.py
File metadata and controls
112 lines (97 loc) · 4.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
"""
Blind Clustering Experiment: Automatic Semantic Embeddings + Permutation Test
==============================================================================
This experiment removes all manual keyword labeling.
Semantic structure is discovered automatically from MW definitions using TF-IDF.
Phonological structure comes from articulation locus.
Alignment is measured by Adjusted Rand Index with permutation test.
No researcher choices about axis labels can influence the result.
"""
import sys, numpy as np, warnings
warnings.filterwarnings('ignore')
sys.path.insert(0, '/home/claude/experiment')
from experiment_v3_option_b import ROOTS
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score, normalized_mutual_info_score
from sklearn.decomposition import PCA
from sklearn.preprocessing import LabelEncoder
np.random.seed(42)
GROUP_ORDER = ['THROAT','PALATE','CEREBRAL','DENTAL','LABIAL']
phonological_labels = np.array([GROUP_ORDER.index(r['group']) for r in ROOTS])
definitions = [r['mw_def'] for r in ROOTS]
print("="*65)
print("BLIND CLUSTERING EXPERIMENT")
print("Automatic semantic clustering vs phonological grouping")
print("="*65)
print(f"\nDataset: {len(ROOTS)} Sanskrit roots")
print("Semantic source: Monier-Williams definitions (automatic TF-IDF)")
print("No manual axis labels. No researcher keyword choices.")
# Step 1: Automatic semantic representation
tfidf = TfidfVectorizer(max_features=300, ngram_range=(1,2), min_df=1)
X_semantic = tfidf.fit_transform(definitions).toarray()
pca = PCA(n_components=20, random_state=42)
X_reduced = pca.fit_transform(X_semantic)
print(f"\nTF-IDF shape: {X_semantic.shape}")
print(f"After PCA(20): {X_reduced.shape}, variance: {pca.explained_variance_ratio_.sum():.1%}")
# Step 2: Automatic semantic clustering (k=5, matching phonological groups)
km = KMeans(n_clusters=5, random_state=42, n_init=20)
semantic_cluster_labels = km.fit_predict(X_reduced)
# Step 3: Compute alignment between semantic clusters and phonological groups
real_ari = adjusted_rand_score(phonological_labels, semantic_cluster_labels)
real_nmi = normalized_mutual_info_score(phonological_labels, semantic_cluster_labels)
print(f"\nReal ARI (semantic clusters vs phonological groups): {real_ari:.4f}")
print(f"Real NMI: {real_nmi:.4f}")
# Step 4: Permutation test (shuffle phonological labels 1000 times)
print("\nRunning permutation test (1000 shuffles)...")
n_perms = 1000
perm_ari_scores = []
for i in range(n_perms):
shuffled = phonological_labels.copy()
np.random.shuffle(shuffled)
perm_ari_scores.append(adjusted_rand_score(shuffled, semantic_cluster_labels))
perm_ari = np.array(perm_ari_scores)
p_value = (perm_ari >= real_ari).mean()
print(f"\n{'='*65}")
print("PERMUTATION TEST RESULTS")
print(f"{'='*65}")
print(f"Real ARI: {real_ari:.4f}")
print(f"Permutation mean: {perm_ari.mean():.4f}")
print(f"Permutation std: {perm_ari.std():.4f}")
print(f"Permutation max: {perm_ari.max():.4f}")
print(f"p-value: {p_value:.4f} ({int(p_value*n_perms)}/{n_perms} permutations >= real ARI)")
if p_value < 0.001:
sig = "p < 0.001 ***"
elif p_value < 0.01:
sig = "p < 0.01 **"
elif p_value < 0.05:
sig = "p < 0.05 *"
else:
sig = "not significant"
print(f"Significance: {sig}")
# Step 5: Per-cluster breakdown — what phonological groups dominate each semantic cluster?
print(f"\n{'='*65}")
print("SEMANTIC CLUSTER COMPOSITION")
print("(Which phonological groups dominate each automatically-found cluster?)")
print(f"{'='*65}")
from collections import Counter
for cluster_id in range(5):
mask = semantic_cluster_labels == cluster_id
roots_in_cluster = [r for r, m in zip(ROOTS, mask) if m]
group_counts = Counter(r['group'] for r in roots_in_cluster)
dominant = group_counts.most_common(1)[0]
print(f"\nCluster {cluster_id} (n={mask.sum()}):")
for g in GROUP_ORDER:
bar = '█' * group_counts.get(g, 0)
print(f" {g:<10}: {group_counts.get(g,0):2d} {bar}")
print(f" -> Dominant: {dominant[0]} ({dominant[1]}/{mask.sum()})")
# Step 6: Effect size
z_score = (real_ari - perm_ari.mean()) / perm_ari.std() if perm_ari.std() > 0 else 0
print(f"\n{'='*65}")
print("SUMMARY")
print(f"{'='*65}")
print(f"ARI = {real_ari:.4f} ({z_score:.1f} standard deviations above permutation mean)")
print(f"p = {p_value:.4f} — {sig}")
print(f"\nConclusion: Automatic semantic clustering of MW definitions")
print(f"aligns with phonological grouping {'significantly' if p_value < 0.05 else 'non-significantly'}")
print(f"beyond chance, providing bias-free evidence of phonosemantic structure.")