hina 0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hina-0.1/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-2024 Scott Chacon and others
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
hina-0.1/PKG-INFO ADDED
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.2
2
+ Name: hina
3
+ Version: 0.1
4
+ Summary: Heterogenous Interaction Network Analysis in Python
5
+ Home-page: https://hina.readthedocs.io/en/latest/index.html
6
+ Author: Shihui Feng, Baiyue He, Alec Kirkley
7
+ Author-email: shihuife@hku.hk, baiyue.he@connect.hku.hk, akirkley@hku.hk
8
+ License: The MIT License
9
+ Project-URL: Documentation, https://hina.readthedocs.io/en/latest/index.html
10
+ Project-URL: Source, https://hina.readthedocs.io/en/latest/index.html
11
+ Requires-Python: >=3.9, <3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
14
+ Requires-Dist: numpy>=1.24
15
+ Requires-Dist: pandas>=2.2
16
+ Requires-Dist: scipy>=1.10
17
+ Requires-Dist: scikit-network==0.32.1
18
+ Requires-Dist: dash<=3.0.0
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: project-url
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # BNApy (Bipartite Network Analysis)
31
+
32
+ HINA is a learning analytics tool that models and analyzes heterogenous interactions during learning. Heterogenous interactions refer to the interactions occurring between different types of entities during learning processes, such as students’ interactions with learning objects or students’ display of different behaviors coded using multimodal process data. These heterogenous interactions can be modelled with heterogenous interaction networks that consist of different sets of nodes and edges connecting nodes from different sets. Each node in a heterogenous interaction network can represent any meaningful entity reflecting a certain object or construct in a learning process, such as a student, group of students, coded behavior, or learning artefact. Edges in the network pair nodes from different sets and can reflect affiliations, associations, or interactions among the nodes for modeling a specific learning process. Heterogenous interaction networks offer a flexible, adaptive, and widely applicable method to model a wide variety of interactions that can occur during the learning processes, across individual learning, group learning, and community learning.
33
+
34
+ ## Features
35
+
36
+ - **Individual-level Analysis**: Provides functions to compute the node-level measures of gauging the quantity and diversity of individuals’ interactions in the learning process. New package contributions can incorporate other node-level measures for understanding individuals’ interaction patterns.
37
+ - **Dyadic Analysis**: Provides methods to identify statistically significant edges in the heterogeneous interaction network relative to multiple different null models of interaction structure, which can be specified by the user. New package contributions can incorporate other dyad-level analyses for understanding interaction patterns among pairs of nodes.
38
+ - **Mesoscale Clustering**: Provides methods for clustering nodes in a bipartite projection of the heterogeneous interaction network according to shared interaction structure, automatically learning the number of clusters from heterogeneity in the interaction data to find a mesoscale representation. New package contributions can incorporate other measures for understanding the mesoscale structure of the interaction network.
39
+ - **Visualization**: Provides network visualization functions for networks at the group level or cohort level that enable the user to easily project the heterogeneous interaction network onto any subset of node types (e.g. students, tasks, behavioral codes). These methods allow for the inclusion of various metadata such as inferred student clusters or contribution metrics, as well as pruning of the interaction network to identify significant interaction structure. Also enables visualization with Sankey diagrams for an alternative representation of the weighted networks. These visualizations come together with a web-based interface that enables interactions and user engagement. New package contributions can incorporate other relevant metadata for visualization, as well as other preprocessing tools for augmenting or modifying the network structure to highlight particular features.
40
+ - **Web Interface**: Includes a web-based interface for engaging with the visualizations.
41
+ - **Extensibility**: Easily incorporate additional measures, clustering methods, or visualizations.
42
+
43
+
44
+ ## Installation
45
+
46
+ To install BNA, you can use `pip` from the PyPI repository:
47
+
48
+ ```bash
49
+ pip install bna
50
+
51
+ BNA_Package/
52
+ ├── __init__.py
53
+ ├── individual/
54
+ │ ├── __init__.py
55
+ │ ├── quantity_diversity.py
56
+ │ └── tests/
57
+ │ ├── __init__.py
58
+ │ └── test_quantity_diversity.py
59
+ ├── dyad/
60
+ │ ├── __init__.py
61
+ │ ├── significant_edges.py
62
+ │ └── tests/
63
+ │ ├── __init__.py
64
+ │ └── test_significant_edges.py
65
+ ├── mesoscale/
66
+ │ ├── __init__.py
67
+ │ ├── clustering.py
68
+ │ └── tests/
69
+ │ ├── __init__.py
70
+ │ └── test_clustering.py
71
+ ├── visualization/
72
+ │ ├── __init__.py
73
+ │ ├── network_visualization.py
74
+ │ ├── web_interface.py
75
+ │ └── tests/
76
+ │ ├── __init__.py
77
+ │ └── test_network_visualization.py
78
+ ├── utils/
79
+ │ ├── __init__.py
80
+ │ ├── graph_tools.py
81
+ │ ├── plot_tools.py
82
+ │ └── tests/
83
+ │ ├── __init__.py
84
+ │ ├── test_graph_tools.py
85
+ │ └── test_plot_tools.py
86
+ ├── data/
87
+ ├── __init__.py
88
+ └── synthetic_data.csv
hina-0.1/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # BNApy (Bipartite Network Analysis)
2
+
3
+ HINA is a learning analytics tool that models and analyzes heterogenous interactions during learning. Heterogenous interactions refer to the interactions occurring between different types of entities during learning processes, such as students’ interactions with learning objects or students’ display of different behaviors coded using multimodal process data. These heterogenous interactions can be modelled with heterogenous interaction networks that consist of different sets of nodes and edges connecting nodes from different sets. Each node in a heterogenous interaction network can represent any meaningful entity reflecting a certain object or construct in a learning process, such as a student, group of students, coded behavior, or learning artefact. Edges in the network pair nodes from different sets and can reflect affiliations, associations, or interactions among the nodes for modeling a specific learning process. Heterogenous interaction networks offer a flexible, adaptive, and widely applicable method to model a wide variety of interactions that can occur during the learning processes, across individual learning, group learning, and community learning.
4
+
5
+ ## Features
6
+
7
+ - **Individual-level Analysis**: Provides functions to compute the node-level measures of gauging the quantity and diversity of individuals’ interactions in the learning process. New package contributions can incorporate other node-level measures for understanding individuals’ interaction patterns.
8
+ - **Dyadic Analysis**: Provides methods to identify statistically significant edges in the heterogeneous interaction network relative to multiple different null models of interaction structure, which can be specified by the user. New package contributions can incorporate other dyad-level analyses for understanding interaction patterns among pairs of nodes.
9
+ - **Mesoscale Clustering**: Provides methods for clustering nodes in a bipartite projection of the heterogeneous interaction network according to shared interaction structure, automatically learning the number of clusters from heterogeneity in the interaction data to find a mesoscale representation. New package contributions can incorporate other measures for understanding the mesoscale structure of the interaction network.
10
+ - **Visualization**: Provides network visualization functions for networks at the group level or cohort level that enable the user to easily project the heterogeneous interaction network onto any subset of node types (e.g. students, tasks, behavioral codes). These methods allow for the inclusion of various metadata such as inferred student clusters or contribution metrics, as well as pruning of the interaction network to identify significant interaction structure. Also enables visualization with Sankey diagrams for an alternative representation of the weighted networks. These visualizations come together with a web-based interface that enables interactions and user engagement. New package contributions can incorporate other relevant metadata for visualization, as well as other preprocessing tools for augmenting or modifying the network structure to highlight particular features.
11
+ - **Web Interface**: Includes a web-based interface for engaging with the visualizations.
12
+ - **Extensibility**: Easily incorporate additional measures, clustering methods, or visualizations.
13
+
14
+
15
+ ## Installation
16
+
17
+ To install BNA, you can use `pip` from the PyPI repository:
18
+
19
+ ```bash
20
+ pip install bna
21
+
22
+ BNA_Package/
23
+ ├── __init__.py
24
+ ├── individual/
25
+ │ ├── __init__.py
26
+ │ ├── quantity_diversity.py
27
+ │ └── tests/
28
+ │ ├── __init__.py
29
+ │ └── test_quantity_diversity.py
30
+ ├── dyad/
31
+ │ ├── __init__.py
32
+ │ ├── significant_edges.py
33
+ │ └── tests/
34
+ │ ├── __init__.py
35
+ │ └── test_significant_edges.py
36
+ ├── mesoscale/
37
+ │ ├── __init__.py
38
+ │ ├── clustering.py
39
+ │ └── tests/
40
+ │ ├── __init__.py
41
+ │ └── test_clustering.py
42
+ ├── visualization/
43
+ │ ├── __init__.py
44
+ │ ├── network_visualization.py
45
+ │ ├── web_interface.py
46
+ │ └── tests/
47
+ │ ├── __init__.py
48
+ │ └── test_network_visualization.py
49
+ ├── utils/
50
+ │ ├── __init__.py
51
+ │ ├── graph_tools.py
52
+ │ ├── plot_tools.py
53
+ │ └── tests/
54
+ │ ├── __init__.py
55
+ │ ├── test_graph_tools.py
56
+ │ └── test_plot_tools.py
57
+ ├── data/
58
+ ├── __init__.py
59
+ └── synthetic_data.csv
@@ -0,0 +1 @@
1
+ from . import individual, dyad, mesoscale, visualization, utils
@@ -0,0 +1 @@
1
+ from .app import *
@@ -0,0 +1,502 @@
1
+ import base64
2
+ import io
3
+ import dash
4
+ from dash import dcc, html, Input, Output, State, no_update
5
+ import dash_cytoscape as cyto
6
+ import dash_table
7
+ import pandas as pd
8
+ import networkx as nx
9
+ from bnapy.dyad.significant_edges import prune_edges
10
+ from bnapy.mesoscale.clustering import cluster_nodes
11
+ from bnapy.individual.quantity_diversity import get_bipartite, quantity_and_diversity
12
+
13
+ #############################
14
+ # Helper Functions
15
+ #############################
16
+
17
+ def parse_contents(contents, filename):
18
+ """Decodes an uploaded CSV file and returns a DataFrame."""
19
+ content_type, content_string = contents.split(',')
20
+ decoded = base64.b64decode(content_string)
21
+ try:
22
+ df = pd.read_csv(io.StringIO(decoded.decode('utf-8')))
23
+ except Exception as e:
24
+ return None, f"Error processing file: {str(e)}"
25
+ return df, f"{filename} uploaded successfully!"
26
+
27
+ def build_hina_network(df, group, attribute_1, attribute_2, pruning, layout):
28
+ """
29
+ Build a NetworkX graph for the HINA network using bnapy logic.
30
+ Node labels are converted to strings.
31
+ """
32
+ if attribute_1 is None or attribute_2 is None:
33
+ raise ValueError("Both attribute_1 and attribute_2 must be specified.")
34
+ if group != 'All':
35
+ df = df[df['group'] == group]
36
+
37
+ G = nx.Graph()
38
+ for _, row in df.iterrows():
39
+ n1 = str(row[attribute_1])
40
+ n2 = str(row[attribute_2])
41
+ weight = row.get('task weight', 1)
42
+ G.add_node(n1)
43
+ G.add_node(n2)
44
+ G.add_edge(n1, n2, weight=weight)
45
+
46
+ # Apply pruning if specified.
47
+ if pruning != "none":
48
+ edge_tuples = [(u, v, d['weight']) for u, v, d in G.edges(data=True)]
49
+ if isinstance(pruning, dict):
50
+ significant_edges = prune_edges(edge_tuples, **pruning)
51
+ else:
52
+ significant_edges = prune_edges(edge_tuples)
53
+ G_new = nx.Graph()
54
+ for u, v, w in significant_edges:
55
+ G_new.add_edge(u, v, weight=w)
56
+ G = G_new
57
+
58
+ # Set node attributes and colors.
59
+ for node in G.nodes():
60
+ if node in df[attribute_1].astype(str).values:
61
+ G.nodes[node]['type'] = 'attribute_1'
62
+ G.nodes[node]['color'] = 'blue'
63
+ elif node in df[attribute_2].astype(str).values:
64
+ G.nodes[node]['type'] = 'attribute_2'
65
+ G.nodes[node]['color'] = 'grey'
66
+ else:
67
+ G.nodes[node]['type'] = 'unknown'
68
+ G.nodes[node]['color'] = 'black'
69
+
70
+ # Compute layout positions.
71
+ if layout == 'bipartite':
72
+ attribute_1_nodes = {n for n, d in G.nodes(data=True) if d['type'] == 'attribute_1'}
73
+ if not nx.is_bipartite(G):
74
+ raise ValueError("The graph is not bipartite; check the input data.")
75
+ pos = nx.bipartite_layout(G, attribute_1_nodes, align='vertical', scale=2, aspect_ratio=4)
76
+ elif layout == 'spring':
77
+ pos = nx.spring_layout(G, k=0.2)
78
+ elif layout == 'circular':
79
+ pos = nx.circular_layout(G)
80
+ else:
81
+ raise ValueError(f"Unsupported layout: {layout}")
82
+ return G, pos
83
+
84
+ def cy_elements_from_graph(G, pos):
85
+ """
86
+ Convert a NetworkX graph and positions into Cytoscape elements.
87
+ All node IDs are forced to strings.
88
+ """
89
+ elements = []
90
+ for node, data in G.nodes(data=True):
91
+ node_str = str(node)
92
+ x = pos[node][0] * 400 + 300
93
+ y = pos[node][1] * 400 + 300
94
+ elements.append({
95
+ 'data': {'id': node_str, 'label': node_str},
96
+ 'position': {'x': x, 'y': y},
97
+ 'classes': data.get('type', '')
98
+ })
99
+ for u, v, d in G.edges(data=True):
100
+ elements.append({
101
+ 'data': {'source': str(u), 'target': str(v), 'weight': d.get('weight', 1)}
102
+ })
103
+ return elements
104
+
105
+ def build_clustered_network(df, group, attribute_1, attribute_2, clustering_method, pruning="none", layout="spring"):
106
+ """
107
+ Build a clustered bipartite network using bnapy's get_bipartite and cluster_nodes.
108
+ An optional pruning parameter (either "none" or a dict) is applied before clustering.
109
+ All node IDs are cast to strings.
110
+ """
111
+ if attribute_1 is None or attribute_2 is None:
112
+ raise ValueError("Both attribute_1 and attribute_2 must be specified.")
113
+ if group != 'All':
114
+ df = df[df['group'] == group]
115
+
116
+ G_edges = get_bipartite(df, attribute_1, attribute_2)
117
+ if pruning != "none":
118
+ edge_tuples = list(G_edges)
119
+ if isinstance(pruning, dict):
120
+ pruned = prune_edges(edge_tuples, **pruning)
121
+ else:
122
+ pruned = prune_edges(edge_tuples)
123
+ G_edges = pruned
124
+ cluster_labels = cluster_nodes(G_edges, method=clustering_method)
125
+ nx_G = nx.Graph()
126
+ for edge in G_edges:
127
+ nx_G.add_edge(str(edge[0]), str(edge[1]), weight=edge[2])
128
+ for node in nx_G.nodes():
129
+ nx_G.nodes[node]['cluster'] = cluster_labels.get(str(node), -1)
130
+ unique_clusters = sorted(set(cluster_labels.values()) | {-1})
131
+ colors = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33']
132
+ color_map = {}
133
+ for i, label in enumerate(unique_clusters):
134
+ color_map[label] = 'grey' if label == -1 else colors[i % len(colors)]
135
+ for node in nx_G.nodes():
136
+ cl = nx_G.nodes[node]['cluster']
137
+ nx_G.nodes[node]['color'] = color_map.get(cl, 'black')
138
+ if layout == 'bipartite':
139
+ # For bipartite layout, you could add custom logic; here we use spring layout.
140
+ pos = nx.spring_layout(nx_G, k=0.2)
141
+ elif layout == 'spring':
142
+ pos = nx.spring_layout(nx_G, k=0.2)
143
+ elif layout == 'circular':
144
+ pos = nx.circular_layout(nx_G)
145
+ else:
146
+ pos = nx.spring_layout(nx_G, k=0.2)
147
+ return nx_G, pos
148
+
149
+ #############################
150
+ # Dash App Layout
151
+ #############################
152
+
153
+ app = dash.Dash(__name__, suppress_callback_exceptions=True)
154
+ server = app.server
155
+
156
+ app.layout = html.Div([
157
+ html.H1("HINA Visualization Web App"),
158
+ # File Upload
159
+ dcc.Upload(
160
+ id='upload-data',
161
+ children=html.Div(['Drag and drop or ', html.A('Select a CSV File')]),
162
+ style={
163
+ 'width': '90%', 'height': '60px', 'lineHeight': '60px',
164
+ 'borderWidth': '1px', 'borderStyle': 'dashed', 'borderRadius': '5px',
165
+ 'textAlign': 'center', 'margin': '10px'
166
+ },
167
+ multiple=False
168
+ ),
169
+ html.Div(id='upload-status'),
170
+ # Store uploaded data as JSON
171
+ dcc.Store(id='stored-data'),
172
+ # Common dropdowns
173
+ html.Div([
174
+ html.Label("Group:"),
175
+ dcc.Dropdown(
176
+ id="common-group",
177
+ options=[], value="All",
178
+ style={'width': '25%', 'display': 'inline-block', 'margin-right': '10px'}
179
+ ),
180
+ html.Label("Attribute 1:"),
181
+ dcc.Dropdown(
182
+ id="common-attr1",
183
+ options=[], value="",
184
+ style={'width': '25%', 'display': 'inline-block', 'margin-right': '10px'}
185
+ ),
186
+ html.Label("Attribute 2:"),
187
+ dcc.Dropdown(
188
+ id="common-attr2",
189
+ options=[], value="",
190
+ style={'width': '25%', 'display': 'inline-block','margin-right': '10px'}
191
+ ),
192
+ html.Br(), html.Br(),
193
+ html.Label("Task Weight Column:"),
194
+ dcc.Dropdown(
195
+ id="common-weight",
196
+ options=[], value="",
197
+ style={'width': '25%', 'display': 'inline-block', 'margin-right': '10px'}
198
+ )
199
+ ], style={'padding': '2px', 'border': '1px solid #ccc', 'margin': '2px'}),
200
+ # Tabs
201
+ dcc.Tabs(id='tabs', value='tab-hina', children=[
202
+ dcc.Tab(label='HINA Network', value='tab-hina'),
203
+ dcc.Tab(label='Clustered Network', value='tab-cluster'),
204
+ dcc.Tab(label='Quantity & Diversity', value='tab-qd')
205
+ ]),
206
+ html.Div(id='tabs-content')
207
+ ])
208
+
209
+ #############################
210
+ # Callbacks
211
+ #############################
212
+
213
+ # Update common dropdown options.
214
+ @app.callback(
215
+ Output('common-group', 'options'),
216
+ Output('common-attr1', 'options'),
217
+ Output('common-attr2', 'options'),
218
+ Output('common-weight', 'options'),
219
+ Input('stored-data', 'data')
220
+ )
221
+ def update_common_dropdowns(data):
222
+ if data is None:
223
+ return [], [], [], []
224
+ df = pd.read_json(data, orient='split')
225
+ if 'group' in df.columns:
226
+ group_options = [{'label': str(x), 'value': str(x)} for x in sorted(df['group'].unique())]
227
+ group_options.insert(0, {'label': 'All', 'value': 'All'})
228
+ else:
229
+ group_options = [{'label': 'All', 'value': 'All'}]
230
+ col_options = [{'label': col, 'value': col} for col in df.columns]
231
+ return group_options, col_options, col_options, col_options
232
+
233
+ # Process file upload.
234
+ @app.callback(
235
+ Output('stored-data', 'data'),
236
+ Output('upload-status', 'children'),
237
+ Input('upload-data', 'contents'),
238
+ State('upload-data', 'filename')
239
+ )
240
+ def store_file(contents, filename):
241
+ if contents is None:
242
+ return no_update, "Awaiting file upload..."
243
+ df, msg = parse_contents(contents, filename)
244
+ if df is None:
245
+ return no_update, msg
246
+ return df.to_json(date_format='iso', orient='split'), msg
247
+
248
+ # Render tab content.
249
+ @app.callback(
250
+ Output('tabs-content', 'children'),
251
+ Input('tabs', 'value')
252
+ )
253
+ def render_tab(tab):
254
+ if tab == 'tab-hina':
255
+ return html.Div([
256
+ html.H3("HINA Network Visualization"),
257
+ html.Div([
258
+ html.Label("Pruning:"),
259
+ dcc.RadioItems(
260
+ id='hina-pruning',
261
+ options=[
262
+ {'label': 'No Pruning', 'value': 'none'},
263
+ {'label': 'Custom Pruning', 'value': 'custom'}
264
+ ],
265
+ value='none',
266
+ labelStyle={'display': 'inline-block', 'margin-right': '10px'}
267
+ ),
268
+ html.Div(id='custom-pruning-options', children=[
269
+ html.Label("Alpha:"),
270
+ dcc.Input(id='pruning-alpha', type='number', value=0.05, step=0.01, style={'margin-right': '10px'}),
271
+ html.Label("Fix Deg:"),
272
+ dcc.Input(id='pruning-fix-deg', type='text', value='Set 1')
273
+ ], style={'display': 'none', 'margin-top': '10px'}),
274
+ html.Br(),
275
+ html.Label("Layout:"),
276
+ dcc.Dropdown(
277
+ id='hina-layout',
278
+ options=[
279
+ {'label': 'Bipartite', 'value': 'bipartite'},
280
+ {'label': 'Spring', 'value': 'spring'},
281
+ {'label': 'Circular', 'value': 'circular'}
282
+ ],
283
+ value='spring',
284
+ style={'width': '50%'}
285
+ ),
286
+ html.Br(),
287
+ html.Button("Update HINA Network", id='update-hina', n_clicks=0)
288
+ ], style={'padding': '10px', 'border': '1px solid #ccc', 'margin-bottom': '10px'}),
289
+ html.Div([
290
+ cyto.Cytoscape(
291
+ id='hina-network',
292
+ layout={'name': 'preset'},
293
+ style={'width': '100%', 'height': '800px'}, #canvas size
294
+ elements=[],
295
+ zoom=1,
296
+ userZoomingEnabled=False,
297
+ userPanningEnabled=True # Enable dragging/panning
298
+ ),
299
+ html.Div([
300
+ html.Button("+", id="zoom-in", n_clicks=0, style={'font-size': '24px', 'margin': '2px'}),
301
+ html.Button("–", id="zoom-out", n_clicks=0, style={'font-size': '24px', 'margin': '2px'})
302
+ ], style={'position': 'absolute', 'top': '10px', 'right': '10px', 'zIndex': 999})
303
+ ], style={'position': 'relative', 'width': '100%', 'height': '800px'})
304
+ ])
305
+ elif tab == 'tab-cluster':
306
+ return html.Div([
307
+ html.H3("Clustered Network Visualization"),
308
+ html.Div([
309
+ html.Label("Pruning:"),
310
+ dcc.RadioItems(
311
+ id='cluster-pruning',
312
+ options=[
313
+ {'label': 'No Pruning', 'value': 'none'},
314
+ {'label': 'Custom Pruning', 'value': 'custom'}
315
+ ],
316
+ value='none',
317
+ labelStyle={'display': 'inline-block', 'margin-right': '10px'}
318
+ ),
319
+ html.Div(id='cluster-custom-pruning-options', children=[
320
+ html.Label("Alpha:"),
321
+ dcc.Input(id='cluster-pruning-alpha', type='number', value=0.05, step=0.01, style={'margin-right': '10px'}),
322
+ html.Label("Fix Deg:"),
323
+ dcc.Input(id='cluster-pruning-fix-deg', type='text', value='Set 1')
324
+ ], style={'display': 'none', 'margin-top': '10px'}),
325
+ html.Br(),
326
+ html.Label("Layout:"),
327
+ dcc.Dropdown(
328
+ id='cluster-layout',
329
+ options=[
330
+ {'label': 'Bipartite', 'value': 'bipartite'},
331
+ {'label': 'Spring', 'value': 'spring'},
332
+ {'label': 'Circular', 'value': 'circular'}
333
+ ],
334
+ value='spring',
335
+ style={'width': '50%'}
336
+ ),
337
+ html.Br(),
338
+ html.Label("Clustering Method:"),
339
+ dcc.Dropdown(
340
+ id='clustering-method',
341
+ options=[{'label': 'Modularity', 'value': 'modularity'}],
342
+ value='modularity',
343
+ style={'width': '50%'}
344
+ ),
345
+ html.Br(),
346
+ html.Button("Update Clustered Network", id='update-cluster', n_clicks=0)
347
+ ], style={'padding': '10px', 'border': '1px solid #ccc', 'margin-bottom': '10px'}),
348
+ html.Div([
349
+ cyto.Cytoscape(
350
+ id='cluster-network',
351
+ layout={'name': 'preset'},
352
+ style={'width': '100%', 'height': '800px'},
353
+ elements=[],
354
+ zoom=1,
355
+ userZoomingEnabled=False,
356
+ userPanningEnabled=True
357
+ ),
358
+ html.Div([
359
+ html.Button("+", id="cluster-zoom-in", n_clicks=0, style={'font-size': '24px', 'margin': '2px'}),
360
+ html.Button("–", id="cluster-zoom-out", n_clicks=0, style={'font-size': '24px', 'margin': '2px'})
361
+ ], style={'position': 'absolute', 'top': '10px', 'right': '10px', 'zIndex': 999})
362
+ ], style={'position': 'relative', 'width': '100%', 'height': '800px'})
363
+ ])
364
+ elif tab == 'tab-qd':
365
+ return html.Div([
366
+ html.H3("Quantity and Diversity"),
367
+ html.Br(),
368
+ html.Button("Compute Quantity & Diversity", id='compute-qd', n_clicks=0),
369
+ html.Br(),
370
+ html.Div(id='qd-output')
371
+ ])
372
+
373
+ # Show/hide custom pruning options in HINA tab.
374
+ @app.callback(
375
+ Output('custom-pruning-options', 'style'),
376
+ Input('hina-pruning', 'value')
377
+ )
378
+ def toggle_custom_pruning_hina(pruning_value):
379
+ if pruning_value == 'custom':
380
+ return {'display': 'block', 'margin-top': '10px'}
381
+ else:
382
+ return {'display': 'none'}
383
+
384
+ # Show/hide custom pruning options in Clustered tab.
385
+ @app.callback(
386
+ Output('cluster-custom-pruning-options', 'style'),
387
+ Input('cluster-pruning', 'value')
388
+ )
389
+ def toggle_custom_pruning_cluster(pruning_value):
390
+ if pruning_value == 'custom':
391
+ return {'display': 'block', 'margin-top': '10px'}
392
+ else:
393
+ return {'display': 'none'}
394
+
395
+ # Update HINA Network visualization.
396
+ @app.callback(
397
+ Output('hina-network', 'elements'),
398
+ Input('update-hina', 'n_clicks'),
399
+ State('stored-data', 'data'),
400
+ State('common-group', 'value'),
401
+ State('common-attr1', 'value'),
402
+ State('common-attr2', 'value'),
403
+ State('hina-pruning', 'value'),
404
+ State('pruning-alpha', 'value'),
405
+ State('pruning-fix-deg', 'value'),
406
+ State('hina-layout', 'value')
407
+ )
408
+ def update_hina(n_clicks, data, group, attr1, attr2, pruning_value, alpha, fix_deg, layout_choice):
409
+ if data is None or n_clicks == 0:
410
+ return no_update
411
+ df = pd.read_json(data, orient='split')
412
+ if pruning_value == 'custom':
413
+ pruning_param = {'alpha': alpha, 'fix_deg': fix_deg}
414
+ else:
415
+ pruning_param = "none"
416
+ try:
417
+ G, pos = build_hina_network(df, group, attr1, attr2, pruning_param, layout_choice)
418
+ elements = cy_elements_from_graph(G, pos)
419
+ except Exception as e:
420
+ elements = []
421
+ return elements
422
+
423
+ # Update Clustered Network visualization.
424
+ @app.callback(
425
+ Output('cluster-network', 'elements'),
426
+ Input('update-cluster', 'n_clicks'),
427
+ State('stored-data', 'data'),
428
+ State('common-group', 'value'),
429
+ State('common-attr1', 'value'),
430
+ State('common-attr2', 'value'),
431
+ State('cluster-pruning', 'value'),
432
+ State('cluster-pruning-alpha', 'value'),
433
+ State('cluster-pruning-fix-deg', 'value'),
434
+ State('cluster-layout', 'value'),
435
+ State('clustering-method', 'value')
436
+ )
437
+ def update_cluster(n_clicks, data, group, attr1, attr2, pruning_value, alpha, fix_deg, layout_choice, clustering_method):
438
+ if data is None or n_clicks == 0:
439
+ return no_update
440
+ df = pd.read_json(data, orient='split')
441
+ if pruning_value == 'custom':
442
+ pruning_param = {'alpha': alpha, 'fix_deg': fix_deg}
443
+ else:
444
+ pruning_param = "none"
445
+ try:
446
+ nx_G, pos = build_clustered_network(df, group, attr1, attr2, clustering_method, pruning=pruning_param, layout=layout_choice)
447
+ elements = cy_elements_from_graph(nx_G, pos)
448
+ except Exception as e:
449
+ elements = []
450
+ return elements
451
+
452
+ # Compute Quantity & Diversity.
453
+ @app.callback(
454
+ Output('qd-output', 'children'),
455
+ Input('compute-qd', 'n_clicks'),
456
+ State('stored-data', 'data'),
457
+ State('common-attr1', 'value'),
458
+ State('common-attr2', 'value'),
459
+ State('common-weight', 'value')
460
+ )
461
+ def compute_qd(n_clicks, data, attr1, attr2, weight_col):
462
+ if data is None or n_clicks == 0:
463
+ return no_update
464
+ df = pd.read_json(data, orient='split')
465
+ q, d = quantity_and_diversity(df, student_col=attr1, task_col=attr2, weight_col=weight_col)
466
+ table_df = pd.DataFrame({
467
+ attr1: list(q.keys()),
468
+ 'Quantity': list(q.values()),
469
+ 'Diversity': list(d.values())
470
+ })
471
+ return dash_table.DataTable(
472
+ data=table_df.to_dict('records'),
473
+ columns=[{'name': i, 'id': i} for i in table_df.columns],
474
+ style_table={'overflowX': 'auto'},
475
+ page_size=10
476
+ )
477
+
478
+ # Callback to update the zoom level of the HINA network.
479
+ @app.callback(
480
+ Output('hina-network', 'zoom'),
481
+ Input('zoom-in', 'n_clicks'),
482
+ Input('zoom-out', 'n_clicks')
483
+ )
484
+ def update_hina_zoom(n_zoom_in, n_zoom_out):
485
+ zoom = 1 * (1.2 ** n_zoom_in) * (0.8 ** n_zoom_out)
486
+ return zoom
487
+
488
+ # Callback to update the zoom level of the Clustered network.
489
+ @app.callback(
490
+ Output('cluster-network', 'zoom'),
491
+ Input('cluster-zoom-in', 'n_clicks'),
492
+ Input('cluster-zoom-out', 'n_clicks')
493
+ )
494
+ def update_cluster_zoom(n_zoom_in, n_zoom_out):
495
+ zoom = 1 * (1.2 ** n_zoom_in) * (0.8 ** n_zoom_out)
496
+ return zoom
497
+
498
+ #############################
499
+ # Run the App
500
+ #############################
501
+ if __name__ == '__main__':
502
+ app.run_server(debug=True)
File without changes
hina-0.1/hina/cli.py ADDED
@@ -0,0 +1,2 @@
1
+ def main():
2
+ print("Package loading!")
File without changes
@@ -0,0 +1 @@
1
+ from .significant_edges import *
@@ -0,0 +1,49 @@
1
+ import scipy.stats as stats
2
+
3
+ def prune_edges(G,alpha=0.05,fix_deg='Set 1'):
4
+ """
5
+ compute edges that are statistically significant in a null model where degree
6
+ of nodes in the set specified are fixed, as in Feng et al 2023
7
+ inputs:
8
+ G is a set of tuples (i,j,w)
9
+ alpha is desired significance level
10
+ fix_deg fixes degrees of desired node set
11
+ options are: 'None', 'Set 1', 'Set 2'
12
+ returns:
13
+ subset of G corresponding to statistically significant edges under the null model
14
+ """
15
+
16
+ set1,set2 = set([e[0] for e in G]),set([e[1] for e in G])
17
+ N1,N2 = len(set1),len(set2)
18
+
19
+ if fix_deg is None:
20
+
21
+ E = sum(e[-1] for e in G)
22
+ p = 1./(N1*N2)
23
+ weight_threshold = stats.binom.ppf(1-alpha, E, p)
24
+
25
+ return set([e for e in G if e[-1] > weight_threshold])
26
+
27
+ if fix_deg == 'Set 1':
28
+
29
+ degs = {}
30
+ for e in G:
31
+ i = e[0]
32
+ if not(i in degs): degs[i] = 0
33
+ degs[i] += e[-1]
34
+
35
+ p = 1./N2
36
+
37
+ return set([e for e in G if e[-1] > stats.binom.ppf(1-alpha, degs[e[0]], p)])
38
+
39
+ if fix_deg == 'Set 2':
40
+
41
+ degs = {}
42
+ for e in G:
43
+ i = e[1]
44
+ if not(i in degs): degs[i] = 0
45
+ degs[i] += e[-1]
46
+
47
+ p = 1./N1
48
+
49
+ return set([e for e in G if e[-1] > stats.binom.ppf(1-alpha, degs[e[1]], p)])
File without changes
@@ -0,0 +1 @@
1
+ from .quantity_diversity import *
@@ -0,0 +1,75 @@
1
+ from collections import Counter
2
+ import numpy as np
3
+
4
+ def get_bipartite(df,col1,col2):
5
+ """
6
+ projection of dataset onto bipartite network with nodes in col1 and col2 of dataset
7
+ can use composite indices, e.g. col2 = (col A,col B) to merge attributes A,B
8
+ weight of edge (i,j) determined by the number of times (i,j) occurs in the dataset
9
+ inputs:
10
+ dataframe df
11
+ strings col1 and col2 representing columns of the dataframe
12
+ returns:
13
+ set of tuples (i,j,w) representing (source node, destination node, weight of edge)
14
+ """
15
+
16
+ if isinstance(col1, tuple):
17
+ new_col1 = '_'.join(list(col1))
18
+ df[new_col1] = df[list(col1)].apply(lambda row: tuple(row), axis=1)
19
+ else:
20
+ new_col1 = col1
21
+
22
+ if isinstance(col2, tuple):
23
+ new_col2 = '_'.join(list(col2))
24
+ df[new_col2] = df[list(col2)].apply(lambda row: tuple(row), axis=1)
25
+ else:
26
+ new_col2 = col2
27
+
28
+ edge_dict = Counter([tuple(e) for e in df[[new_col1,new_col2]].values])
29
+ G = set([tuple([it[0][0],it[0][1],it[1]]) for it in edge_dict.items()])
30
+
31
+ return G
32
+
33
+ def quantity_and_diversity(df,student_col,task_col,weight_col):
34
+ """
35
+ compute quantity and diversity measures of Feng et al 2024 for students in student_col
36
+ inputs:
37
+ df is dataframe
38
+ student_col is column name for student identifiers
39
+ task_col is column name for tasks
40
+ weight_col is column name for weights of tasks
41
+ returns:
42
+ quantity and diversity measures for student nodes with respect to indicated tasks and weights,
43
+ in form of dictionaries with {node name: quantity} and {node name: diversity}
44
+ """
45
+ G = get_bipartite(df,student_col,task_col)
46
+ weights_dict = dict(df[[task_col,weight_col]].values)
47
+ W = sum(weights_dict.values())
48
+ N2 = len(set([e[1] for e in G]))
49
+
50
+ quantities = {}
51
+ for e in G:
52
+ i,j = e[0],e[1]
53
+ if not(i in quantities): quantities[i] = 0
54
+ quantities[i] += weights_dict[j]/W
55
+
56
+ degs = {}
57
+ for e in G:
58
+ i = e[0]
59
+ if not(i in degs): degs[i] = 0
60
+ degs[i] += e[-1]
61
+
62
+ out_weights = {}
63
+ for e in G:
64
+ i,j,w = e
65
+ if not(i in out_weights): out_weights[i] = {}
66
+ if not(j in out_weights[i]): out_weights[i][j] = 0
67
+ out_weights[i][j] += w
68
+
69
+ diversities = {}
70
+ for i in out_weights:
71
+ wi = sum(out_weights[i].values())
72
+ diversities[i] = -sum([(out_weights[i][j]/wi)*np.log(out_weights[i][j]/wi) for j in out_weights[i]])
73
+ diversities[i] /= np.log(N2)
74
+
75
+ return quantities,diversities
File without changes
@@ -0,0 +1 @@
1
+ from .clustering import *
@@ -0,0 +1,43 @@
1
+ from sknetwork.data import from_edge_list
2
+ from sknetwork.clustering import Louvain
3
+ import numpy as np
4
+
5
+ def cluster_nodes(G,num_clusters=None,method='modularity',nodes='All'):
6
+ """
7
+ compute node clusters in bipartite network using method of Feng et al 2024
8
+ for network aggregated over nodes in indicated node list 'nodes'
9
+ computes for first node set (typically student nodes) in the bipartite network
10
+ inputs:
11
+ set of tuples G
12
+ num_clusters is fixed # of node clusters if desired
13
+ defaults to None to learn # of clusters automatically
14
+ method specifies clustering objective and optimization algorithm.
15
+ current options are:
16
+ 'modularity': Barber's (2007) modularity for bipartite graphs, through sknetwork
17
+ 'SBM': Peixoto's microcanonical SBM (2014), through graph-tool
18
+ nodes is list of node names to which we apply the clustering. 'All' includes all nodes
19
+ returns:
20
+ dict of form {node name:cluster label}
21
+ """
22
+ if nodes == 'All':
23
+ nodes = set(np.unique([e[0] for e in G]))
24
+ else:
25
+ nodes = set(nodes)
26
+
27
+ G = set([e for e in G if e[0] in nodes])
28
+
29
+ if method == 'modularity':
30
+
31
+ e_list = list(G)
32
+ G_b = from_edge_list(e_list, bipartite=True)
33
+ A_b = G_b.biadjacency
34
+
35
+ louvain = Louvain()
36
+ louvain.fit(A_b,force_bipartite=True)
37
+
38
+ community_labels = {stud:louvain.labels_row_[s] for s,stud in enumerate(G_b.names_row)}
39
+
40
+ if method == 'SBM':
41
+
42
+ community_labels = None
43
+ return community_labels
File without changes
@@ -0,0 +1 @@
1
+ from .graph_tools import *
@@ -0,0 +1,20 @@
1
+ import networkx as nx
2
+
3
+ def save_network(G, filename, format='gml'):
4
+ """
5
+ Saves the bipartite network G (set of tuples (i,j,w)) to a specified file format.
6
+ Supported formats: 'graphml', 'gexf', 'gpickle', etc.
7
+ """
8
+ # Convert edge set to a NetworkX graph
9
+ B = nx.Graph()
10
+ for s, t, w in G:
11
+ B.add_edge(s, t, weight=w)
12
+
13
+ if format == 'gml':
14
+ nx.write_gml(B, filename + '.gml')
15
+ elif format == 'gexf':
16
+ nx.write_gexf(B, filename + '.gexf')
17
+ elif format == 'gpickle':
18
+ nx.write_gpickle(B, filename + '.gpickle')
19
+ else:
20
+ raise ValueError("Unsupported format: {}".format(format))
File without changes
@@ -0,0 +1 @@
1
+ from .network_visualization import *
@@ -0,0 +1,204 @@
1
+ import networkx as nx
2
+ import numpy as np
3
+ import pandas as pd
4
+ import matplotlib.pyplot as plt
5
+ from matplotlib.lines import Line2D
6
+ from bnapy.dyad.significant_edges import prune_edges
7
+ from bnapy.mesoscale.clustering import cluster_nodes
8
+ from bnapy.individual.quantity_diversity import get_bipartite
9
+
10
+
11
+ def plot_HINA(df, group='All', attribute_1=None, attribute_2=None, pruning=False, layout='spring', NetworkX_kwargs=None):
12
+ """
13
+ Plots a bipartite network visualization with specified attributes and layout.
14
+
15
+ Parameters:
16
+ - df (pd.DataFrame): Input dataframe containing the network data.
17
+ - group (str): Group to filter and plot (default: 'All' for entire dataset).
18
+ - attribute_1 (str): Column name for the first node set (e.g., 'student id').
19
+ - attribute_2 (str): Column name for the second node set (e.g., 'task').
20
+ - pruning (bool or dict): Whether to prune edges using significance logic.
21
+ If dict, specifies parameters for pruning.
22
+ - layout (str): Layout to use for node positioning. Supported layouts:
23
+ - 'bipartite': Nodes are positioned in two vertical columns.
24
+ - 'spring': Force-directed layout for a visually appealing arrangement.
25
+ - 'circular': Nodes are arranged in a circle.
26
+ - NetworkX_kwargs (dict): Additional arguments for NetworkX visualization.
27
+
28
+ Returns:
29
+ - None: Displays a plot of the bipartite network.
30
+
31
+ """
32
+ if NetworkX_kwargs is None:
33
+ NetworkX_kwargs = {}
34
+
35
+ if attribute_1 is None or attribute_2 is None:
36
+ raise ValueError("Both 'attribute_1' and 'attribute_2' must be specified.")
37
+
38
+ if group != 'All':
39
+ df = df[df['group'] == group]
40
+
41
+ G = nx.Graph()
42
+ for _, row in df.iterrows():
43
+ G.add_node(row[attribute_1])
44
+ G.add_node(row[attribute_2])
45
+ G.add_edge(row[attribute_1], row[attribute_2], weight=row['task weight'])
46
+
47
+ if pruning:
48
+ edge_tuples = [(u, v, d['weight']) for u, v, d in G.edges(data=True)]
49
+ if isinstance(pruning, dict):
50
+ significant_edges = prune_edges(edge_tuples, **pruning)
51
+ else:
52
+ significant_edges = prune_edges(edge_tuples)
53
+ G = nx.Graph()
54
+ for u, v, w in significant_edges:
55
+ G.add_edge(u, v, weight=w)
56
+
57
+ for node in G.nodes:
58
+ if node in df[attribute_1].values:
59
+ G.nodes[node]['type'] = 'attribute_1'
60
+ G.nodes[node]['color'] = 'blue'
61
+ elif node in df[attribute_2].values:
62
+ G.nodes[node]['type'] = 'attribute_2'
63
+ G.nodes[node]['color'] = 'grey'
64
+ else:
65
+ G.nodes[node]['type'] = 'unknown'
66
+ G.nodes[node]['color'] = 'black'
67
+
68
+ if layout == 'bipartite':
69
+ attribute_1_nodes = {n for n, d in G.nodes(data=True) if d['type'] == 'attribute_1'}
70
+ if not nx.is_bipartite(G):
71
+ raise ValueError("The graph is not bipartite; check the input data.")
72
+ pos = nx.bipartite_layout(G, attribute_1_nodes, align='vertical', scale=2, aspect_ratio=4)
73
+ elif layout == 'spring':
74
+ pos = nx.spring_layout(G, k=0.2)
75
+ elif layout == 'circular':
76
+ pos = nx.circular_layout(G)
77
+ else:
78
+ raise ValueError(f"Unsupported layout: {layout}")
79
+
80
+ max_y = max(abs(y) for _, y in pos.values())
81
+ label_offset = max_y * 0.03
82
+
83
+ node_colors = [d['color'] for _, d in G.nodes(data=True)]
84
+ edge_widths = [d['weight'] / 15 for _, _, d in G.edges(data=True)]
85
+
86
+ plt.figure(figsize=(12, 12))
87
+ nx.draw(
88
+ G,
89
+ pos,
90
+ with_labels=False,
91
+ node_color=node_colors,
92
+ width=edge_widths,
93
+ node_size=200,
94
+ **NetworkX_kwargs
95
+ )
96
+
97
+ for node, (x, y) in pos.items():
98
+ label = str(node)
99
+ plt.text(
100
+ x, y + label_offset,
101
+ label,
102
+ fontsize=9,
103
+ ha='center',
104
+ va='center',
105
+ color='black'
106
+ )
107
+
108
+ plt.title(f"HINA Network Visualization: Group = {group}")
109
+ plt.show()
110
+
111
+ def plot_clusters(df, group='All', attribute_1=None, attribute_2=None, pruning=False, clustering_method='modularity', NetworkX_kwargs=None):
112
+ """
113
+ Plots a clustered bipartite network for the selected group.
114
+
115
+ Parameters:
116
+ - df (pd.DataFrame): Input dataframe containing the network data.
117
+ - group (str): Group to filter and plot (default: 'All' for the entire dataset).
118
+ - attribute_1 (str): Column name for the first node set (e.g., 'student id').
119
+ - attribute_2 (str): Column name for the second node set (e.g., 'task').
120
+ - pruning (bool or dict): Whether to prune edges using significance logic.
121
+ If dict, specifies parameters for pruning.
122
+ - clustering_method (str): Clustering method to use for grouping nodes.
123
+ Supported methods:
124
+ - 'modularity': Louvain modularity clustering for bipartite graphs.
125
+ - 'SBM': Stochastic Block Model (for future implementation).
126
+ - NetworkX_kwargs (dict): Additional arguments for NetworkX visualization.
127
+
128
+ Returns:
129
+ - None: Displays a plot of the clustered bipartite network.
130
+ """
131
+ if NetworkX_kwargs is None:
132
+ NetworkX_kwargs = {}
133
+
134
+ # Filter the dataframe by the selected group
135
+ if group != 'All':
136
+ df = df[df['group'] == group]
137
+
138
+ if attribute_1 is None or attribute_2 is None:
139
+ raise ValueError("Both 'attribute_1' and 'attribute_2' must be specified.")
140
+
141
+ G = get_bipartite(df, attribute_1, attribute_2)
142
+
143
+ cluster_labels = cluster_nodes(G, method=clustering_method)
144
+ print("Cluster labels:", cluster_labels)
145
+
146
+ nx_G = nx.Graph()
147
+ for edge in G:
148
+ nx_G.add_edge(edge[0], edge[1], weight=edge[2])
149
+
150
+ for node in nx_G.nodes:
151
+ nx_G.nodes[node]['cluster'] = cluster_labels.get(str(node), -1) # Default cluster is -1 for unclustered nodes
152
+
153
+ unique_clusters = sorted(set(cluster_labels.values()) | {-1}) # Include -1 for unclustered nodes
154
+ label_to_color = {label: idx for idx, label in enumerate(unique_clusters)} # Map labels to indices
155
+
156
+ cmap = plt.get_cmap('tab10', len(unique_clusters)) # Dynamically get a colormap with enough colors
157
+ label_to_color[-1] = 'grey' # Set grey for unclustered nodes
158
+
159
+ node_colors = [
160
+ 'grey' if nx_G.nodes[node]['cluster'] == -1 else cmap(label_to_color[nx_G.nodes[node]['cluster']])
161
+ for node in nx_G.nodes
162
+ ]
163
+
164
+ pos = nx.spring_layout(nx_G, k=0.2)
165
+ max_y = max(abs(y) for _, y in pos.values())
166
+ label_offset = max_y * 0.07
167
+
168
+ plt.figure(figsize=(12, 8))
169
+ nx.draw(
170
+ nx_G,
171
+ pos,
172
+ with_labels=False,
173
+ node_color=node_colors,
174
+ node_size=800,
175
+ edge_color='gray',
176
+ width=1,
177
+ **NetworkX_kwargs
178
+ )
179
+
180
+ for node, (x, y) in pos.items():
181
+ plt.text(
182
+ x, y + label_offset,
183
+ str(node),
184
+ fontsize=9,
185
+ ha='center',
186
+ va='center',
187
+ color='black'
188
+ )
189
+
190
+ legend_elements = [
191
+ Line2D(
192
+ [0], [0],
193
+ marker='o',
194
+ color='w',
195
+ markerfacecolor='grey' if label == -1 else cmap(idx),
196
+ markersize=10,
197
+ label=f'Cluster {label}' if label != -1 else attribute_2
198
+ )
199
+ for label, idx in label_to_color.items()
200
+ ]
201
+ plt.legend(handles=legend_elements, loc='upper right', title='Clusters', frameon=True)
202
+
203
+ plt.title(f"Clustered Bipartite Network ({clustering_method.capitalize()} Method) for Group: {group}")
204
+ plt.show()
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.2
2
+ Name: hina
3
+ Version: 0.1
4
+ Summary: Heterogenous Interaction Network Analysis in Python
5
+ Home-page: https://hina.readthedocs.io/en/latest/index.html
6
+ Author: Shihui Feng, Baiyue He, Alec Kirkley
7
+ Author-email: shihuife@hku.hk, baiyue.he@connect.hku.hk, akirkley@hku.hk
8
+ License: The MIT License
9
+ Project-URL: Documentation, https://hina.readthedocs.io/en/latest/index.html
10
+ Project-URL: Source, https://hina.readthedocs.io/en/latest/index.html
11
+ Requires-Python: >=3.9, <3.12
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE.txt
14
+ Requires-Dist: numpy>=1.24
15
+ Requires-Dist: pandas>=2.2
16
+ Requires-Dist: scipy>=1.10
17
+ Requires-Dist: scikit-network==0.32.1
18
+ Requires-Dist: dash<=3.0.0
19
+ Dynamic: author
20
+ Dynamic: author-email
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: project-url
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # BNApy (Bipartite Network Analysis)
31
+
32
+ HINA is a learning analytics tool that models and analyzes heterogenous interactions during learning. Heterogenous interactions refer to the interactions occurring between different types of entities during learning processes, such as students’ interactions with learning objects or students’ display of different behaviors coded using multimodal process data. These heterogenous interactions can be modelled with heterogenous interaction networks that consist of different sets of nodes and edges connecting nodes from different sets. Each node in a heterogenous interaction network can represent any meaningful entity reflecting a certain object or construct in a learning process, such as a student, group of students, coded behavior, or learning artefact. Edges in the network pair nodes from different sets and can reflect affiliations, associations, or interactions among the nodes for modeling a specific learning process. Heterogenous interaction networks offer a flexible, adaptive, and widely applicable method to model a wide variety of interactions that can occur during the learning processes, across individual learning, group learning, and community learning.
33
+
34
+ ## Features
35
+
36
+ - **Individual-level Analysis**: Provides functions to compute the node-level measures of gauging the quantity and diversity of individuals’ interactions in the learning process. New package contributions can incorporate other node-level measures for understanding individuals’ interaction patterns.
37
+ - **Dyadic Analysis**: Provides methods to identify statistically significant edges in the heterogeneous interaction network relative to multiple different null models of interaction structure, which can be specified by the user. New package contributions can incorporate other dyad-level analyses for understanding interaction patterns among pairs of nodes.
38
+ - **Mesoscale Clustering**: Provides methods for clustering nodes in a bipartite projection of the heterogeneous interaction network according to shared interaction structure, automatically learning the number of clusters from heterogeneity in the interaction data to find a mesoscale representation. New package contributions can incorporate other measures for understanding the mesoscale structure of the interaction network.
39
+ - **Visualization**: Provides network visualization functions for networks at the group level or cohort level that enable the user to easily project the heterogeneous interaction network onto any subset of node types (e.g. students, tasks, behavioral codes). These methods allow for the inclusion of various metadata such as inferred student clusters or contribution metrics, as well as pruning of the interaction network to identify significant interaction structure. Also enables visualization with Sankey diagrams for an alternative representation of the weighted networks. These visualizations come together with a web-based interface that enables interactions and user engagement. New package contributions can incorporate other relevant metadata for visualization, as well as other preprocessing tools for augmenting or modifying the network structure to highlight particular features.
40
+ - **Web Interface**: Includes a web-based interface for engaging with the visualizations.
41
+ - **Extensibility**: Easily incorporate additional measures, clustering methods, or visualizations.
42
+
43
+
44
+ ## Installation
45
+
46
+ To install BNA, you can use `pip` from the PyPI repository:
47
+
48
+ ```bash
49
+ pip install bna
50
+
51
+ BNA_Package/
52
+ ├── __init__.py
53
+ ├── individual/
54
+ │ ├── __init__.py
55
+ │ ├── quantity_diversity.py
56
+ │ └── tests/
57
+ │ ├── __init__.py
58
+ │ └── test_quantity_diversity.py
59
+ ├── dyad/
60
+ │ ├── __init__.py
61
+ │ ├── significant_edges.py
62
+ │ └── tests/
63
+ │ ├── __init__.py
64
+ │ └── test_significant_edges.py
65
+ ├── mesoscale/
66
+ │ ├── __init__.py
67
+ │ ├── clustering.py
68
+ │ └── tests/
69
+ │ ├── __init__.py
70
+ │ └── test_clustering.py
71
+ ├── visualization/
72
+ │ ├── __init__.py
73
+ │ ├── network_visualization.py
74
+ │ ├── web_interface.py
75
+ │ └── tests/
76
+ │ ├── __init__.py
77
+ │ └── test_network_visualization.py
78
+ ├── utils/
79
+ │ ├── __init__.py
80
+ │ ├── graph_tools.py
81
+ │ ├── plot_tools.py
82
+ │ └── tests/
83
+ │ ├── __init__.py
84
+ │ ├── test_graph_tools.py
85
+ │ └── test_plot_tools.py
86
+ ├── data/
87
+ ├── __init__.py
88
+ └── synthetic_data.csv
@@ -0,0 +1,29 @@
1
+ LICENSE.txt
2
+ README.md
3
+ setup.py
4
+ hina/__init__.py
5
+ hina/cli.py
6
+ hina.egg-info/PKG-INFO
7
+ hina.egg-info/SOURCES.txt
8
+ hina.egg-info/dependency_links.txt
9
+ hina.egg-info/requires.txt
10
+ hina.egg-info/top_level.txt
11
+ hina/app/__init__.py
12
+ hina/app/app.py
13
+ hina/app/tests/__init__.py
14
+ hina/data/__init__.py
15
+ hina/dyad/__init__.py
16
+ hina/dyad/significant_edges.py
17
+ hina/dyad/tests/__init__.py
18
+ hina/individual/__init__.py
19
+ hina/individual/quantity_diversity.py
20
+ hina/individual/tests/__init__.py
21
+ hina/mesoscale/__init__.py
22
+ hina/mesoscale/clustering.py
23
+ hina/mesoscale/tests/__init__.py
24
+ hina/utils/__init__.py
25
+ hina/utils/graph_tools.py
26
+ hina/utils/tests/__init__.py
27
+ hina/visualization/__init__.py
28
+ hina/visualization/network_visualization.py
29
+ hina/visualization/tests/__init__.py
@@ -0,0 +1,5 @@
1
+ numpy>=1.24
2
+ pandas>=2.2
3
+ scipy>=1.10
4
+ scikit-network==0.32.1
5
+ dash<=3.0.0
@@ -0,0 +1 @@
1
+ hina
hina-0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
hina-0.1/setup.py ADDED
@@ -0,0 +1,27 @@
1
+ from setuptools import setup, find_packages, Extension
2
+
3
+ setup(
4
+ name='hina',
5
+ version='0.1',
6
+ description='Heterogenous Interaction Network Analysis in Python',
7
+ long_description=open('README.md', encoding='utf-8').read(),
8
+ long_description_content_type='text/markdown',
9
+ url='https://hina.readthedocs.io/en/latest/index.html',
10
+ author='Shihui Feng, Baiyue He, Alec Kirkley',
11
+ author_email='shihuife@hku.hk, baiyue.he@connect.hku.hk, akirkley@hku.hk',
12
+ license='The MIT License',
13
+ project_urls={
14
+ "Documentation": "https://hina.readthedocs.io/en/latest/index.html",
15
+ "Source": "https://hina.readthedocs.io/en/latest/index.html"
16
+ },
17
+ python_requires=">=3.9, <3.12",
18
+ install_requires=[
19
+ "numpy>=1.24",
20
+ "pandas>=2.2",
21
+ "scipy>=1.10",
22
+ "scikit-network==0.32.1",
23
+ "dash<=3.0.0"
24
+ ],
25
+ packages=find_packages(),
26
+ include_package_data=True,
27
+ )