Determine outliers to remove from a given moving average.

    Note:
        The index returned is when the moving average first drops below the threshold for a moving average
        calculated with that bin at the center. This is somewhat different from a standard moving average
        calculation which would only look forward in the array.

    Args:
        moving_average: Moving average.
        moving_average_threshold: Value of moving average under which we consider the moving average
            to be 0. Default: 2.
        number_of_values_to_search_ahead: Number of values to search ahead in the array when calculating
            the moving average. Default: 5.
        limit_of_number_of_values_below_threshold: Number of consecutive bins below the threshold to be considered
            the beginning of outliers. Default: None, which will correspond to number_of_values_to_search_ahead - 1.
    Returns:
        0-indexed index of the histogram axes where the outliers begin.

def _determine_outliers_for_moving_average(moving_average: np.ndarray,
                                           moving_average_threshold: float,
                                           number_of_values_to_search_ahead: int,
                                           limit_of_number_of_values_below_threshold: int) -> int:
    """ Determine outliers to remove from a given moving average.

    Note:
        The index returned is when the moving average first drops below the threshold for a moving average
        calculated with that bin at the center. This is somewhat different from a standard moving average
        calculation which would only look forward in the array.

    Args:
        moving_average: Moving average.
        moving_average_threshold: Value of moving average under which we consider the moving average
            to be 0. Default: 2.
        number_of_values_to_search_ahead: Number of values to search ahead in the array when calculating
            the moving average. Default: 5.
        limit_of_number_of_values_below_threshold: Number of consecutive bins below the threshold to be considered
            the beginning of outliers. Default: None, which will correspond to number_of_values_to_search_ahead - 1.
    Returns:
        0-indexed index of the histogram axes where the outliers begin.
    """
    below_threshold = moving_average < moving_average_threshold

    # Build up a list of values to check if they are below threshold. This list allows us to easily look
    # forward in the below_threshold array.
    values_to_check = []
    for i in range(limit_of_number_of_values_below_threshold):
        # Basically, this gives us (for limit_of_number_of_values_below_threshold = 4):
        # below_threshold[0:-3], below_threshold[1:-2], below_threshold[2:-1], below_threshold[3:None]
        values_to_check.append(
            below_threshold[i:-(limit_of_number_of_values_below_threshold - 1 - i) or None]
        )

    # Some helpful logging information.
    #logger.debug(f"values_to_check: {values_to_check}")
    #logger.debug(f"moving avg length: {len(moving_average)}, length of values_to_check entries: {[len(v) for v in values_to_check]}")

    # Must have at least one bin above the specified threshold.
    found_at_least_one_bin_above_threshold = False
    # Index we will search for from which outliers will be cut.
    cut_index = -1

    # Determine the index where the limit_of_number_of_values_below_threshold bins are consequentially below the threshold.
    for i, values in enumerate(zip(*values_to_check)):
        # Skip the first bin because some old pt hard bin trains had a large number of erroneous entries
        # in the first bin (regardless of the actual pt hard bin). This should be resolved in the embedding
        # helper now. In any case, it doesn't make sense to encounter outliers in the first bin, so this is a
        # fine bin to skip.
        if i == 0:
            continue

        # True if below threshold, so check if not True.
        above_threshold = [not value for value in values]
        # We require the values to go above the moving average threshold at least once.
        if any(above_threshold):
            #logger.debug(f"Found bin i {i} above threshold with moving average: {moving_average[i]}")
            found_at_least_one_bin_above_threshold = True

        # All values from which we are looking ahead must be below the threshold to consider the index
        # as below threshold.
        if found_at_least_one_bin_above_threshold and all(np.invert(above_threshold)):
            # The previous outlier removal implementation used a moving average centered on a value
            # (ie. it checked ``arr[-2 + current_index:current_index + 3]``). Thus, we need to
            # shift the cut_index that we assign by limit_of_number_of_values_below_threshold // 2 for
            # the index where we have found all values below the threshold.
            logger.debug(f"i at found cut_index: {i} with moving_average: {moving_average[i]}")
            cut_index = i + limit_of_number_of_values_below_threshold // 2
            break

    return cut_index