import numpy as np
import pandas as pd
from numba import njit
import urbangrammar_graphics as ugg
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
Simulated Annealing
# Generate a random 12x12 confusion matrix with integer values
42) # For reproducibility
np.random.seed(= np.random.randint(0, 100, size=(12, 12))
conf_matrix
# Define class labels
= {
class_dict 'Accessible suburbia': 0,
'Connected residential neighbourhoods': 1,
'Countryside agriculture': 2,
'Dense residential neighbourhoods': 3,
'Dense urban neighbourhoods': 4,
'Disconnected suburbia': 5,
'Gridded residential quarters': 6,
'Open sprawl': 7,
'Urban buffer': 8,
'Urbanity': 9,
'Warehouse/Park land': 10,
'Wild countryside': 11
}
# Reverse lookup for labels
= {v: k for k, v in class_dict.items()}
class_labels
# Plot the heatmap
=(10, 8))
plt.figure(figsize=True, fmt='d', cmap=sns.light_palette(ugg.HEX[1], n_colors=100), xticklabels=class_labels, yticklabels=class_labels)
sns.heatmap(conf_matrix, annot"Predicted Label")
plt.xlabel("True Label")
plt.ylabel("12x12 Confusion Matrix")
plt.title( plt.show()
@njit
def nscore(a):
= []
ds = range(len(a))
r for i in r:
for j in r:
if a[i, j] != 0:
abs(i-j))
ds.append(return np.array(ds).mean()
@njit
def wnscore(a):
= []
ds = range(len(a))
r = a.sum()
total for i in r:
for j in r:
= a[i, j]
aij if aij != 0:
abs(i-j) * aij / total)
ds.append(return np.array(ds).mean()
#Bit hacky but fast(er) sorting algorithm that doesn't use all permutations (which takes more than 1h for a 12x12 grid).
def simulated_annealing(a, w=True, max_iters=500000, temp=1.0, cooling_rate=0.995):
"""
Optimizes the row/column order using simulated annealing.
Args:
a: The 2D numpy array.
w: Whether to use weighted nscore.
max_iters: Maximum number of iterations.
temp: Initial temperature for annealing.
cooling_rate: Decay rate for temperature.
Returns:
The best found permutation.
"""
if w:
= wnscore
scorer else:
= nscore
scorer
= len(a)
la
# Initial permutation (sorted by row sums as a heuristic)
= np.argsort(a.sum(axis=1))
current_perm = current_perm.copy()
best_perm = scorer(a[current_perm, :][:, current_perm])
best_score
for _ in range(max_iters):
*= cooling_rate # Reduce temperature
temp
# Swap two random indices
= current_perm.copy()
new_perm = np.random.choice(la, 2, replace=False)
i, j = new_perm[j], new_perm[i]
new_perm[i], new_perm[j]
# Compute new score
= scorer(a[new_perm, :][:, new_perm])
new_score
# Accept new permutation if it's better or with a probability
if new_score < best_score or np.exp((best_score - new_score) / temp) > np.random.rand():
= new_perm
current_perm = new_score
best_score = new_perm
best_perm
# Stop early if temperature is too low
if temp < 1e-6:
break
return best_perm
#cols = list(class_labels.values()) # Get class labels as list
= list(range(0,12))
cols = pd.DataFrame(conf_matrix) # Copy the transition matrix
tab
# ensure column names match correctly
#cols = [i.strip() for i in cols] # Remove any unwanted spaces
= tab.loc[cols, cols] # Reorder based on labels
stab
# remove the diagonal
= stab.copy()
stab_no_diag 0) # Set diagonal to zero
np.fill_diagonal(stab_no_diag.values,
# Sorting
= simulated_annealing(stab_no_diag.values, w=False) # Unweighted score sorting
order = simulated_annealing(stab_no_diag.values, w=True) # Weighted score sorting
weighted_order
= stab.iloc[order, order] # Reorder rows and columns stab_sorted
=(10, 8))
plt.figure(figsize
# Plot heatmap
0, np.nan).astype('float'),
sns.heatmap((stab_no_diag.iloc[weighted_order, weighted_order]).replace(=sns.light_palette(ugg.HEX[1], n_colors=100),
cmap=True)
annot
# Show plot
plt.show()