Summary
In causallearn/search/HiddenCausal/GIN/GIN.py, the GIN residual is tested against data[:, [z]] where z is a loop counter over positions, not a variable index. The residual is therefore tested against the first len(remain_var_set) columns of data, which include the candidate cluster's own columns, instead of against the remaining variables. The returned clusters then depend on the column order of the input, which the GIN condition does not.
Where
Cluster search, lines 67-68:
remain_var_set = var_set - set(cluster)
e = cal_e_with_gin(data, cov, list(cluster), list(remain_var_set))
pvals = []
for z in range(len(remain_var_set)):
pvals.append(indep_test(data[:, [z]], e[:, None], method=indep_test_method))
Causal-order phase, lines 98-99, same pattern:
for z in range(len(Z + cluster_i2)):
pvals.append(indep_test(data[:, [z]], e[:, None], method=indep_test_method))
cal_e_with_gin is given the correct variable lists, so only the independence-test loop is affected. GIN_MI in the same file does not use positional indexing.
Reproduction
The GIN condition is a statement about variables, so relabelling columns must not change the clusters, up to that relabelling. It does:
import numpy as np
from causallearn.search.HiddenCausal.GIN.GIN import GIN
def two_factor(n=500, seed=0):
rng = np.random.default_rng(seed)
L1 = rng.uniform(-1, 1, n)
L2 = 0.6 * L1 + rng.uniform(-1, 1, n)
cols = []
for L in (L1, L2):
for lam in (0.9, 0.8, 0.7):
cols.append(lam * L + 0.5 * rng.uniform(-1, 1, n))
return np.column_stack(cols)
X = two_factor()
perm = np.array([3, 4, 5, 0, 1, 2]) # swap the two factor blocks
_, order_a = GIN(X, indep_test_method='hsic', alpha=0.05)
_, order_b = GIN(X[:, perm], indep_test_method='hsic', alpha=0.05)
print([sorted(c) for c in order_a]) # []
print([sorted(perm[i] for i in c) for c in order_b]) # [[1, 2]]
Two latent factors, three pure indicators each, uniform (non-Gaussian) noise. The same data in a different column order gives no clusters in one case and one cluster in the other.
Suggested fix
Iterate over the variables rather than over positions:
remain = sorted(remain_var_set)
for z in remain:
pvals.append(indep_test(data[:, [z]], e[:, None], method=indep_test_method))
and likewise for z in Z + cluster_i2: in the causal-order phase.
Environment
causal-learn 0.1.4.7 at commit dd13787, Python 3.12, numpy 2.x.
Summary
In
causallearn/search/HiddenCausal/GIN/GIN.py, the GIN residual is tested againstdata[:, [z]]wherezis a loop counter over positions, not a variable index. The residual is therefore tested against the firstlen(remain_var_set)columns ofdata, which include the candidate cluster's own columns, instead of against the remaining variables. The returned clusters then depend on the column order of the input, which the GIN condition does not.Where
Cluster search, lines 67-68:
Causal-order phase, lines 98-99, same pattern:
cal_e_with_ginis given the correct variable lists, so only the independence-test loop is affected.GIN_MIin the same file does not use positional indexing.Reproduction
The GIN condition is a statement about variables, so relabelling columns must not change the clusters, up to that relabelling. It does:
Two latent factors, three pure indicators each, uniform (non-Gaussian) noise. The same data in a different column order gives no clusters in one case and one cluster in the other.
Suggested fix
Iterate over the variables rather than over positions:
and likewise
for z in Z + cluster_i2:in the causal-order phase.Environment
causal-learn 0.1.4.7 at commit dd13787, Python 3.12, numpy 2.x.