code: def anova2(
      data=None, dv=None, between=None, ss_type=2, effsize="np2"
  ):
      """Two-way balanced ANOVA in pure Python + Pandas.
  
      This is an internal function. The main call to this function should be done
      by the :py:func:`pingouin.anova` function.
      """
      # Validate the dataframe
      data = _check_dataframe(
          dv=dv, between=between, data=data, effects="between"
      )
  
      assert (
          len(between) == 2
      ), "Must have exactly two between-factors variables"
      fac1, fac2 = between
  
      # Drop missing values
      data = data[[dv, fac1, fac2]].dropna()
      assert (
          data.shape[0] >= 5
      ), "Data must have at least 5 non-missing values."
  
      # Reset index (avoid duplicate axis error)
      data = data.reset_index(drop=True)
      grp_both = data.groupby(between, observed=True, group_keys=False)[
          dv
      ]
  
      if grp_both.count().nunique() == 1:
          # BALANCED DESIGN
          aov_fac1 = anova(
              data=data, dv=dv, between=fac1, detailed=True
          )
          aov_fac2 = anova(
              data=data, dv=dv, between=fac2, detailed=True
          )
          ng1, ng2 = data[fac1].nunique(), data[fac2].nunique()
          # Sums of squares
          ss_fac1 = aov_fac1.at[0, "SS"]
          ss_fac2 = aov_fac2.at[0, "SS"]
          ss_tot = (
              (data[dv] - data[dv].mean(numeric_only=True)) ** 2
          ).sum()
          ss_resid = np.sum(
              grp_both.apply(lambda x: (x - x.mean()) ** 2)
          )
          ss_inter = ss_tot - (ss_resid + ss_fac1 + ss_fac2)
          # Degrees of freedom
          df_fac1 = aov_fac1.at[0, "DF"]
          df_fac2 = aov_fac2.at[0, "DF"]
          df_inter = (ng1 - 1) * (ng2 - 1)
          df_resid = data[dv].size - (ng1 * ng2)
      else:
          # UNBALANCED DESIGN
          return anovan(
              dv=dv,
              between=between,
              data=data,
              ss_type=ss_type,
              effsize=effsize,
          )
  
      # Mean squares
      ms_fac1 = ss_fac1 / df_fac1
      ms_fac2 = ss_fac2 / df_fac2
      ms_inter = ss_inter / df_inter
      ms_resid = ss_resid / df_resid
  
      # F-values
      fval_fac1 = ms_fac1 / ms_resid
      fval_fac2 = ms_fac2 / ms_resid
      fval_inter = ms_inter / ms_resid
  
      # P-values
      pval_fac1 = f(df_fac1, df_resid).sf(fval_fac1)
      pval_fac2 = f(df_fac2, df_resid).sf(fval_fac2)
      pval_inter = f(df_inter, df_resid).sf(fval_inter)
  
      # Effect size
      if effsize == "n2":
          # Standard eta-square
          n2_fac1 = ss_fac1 / ss_tot
          n2_fac2 = ss_fac2 / ss_tot
          n2_inter = ss_inter / ss_tot
          all_effsize = [n2_fac1, n2_fac2, n2_inter, np.nan]
      else:
          # ..Partial eta-square
          np2_fac1 = ss_fac1 / (ss_fac1 + ss_resid)
          np2_fac2 = ss_fac2 / (ss_fac2 + ss_resid)
          np2_inter = ss_inter / (ss_inter + ss_resid)
          all_effsize = [np2_fac1, np2_fac2, np2_inter, np.nan]
  
      # Create output dataframe
      aov = pd.DataFrame(
          {
              "Source": [fac1, fac2, fac1 + " * " + fac2, "Residual"],
              "SS": [ss_fac1, ss_fac2, ss_inter, ss_resid],
              "DF": [df_fac1, df_fac2, df_inter, df_resid],
              "MS": [ms_fac1, ms_fac2, ms_inter, ms_resid],
              "F": [fval_fac1, fval_fac2, fval_inter, np.nan],
              "p-unc": [pval_fac1, pval_fac2, pval_inter, np.nan],
              effsize: all_effsize,
          }
      )
  
      aov.dropna(how="all", axis=1, inplace=True)
      return _postprocess_dataframe(aov)
  
file_path: pingouin/pingouin/parametric.py
imports: import warnings
  from collections.abc import Iterable
  import numpy
  import pandas
  from scipy.stats import f
  import pandas_flavor
  from pingouin import _check_dataframe, remove_na, _flatten_list, bayesfactor_ttest, epsilon, sphericity, _postprocess_dataframe
label_desc: Use image node_id 5 for calling a global function with example usage: anova2(data, dv, between, ss_type, effsize) and returns: _postprocess_dataframe, anovan