docs: def lf_summary(L, Y=None, lf_names=None, est_accs=None):
      """Returns a pandas DataFrame with the various per-LF statistics.
  
      Args:
          L: an n x m scipy.sparse matrix where L_{i,j} is the label given by the
              jth LF to the ith candidate
          Y: an [n] or [n, 1] np.ndarray of gold labels.
              If provided, the empirical accuracy for each LF will be calculated
      """
      n, m = L.shape
      if lf_names is not None:
          col_names = ["j"]
          d = {"j": list(range(m))}
      else:
          lf_names = list(range(m))
          col_names = []
          d = {}
  
      # Default LF stats
      col_names.extend(["Polarity", "Coverage", "Overlaps", "Conflicts"])
      d["Polarity"] = Series(data=lf_polarities(L), index=lf_names)
      d["Coverage"] = Series(data=lf_coverages(L), index=lf_names)
      d["Overlaps"] = Series(data=lf_overlaps(L), index=lf_names)
      d["Conflicts"] = Series(data=lf_conflicts(L), index=lf_names)
  
      if Y is not None:
          col_names.extend(["Correct", "Incorrect", "Emp. Acc."])
          confusions = [
              confusion_matrix(Y, L[:, i], pretty_print=False) for i in range(m)
          ]
          corrects = [np.diagonal(conf).sum() for conf in confusions]
          incorrects = [
              conf.sum() - correct for conf, correct in zip(confusions, corrects)
          ]
          accs = lf_empirical_accuracies(L, Y)
          d["Correct"] = Series(data=corrects, index=lf_names)
          d["Incorrect"] = Series(data=incorrects, index=lf_names)
          d["Emp. Acc."] = Series(data=accs, index=lf_names)
  
      if est_accs is not None:
          col_names.append("Learned Acc.")
          d["Learned Acc."] = Series(est_accs, index=lf_names)
  
      return DataFrame(data=d, index=lf_names)[col_names]