class_docstr: Description
  -----------
  Boost GNN predictor for semi-supervised node classification or regression problems.
  Publication: https://arxiv.org/abs/2101.08543
  
  Parameters
  ----------
  gnn_model : nn.Module
      DGL implementation of GNN model.
  task: str, optional
      Regression or classification task.
  loss_fn : callable, optional
      Function that takes torch tensors, pred and true, and returns a scalar.
  trees_per_epoch : int, optional
      Number of GBDT trees to build each epoch.
  backprop_per_epoch : int, optional
      Number of backpropagation steps to make each epoch.
  lr : float, optional
      Learning rate of gradient descent optimizer.
  append_gbdt_pred : bool, optional
      Append GBDT predictions or replace original input node features.
  train_input_features : bool, optional
      Train original input node features.
  gbdt_depth : int, optional
      Depth of each tree in GBDT model.
  gbdt_lr : float, optional
      Learning rate of GBDT model.
  gbdt_alpha : int, optional
      Weight to combine previous and new GBDT trees.
  random_seed : int, optional
      random seed for GNN and GBDT models.
  
  Examples
  ----------
  gnn_model = GAT(10, 20, num_heads=5),
  bgnn = BGNNPredictor(gnn_model)
  metrics = bgnn.fit(graph, X, y, train_mask, val_mask, test_mask, cat_features)
docstr: :param graph : dgl.DGLGraph
      Input graph
  :param X : pd.DataFrame
      Input node features. Each column represents one input feature. Each row is a node.
      Values in dataframe are numerical, after preprocessing.
  :param y : pd.DataFrame
      Input node targets. Each column represents one target. Each row is a node
      (order of nodes should be the same as in X).
  :param train_mask : list[int]
      Node indexes (rows) that belong to train set.
  :param val_mask : list[int]
      Node indexes (rows) that belong to validation set.
  :param test_mask : list[int]
      Node indexes (rows) that belong to test set.
  :param original_X : pd.DataFrame, optional
      Input node features before preprocessing. Each column represents one input feature. Each row is a node.
      Values in dataframe can be of any type, including categorical (e.g. string, bool) or
      missing values (None). This is useful if you want to preprocess X with GBDT model.
  :param cat_features: list[int]
      Feature indexes (columns) which are categorical features.
  :param num_epochs : int
      Number of epochs to run.
  :param patience : int
      Number of epochs to wait until early stopping.
  :param logging_epochs : int
      Log every n epoch.
  :param metric_name : str
      Metric to use for early stopping.
  :param normalize_features : bool
      If to normalize original input features X (column wise).
  :param replace_na: bool
      If to replace missing values (None) in X.
  :return: metrics evaluated during training
