Repository: dmlc/dgl

Issue:
Adding new datasets to dgl.data
## 🚀 Feature

Adding a new graph dataset related to node classification (fraud detection) and a new graph dataset related to graph classification (fake news detection) as the default datasets in `dgl.data`. 

## Motivation

[The first graph dataset](https://github.com/YingtongDou/CARE-GNN) includes two homogeneous multi-relational graphs extracted from Yelp and Amazon where nodes represent fraudulent reviews or fraudulent reviewers. It was first proposed in a [CIKM'20 paper](https://arxiv.org/pdf/2008.08692.pdf) and has been used by a recent [WWW'21 paper](https://ponderly.github.io/pub/PCGNN_WWW2021.pdf) as a benchmark. Another [paper](https://arxiv.org/pdf/2104.01404) also takes the dataset as an example to study the non-homophilous graphs. This dataset is built upon industrial data and has rich relational information and unique properties like class-imbalance and feature inconsistency, which makes the dataset be a good instance to investigate how GNNs perform on real-world noisy graphs.

[The second graph dataset](https://github.com/safe-graph/GNN-FakeNews) is composed of two sets of tree-structured fake/real news propagation graphs extracted from Twitter. Different from most of the benchmark datasets for the graph classification task, the graphs in this dataset are tree-structured graphs where the root node represents the news, the leaf nodes are Twitter users who retweeted the root news. Besides, the node features are encoded user historical tweets using different pretrained language models. The dataset could help GNNs learn how to fuse multi-modal information and learn representations for tree-structured graphs. It would be a good addition to current graph classification benchmarks.


## Alternatives

N/A

## Pitch

Adding the above two new datasets as default datasets in `dgl.data`.

## Additional context

N/A


Discussion:
Hi, can you follow this guildline to provide your dataset? https://docs.dgl.ai/en/latest/guide/data-dataset.html#guide-data-pipeline-dataset

BTW, we can help upload the dataset into DGL's dataset S3.
> Hi, can you follow this guildline to provide your dataset? https://docs.dgl.ai/en/latest/guide/data-dataset.html#guide-data-pipeline-dataset
> 
> BTW, we can help upload the dataset into DGL's dataset S3.

Thanks for your suggestions!
We will follow the instructions to prepare the dataset and pin you in this issue after finishing it.
Hi! I find in this tutorial https://docs.dgl.ai/en/0.6.x/new-tutorial/6_load_data.html, under **Creating a Dataset for Node Classification** section:
![image](https://user-images.githubusercontent.com/37773620/116950751-f81f5c80-ac53-11eb-860b-778d04c0fb75.png)
This part of the code just simply splits the data from head to end into 3 sections and doesn't add any randomness. This is too misleading because normally we need to add randomness when we split data into train data, validation data, and test data.
> This part of the code just simply splits the data from head to end into 3 sections and doesn't add any randomness. This is too misleading because normally we need to add randomness when we split data into train data, validation data, and test data.

Normally we want to fix the training, validation, and test set across all experiments for fair comparisons.  So indeed we do not want to introduce randomness during dataset split.
Isn't setting a random seed a better way to control randomness? From my experience, if you simply cut data into 3 sections, the validation accuracy and test accuracy would vary a lot.
> Isn't setting a random seed a better way to control randomness? From my experience, if you simply cut data into 3 sections, the validation accuracy and test accuracy would vary a lot.

Random seed works but I think storing the fixed train-val-test ids is more safe and standard.

Resolving patch:
diff --git a/python/dgl/data/fakenews.py b/python/dgl/data/fakenews.py
new file mode 100644
--- /dev/null
+++ b/python/dgl/data/fakenews.py
@@ -0,0 +1,217 @@
+import torch
+import os
+import numpy as np
+import pandas as pd
+import scipy.sparse as sp
+
+from .dgl_dataset import DGLBuiltinDataset
+from .utils import save_graphs, load_graphs, _get_dgl_url
+from .utils import save_info, load_info
+from ..convert import graph
+
+
+class FakeNewsDataset(DGLBuiltinDataset):
+    r"""Fake News Graph Classification dataset.
+
+    The dataset is composed of two sets of tree-structured fake/real
+    news propagation graphs extracted from Twitter. Different from
+    most of the benchmark datasets for the graph classification task,
+    the graphs in this dataset are directed tree-structured graphs where
+    the root node represents the news, the leaf nodes are Twitter users
+    who retweeted the root news. Besides, the node features are encoded
+    user historical tweets using different pretrained language models:
+        bert: the 768-dimensional node feature composed of Twitter user
+              historical tweets encoded by the bert-as-service
+        content: the 310-dimensional node feature composed of a
+                 300-dimensional “spacy” vector plus a 10-dimensional
+                 “profile” vector
+        profile: the 10-dimensional node feature composed of ten Twitter
+                 user profile attributes.
+        spacy: the 300-dimensional node feature composed of Twitter user
+               historical tweets encoded by the spaCy word2vec encoder.
+
+    Statistics:
+
+        Politifact:
+
+        - Graphs: 314
+        - Nodes: 41,054
+        - Edges: 40,740
+        - Number of Classes: 2
+        - Node feature size:
+            bert: 768
+            content: 310
+            profile: 10
+            spacy: 300
+
+        Gossipcop:
+
+        - Graphs: 5464
+        - Nodes: 314,262
+        - Edges: 308,798
+        - Number of Classes: 2
+        - Node feature size:
+            bert: 768
+            content: 310
+            profile: 10
+            spacy: 300
+
+    Parameters
+    ----------
+    name : str
+        Name of the dataset (gossipcop, or politifact)
+    feature_name : str
+        Name of the feature (bert, content, profile, or spacy)
+    raw_dir : str
+        Specifying the directory that will store the
+        downloaded data or the directory that
+        already stores the input data.
+        Default: ~/.dgl/
+
+    Attributes
+    ----------
+    name : str
+        Name of the dataset (gossipcop, or politifact)
+    num_classes : int
+        Number of label classes
+    num_graphs : int
+        Number of graphs
+    graphs : list
+        A list of DGLGraph objects
+    labels : Tensor
+        Graph labels
+    feature_name : str
+        Name of the feature (bert, content, profile, or spacy)
+    feature : scipy.sparse.csr.csr_matrix
+        Node features
+    train_mask : Tensor
+        Mask of training set
+    val_mask : Tensor
+        Mask of validation set
+    test_mask : Tensor
+        Mask of testing set
+
+    Examples
+    --------
+    >>> dataset = FakeNewsDataset('gossipcop', 'bert')
+    >>> graph, label = dataset[0]
+    >>> num_classes = dataset.num_classes
+    >>> feat = dataset.feature
+    >>> labels = dataset.labels
+    """
+    file_urls = {
+        'gossipcop': 'dataset/FakeNewsGOS.zip',
+        'politifact': 'dataset/FakeNewsPOL.zip'
+    }
+
+    def __init__(self, name, feature_name, raw_dir=None):
+        assert name in ['gossipcop', 'politifact'], \
+            "Only supports 'gossipcop' or 'politifact'."
+        url = _get_dgl_url(self.file_urls[name])
+
+        assert feature_name in ['bert', 'content', 'profile', 'spacy'], \
+            "Only supports 'bert', 'content', 'profile', or 'spacy'"
+        self.feature_name = feature_name
+        super(FakeNewsDataset, self).__init__(name=name,
+                                              url=url,
+                                              raw_dir=raw_dir)
+
+    def process(self):
+        """process raw data to graph, labels and masks"""
+        self.labels = np.load(os.path.join(self.raw_path, 'graph_labels.npy'))
+        self.labels = torch.LongTensor(self.labels)
+        num_graphs = self.labels.shape[0]
+
+        node_graph_id = np.load(os.path.join(self.raw_path, 'node_graph_id.npy'))
+        edges = pd.read_csv(os.path.join(self.raw_path, 'A.txt'), header=None)
+        src = edges[0].to_numpy()
+        dst = edges[1].to_numpy()
+        g = graph((src, dst))
+
+        node_idx_list = []
+        for idx in range(np.max(node_graph_id) + 1):
+            node_idx = np.where(node_graph_id == idx)
+            node_idx_list.append(node_idx[0])
+
+        self.graphs = [g.subgraph(node_idx) for node_idx in node_idx_list]
+
+        train_idx = np.load(os.path.join(self.raw_path, 'train_idx.npy'))
+        val_idx = np.load(os.path.join(self.raw_path, 'val_idx.npy'))
+        test_idx = np.load(os.path.join(self.raw_path, 'test_idx.npy'))
+        train_mask = torch.zeros(num_graphs, dtype=torch.bool)
+        val_mask = torch.zeros(num_graphs, dtype=torch.bool)
+        test_mask = torch.zeros(num_graphs, dtype=torch.bool)
+        train_mask[train_idx] = True
+        val_mask[val_idx] = True
+        test_mask[test_idx] = True
+        self.train_mask = train_mask
+        self.val_mask = val_mask
+        self.test_mask = test_mask
+
+        feature_file = 'new_' + self.feature_name + '_feature.npz'
+        self.feature = sp.load_npz(os.path.join(self.raw_path, feature_file))
+
+    def save(self):
+        """save the graph list and the labels"""
+        graph_path = os.path.join(self.save_path, self.name + '_dgl_graph.bin')
+        info_path = os.path.join(self.save_path, self.name + '_dgl_graph.pkl')
+        save_graphs(str(graph_path), self.graphs)
+        save_info(info_path, {'label': self.labels,
+                              'feature': self.feature,
+                              'train_mask': self.train_mask,
+                              'val_mask': self.val_mask,
+                              'test_mask': self.train_mask})
+
+    def has_cache(self):
+        """ check whether there are processed data in `self.save_path` """
+        graph_path = os.path.join(self.save_path, self.name + '_dgl_graph.bin')
+        info_path = os.path.join(self.save_path, self.name + '_dgl_graph.pkl')
+        return os.path.exists(graph_path) and os.path.exists(info_path)
+
+    def load(self):
+        """load processed data from directory `self.save_path`"""
+        graph_path = os.path.join(self.save_path, self.name + '_dgl_graph.bin')
+        info_path = os.path.join(self.save_path, self.name + '_dgl_graph.pkl')
+
+        graphs, _ = load_graphs(str(graph_path))
+        info = load_info(str(info_path))
+        self.graphs = graphs
+        self.labels = info['label']
+        self.feature = info['feature']
+
+        self.train_mask = info['train_mask']
+        self.val_mask = info['val_mask']
+        self.test_mask = info['test_mask']
+
+    @property
+    def num_classes(self):
+        """Number of classes for each graph, i.e. number of prediction tasks."""
+        return 2
+
+    @property
+    def num_graphs(self):
+        """Number of graphs."""
+        return self.labels.shape[0]
+
+    def __getitem__(self, i):
+        r""" Get graph and label by index
+
+        Parameters
+        ----------
+        i : int
+            Item index
+
+        Returns
+        -------
+        (:class:`dgl.DGLGraph`, Tensor)
+        """
+        return self.graphs[i], self.labels[i]
+
+    def __len__(self):
+        r"""Number of graphs in the dataset.
+
+        Return
+        -------
+        int
+        """
+        return len(self.graphs)


Test patch: