code: def _computations_complete(
      df: pd.DataFrame,
      columns: List[Union[List, Tuple, Dict, str]],
      sort: bool,
      by: Optional[Union[list, str]],
      fill_value: Optional[Union[Dict, Any]],
      explicit: bool,
  ) -> pd.DataFrame:
      """
      This function computes the final output for the `complete` function.
  
      If `by` is present, then `groupby()` is used.
  
      A DataFrame, with rows of missing values, if any, is returned.
      """
      (
          columns,
          column_checker,
          sort,
          by,
          fill_value,
          explicit,
      ) = _data_checks_complete(
          df, columns, sort, by, fill_value, explicit
      )
  
      all_scalars = all(map(is_scalar, columns))
  
      # nothing to 'complete' here
      if (all_scalars and len(columns) == 1) or df.empty:
          return df
  
      # under the right conditions, stack/unstack can be faster
      # plus it always returns a sorted DataFrame
      # which does help in viewing the missing rows
      # however, using a merge keeps things simple
      # with a stack/unstack,
      # the relevant columns combination should be unique
      # and there should be no nulls
      # trade-off for the simplicity of merge is not so bad
      # of course there could be a better way ...
      if by is None:
          uniques = _generic_complete(
              df=df,
              columns=columns,
              all_scalars=all_scalars,
              sort=sort,
          )
      else:
          column_checker = by + column_checker
          uniques = df.groupby(by, sort=False)
          uniques = {
              key: _generic_complete(
                  df=value,
                  columns=columns,
                  all_scalars=all_scalars,
                  sort=sort,
              )
              for key, value in uniques
          }
          uniques = pd.concat(uniques, names=by, copy=False)
          uniques = uniques.droplevel(-1, axis=0)
      columns = df.columns
      indicator = False
      if fill_value is not None and not explicit:
          # to get a name that does not exist in the columns
          indicator = "".join(columns)
      out = pd.merge(
          uniques,
          df,
          how="outer",
          on=column_checker,
          copy=False,
          sort=False,
          indicator=indicator,
      )
  
      if fill_value is not None:
          null_columns = [col for col in columns if out[col].hasnans]
          if null_columns:
              if is_scalar(fill_value):
                  # faster when fillna operates on a Series basis
                  fill_value = {col: fill_value for col in null_columns}
              if explicit:
                  out = out.fillna(fill_value)
              else:
                  # keep only columns that are not part of column_checker
                  # IOW, we are excluding columns that were not used
                  # to generate the combinations
                  fill_value = {
                      col: value
                      for col, value in fill_value.items()
                      if col not in column_checker
                  }
                  if fill_value:
                      # when explicit is False
                      # use the indicator parameter to identify rows
                      # for `left_only`, and fill the relevant columns
                      # in fill_value with the associated value.
                      boolean_filter = (
                          out.loc[:, indicator] == "left_only"
                      )
                      out = out.drop(columns=indicator)
                      # iteration used here,
                      # instead of assign (which is also a for loop),
                      # to cater for scenarios where the column_name
                      # is not a string
                      # assign only works with keys that are strings
                      # Also, the output wil be floats (for numeric types),
                      # even if all the columns could be integers
                      # user can always convert to int if required
                      for column_name, value in fill_value.items():
                          # for categorical dtypes, set the categories first
                          if isinstance(
                              out[column_name].dtype,
                              pd.CategoricalDtype,
                          ):
                              out[column_name] = out[
                                  column_name
                              ].cat.add_categories([value])
                          out.loc[boolean_filter, column_name] = value
      if not out.columns.equals(columns):
          return out.loc(axis=1)[columns]
      return out
  
file_path: pyjanitor/janitor/functions/complete.py
imports: import functools
  from typing import Any, Dict, List, Optional, Tuple, Union
  import pandas
  import pandas_flavor
  from pandas.api.types import is_list_like, is_scalar
  from pandas.core.common import apply_if_callable
  from janitor.functions.utils import _computations_expand_grid
  from janitor.utils import check, check_column
label_desc: Use image node_id 2 for calling a global function with example usage: _computations_complete(df, columns, sort, by, fill_value, explicit) and returns: out, df, out