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 +20 -0
- hina-0.1/PKG-INFO +88 -0
- hina-0.1/README.md +59 -0
- hina-0.1/hina/__init__.py +1 -0
- hina-0.1/hina/app/__init__.py +1 -0
- hina-0.1/hina/app/app.py +502 -0
- hina-0.1/hina/app/tests/__init__.py +0 -0
- hina-0.1/hina/cli.py +2 -0
- hina-0.1/hina/data/__init__.py +0 -0
- hina-0.1/hina/dyad/__init__.py +1 -0
- hina-0.1/hina/dyad/significant_edges.py +49 -0
- hina-0.1/hina/dyad/tests/__init__.py +0 -0
- hina-0.1/hina/individual/__init__.py +1 -0
- hina-0.1/hina/individual/quantity_diversity.py +75 -0
- hina-0.1/hina/individual/tests/__init__.py +0 -0
- hina-0.1/hina/mesoscale/__init__.py +1 -0
- hina-0.1/hina/mesoscale/clustering.py +43 -0
- hina-0.1/hina/mesoscale/tests/__init__.py +0 -0
- hina-0.1/hina/utils/__init__.py +1 -0
- hina-0.1/hina/utils/graph_tools.py +20 -0
- hina-0.1/hina/utils/tests/__init__.py +0 -0
- hina-0.1/hina/visualization/__init__.py +1 -0
- hina-0.1/hina/visualization/network_visualization.py +204 -0
- hina-0.1/hina/visualization/tests/__init__.py +0 -0
- hina-0.1/hina.egg-info/PKG-INFO +88 -0
- hina-0.1/hina.egg-info/SOURCES.txt +29 -0
- hina-0.1/hina.egg-info/dependency_links.txt +1 -0
- hina-0.1/hina.egg-info/requires.txt +5 -0
- hina-0.1/hina.egg-info/top_level.txt +1 -0
- hina-0.1/setup.cfg +4 -0
- hina-0.1/setup.py +27 -0
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 *
|
hina-0.1/hina/app/app.py
ADDED
|
@@ -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
|
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hina
|
hina-0.1/setup.cfg
ADDED
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
|
+
)
|