GraKeL icon indicating copy to clipboard operation
GraKeL copied to clipboard

Generating graphs from dense/numpy matrices

Open VHolstein opened this issue 2 years ago • 1 comments

Is your feature request related to a problem? Please describe. Dear Grakel developers, I'm trying to generate grakel graphs from numpy connectivity matrices. I have two possible scenarios.

  1. A stack of adjacency matrices with Subjects x Nodes x Nodes. For each subject matrix I have edge strength indicated in node x node.
  2. A stack of adjacency/features matrices with Subjects x Nodes x Nodes x Adjacency/Features. The adjacency matrix is the same as in the first case, the feature matrix has the same dimensions (nodes x nodes) with slightly different features.

Now I want to create grakel graphs from these. What is the best way to do this?

Describe the solution you'd like Would it be possible to implement a method that automatically generates grakel graphs from dense adjacency matrices?

Describe alternatives you've considered Currently I have written my own (somewhat complicated) graph kernel conversion (https://github.com/wwu-mmll/photonai_graph/blob/main/photonai_graph/GraphKernels/grakel_adapter.py).

I first generate lists of node and edge features and then loop over each subject adjacency constructing the graph from the adjacency + features.

    def transform(self, X):
        """sklearn compatible graph conversion"""
        if self.input_type == "dense":
            node_features = self.construct_node_features(X)
            edge_features = self.construct_edge_features(X)
        else:
            node_features = None
            edge_features = None
        transformed = self.convert_grakel(X, self.input_type, node_features, edge_features, self.adjacency_axis)

I have attached the different functions for reference.

    @staticmethod
    def convert_grakel(graphs, in_format, node_labels, edge_features, adjacency_axis):
        """convert graphs into grakel format"""
        g_trans = []
        if in_format == "dense":
            for g in range(graphs.shape[0]):
                conv_g = graph.Graph(graphs[g, :, :, adjacency_axis], node_labels=node_labels[g],
                                     edge_labels=edge_features[g])
                g_trans.append(conv_g)

        if in_format == "networkx":
            g_trans = grakel.graph_from_networkx(graphs)

        return g_trans

    def construct_node_features(self, matrices):
        """construct node features from the feature matrix"""
        label_list = []
        for mtrx in range(matrices.shape[0]):
            feat = self.get_dense_feature(matrices[mtrx, :, :, :], adjacency_axis=self.adjacency_axis,
                                          feature_axis=self.feature_axis, aggregation=self.node_feature_construction)
            label_list.append(feat)

        return label_list

    def construct_edge_features(self, matrices):
        """construct edge features from the feature or adjacency matrix"""
        label_list = []
        for mtrx in range(matrices.shape[0]):
            feat = self.get_dense_edge_features(matrices[mtrx, :, :, :], adjacency_axis=self.adjacency_axis,
                                                feature_axis=self.feature_axis)
            label_list.append(feat)

        return label_list

    @staticmethod
    def get_dense_edge_features(matrix, adjacency_axis, feature_axis):
        """returns the features for an edge label dictionary
            Parameters
            ---------
            matrix: np.matrix/np.ndarray
                feature matrix
            adjacency_axis: int
                position of the adjacency matrix
            feature_axis: int
                position of the feature matrix
        """
        edge_feat = {}
        for index, value in np.ndenumerate(matrix[:, :, adjacency_axis]):
            conn_key = (str(index[0]), str(index[1]))
            key_val = {conn_key: value}
            edge_feat.update(key_val)
        return edge_feat

    @staticmethod
    def get_dense_feature(matrix, adjacency_axis, feature_axis, aggregation="sum"):
        """returns the features for a networkx graph
            Parameters
            ---------
            matrix: np.matrix/np.ndarray
                feature matrix
            adjacency_axis: int
                position of the adjacency matrix
            feature_axis: int
                position of the feature matrix
            aggregation:
                method of feature construction, sum gives a row-wise sum,
                "mean" gives a row-wise mean, "node_degree" give a row-wise node-degree,
                features returns the entire row as the feature vector
        """
        if aggregation == "sum":
            features = np.sum(matrix[:, :, feature_axis], axis=1)
            features = features.tolist()
        elif aggregation == "mean":
            features = (np.sum(matrix[:, :, feature_axis], axis=1)) / matrix.shape[0]
            features = features.tolist()
        elif aggregation == "node_degree":
            features = np.count_nonzero(matrix[:, :, adjacency_axis], axis=1, keepdims=False)
            features = features.tolist()
        elif aggregation == "features":
            features = matrix[:, :, feature_axis]
            features = features.reshape((features.shape[0], -1))
            features = features.tolist()
        else:
            raise KeyError('Only sum, mean, node_degree and all features are supported')

        features = dict(enumerate(features, 0))

        return features

VHolstein avatar May 02 '23 18:05 VHolstein

@VHolstein thank you for opening this issue! We are really welcome to contributions! You are welcome to create a pull request about this issue, with the proper unit tests and optionally add the relevant documentation and examples.

ysig avatar Sep 07 '23 21:09 ysig