Read CmdStan output.csv.

    Parameters
    ----------
    path : str

    Returns
    -------
    List[DataFrame, DataFrame, List[str], List[str], List[str]]
        pandas.DataFrame
            Sample data
        pandas.DataFrame
            Sample stats
        List[str]
            Configuration information
        List[str]
            Adaptation information
        List[str]
            Timing info

def _read_output(path):
    """Read CmdStan output.csv.

    Parameters
    ----------
    path : str

    Returns
    -------
    List[DataFrame, DataFrame, List[str], List[str], List[str]]
        pandas.DataFrame
            Sample data
        pandas.DataFrame
            Sample stats
        List[str]
            Configuration information
        List[str]
            Adaptation information
        List[str]
            Timing info
    """
    chains = []
    configuration_info = []
    adaptation_info = []
    timing_info = []
    i = 0
    # Read (first) configuration and adaption
    with open(path, "r") as f_obj:
        column_names = False
        for i, line in enumerate(f_obj):
            line = line.strip()
            if line.startswith("#"):
                if column_names:
                    adaptation_info.append(line.strip())
                else:
                    configuration_info.append(line.strip())
            elif not column_names:
                column_names = True
                pconf = _process_configuration(configuration_info)
                if pconf["save_warmup"]:
                    warmup_range = range(pconf["num_warmup"] // pconf["thin"])
                    for _, _ in zip(warmup_range, f_obj):
                        continue
            else:
                break

    # Read data
    with open(path, "r") as f_obj:
        df = pd.read_csv(f_obj, comment="#")

    # split dataframe if header found multiple times
    if df.iloc[:, 0].dtype.kind == "O":
        first_col = df.columns[0]
        col_locations = first_col == df.loc[:, first_col]
        col_locations = list(col_locations.loc[col_locations].index)
        dfs = []
        for idx, last_idx in zip(col_locations, [-1] + list(col_locations[:-1])):
            df_ = deepcopy(df.loc[last_idx + 1 : idx - 1, :])
            for col in df_.columns:
                df_.loc[:, col] = pd.to_numeric(df_.loc[:, col])
            if len(df_):
                dfs.append(df_.reset_index(drop=True))
            df = df.loc[idx + 1 :, :]
        for col in df.columns:
            df.loc[:, col] = pd.to_numeric(df.loc[:, col])
        dfs.append(df)
    else:
        dfs = [df]

    for j, df in enumerate(dfs):
        if j == 0:
            # Read timing info (first) from the end of the file
            line_num = i + df.shape[0] + 1
            for k in range(5):
                line = linecache.getline(path, line_num + k).strip()
                if len(line):
                    timing_info.append(line)
            configuration_info_len = len(configuration_info)
            adaptation_info_len = len(adaptation_info)
            timing_info_len = len(timing_info)
            num_of_samples = df.shape[0]
            header_count = 1
            last_line_num = (
                configuration_info_len
                + adaptation_info_len
                + timing_info_len
                + num_of_samples
                + header_count
            )
        else:
            # header location found in the dataframe (not first)
            configuration_info = []
            adaptation_info = []
            timing_info = []

            # line number for the next dataframe in csv
            line_num = last_line_num + 1

            # row ranges
            config_start = line_num
            config_end = config_start + configuration_info_len

            # read configuration_info
            for reading_line in range(config_start, config_end):
                line = linecache.getline(path, reading_line)
                if line.startswith("#"):
                    configuration_info.append(line)
                else:
                    msg = (
                        "Invalid input file. "
                        "Header information missing from combined csv. "
                        "Configuration: {}".format(path)
                    )
                    raise ValueError(msg)

            pconf = _process_configuration(configuration_info)
            warmup_rows = pconf["save_warmup"] * pconf["num_warmup"] // pconf["thin"]
            adaption_start = config_end + 1 + warmup_rows
            adaption_end = adaption_start + adaptation_info_len
            # read adaptation_info
            for reading_line in range(adaption_start, adaption_end):
                line = linecache.getline(path, reading_line)
                if line.startswith("#"):
                    adaptation_info.append(line)
                else:
                    msg = (
                        "Invalid input file. "
                        "Header information missing from combined csv. "
                        "Adaptation: {}".format(path)
                    )
                    raise ValueError(msg)

            timing_start = adaption_end + len(df) - warmup_rows
            timing_end = timing_start + timing_info_len
            # read timing_info
            raise_timing_error = False
            for reading_line in range(timing_start, timing_end):
                line = linecache.getline(path, reading_line)
                if line.startswith("#"):
                    timing_info.append(line)
                else:
                    raise_timing_error = True
                    break
            no_elapsed_time = not any("elapsed time" in row.lower() for row in timing_info)
            if raise_timing_error or no_elapsed_time:
                msg = (
                    "Invalid input file. "
                    "Header information missing from combined csv. "
                    "Timing: {}".format(path)
                )
                raise ValueError(msg)

            last_line_num = reading_line

        # Remove warmup
        if pconf["save_warmup"]:
            saved_samples = pconf["num_samples"] // pconf["thin"]
            df = df.iloc[-saved_samples:, :]

        # Split data to sample_stats and sample
        sample_stats_columns = [col for col in df.columns if col.endswith("__")]
        sample_columns = [col for col in df.columns if col not in sample_stats_columns]

        sample_stats = df.loc[:, sample_stats_columns]
        sample_df = df.loc[:, sample_columns]

        chains.append((sample_df, sample_stats, configuration_info, adaptation_info, timing_info))

    return chains