<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def estimate_entropy(X, epsilon=None):
 r"""Estimate a dataset's Shannon entropy. This function can take datasets of mixed discrete and continuous features, and uses a set of heuristics to determine which functions to apply to each. Because this function is a subroutine in a mutual information estimator, we employ the Kozachenko Estimator[1] for continuous features when this function is _not_ used for mutual information and an adaptation of the Kraskov Estimator[2] when it is. Let X be made of continuous features c and discrete features d. To deal with both continuous and discrete features, We use the following reworking of entropy: $ H(X) = H(c,d) = \sum_{x \in d} p(x) \times H(c(x)) + H(d) $ Where c(x) is a dataset that represents the rows of the continuous dataset in the same row as a discrete column with value x in the original dataset. Args: X (array-like):
 An array-like (np arr, pandas df, etc.) with shape (n_samples, n_features) or (n_samples) epsilon (array-like):
 An array with shape (n_samples, 1) that is the epsilon used in Kraskov Estimator. Represents the chebyshev distance from an element to its k-th nearest neighbor in the full dataset. Returns: float: A floating-point number representing the entropy in X. If the dataset is fully discrete, an exact calculation is done. If this is not the case and epsilon is not provided, this will be the Kozacheko Estimator of the dataset's entropy. If epsilon is provided, this is a partial estimation of the Kraskov entropy estimator. The bias is cancelled out when computing mutual information. References: .. [1] A. Kraskov, H. Stogbauer and P. Grassberger, "Estimating mutual information". Phys. Rev. E 69, 2004. .. [2] L. F. Kozachenko, N. N. Leonenko, "Sample Estimate of the Entropy of a Random Vector:, Probl. Peredachi Inf., 23:2 (1987), 9-16. """

    X = asarray2d(X)
    n_samples, n_features = X.shape
    if n_features < 1:
        return 0
    disc_mask = _get_discrete_columns(X)
    cont_mask = ~disc_mask
    # If our dataset is fully discrete/continuous, do something easier
    if np.all(disc_mask):
        return calculate_disc_entropy(X)
    elif np.all(cont_mask):
        return estimate_cont_entropy(X, epsilon)

    # Separate the dataset into discrete and continuous datasets d,c
    disc_features = asarray2d(X[:, disc_mask])
    cont_features = asarray2d(X[:, cont_mask])

    entropy = 0
    uniques, counts = np.unique(disc_features, axis=0, return_counts=True)
    empirical_p = counts / n_samples

    # $\sum_{x \in d} p(x) \times H(c(x))$
    for i in range(counts.size):
        unique_mask = np.all(disc_features == uniques[i], axis=1)
        selected_cont_samples = cont_features[unique_mask, :]
        if epsilon is None:
            selected_epsilon = None
        else:
            selected_epsilon = epsilon[unique_mask, :]
        conditional_cont_entropy = estimate_cont_entropy(
            selected_cont_samples, selected_epsilon)
        entropy += empirical_p[i] * conditional_cont_entropy

    # H(d)
    entropy += calculate_disc_entropy(disc_features)
    if epsilon is None:
        entropy = max(0, entropy)
    return entropy