docstr_tok: [":","param","graph",":","dgl.DGLGraph","Input","graph",":","param","X",":","pd.DataFrame","Input","node","features",".","Each","column","represents","one","input","feature",".","Each","row","is","a","node",".","Values","in","dataframe","are","numerical",",","after","preprocessing",".",":","param","y",":","pd.DataFrame","Input","node","targets",".","Each","column","represents","one","target",".","Each","row","is","a","node","(","order","of","nodes","should","be","the","same","as","in","X",")",".",":","param","train_mask",":","list","[","int","]","Node","indexes","(","rows",")","that","belong","to","train","set",".",":","param","val_mask",":","list","[","int","]","Node","indexes","(","rows",")","that","belong","to","validation","set",".",":","param","test_mask",":","list","[","int","]","Node","indexes","(","rows",")","that","belong","to","test","set",".",":","param","original_X",":","pd.DataFrame",",","optional","Input","node","features","before","preprocessing",".","Each","column","represents","one","input","feature",".","Each","row","is","a","node",".","Values","in","dataframe","can","be","of","any","type",",","including","categorical","(","e.g",".","string",",","bool",")","or","missing","values","(","None",")",".","This","is","useful","if","you","want","to","preprocess","X","with","GBDT","model",".",":","param","cat_features",":","list","[","int","]","Feature","indexes","(","columns",")","which","are","categorical","features",".",":","param","num_epochs",":","int","Number","of","epochs","to","run",".",":","param","patience",":","int","Number","of","epochs","to","wait","until","early","stopping",".",":","param","logging_epochs",":","int","Log","every","n","epoch",".",":","param","metric_name",":","str","Metric","to","use","for","early","stopping",".",":","param","normalize_features",":","bool","If","to","normalize","original","input","features","X","(","column","wise",")",".",":","param","replace_na",":","bool","If","to","replace","missing","values","(","None",")","in","X.",":","return",":","metrics","evaluated","during","training"]
code: def fit(
      self,
      graph,
      X,
      y,
      train_mask,
      val_mask,
      test_mask,
      original_X=None,
      cat_features=None,
      num_epochs=100,
      patience=10,
      logging_epochs=1,
      metric_name="loss",
  ):
      """
  
      :param graph : dgl.DGLGraph
          Input graph
      :param X : pd.DataFrame
          Input node features. Each column represents one input feature. Each row is a node.
          Values in dataframe are numerical, after preprocessing.
      :param y : pd.DataFrame
          Input node targets. Each column represents one target. Each row is a node
          (order of nodes should be the same as in X).
      :param train_mask : list[int]
          Node indexes (rows) that belong to train set.
      :param val_mask : list[int]
          Node indexes (rows) that belong to validation set.
      :param test_mask : list[int]
          Node indexes (rows) that belong to test set.
      :param original_X : pd.DataFrame, optional
          Input node features before preprocessing. Each column represents one input feature. Each row is a node.
          Values in dataframe can be of any type, including categorical (e.g. string, bool) or
          missing values (None). This is useful if you want to preprocess X with GBDT model.
      :param cat_features: list[int]
          Feature indexes (columns) which are categorical features.
      :param num_epochs : int
          Number of epochs to run.
      :param patience : int
          Number of epochs to wait until early stopping.
      :param logging_epochs : int
          Log every n epoch.
      :param metric_name : str
          Metric to use for early stopping.
      :param normalize_features : bool
          If to normalize original input features X (column wise).
      :param replace_na: bool
          If to replace missing values (None) in X.
      :return: metrics evaluated during training
      """
  
      # initialize for early stopping and metrics
      if metric_name in ["r2", "accuracy"]:
          best_metric = [np.cfloat("-inf")] * 3  # for train/val/test
      else:
          best_metric = [np.cfloat("inf")] * 3  # for train/val/test
  
      best_val_epoch = 0
      epochs_since_last_best_metric = 0
      metrics = ddict(list)
      if cat_features is None:
          cat_features = []
  
      if self.task == "regression":
          self.out_dim = y.shape[1]
      elif self.task == "classification":
          self.out_dim = len(set(y.iloc[test_mask, 0]))
      self.in_dim = (
          self.out_dim + X.shape[1]
          if self.append_gbdt_pred
          else self.out_dim
      )
  
      if original_X is None:
          original_X = X.copy()
          cat_features = []
  
      gbdt_X_train = original_X.iloc[train_mask]
      gbdt_y_train = y.iloc[train_mask]
      gbdt_alpha = self.gbdt_alpha
      self.gbdt_model = None
  
      node_features = self.init_node_features(X)
      optimizer = self.init_optimizer(
          node_features,
          optimize_node_features=True,
          learning_rate=self.lr,
      )
  
      y = (
          torch.from_numpy(y.to_numpy(copy=True))
          .float()
          .squeeze()
          .to(self.device)
      )
      graph = graph.to(self.device)
  
      pbar = tqdm(range(num_epochs))
      for epoch in pbar:
          start2epoch = time.time()
  
          # gbdt part
          self.train_gbdt(
              gbdt_X_train,
              gbdt_y_train,
              cat_features,
              epoch,
              self.trees_per_epoch,
              gbdt_alpha,
          )
  
          self.update_node_features(node_features, X, original_X)
          node_features_before = node_features.clone()
          model_in = (graph, node_features)
          loss = self.train_and_evaluate(
              model_in,
              y,
              train_mask,
              val_mask,
              test_mask,
              optimizer,
              metrics,
              self.backprop_per_epoch,
          )
          gbdt_y_train = self.update_gbdt_targets(
              node_features, node_features_before, train_mask
          )
  
          self.log_epoch(
              pbar,
              metrics,
              epoch,
              loss,
              time.time() - start2epoch,
              logging_epochs,
              metric_name=metric_name,
          )
  
          # check early stopping
          (
              best_metric,
              best_val_epoch,
              epochs_since_last_best_metric,
          ) = self.update_early_stopping(
              metrics,
              epoch,
              best_metric,
              best_val_epoch,
              epochs_since_last_best_metric,
              metric_name,
              lower_better=(metric_name not in ["r2", "accuracy"]),
          )
          if patience and epochs_since_last_best_metric > patience:
              break
  
          if np.isclose(gbdt_y_train.sum(), 0.0):
              print(
                  "Node embeddings do not change anymore. Stopping..."
              )
              break
  
      print(
          "Best {} at iteration {}: {:.3f}/{:.3f}/{:.3f}".format(
              metric_name, best_val_epoch, *best_metric
          )
      )
      return metrics
  
imports: import itertools
  import time
  from collections import defaultdict
  import numpy
  import pandas
  import torch
  import torch.nn.functional
  from catboost import CatBoostClassifier, CatBoostRegressor, Pool, sum_models
  from sklearn import preprocessing
  from sklearn.metrics import r2_score
  from tqdm import tqdm
label_desc: Use image node_id 15 for calling the BGNNPredictor obj's underlying member method code with example usage: obj.fit(graph, X, y, train_mask, val_mask, test_mask, original_X, cat_features, num_epochs, patience, logging_epochs, metric_name) and returns: metrics