4.6 从CSV文件加载数据

逗号分隔值(CSV)是一种广泛使用的数据存储格式。DGL提供了 CSVDataset 用于加载和解析以CSV格式存储的图数据。

要创建一个 CSVDataset 对象:

import dgl
ds = dgl.data.CSVDataset('/path/to/dataset')

返回的 ds 对象是一个标准的 DGLDataset。例如,可以使用 __getitem__ 获取图样本,并使用 ndata/edata 获取节点/边特征。

# A demonstration of how to use the loaded dataset. The feature names
# may vary depending on the CSV contents.
g = ds[0] # get the graph
label = g.ndata['label']
feat = g.ndata['feat']

数据文件夹结构

/path/to/dataset/
|-- meta.yaml     # metadata of the dataset
|-- edges_0.csv   # edge data including src_id, dst_id, feature, label and so on
|-- ...           # you can have as many CSVs for edge data as you want
|-- nodes_0.csv   # node data including node_id, feature, label and so on
|-- ...           # you can have as many CSVs for node data as you want
|-- graphs.csv    # graph-level features

节点/边/图级别的数据存储在CSV文件中。meta.yaml是一个元数据文件,指定了从哪里读取节点/边/图数据以及如何解析它们以构建数据集对象。一个最小的数据文件夹包含一个meta.yaml和两个CSV文件,一个用于节点数据,一个用于边数据,在这种情况下,数据集只包含一个没有图级别数据的图。

无特征图的单一数据集

当数据集中只包含一个没有节点或边特征的图时,数据文件夹中只需要三个文件:meta.yaml,一个用于节点ID的CSV文件和一个用于边的CSV文件:

./mini_featureless_dataset/
|-- meta.yaml
|-- nodes.csv
|-- edges.csv

meta.yaml 包含以下信息:

dataset_name: mini_featureless_dataset
edge_data:
- file_name: edges.csv
node_data:
- file_name: nodes.csv

nodes.csv 列出了 node_id 字段下的节点ID:

node_id
0
1
2
3
4

edges.csv 列出了所有边的两列(src_iddst_id),指定每条边的源节点ID和目标节点ID:

src_id,dst_id
4,4
4,1
3,0
4,1
4,0
1,2
1,3
3,3
1,1
4,1

加载后,数据集包含一个没有任何特征的图:

>>> import dgl
>>> dataset = dgl.data.CSVDataset('./mini_featureless_dataset')
>>> g = dataset[0]  # only one graph
>>> print(g)
Graph(num_nodes=5, num_edges=10,
      ndata_schemes={}
      edata_schemes={})

注意

允许使用非整数节点ID。在构建图时,CSVDataset会将每个原始ID映射为从零开始的整数ID。如果节点ID已经是0到num_nodes-1之间的不同整数,则不进行映射。

注意

边总是有方向的。要拥有双向边,可以在边的CSV文件中添加反向边,或者使用AddReverse来转换加载的图。

没有任何特征的图通常不太有趣。在下一个示例中,我们将展示如何加载和解析节点或边的特征。

带有特征和标签的单个图的数据集

当数据集包含具有节点或边特征和标签的单个图时,数据文件夹中仍然只需要三个文件:meta.yaml,一个用于节点ID的CSV文件和一个用于边的CSV文件:

./mini_feature_dataset/
|-- meta.yaml
|-- nodes.csv
|-- edges.csv

meta.yaml:

dataset_name: mini_feature_dataset
edge_data:
- file_name: edges.csv
node_data:
- file_name: nodes.csv

edges.csv 包含五个合成的边缘数据 (label, train_mask, val_mask, test_mask, feat):

