code: def pairwise_tests(
      data=None,
      dv=None,
      between=None,
      within=None,
      subject=None,
      parametric=True,
      marginal=True,
      alpha=0.05,
      alternative="two-sided",
      padjust="none",
      effsize="hedges",
      correction="auto",
      nan_policy="listwise",
      return_desc=False,
      interaction=True,
      within_first=True,
  ):
      """Pairwise tests.
  
      Parameters
      ----------
      data : :py:class:`pandas.DataFrame`
          DataFrame. Note that this function can also directly be used as a
          Pandas method, in which case this argument is no longer needed.
      dv : string
          Name of column containing the dependent variable.
      between : string or list with 2 elements
          Name of column(s) containing the between-subject factor(s).
      within : string or list with 2 elements
          Name of column(s) containing the within-subject factor(s), i.e. the
          repeated measurements.
      subject : string
          Name of column containing the subject identifier. This is mandatory
          when ``within`` is specified.
      parametric : boolean
          If True (default), use the parametric :py:func:`ttest` function.
          If False, use :py:func:`pingouin.wilcoxon` or :py:func:`pingouin.mwu`
          for paired or unpaired samples, respectively.
      marginal : boolean
          If True (default), the between-subject pairwise T-test(s) will be calculated
          after averaging across all levels of the within-subject factor in mixed
          design. This is recommended to avoid violating the assumption of
          independence and conflating the degrees of freedom by the
          number of repeated measurements.
  
          .. versionadded:: 0.3.2
      alpha : float
          Significance level
      alternative : string
          Defines the alternative hypothesis, or tail of the test. Must be one of
          "two-sided" (default), "greater" or "less". Both "greater" and "less" return one-sided
          p-values. "greater" tests against the alternative hypothesis that the mean of ``x``
          is greater than the mean of ``y``.
      padjust : string
          Method used for testing and adjustment of pvalues.
  
          * ``'none'``: no correction
          * ``'bonf'``: one-step Bonferroni correction
          * ``'sidak'``: one-step Sidak correction
          * ``'holm'``: step-down method using Bonferroni adjustments
          * ``'fdr_bh'``: Benjamini/Hochberg FDR correction
          * ``'fdr_by'``: Benjamini/Yekutieli FDR correction
      effsize : string or None
          Effect size type. Available methods are:
  
          * ``'none'``: no effect size
          * ``'cohen'``: Unbiased Cohen d
          * ``'hedges'``: Hedges g
          * ``'r'``: Pearson correlation coefficient
          * ``'eta-square'``: Eta-square
          * ``'odds-ratio'``: Odds ratio
          * ``'AUC'``: Area Under the Curve
          * ``'CLES'``: Common Language Effect Size
      correction : string or boolean
          For independent two sample T-tests, specify whether or not to correct for
          unequal variances using Welch separate variances T-test. If `'auto'`,
          it will automatically uses Welch T-test when the sample sizes are
          unequal, as recommended by Zimmerman 2004.
  
          .. versionadded:: 0.3.2
      nan_policy : string
          Can be `'listwise'` for listwise deletion of missing values in repeated
          measures design (= complete-case analysis) or `'pairwise'` for the
          more liberal pairwise deletion (= available-case analysis). The former (default) is more
          appropriate for post-hoc analysis following an ANOVA, however it can drastically reduce
          the power of the test: any subject with one or more missing value(s) will be
          completely removed from the analysis.
  
          .. versionadded:: 0.2.9
      return_desc : boolean
          If True, append group means and std to the output dataframe
      interaction : boolean
          If there are multiple factors and ``interaction`` is True (default),
          Pingouin will also calculate T-tests for the interaction term (see Notes).
  
          .. versionadded:: 0.2.9
      within_first : boolean
          Determines the order of the interaction in mixed design. Pingouin will
          return within * between when this parameter is set to True (default),
          and between * within otherwise.
  
          .. versionadded:: 0.3.6
  
      Returns
      -------
      stats : :py:class:`pandas.DataFrame`
  
          * ``'Contrast'``: Contrast (= independent variable or interaction)
          * ``'A'``: Name of first measurement
          * ``'B'``: Name of second measurement
          * ``'Paired'``: indicates whether the two measurements are paired or
            independent
          * ``'Parametric'``: indicates if (non)-parametric tests were used
          * ``'T'``: T statistic (only if parametric=True)
          * ``'U-val'``: Mann-Whitney U stat (if parametric=False and unpaired
            data)
          * ``'W-val'``: Wilcoxon W stat (if parametric=False and paired data)
          * ``'dof'``: degrees of freedom (only if parametric=True)
          * ``'alternative'``: tail of the test
          * ``'p-unc'``: Uncorrected p-values
          * ``'p-corr'``: Corrected p-values
          * ``'p-adjust'``: p-values correction method
          * ``'BF10'``: Bayes Factor
          * ``'hedges'``: effect size (or any effect size defined in
            ``effsize``)
  
      See also
      --------
      ttest, mwu, wilcoxon, compute_effsize, multicomp
  
      Notes
      -----
      Data are expected to be in long-format. If your data is in wide-format,
      you can use the :py:func:`pandas.melt` function to convert from wide to
      long format.
  
      If ``between`` or ``within`` is a list (e.g. ['col1', 'col2']),
      the function returns 1) the pairwise T-tests between each values of the
      first column, 2) the pairwise T-tests between each values of the second
      column and 3) the interaction between col1 and col2. The interaction is
      dependent of the order of the list, so ['col1', 'col2'] will not yield the
      same results as ['col2', 'col1']. Furthermore, the interaction will only be
      calculated if ``interaction=True``.
  
      If ``between`` is a list with two elements, the output
      model is between1 + between2 + between1 * between2.
  
      Similarly, if ``within`` is a list with two elements, the output model is
      within1 + within2 + within1 * within2.
  
      If both ``between`` and ``within`` are specified, the output model is
      within + between + within * between (= mixed design), unless
      ``within_first=False`` in which case the model becomes between + within +
      between * within.
  
      Missing values in repeated measurements are automatically removed using a
      listwise (default) or pairwise deletion strategy. The former is more conservative, as any
      subject with one or more missing value(s) will be completely removed from the dataframe prior
      to calculating the T-tests. The ``nan_policy`` parameter can therefore have a huge impact
      on the results.
  
      Examples
      --------
      For more examples, please refer to the `Jupyter notebooks
      <https://github.com/raphaelvallat/pingouin/blob/master/notebooks/01_ANOVA.ipynb>`_
  
      1. One between-subject factor
  
      >>> import pandas as pd
      >>> import pingouin as pg
      >>> pd.set_option('display.expand_frame_repr', False)
      >>> pd.set_option('display.max_columns', 20)
      >>> df = pg.read_dataset('mixed_anova.csv')
      >>> pg.pairwise_tests(dv='Scores', between='Group', data=df).round(3)
        Contrast        A           B  Paired  Parametric     T    dof alternative  p-unc   BF10  hedges
      0    Group  Control  Meditation   False        True -2.29  178.0   two-sided  0.023  1.813   -0.34
  
      2. One within-subject factor
  
      >>> post_hocs = pg.pairwise_tests(dv='Scores', within='Time', subject='Subject', data=df)
      >>> post_hocs.round(3)
        Contrast        A        B  Paired  Parametric      T   dof alternative  p-unc   BF10  hedges
      0     Time   August  January    True        True -1.740  59.0   two-sided  0.087  0.582  -0.328
      1     Time   August     June    True        True -2.743  59.0   two-sided  0.008  4.232  -0.483
      2     Time  January     June    True        True -1.024  59.0   two-sided  0.310  0.232  -0.170
  
      3. Non-parametric pairwise paired test (wilcoxon)
  
      >>> pg.pairwise_tests(dv='Scores', within='Time', subject='Subject',
      ...                    data=df, parametric=False).round(3)
        Contrast        A        B  Paired  Parametric  W-val alternative  p-unc  hedges
      0     Time   August  January    True       False  716.0   two-sided  0.144  -0.328
      1     Time   August     June    True       False  564.0   two-sided  0.010  -0.483
      2     Time  January     June    True       False  887.0   two-sided  0.840  -0.170
  
      4. Mixed design (within and between) with bonferroni-corrected p-values
  
      >>> posthocs = pg.pairwise_tests(dv='Scores', within='Time', subject='Subject',
      ...                               between='Group', padjust='bonf', data=df)
      >>> posthocs.round(3)
             Contrast     Time        A           B Paired  Parametric      T   dof alternative  p-unc  p-corr p-adjust   BF10  hedges
      0          Time        -   August     January   True        True -1.740  59.0   two-sided  0.087   0.261     bonf  0.582  -0.328
      1          Time        -   August        June   True        True -2.743  59.0   two-sided  0.008   0.024     bonf  4.232  -0.483
      2          Time        -  January        June   True        True -1.024  59.0   two-sided  0.310   0.931     bonf  0.232  -0.170
      3         Group        -  Control  Meditation  False        True -2.248  58.0   two-sided  0.028     NaN      NaN  2.096  -0.573
      4  Time * Group   August  Control  Meditation  False        True  0.316  58.0   two-sided  0.753   1.000     bonf  0.274   0.081
      5  Time * Group  January  Control  Meditation  False        True -1.434  58.0   two-sided  0.157   0.471     bonf  0.619  -0.365
      6  Time * Group     June  Control  Meditation  False        True -2.744  58.0   two-sided  0.008   0.024     bonf  5.593  -0.699
  
      5. Two between-subject factors. The order of the ``between`` factors matters!
  
      >>> pg.pairwise_tests(dv='Scores', between=['Group', 'Time'], data=df).round(3)
             Contrast       Group        A           B Paired  Parametric      T    dof alternative  p-unc     BF10  hedges
      0         Group           -  Control  Meditation  False        True -2.290  178.0   two-sided  0.023    1.813  -0.340
      1          Time           -   August     January  False        True -1.806  118.0   two-sided  0.074    0.839  -0.328
      2          Time           -   August        June  False        True -2.660  118.0   two-sided  0.009    4.499  -0.483
      3          Time           -  January        June  False        True -0.934  118.0   two-sided  0.352    0.288  -0.170
      4  Group * Time     Control   August     January  False        True -0.383   58.0   two-sided  0.703    0.279  -0.098
      5  Group * Time     Control   August        June  False        True -0.292   58.0   two-sided  0.771    0.272  -0.074
      6  Group * Time     Control  January        June  False        True  0.045   58.0   two-sided  0.964    0.263   0.011
      7  Group * Time  Meditation   August     January  False        True -2.188   58.0   two-sided  0.033    1.884  -0.558
      8  Group * Time  Meditation   August        June  False        True -4.040   58.0   two-sided  0.000  148.302  -1.030
      9  Group * Time  Meditation  January        June  False        True -1.442   58.0   two-sided  0.155    0.625  -0.367
  
      6. Same but without the interaction, and using a directional test
  
      >>> df.pairwise_tests(dv='Scores', between=['Group', 'Time'], alternative="less",
      ...                    interaction=False).round(3)
        Contrast        A           B  Paired  Parametric      T    dof alternative  p-unc   BF10  hedges
      0    Group  Control  Meditation   False        True -2.290  178.0        less  0.012  3.626  -0.340
      1     Time   August     January   False        True -1.806  118.0        less  0.037  1.679  -0.328
      2     Time   August        June   False        True -2.660  118.0        less  0.004  8.998  -0.483
      3     Time  January        June   False        True -0.934  118.0        less  0.176  0.577  -0.170
      """
      from .parametric import ttest
      from .nonparametric import wilcoxon, mwu
  
      # Safety checks
      data = _check_dataframe(
          dv=dv,
          between=between,
          within=within,
          subject=subject,
          effects="all",
          data=data,
      )
      assert alternative in [
          "two-sided",
          "greater",
          "less",
      ], "Alternative must be one of 'two-sided' (default), 'greater' or 'less'."
      assert isinstance(alpha, float), "alpha must be float."
      assert nan_policy in ["listwise", "pairwise"]
  
      # Check if we have multiple between or within factors
      multiple_between = False
      multiple_within = False
      contrast = None
      if isinstance(between, list):
          if len(between) > 1:
              multiple_between = True
              contrast = "multiple_between"
              assert all([b in data.keys() for b in between])
          else:
              between = between[0]
      if isinstance(within, list):
          if len(within) > 1:
              multiple_within = True
              contrast = "multiple_within"
              assert all([w in data.keys() for w in within])
          else:
              within = within[0]
      if all([multiple_within, multiple_between]):
          raise ValueError(
              "Multiple between and within factors are currently not supported. "
              "Please select only one."
          )
      # Check the other cases. Between and within column names can be str or int (not float).
      if isinstance(between, (str, int)) and within is None:
          contrast = "simple_between"
          assert between in data.keys()
      if isinstance(within, (str, int)) and between is None:
          contrast = "simple_within"
          assert within in data.keys()
      if isinstance(between, (str, int)) and isinstance(
          within, (str, int)
      ):
          contrast = "within_between"
          assert all([between in data.keys(), within in data.keys()])
  
      # Create col_order
      col_order = [
          "Contrast",
          "Time",
          "A",
          "B",
          "mean(A)",
          "std(A)",
          "mean(B)",
          "std(B)",
          "Paired",
          "Parametric",
          "T",
          "U-val",
          "W-val",
          "dof",
          "alternative",
          "p-unc",
          "p-corr",
          "p-adjust",
          "BF10",
          effsize,
      ]
  
      # If repeated measures, pivot and melt the table. This has several effects:
      # 1) Force missing values to be explicit (a NaN cell is created)
      # 2) Automatic collapsing to the mean if multiple within factors are present
      # 3) If using dropna, remove rows with missing values (listwise deletion).
      # The latter is the same behavior as JASP (= strict complete-case analysis).
      if within is not None:
          idx_piv = subject if between is None else [subject, between]
          data_piv = data.pivot_table(
              index=idx_piv, columns=within, values=dv, observed=True
          )
          if nan_policy == "listwise":
              # Remove rows (= subject) with missing values. For pairwise deletion, missing values
              # will be removed directly in the lower-level functions (e.g. pg.ttest)
              data_piv = data_piv.dropna()
          data = data_piv.melt(
              ignore_index=False, value_name=dv
          ).reset_index()
  
      if contrast in ["simple_within", "simple_between"]:
          # OPTION A: SIMPLE MAIN EFFECTS, WITHIN OR BETWEEN
          paired = True if contrast == "simple_within" else False
          col = within if contrast == "simple_within" else between
  
          # Extract levels of the grouping variable, sorted in alphabetical order
          grp_col = data.groupby(col, sort=True, observed=True)[dv]
          labels = grp_col.groups.keys()
          # Number and labels of possible comparisons
          if len(labels) >= 2:
              combs = list(combinations(labels, 2))
              combs = np.array(combs)
              A = combs[:, 0]
              B = combs[:, 1]
          else:
              raise ValueError(
                  "Columns must have at least two unique values."
              )
  
          # Initialize dataframe
          stats = pd.DataFrame(
              dtype=np.float64,
              index=range(len(combs)),
              columns=col_order,
          )
  
          # Force dtype conversion
          cols_str = [
              "Contrast",
              "Time",
              "A",
              "B",
              "alternative",
              "p-adjust",
              "BF10",
          ]
          cols_bool = ["Parametric", "Paired"]
          stats[cols_str] = stats[cols_str].astype(object)
          stats[cols_bool] = stats[cols_bool].astype(bool)
  
          # Fill str columns
          stats.loc[:, "A"] = A
          stats.loc[:, "B"] = B
          stats.loc[:, "Contrast"] = col
          stats.loc[:, "alternative"] = alternative
          stats.loc[:, "Paired"] = paired
  
          # For max precision, make sure rounding is disabled
          old_options = options.copy()
          options["round"] = None
  
          for i in range(stats.shape[0]):
              col1, col2 = stats.at[i, "A"], stats.at[i, "B"]
              x = grp_col.get_group(col1).to_numpy(dtype=np.float64)
              y = grp_col.get_group(col2).to_numpy(dtype=np.float64)
              if parametric:
                  stat_name = "T"
                  df_ttest = ttest(
                      x,
                      y,
                      paired=paired,
                      alternative=alternative,
                      correction=correction,
                  )
                  stats.at[i, "BF10"] = df_ttest.at["T-test", "BF10"]
                  stats.at[i, "dof"] = df_ttest.at["T-test", "dof"]
              else:
                  if paired:
                      stat_name = "W-val"
                      df_ttest = wilcoxon(x, y, alternative=alternative)
                  else:
                      stat_name = "U-val"
                      df_ttest = mwu(x, y, alternative=alternative)
  
              options.update(old_options)  # restore options
  
              # Compute Hedges / Cohen
              ef = compute_effsize(
                  x=x, y=y, eftype=effsize, paired=paired
              )
  
              if return_desc:
                  stats.at[i, "mean(A)"] = np.nanmean(x)
                  stats.at[i, "mean(B)"] = np.nanmean(y)
                  stats.at[i, "std(A)"] = np.nanstd(x, ddof=1)
                  stats.at[i, "std(B)"] = np.nanstd(y, ddof=1)
              stats.at[i, stat_name] = df_ttest[stat_name].iat[0]
              stats.at[i, "p-unc"] = df_ttest["p-val"].iat[0]
              stats.at[i, effsize] = ef
  
          # Multiple comparisons
          padjust = None if stats["p-unc"].size <= 1 else padjust
          if padjust is not None:
              if padjust.lower() != "none":
                  _, stats["p-corr"] = multicomp(
                      stats["p-unc"].to_numpy(),
                      alpha=alpha,
                      method=padjust,
                  )
                  stats["p-adjust"] = padjust
          else:
              stats["p-corr"] = None
              stats["p-adjust"] = None
      else:
          # Multiple factors
          if contrast == "multiple_between":
              # B1: BETWEEN1 + BETWEEN2 + BETWEEN1 * BETWEEN2
              factors = between
              fbt = factors
              fwt = [None, None]
              paired = False  # the interaction is not paired
              agg = [False, False]
              # TODO: add a pool SD option, as in JASP and JAMOVI?
          elif contrast == "multiple_within":
              # B2: WITHIN1 + WITHIN2 + WITHIN1 * WITHIN2
              factors = within
              fbt = [None, None]
              fwt = factors
              paired = True
              agg = [
                  True,
                  True,
              ]  # Calculate marginal means for both factors
          else:
              # B3: WITHIN + BETWEEN + INTERACTION
              # Decide which order should be reported
              if within_first:
                  # within + between + within * between
                  factors = [within, between]
                  fbt = [None, between]
                  fwt = [within, None]
                  paired = False  # only for interaction
                  agg = [False, True]
              else:
                  # between + within + between * within
                  factors = [between, within]
                  fbt = [between, None]
                  fwt = [None, within]
                  paired = True
                  agg = [True, False]
  
          stats = pd.DataFrame()
          for i, f in enumerate(factors):
              # Introduced in Pingouin v0.3.2
              # Note that is only has an impact in the between test of mixed
              # designs. Indeed, a similar groupby is applied by default on
              # each within-subject factor of a two-way repeated measures design.
              if all([agg[i], marginal]):
                  tmp = data.groupby(
                      [subject, f],
                      as_index=False,
                      observed=True,
                      sort=True,
                  ).mean(numeric_only=True)
              else:
                  tmp = data
              pt = pairwise_tests(
                  dv=dv,
                  between=fbt[i],
                  within=fwt[i],
                  subject=subject,
                  data=tmp,
                  parametric=parametric,
                  marginal=marginal,
                  alpha=alpha,
                  alternative=alternative,
                  padjust=padjust,
                  effsize=effsize,
                  correction=correction,
                  nan_policy=nan_policy,
                  return_desc=return_desc,
              )
              stats = pd.concat(
                  [stats, pt], axis=0, ignore_index=True, sort=False
              )
  
          # Then compute the interaction between the factors
          if interaction:
              nrows = stats.shape[0]
              # BUGFIX 0.3.9: If subject is present, make sure that we respect
              # the order of subjects.
              if subject is not None:
                  data = data.set_index(subject).sort_index()
              # Extract interaction levels, sorted in alphabetical order
              grp_fac1 = data.groupby(
                  factors[0], observed=True, sort=True
              )[dv]
              grp_fac2 = data.groupby(
                  factors[1], observed=True, sort=True
              )[dv]
              grp_both = data.groupby(
                  factors, observed=True, sort=True
              )[dv]
              labels_fac1 = grp_fac1.groups.keys()
              labels_fac2 = grp_fac2.groups.keys()
              # comb_fac1 = list(combinations(labels_fac1, 2))
              comb_fac2 = list(combinations(labels_fac2, 2))
  
              # Pairwise comparisons
              combs_list = list(product(labels_fac1, comb_fac2))
              ncombs = len(combs_list)
              # np.array(combs_list) does not work because of tuples
              # we therefore need to flatten the tupple
              combs = np.zeros(shape=(ncombs, 3), dtype=object)
              for i in range(ncombs):
                  combs[i] = _flatten_list(
                      combs_list[i], include_tuple=True
                  )
  
              # Append empty rows
              idxiter = np.arange(nrows, nrows + ncombs)
              stats = stats.reindex(stats.index.union(idxiter))
  
              # Update other columns
              stats.loc[idxiter, "Contrast"] = (
                  factors[0] + " * " + factors[1]
              )
              stats.loc[idxiter, "Time"] = combs[:, 0]
              stats.loc[idxiter, "Paired"] = paired
              stats.loc[idxiter, "alternative"] = alternative
              stats.loc[idxiter, "A"] = combs[:, 1]
              stats.loc[idxiter, "B"] = combs[:, 2]
  
              # For max precision, make sure rounding is disabled
              old_options = options.copy()
              options["round"] = None
  
              for i, comb in enumerate(combs):
                  ic = nrows + i  # Take into account previous rows
                  fac1, col1, col2 = comb
                  x = grp_both.get_group((fac1, col1)).to_numpy(
                      dtype=np.float64
                  )
                  y = grp_both.get_group((fac1, col2)).to_numpy(
                      dtype=np.float64
                  )
                  ef = compute_effsize(
                      x=x, y=y, eftype=effsize, paired=paired
                  )
                  if parametric:
                      stat_name = "T"
                      df_ttest = ttest(
                          x,
                          y,
                          paired=paired,
                          alternative=alternative,
                          correction=correction,
                      )
                      stats.at[ic, "BF10"] = df_ttest.at[
                          "T-test", "BF10"
                      ]
                      stats.at[ic, "dof"] = df_ttest.at["T-test", "dof"]
                  else:
                      if paired:
                          stat_name = "W-val"
                          df_ttest = wilcoxon(
                              x, y, alternative=alternative
                          )
                      else:
                          stat_name = "U-val"
                          df_ttest = mwu(x, y, alternative=alternative)
  
                  options.update(old_options)  # restore options
  
                  # Append to stats
                  if return_desc:
                      stats.at[ic, "mean(A)"] = np.nanmean(x)
                      stats.at[ic, "mean(B)"] = np.nanmean(y)
                      stats.at[ic, "std(A)"] = np.nanstd(x, ddof=1)
                      stats.at[ic, "std(B)"] = np.nanstd(y, ddof=1)
                  stats.at[ic, stat_name] = df_ttest[stat_name].iat[0]
                  stats.at[ic, "p-unc"] = df_ttest["p-val"].iat[0]
                  stats.at[ic, effsize] = ef
  
              # Multi-comparison columns
              if padjust is not None and padjust.lower() != "none":
                  _, pcor = multicomp(
                      stats.loc[idxiter, "p-unc"].to_numpy(),
                      alpha=alpha,
                      method=padjust,
                  )
                  stats.loc[idxiter, "p-corr"] = pcor
                  stats.loc[idxiter, "p-adjust"] = padjust
  
      # ---------------------------------------------------------------------
      # Append parametric columns
      stats.loc[:, "Parametric"] = parametric
  
      # Reorder and drop empty columns
      stats = stats[
          np.array(col_order)[np.isin(col_order, stats.columns)]
      ]
      stats = stats.dropna(how="all", axis=1)
  
      # Rename Time columns
      if (
          contrast
          in ["multiple_within", "multiple_between", "within_between"]
          and interaction
      ):
          stats["Time"].fillna("-", inplace=True)
          stats.rename(columns={"Time": factors[0]}, inplace=True)
  
      return _postprocess_dataframe(stats)
  
file_path: pingouin/pingouin/pairwise.py
imports: import numpy
  import pandas
  import pandas_flavor
  from itertools import combinations, product
  from pingouin.config import options
  from pingouin.parametric import anova
  from pingouin.multicomp import multicomp
  from pingouin.effsize import compute_effsize
  from pingouin.utils import _check_dataframe, _flatten_list, _postprocess_dataframe
  from scipy.stats import studentized_range
  import warnings
label_desc: Use image node_id 2 for calling a global function with example usage: pairwise_tests(data, dv, between, within, subject, parametric, marginal, alpha, alternative, padjust, effsize, correction, nan_policy, return_desc, interaction, within_first) and returns: _postprocess_dataframe