-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathaba.py
More file actions
212 lines (144 loc) · 7.15 KB
/
Copy pathaba.py
File metadata and controls
212 lines (144 loc) · 7.15 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
# © 2026, University of Bern, Group for Business Analytics, Operations Research and Quantitative Methods,
# Philipp Baumann
import numpy as np
from scipy.spatial.distance import cdist
from scipy.optimize import linear_sum_assignment
from joblib import Parallel, delayed
def process_cluster(cluster, labels, X, subclusters, categories):
# Get cluster members
idx = labels == cluster
cluster_members = np.where(idx)[0]
if categories is None:
# Subdivide cluster into subclusters
labels = run_aba(X[cluster_members], subclusters)
else:
# Subdivide cluster into subclusters
labels = run_aba(X[cluster_members], subclusters, categories[cluster_members])
return cluster, cluster_members, labels
def assign_objects(X, centers, batch, categories=None, category_upper_bounds=None, cluster_category_counts=None):
# Compute model input
distances = cdist(X[batch, :], centers, 'sqeuclidean')
if categories is not None:
# Get unique categories of objects in batch
unique_categories = np.unique(categories)
# Check for each cluster if category upper bound has been reached
for category in unique_categories:
# Get clusters which are already full
idx = cluster_category_counts[:, category] == category_upper_bounds[category]
# Adjust distances to prevent a violation of the upper bound
if idx.any():
val = -distances.max().max()
distances[np.ix_(categories == category, idx)] = val
# Solve assignment problem
_, col_ind = linear_sum_assignment(distances, maximize=True)
return col_ind
def get_batches(sorted_objects, n_clusters, categories=None):
if categories is None:
# Split sorted objects into batches of size n_clusters
batches = np.array_split(sorted_objects, range(n_clusters, len(sorted_objects), n_clusters))
return batches
else:
# Get unique categories
unique_categories = np.unique(categories)
# Initialize dictionaries
complete_splits = {}
incomplete_splits = {}
for category in unique_categories:
# Get all objects that belong to this category (according to sequence in sorted_objects)
objects_of_category = sorted_objects[categories == category]
# Split these objects into groups of size n_clusters
splits = np.array_split(objects_of_category, range(n_clusters, len(objects_of_category), n_clusters))
# Store complete and incomplete splits separately
complete_splits[category] = splits[:-1]
incomplete_splits[category] = splits[-1]
# Get total number of complete splits
n_complete_splits = sum(len(splits) for splits in complete_splits.values())
# Initialize a dictionary that keeps track of the number of complete splits already assigned to batches
split_counter = {cat: 0 for cat in unique_categories}
# Create batches by looping through all complete splits (alternating between categories in each iteration)
batches = []
while len(batches) < n_complete_splits:
for cat in unique_categories:
if split_counter[cat] < len(complete_splits[cat]):
batches.append(complete_splits[cat][split_counter[cat]])
split_counter[cat] += 1
# Store incomplete batches in a list
incomplete_batches = list(incomplete_splits.values())
# Flatten batches to ensure that all batches except the last one have cardinality=n_clusters
sorted_objects = np.concatenate(batches + incomplete_batches)
splits = np.arange(n_clusters, len(sorted_objects), n_clusters)
batches = np.array_split(sorted_objects, splits)
return batches, n_complete_splits
def run_aba(X, n_anticlusters, categories=None):
# Convert X to float numpy array
X = np.array(X).astype(float)
# Check if n_clusters is a list, tuple or ndarray
if isinstance(n_anticlusters, list) or isinstance(n_anticlusters, tuple) or isinstance(n_anticlusters, np.ndarray):
if len(n_anticlusters) > 1:
n_anticlusters_list = n_anticlusters[1:]
n_anticlusters = n_anticlusters[0]
else:
n_anticlusters_list = []
n_anticlusters = n_anticlusters[0]
else:
n_anticlusters_list = []
# Get number of objects
n_objects = X.shape[0]
# Initialize labels
labels = np.full(n_objects, -1)
# Compute distances to global center
global_center = X.mean(axis=0)
distances = cdist(X, [global_center], 'sqeuclidean')
# Sort objects in descending distance from global center
sorted_objects = np.argsort(-distances[:, 0])
# Get batches
if categories is None:
batches = get_batches(sorted_objects, n_anticlusters)
else:
batches, n_complete_batches = get_batches(sorted_objects, n_anticlusters, categories[sorted_objects])
# Get unique categories and the counts for each category
unique_categories, category_counts = np.unique(categories, return_counts=True)
# Get number of unique categories
n_categories = len(unique_categories)
# Initialize cluster category counts
cluster_category_counts = np.zeros((n_anticlusters, n_categories))
# Compute upper bounds on category counts
category_upper_bounds = np.ceil(category_counts / n_anticlusters)
# Update cluster_category_counts with first batch
cluster_category_counts[np.arange(n_anticlusters), categories[batches[0]]] += 1
# Initialize centers
centers = X[batches[0], :]
# Assign first objects to centers
labels[batches[0]] = np.arange(n_anticlusters)
# Process batches
for i, batch in enumerate(batches[1:]):
# Assign objects
if categories is None:
batch_labels = assign_objects(X, centers, batch)
else:
if i >= n_complete_batches:
batch_labels = assign_objects(X, centers, batch, categories[batch], category_upper_bounds, cluster_category_counts)
else:
batch_labels = assign_objects(X, centers, batch)
# Update categories counter
cluster_category_counts[batch_labels, categories[batch]] += 1
# Update centers
diff = X[batch, :] - centers[batch_labels, :]
centers[batch_labels, :] += diff * (1 / (i + 2))
# Update labels
labels[batch] = batch_labels
# Perform recursive partitioning (if needed)
if len(n_anticlusters_list) > 0:
# Initialize counter
counter = 0
# Initialize new labels
new_labels = np.full(n_objects, -1)
# Run in parallel
results = Parallel(n_jobs=-1, backend='threading')(
delayed(process_cluster)(cluster, labels, X, n_anticlusters_list, categories) for cluster in range(n_anticlusters)
)
for cluster, cluster_members, labels_ in results:
new_labels[cluster_members] = counter + labels_
counter += np.prod(n_anticlusters_list)
labels = new_labels
return labels