src_id,dst_id,label,train_mask,val_mask,test_mask,feat
4,0,2,False,True,True,"0.5477868606453535, 0.4470617033458436, 0.936706701616337"
4,0,0,False,False,True,"0.9794634290792008, 0.23682038840665198, 0.049629338970987646"
0,3,1,True,True,True,"0.8586722047523594, 0.5746912787380253, 0.6462162561249654"
0,1,2,True,False,False,"0.2730008213674695, 0.5937484188166621, 0.765544096939567"
0,2,1,True,True,True,"0.45441619816038514, 0.1681403185591509, 0.9952376085297715"
0,0,0,False,False,False,"0.4197669213305396, 0.849983324532477, 0.16974127573016262"
2,2,1,False,True,True,"0.5495035052928215, 0.21394654203489705, 0.7174910641836348"
1,0,2,False,True,False,"0.008790817766266334, 0.4216530595907526, 0.529195480661293"
3,0,0,True,True,True,"0.6598715708878852, 0.1932390907048961, 0.9774471538377553"
4,0,1,False,False,False,"0.16846068931179736, 0.41516080644186737, 0.002158116134429955"

nodes.csv 包含五个合成节点数据 (label, train_mask, val_mask, test_mask, feat):

node_id,label,train_mask,val_mask,test_mask,feat
0,1,False,True,True,"0.07816474278491703, 0.9137336384979067, 0.4654086994009452"
1,1,True,True,True,"0.05354099924658973, 0.8753101998792645, 0.33929432608774135"
2,1,True,False,True,"0.33234211884156384, 0.9370522452510665, 0.6694943496824788"
3,0,False,True,False,"0.9784264442230887, 0.22131880861864428, 0.3161154827254189"
4,1,True,True,False,"0.23142237259162102, 0.8715767748481147, 0.19117861103555467"

加载后,数据集包含一个图。节点/边特征存储在ndataedata中,列名相同。该示例演示了如何使用双引号"..."括起来的逗号分隔列表来指定向量形状的特征。

>>> import dgl
>>> dataset = dgl.data.CSVDataset('./mini_feature_dataset')
>>> g = dataset[0]  # only one graph
>>> print(g)
Graph(num_nodes=5, num_edges=10,
      ndata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'val_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool), 'feat': Scheme(shape=(3,), dtype=torch.float64)}
      edata_schemes={'label': Scheme(shape=(), dtype=torch.int64), 'train_mask': Scheme(shape=(), dtype=torch.bool), 'val_mask': Scheme(shape=(), dtype=torch.bool), 'test_mask': Scheme(shape=(), dtype=torch.bool), 'feat': Scheme(shape=(3,), dtype=torch.float64)})

注意

默认情况下,CSVDatatset 假设所有特征数据都是数值(例如,int、float、bool 或 list),并且不允许缺失值。用户可以为这些情况提供自定义数据解析器。有关更多详细信息,请参阅 自定义数据解析器

单个异构图的数据集

可以指定多个节点和边的CSV文件(每种类型一个)来表示异构图。 以下是一个包含两种节点类型和两种边类型的示例数据:

./mini_hetero_dataset/
|-- meta.yaml
|-- nodes_0.csv
|-- nodes_1.csv
|-- edges_0.csv
|-- edges_1.csv

meta.yaml 指定了每个CSV文件的节点类型名称(使用 ntype)和边类型名称(使用 etype)。边类型名称是一个包含源节点类型名称、关系名称和目标节点类型名称的字符串三元组。

dataset_name: mini_hetero_dataset
edge_data:
- file_name: edges_0.csv
  etype: [user, follow, user]
- file_name: edges_1.csv
  etype: [user, like, item]
node_data:
- file_name: nodes_0.csv
  ntype: user
- file_name: nodes_1.csv
  ntype: item

节点和边的CSV文件遵循与同质图相同的格式。以下是一些用于演示的合成数据:

edges_0.csvedges_1.csv:

