Perform cross validation on a dataset. If multiple models are provided,
    all models will train on each of the k subsets.

    Parameters
    ----------
    fitters: model
      one or several objects which possess a method: ``fit(self, data, duration_col, event_col)``
      Note that the last two arguments will be given as keyword arguments,
      and that event_col is optional. The objects must also have
      the "predictor" method defined below.
    df: DataFrame
      a Pandas DataFrame with necessary columns `duration_col` and (optional) `event_col`, plus
      other covariates. `duration_col` refers to the lifetimes of the subjects. `event_col`
      refers to whether the 'death' events was observed: 1 if observed, 0 else (censored).
    duration_col: (n,) array
      the column in DataFrame that contains the subjects lifetimes.
    event_col: (n,) array
      the column in DataFrame that contains the subject's death observation. If left
      as None, assumes all individuals are non-censored.
    k: int
      the number of folds to perform. n/k data will be withheld for testing on.
    evaluation_measure: function
      a function that accepts either (event_times, predicted_event_times),
      or (event_times, predicted_event_times, event_observed)
      and returns something (could be anything).
      Default: statistics.concordance_index: (C-index)
      between two series of event times
    predictor: string
      a string that matches a prediction method on the fitter instances.
      For example, ``predict_expectation`` or ``predict_percentile``.
      Default is "predict_expectation"
      The interface for the method is: ``predict(self, data, **optional_kwargs)``
    fitter_kwargs:
      keyword args to pass into fitter.fit method
    predictor_kwargs:
      keyword args to pass into predictor-method.

    Returns
    -------
    results: list
      (k,1) list of scores for each fold. The scores can be anything.

def k_fold_cross_validation(
    fitters,
    df,
    duration_col,
    event_col=None,
    k=5,
    evaluation_measure=concordance_index,
    predictor="predict_expectation",
    predictor_kwargs={},
    fitter_kwargs={},
):  # pylint: disable=dangerous-default-value,too-many-arguments,too-many-locals
    """
    Perform cross validation on a dataset. If multiple models are provided,
    all models will train on each of the k subsets.

    Parameters
    ----------
    fitters: model
      one or several objects which possess a method: ``fit(self, data, duration_col, event_col)``
      Note that the last two arguments will be given as keyword arguments,
      and that event_col is optional. The objects must also have
      the "predictor" method defined below.
    df: DataFrame
      a Pandas DataFrame with necessary columns `duration_col` and (optional) `event_col`, plus
      other covariates. `duration_col` refers to the lifetimes of the subjects. `event_col`
      refers to whether the 'death' events was observed: 1 if observed, 0 else (censored).
    duration_col: (n,) array
      the column in DataFrame that contains the subjects lifetimes.
    event_col: (n,) array
      the column in DataFrame that contains the subject's death observation. If left
      as None, assumes all individuals are non-censored.
    k: int
      the number of folds to perform. n/k data will be withheld for testing on.
    evaluation_measure: function
      a function that accepts either (event_times, predicted_event_times),
      or (event_times, predicted_event_times, event_observed)
      and returns something (could be anything).
      Default: statistics.concordance_index: (C-index)
      between two series of event times
    predictor: string
      a string that matches a prediction method on the fitter instances.
      For example, ``predict_expectation`` or ``predict_percentile``.
      Default is "predict_expectation"
      The interface for the method is: ``predict(self, data, **optional_kwargs)``
    fitter_kwargs:
      keyword args to pass into fitter.fit method
    predictor_kwargs:
      keyword args to pass into predictor-method.

    Returns
    -------
    results: list
      (k,1) list of scores for each fold. The scores can be anything.
    """
    # Make sure fitters is a list
    try:
        fitters = list(fitters)
    except TypeError:
        fitters = [fitters]
    # Each fitter has its own scores
    fitterscores = [[] for _ in fitters]

    n, _ = df.shape
    df = df.copy()

    if event_col is None:
        event_col = "E"
        df[event_col] = 1.0

    df = df.reindex(np.random.permutation(df.index)).sort_values(event_col)

    assignments = np.array((n // k + 1) * list(range(1, k + 1)))
    assignments = assignments[:n]

    testing_columns = df.columns.drop([duration_col, event_col])

    for i in range(1, k + 1):

        ix = assignments == i
        training_data = df.loc[~ix]
        testing_data = df.loc[ix]

        T_actual = testing_data[duration_col].values
        E_actual = testing_data[event_col].values
        X_testing = testing_data[testing_columns]

        for fitter, scores in zip(fitters, fitterscores):
            # fit the fitter to the training data
            fitter.fit(training_data, duration_col=duration_col, event_col=event_col, **fitter_kwargs)
            T_pred = getattr(fitter, predictor)(X_testing, **predictor_kwargs).values

            try:
                scores.append(evaluation_measure(T_actual, T_pred, E_actual))
            except TypeError:
                scores.append(evaluation_measure(T_actual, T_pred))

    # If a single fitter was given as argument, return a single result
    if len(fitters) == 1:
        return fitterscores[0]
    return fitterscores