Skip to content

Commit 77bd115

Browse files
authored
Merge pull request #98 from janezd/nxgroups-weights
NXGroups: Add different weights and normalizations
2 parents 679a916 + 88137ae commit 77bd115

File tree

2 files changed

+104
-49
lines changed

2 files changed

+104
-49
lines changed

orangecontrib/network/widgets/OWNxGroups.py

Lines changed: 101 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from itertools import repeat
2+
13
import numpy as np
24

35
from Orange.data import DiscreteVariable, Table, Domain
46
from Orange.widgets import gui
5-
from Orange.widgets.settings import ContextSetting, DomainContextHandler
7+
from Orange.widgets.settings import ContextSetting, DomainContextHandler, \
8+
Setting
69
from Orange.widgets.utils.itemmodels import DomainModel
710
from Orange.widgets.widget import Input, Output, OWWidget, Msg
811
from orangecontrib.network import Graph
@@ -33,42 +36,70 @@ class Error(OWWidget.Error):
3336
resizing_enabled = False
3437
want_main_area = False
3538

39+
NoWeights, WeightByDegrees, WeightByWeights = range(3)
40+
weight_labels =\
41+
["No weights", "Number of connections", "Sum of connection weights"]
42+
3643
settingsHandler = DomainContextHandler()
3744
feature = ContextSetting(None)
45+
weighting = Setting(2)
46+
normalize = Setting(True)
3847

3948
def __init__(self):
4049
super().__init__()
4150
self.network = None
4251
self.data = None
4352
self.effective_data = None
44-
self.output_network = None
53+
self.out_nodes = self.out_edges = None
4554