src_id,dst_id,label,feat
4,4,1,"0.736833152378035,0.10522806046048205,0.9418796835016118"
3,4,2,"0.5749339182767451,0.20181320245665535,0.490938012147181"
1,4,2,"0.7697294432580938,0.49397782380750765,0.10864079337442234"
0,4,0,"0.1364240150959487,0.1393107840629273,0.7901988878812207"
2,3,1,"0.42988138237505735,0.18389137408509248,0.18431292077750894"
0,4,2,"0.8613368738351794,0.67985810014162,0.6580438064356824"
2,4,1,"0.6594951663841697,0.26499036865016423,0.7891429392727503"
4,1,0,"0.36649684241348557,0.9511783938523962,0.8494919263589972"
1,1,2,"0.698592283371875,0.038622249776255946,0.5563827995742111"
0,4,1,"0.5227112950269823,0.3148264185956532,0.47562693094002173"

nodes_0.csvnodes_1.csv:

node_id,label,feat
0,2,"0.5400687466285844,0.7588441197954202,0.4268254673041745"
1,1,"0.08680051341900807,0.11446843700743892,0.7196969604886617"
2,2,"0.8964389655603473,0.23368113896545695,0.8813472954005022"
3,1,"0.5454703921677284,0.7819383771535038,0.3027939452162367"
4,1,"0.5365210052235699,0.8975240205792763,0.7613943085507672"

加载后,数据集包含一个具有特征和标签的异构图:

>>> import dgl
>>> dataset = dgl.data.CSVDataset('./mini_hetero_dataset')
>>> g = dataset[0]  # only one graph
>>> print(g)
Graph(num_nodes={'item': 5, 'user': 5},
      num_edges={('user', 'follow', 'user'): 10, ('user', 'like', 'item'): 10},
      metagraph=[('user', 'user', 'follow'), ('user', 'item', 'like')])
>>> g.nodes['user'].data
{'label': tensor([2, 1, 2, 1, 1]), 'feat': tensor([[0.5401, 0.7588, 0.4268],
        [0.0868, 0.1145, 0.7197],
        [0.8964, 0.2337, 0.8813],
        [0.5455, 0.7819, 0.3028],
        [0.5365, 0.8975, 0.7614]], dtype=torch.float64)}
>>> g.edges['like'].data
{'label': tensor([1, 2, 2, 0, 1, 2, 1, 0, 2, 1]), 'feat': tensor([[0.7368, 0.1052, 0.9419],
        [0.5749, 0.2018, 0.4909],
        [0.7697, 0.4940, 0.1086],
        [0.1364, 0.1393, 0.7902],
        [0.4299, 0.1839, 0.1843],
        [0.8613, 0.6799, 0.6580],
        [0.6595, 0.2650, 0.7891],
        [0.3665, 0.9512, 0.8495],
        [0.6986, 0.0386, 0.5564],
        [0.5227, 0.3148, 0.4756]], dtype=torch.float64)}

多图数据集

当存在多个图时,可以包含一个额外的CSV文件来存储图级别的特征。 以下是一个示例:

./mini_multi_dataset/
|-- meta.yaml
|-- nodes.csv
|-- edges.csv
|-- graphs.csv

因此,meta.yaml 应该包含一个额外的 graph_data 键,以指示从哪个CSV文件加载图级特征。

dataset_name: mini_multi_dataset
edge_data:
- file_name: edges.csv
node_data:
- file_name: nodes.csv
graph_data:
  file_name: graphs.csv

为了区分不同图的节点和边,node.csvedge.csv 必须包含一个额外的列 graph_id

edges.csv:

