def _derive_internal_node_G(tree_df):
    """Function to derive predictons for internal nodes in a single tree.

    This involves starting at each leaf node in the tree and propagating the
    G value back up through the tree, adding this leaf node G to each node
    that is travelled to.

    Parameters:
    -----------
    tree_df : pd.DataFrame
        Rows from corresponding to a single tree, from _derive_predictions.

    Returns
    -------
    pd.DataFrame
        Updated tree_df with G propagated up the tree s.t. each internal node's G value
        is the sum of G for it's child nodes.

    """

    tree_df = tree_df.copy()

    leaf_df = tree_df.loc[tree_df["node_type"] == "leaf"]

    # loop through each leaf node
    for i in leaf_df.index:

        # print(i, 'leaf------------------')

        leaf_row = leaf_df.loc[[i]]
        current_node = leaf_row["nodeid"].item()

        leaf_G = leaf_row["G"].item()

        # print('current_node', current_node)
        # print(tree_df)
        # print('----')

        # if the leaf node is not also the first node in the tree
        if current_node > 0:

            # traverse the tree bottom from bottom to top and propagate the G value upwards
            while True:

                # find parent node row
                parent = (tree_df["yes"] == current_node) | (
                    tree_df["no"] == current_node
                )

                # get parent node G
                tree_df.loc[parent, "G"] = tree_df.loc[parent, "G"] + leaf_G

                # update the current node to be the parent node
                leaf_row = tree_df.loc[parent]
                current_node = leaf_row["nodeid"].item()

                # print('current_node', current_node)
                # print(tree_df)
                # print('----')

                # if we have made it back to the top node in the tree then stop
                if current_node == 0:

                    break

    return tree_df