4655
info_box = gui.widgetBox(self.controlArea, "Info")
4756
self.input_label = gui.widgetLabel(info_box, "")
48-
self.__set_input_label_text()
49-
50-
feature_box = gui.widgetBox(self.controlArea, "Group by")
51-
self.feature_model = DomainModel(valid_types=DiscreteVariable)
52-
self.feature_combo = gui.comboBox(
53-
feature_box, self, "feature", contentsLength=15,
54-
callback=self.__feature_combo_changed, model=self.feature_model
57+
self.output_label = gui.widgetLabel(info_box, "")
58+
self._set_input_label_text()
59+
self._set_output_label_text(None)
60+
61+
gui.comboBox(
62+
self.controlArea, self, "feature", box="Group by",
63+
callback=self.__feature_combo_changed,
64+
model=DomainModel(valid_types=DiscreteVariable)
65+
)
66+
radios = gui.radioButtons(
67+
self.controlArea, self, "weighting", box="Output weights",
68+
btnLabels=self.weight_labels, callback=self.__feature_combo_changed
69+
)
70+
gui.separator(radios)
71+
gui.checkBox(
72+
radios, self, "normalize", "Normalize by geometric mean",
73+
callback=self.__feature_combo_changed
5574
)
5675

57-
def __set_input_label_text(self):
58-
text = "No data on input."
59-
if self.network is not None:
60-
dir_ = "Directed" if self.network.is_directed() else "Undirected"
61-
text = f"{dir_} graph\n{self.network.number_of_nodes()}" \
62-
f" nodes, {self.network.number_of_edges()} edges"
63-
self.input_label.setText(text)
76+
def _set_input_label_text(self):
77+
if self.network is None:
78+
self.input_label.setText("Input: no data")
79+
else:
80+
self.input_label.setText(
81+
f"Input: "
82+
f"{self.network.number_of_nodes()} nodes, "
83+
f"{self.network.number_of_edges()} edges")
84+
85+
def _set_output_label_text(self, output_network):
86+
if output_network is None:
87+
self.output_label.setText("Output: no data")
88+
self.out_nodes = self.out_edges = None
89+
else:
90+
self.out_nodes = output_network.number_of_nodes()
91+
self.out_edges = output_network.number_of_edges()
92+
self.output_label.setText(
93+
f"Output: {self.out_nodes} nodes, {self.out_edges} edges"
94+
)
6495

6596
def __feature_combo_changed(self):
6697
self.commit()
6798

6899
@Inputs.network
69100
def set_network(self, network):
70101
self.network = network
71-
self.__set_input_label_text()
102+
self._set_input_label_text()
72103

73104
@Inputs.data
74105
def set_data(self, data):
@@ -79,7 +110,7 @@ def handleNewSignals(self):
79110
self.clear_messages()
80111
self.set_effective_data()
81112
self.set_feature_model()
82-
if self.feature_model:
113+
if self.controls.feature.model():
83114
self.openContext(self.effective_data)
84115
self.commit()
85116

@@ -101,56 +132,77 @@ def set_effective_data(self):
101132

102133
def set_feature_model(self):
103134
data = self.effective_data
104-
self.feature_model.set_domain(data and data.domain)
105-
self.feature = self.feature_model[0] if self.feature_model else None
135+
feature_model = self.controls.feature.model()
136+
feature_model.set_domain(data and data.domain)
137+
self.feature = feature_model[0] if feature_model else None
106138

107139
def commit(self):
108140
if self.feature is None:
109-
self.output_network = None
110-
self.Outputs.network.send(None)
111-
self.Outputs.data.send(None)
112-
return
113-
114-
self._map_network()
115-
self.Outputs.network.send(self.output_network)
116-
self.Outputs.data.send(self.output_network.items())
141+
output_network = None
142+
else:
143+
output_network = self._map_network()
144+
self.Outputs.network.send(output_network)
145+
self.Outputs.data.send(output_network and output_network.items())
146+
self._set_output_label_text(output_network)
117147

118148
def _map_network(self):
119-
edges = self.network.edges(data='weight')
120-
row, col, weights = zip(*edges)
121-
row, col = self._map_into_feature_values(np.array(row), np.array(col))
122-
edges = self._construct_edges(row, col, np.array(weights))
149+
if self.weighting == self.WeightByWeights:
150+
edges = self.network.edges(data='weight')
151+
row, col, weights = map(np.array, zip(*edges))
152+
else:
153+
edges = self.network.edges()
154+
row, col = map(np.array, zip(*edges))
155+
weights = None
156+
if self.normalize:
157+
self._normalize_weights(row, col, weights)
158+
row, col = self._map_into_feature_values(row, col)
159+
edges = self._construct_edges(row, col, weights)
123160

124161
network = Graph()
125162
network.add_nodes_from(range(len(self.feature.values)))
126163
network.add_weighted_edges_from(edges)
127164
network.set_items(self._construct_items())
128-
self.output_network = network
165+
return network
166+
167+
def _normalize_weights(self, row, col, weights):
168+
if weights is None:
169+
weights = np.ones((len(row)), dtype=float)
170+
degs = np.array(sorted(self.network.degree()))[:, 1]
171+
else:
172+
degs = np.array(sorted(self.network.degree(weight="weight")))[:, 1]
173+
weights /= np.sqrt(degs.T[row] * degs.T[col])
129174

130175
def _map_into_feature_values(self, row, col):
131176
selected_column = self.effective_data.get_column_view(self.feature)[0]
132177
return (selected_column[row].astype(np.float64),
133178
selected_column[col].astype(np.float64))
134179

135-
@staticmethod
136-
def _construct_edges(col, row, weights):
180+
def _construct_edges(self, col, row, weights):
137181
# remove edges that connect to "unknown" group
138182
mask = ~np.any(np.isnan(np.vstack((row, col))), axis=0)
139183
# remove edges within a node
140184
mask = np.logical_and((row != col), mask)
141-
row, col, weights = row[mask], col[mask], weights[mask]
185+
row, col = row[mask], col[mask]
186+
if weights is not None:
187+
weights = weights[mask]
142188

143189
# find unique edges
144190
mask = row > col
145191
row[mask], col[mask] = col[mask], row[mask]
146192

147-
array = np.vstack((row, col))
193+
array = np.vstack((row.astype(int), col.astype(int)))
148194
(row, col), inverse = np.unique(array, axis=1, return_inverse=True)
149195

150-
# assign each edge the sum of weights of belonging original edges
151-
weights = np.array(
152-
[np.sum(weights[inverse == i]) for i in range(len(row))])
153-
return [(int(u), int(v), w) for u, v, w in zip(row, col, weights)]
196+
if self.weighting == self.NoWeights:
197+
return zip(row, col, repeat(1.0))
198+
elif self.weighting == self.WeightByDegrees:
199+
return zip(
200+
row, col,
201+
(np.sum(inverse == i).astype(float) for i in range(len(row))))
202+
else: # self.WeightByWeights
203+
return zip(
204+
row, col,
205+
(np.sum(weights[inverse == i]) for i in range(len(row))))
154206

155207
def _construct_items(self):
156208
domain = Domain([self.feature])
@@ -164,12 +216,15 @@ def send_report(self):
164216
("Number of vertices", self.network.number_of_nodes()),
165217
("Number of edges", self.network.number_of_edges())])
166218
self.report_data("Input data", self.effective_data)
167-
self.report_items("Group by", [("Feature", self.feature.name)])
168-
if self.output_network:
219+
self.report_items("Settings", [
220+
("Group by", self.feature.name),
221+
("Weights", self.weight_labels[self.weighting].lower() +
222+
(", normalized by geometric mean" if self.normalize else ""))
223+
])
224+
if self.out_nodes is not None:
169225
self.report_items("Output network", [
170-
("Number of vertices", self.output_network.number_of_nodes()),
171-
("Number of edges", self.output_network.number_of_edges())])
172-
self.report_data("Output data", self.output_network.items())
226+
("Number of vertices", self.out_nodes),
227+
("Number of edges", self.out_edges)])
173228

174229

175230
def main():

orangecontrib/network/widgets/tests/test_OWNxGroups.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ def test_outputs(self):
6666

6767
self.assertIsInstance(network, Graph)
6868
self.assertSetEqual(set(network.nodes()), set(range(3)))
69-
edges_ = list(network.edges(data='weight'))
70-
for n1, n2, w in [(0, 1, 12.), (1, 2, 7.), (0, 2, 20.)]:
71-
self.assertTrue((n1, n2, w) in edges_)
69+
edges_ = list(network.edges())
70+
for n1, n2 in [(0, 1), (1, 2), (0, 2)]:
71+
self.assertTrue((n1, n2) in edges_)
7272

7373
self.assertIsInstance(network.items(), Table)
7474

0 commit comments

Comments
 (0)