graph_id,src_id,dst_id,feat
0,0,4,"0.39534097273254654,0.9422093637539785,0.634899790318452"
0,3,0,"0.04486384200747007,0.6453746567017163,0.8757520744192612"
0,3,2,"0.9397636966928355,0.6526403892728874,0.8643238446466464"
0,1,1,"0.40559906615287566,0.9848072295736628,0.493888090726854"
0,4,1,"0.253458867276219,0.9168191778828504,0.47224962583565544"
0,0,1,"0.3219496197945605,0.3439899477636117,0.7051530741717352"
0,2,1,"0.692873149428549,0.4770019763881086,0.21937428942781778"
0,4,0,"0.620118223673067,0.08691420300562658,0.86573472329756"
0,2,1,"0.00743445923710373,0.5251800239734318,0.054016385555202384"
0,4,1,"0.6776417760682221,0.7291568018841328,0.4523600060547709"
1,1,3,"0.6375445528248924,0.04878384701995819,0.4081642382536248"
1,0,4,"0.776002616178397,0.8851294998284638,0.7321742043493028"
1,1,0,"0.0928555079874982,0.6156748364694707,0.6985674921582508"
1,0,2,"0.31328748118329997,0.8326121496142408,0.04133991340612775"
1,1,0,"0.36786902637778773,0.39161865931662243,0.9971749359397111"
1,1,1,"0.4647410679872376,0.8478810655406659,0.6746269314422184"
1,0,2,"0.8117650553546695,0.7893727601272978,0.41527155506593394"
1,1,3,"0.40707309111756307,0.2796588354307046,0.34846782265758314"
1,1,0,"0.18626464175355095,0.3523777809254057,0.7863421810531344"
1,3,0,"0.28357022069634585,0.13774964202156292,0.5913335505943637"

nodes.csv:

graph_id,node_id,feat
0,0,"0.5725330322207948,0.8451870383322376,0.44412796119211184"
0,1,"0.6624186423087752,0.6118386331195641,0.7352138669985214"
0,2,"0.7583372765843964,0.15218126307872892,0.6810484348765842"
0,3,"0.14627522432017592,0.7457985352827006,0.1037097085190507"
0,4,"0.49037522512771525,0.8778998699783784,0.0911194482288028"
1,0,"0.11158102039672668,0.08543289788089736,0.6901745368284345"
1,1,"0.28367647637469273,0.07502571020414439,0.01217200152200748"
1,2,"0.2472495901894738,0.24285506608575758,0.6494437360242048"
1,3,"0.5614197853127827,0.059172654879085296,0.4692371689047904"
1,4,"0.17583413999295983,0.5191278830882644,0.8453123358491914"

graphs.csv 包含一个 graph_id 列和任意数量的特征列。 这里的示例数据集有两个图,每个图都有一个 feat 和一个 label 图级数据。

graph_id,feat,label
0,"0.7426272601929126,0.5197462471155317,0.8149104951283953",0
1,"0.534822233529295,0.2863627767733977,0.1154897249106891",0

加载后,数据集具有多个同形异义词,包含特征和标签:

>>> import dgl
>>> dataset = dgl.data.CSVDataset('./mini_multi_dataset')
>>> print(len(dataset))
2
>>> graph0, data0 = dataset[0]
>>> print(graph0)
Graph(num_nodes=5, num_edges=10,
      ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)}
      edata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)})
>>> print(data0)
{'feat': tensor([0.7426, 0.5197, 0.8149], dtype=torch.float64), 'label': tensor(0)}
>>> graph1, data1 = dataset[1]
>>> print(graph1)
Graph(num_nodes=5, num_edges=10,
      ndata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)}
      edata_schemes={'feat': Scheme(shape=(3,), dtype=torch.float64)})
>>> print(data1)
{'feat': tensor([0.5348, 0.2864, 0.1155], dtype=torch.float64), 'label': tensor(0)}

如果graphs.csv中只有一个特征列,data0将直接是特征的张量。

自定义数据解析器

默认情况下,CSVDataset 假设所有存储的节点/边/图级数据都是数值。用户可以提供自定义的 DataParserCSVDataset 来处理更复杂的数据类型。一个 DataParser 需要实现 __call__ 方法,该方法接收从 CSV 文件创建的 pandas.DataFrame 对象,并应返回解析后的特征数据字典。解析后的特征数据将保存到相应 DGLGraph 对象的 ndataedata 中,因此必须是张量或 numpy 数组。下面展示了一个将字符串类型标签转换为整数的 DataParser 示例:

给定一个数据集如下,

./customized_parser_dataset/
|-- meta.yaml
|-- nodes.csv
|-- edges.csv

meta.yaml:

dataset_name: customized_parser_dataset
edge_data:
- file_name: edges.csv
node_data:
- file_name: nodes.csv

edges.csv:

src_id,dst_id,label
4,0,positive
4,0,negative
0,3,positive
0,1,positive
0,2,negative
0,0,positive
2,2,negative
1,0,positive
3,0,negative
4,0,positive

nodes.csv:

node_id,label
0,positive
1,negative
2,positive
3,negative
4,positive

要解析字符串类型的标签,可以定义一个DataParser类如下:

import numpy as np
import pandas as pd

class MyDataParser:
    def __call__(self, df: pd.DataFrame):
        parsed = {}
        for header in df:
            if 'Unnamed' in header:  # Handle Unnamed column
                print("Unnamed column is found. Ignored...")
                continue
            dt = df[header].to_numpy().squeeze()
            if header == 'label':
                dt = np.array([1 if e == 'positive' else 0 for e in dt])
            parsed[header] = dt
        return parsed

使用定义的DataParser创建一个CSVDataset

>>> import dgl
>>> dataset = dgl.data.CSVDataset('./customized_parser_dataset',
...                               ndata_parser=MyDataParser(),
...                               edata_parser=MyDataParser())
>>> print(dataset[0].ndata['label'])
tensor([1, 0, 1, 0, 1])
>>> print(dataset[0].edata['label'])
tensor([1, 0, 1, 1, 0, 1, 0, 1, 0, 1])

注意

要为不同的节点/边类型指定不同的DataParser,请将字典传递给 ndata_parseredata_parser,其中键是类型名称(节点类型为单个字符串;边类型为字符串三元组),值是使用的DataParser

完整的YAML规范

CSVDataset 允许对加载和解析过程进行更灵活的控制。例如,可以通过 meta.yaml 更改 ID 列名。下面的示例列出了所有支持的键。

version: 1.0.0
dataset_name: some_complex_data
separator: ','                   # CSV separator symbol. Default: ','
edge_data:
- file_name: edges_0.csv
  etype: [user, follow, user]
  src_id_field: src_id           # Column name for source node IDs. Default: src_id
  dst_id_field: dst_id           # Column name for destination node IDs. Default: dst_id
- file_name: edges_1.csv
  etype: [user, like, item]
  src_id_field: src_id
  dst_id_field: dst_id
node_data:
- file_name: nodes_0.csv
  ntype: user
  node_id_field: node_id         # Column name for node IDs. Default: node_id
- file_name: nodes_1.csv
  ntype: item
  node_id_field: node_id         # Column name for node IDs. Default: node_id
graph_data:
  file_name: graphs.csv
  graph_id_field: graph_id       # Column name for graph IDs. Default: graph_id

顶级

在顶层,只有6个键可用:

  • version: 可选。字符串。 它指定了使用哪个版本的meta.yaml。未来可能会添加更多功能。

  • dataset_name: 必需。字符串。 它指定了数据集名称。

  • separator: 可选的。字符串。 它指定如何解析CSV文件中的数据。默认值:','

  • edge_data: 必需。EdgeData 的列表。 用于解析边缘 CSV 文件的元数据。

  • node_data: 必需。NodeData 的列表。 用于解析节点 CSV 文件的元数据。

  • graph_data: 可选的。 GraphData. 用于解析图形CSV文件的元数据。

EdgeData

有4个键:

  • file_name: 必需。字符串。 用于加载数据的CSV文件。

  • etype: 可选。字符串列表。 字符串三元组中的边类型名称:[源节点类型,关系类型,目标节点类型]。

  • src_id_field: 可选。字符串。 读取源节点ID的列。默认值:src_id

  • dst_id_field: 可选。字符串。 用于读取目标节点ID的列。默认值:dst_id

NodeData

有3个关键点:

  • file_name: 必需。字符串。 用于加载数据的CSV文件。

  • ntype: 可选。字符串。 节点类型名称。

  • node_id_field: 可选的。字符串。 读取节点ID的列。默认值:node_id

GraphData

有2个键:

  • file_name: Required. String. The CSV file to load data from.

  • graph_id_field: 可选。字符串。 用于读取图形ID的列。默认值:graph_id