<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def _get_efron_values_batch(self, X, T, E, weights, beta):
 # pylint: disable=too-many-locals """ Assumes sorted on ascending on T Calculates the first and second order vector differentials, with respect to beta. A good explanation for how Efron handles ties. Consider three of five subjects who fail at the time. As it is not known a priori that who is the first to fail, so one-third of (φ1 + φ2 + φ3) is adjusted from sum_j^{5} φj after one fails. Similarly two-third of (φ1 + φ2 + φ3) is adjusted after first two individuals fail, etc. Returns ------- hessian: (d, d) numpy array, gradient: (1, d) numpy array log_likelihood: float """


        n, d = X.shape
        hessian = np.zeros((d, d))
        gradient = np.zeros((d,))
        log_lik = 0
        # weights = weights[:, None]

        # Init risk and tie sums to zero
        risk_phi, tie_phi = 0, 0
        risk_phi_x, tie_phi_x = np.zeros((d,)), np.zeros((d,))
        risk_phi_x_x, tie_phi_x_x = np.zeros((d, d)), np.zeros((d, d))

        # counts are sorted by -T
        _, counts = np.unique(-T, return_counts=True)
        scores = weights * np.exp(np.dot(X, beta))
        pos = n

        for count_of_removals in counts:

            slice_ = slice(pos - count_of_removals, pos)

            X_at_t = X[slice_]
            weights_at_t = weights[slice_]

            phi_i = scores[slice_, None]
            phi_x_i = phi_i * X_at_t
            phi_x_x_i = np.dot(X_at_t.T, phi_x_i)

            # Calculate sums of Risk set
            risk_phi = risk_phi + array_sum_to_scalar(phi_i)
            risk_phi_x = risk_phi_x + matrix_axis_0_sum_to_array(phi_x_i)
            risk_phi_x_x = risk_phi_x_x + phi_x_x_i

            # Calculate the sums of Tie set
            deaths = E[slice_]

            tied_death_counts = array_sum_to_scalar(deaths.astype(int))
            if tied_death_counts == 0:
                # no deaths, can continue
                pos -= count_of_removals
                continue

            xi_deaths = X_at_t[deaths]
            weights_deaths = weights_at_t[deaths]

            x_death_sum = matrix_axis_0_sum_to_array(weights_deaths[:, None] * xi_deaths)

            weight_count = array_sum_to_scalar(weights_deaths)
            weighted_average = weight_count / tied_death_counts

            if tied_death_counts > 1:

                # a lot of this is now in Einstein notation for performance, but see original "expanded" code here
                # https://github.com/CamDavidsonPilon/lifelines/blob/e7056e7817272eb5dff5983556954f56c33301b1/lifelines/fitters/coxph_fitter.py#L755-L789

                # it's faster if we can skip computing these when we don't need to.
                tie_phi = array_sum_to_scalar(phi_i[deaths])
                tie_phi_x = matrix_axis_0_sum_to_array(phi_x_i[deaths])
                tie_phi_x_x = np.dot(xi_deaths.T, phi_i[deaths] * xi_deaths)

                increasing_proportion = np.arange(tied_death_counts) / tied_death_counts
                denom = 1.0 / (risk_phi - increasing_proportion * tie_phi)
                numer = risk_phi_x - np.outer(increasing_proportion, tie_phi_x)

                # computes outer products and sums them together.
                # Naive approach is to
                # 1) broadcast tie_phi_x_x and increasing_proportion into a (tied_death_counts, d, d) matrix
                # 2) broadcast risk_phi_x_x and denom into a (tied_death_counts, d, d) matrix
                # 3) subtract them, and then sum to (d, d)
                # Alternatively, we can sum earlier without having to explicitly create (_, d, d) matrices. This is used here.
                #
                a1 = np.einsum("ab,i->ab", risk_phi_x_x, denom) - np.einsum(
                    "ab,i->ab", tie_phi_x_x, increasing_proportion * denom
                )
            else:
                # no tensors here, but do some casting to make it easier in the converging step next.
                denom = 1.0 / np.array([risk_phi])
                numer = risk_phi_x
                a1 = risk_phi_x_x * denom

            summand = numer * denom[:, None]
            # This is a batch outer product.
            # given a matrix t, for each row, m, compute it's outer product: m.dot(m.T), and stack these new matrices together.
            # which would be: np.einsum("Bi, Bj->Bij", t, t)
            a2 = summand.T.dot(summand)

            gradient = gradient + x_death_sum - weighted_average * summand.sum(0)
            log_lik = log_lik + np.dot(x_death_sum, beta) + weighted_average * np.log(denom).sum()
            hessian = hessian + weighted_average * (a2 - a1)
            pos -= count_of_removals

        return hessian, gradient, log_lik