class_docstr: Iterative estimation of row and column centering/scaling
  using the algorithm from page 31 of:
      Matrix Completion and Low-Rank SVD via Fast Alternating Least Squares
code: def fit(self, X):
      X = self.clamp(X)
      n_rows, n_cols = X.shape
      dtype = X.dtype
  
      # To avoid inefficient memory access we keep around two copies
      # of the array, one contiguous in the rows and the other
      # contiguous in the columns
      X_row_major = np.asarray(X, order="C")
      X_column_major = np.asarray(X, order="F")
  
      observed_row_major = ~np.isnan(X_row_major)
      n_observed_per_row = observed_row_major.sum(axis=1)
      n_empty_rows = (n_observed_per_row == 0).sum()
  
      if n_empty_rows > 0:
          raise ValueError(
              "%d rows have no observed values" % n_empty_rows
          )
  
      observed_column_major = np.asarray(observed_row_major, order="F")
      n_observed_per_column = observed_column_major.sum(axis=0)
      n_empty_columns = (n_observed_per_column == 0).sum()
      if n_empty_columns > 0:
          raise ValueError(
              "%d columns have no observed values" % (n_empty_columns,)
          )
      # initialize by assuming that rows are zero-mean/unit variance and
      # with a direct estimate of mean and standard deviation
      # of each column
      row_means = np.zeros(n_rows, dtype=dtype)
      row_scales = np.ones(n_rows, dtype=dtype)
  
      if self.center_columns:
          column_means = np.nanmean(X, axis=0)
      else:
          column_means = np.zeros(n_cols, dtype=dtype)
  
      if self.scale_columns:
          column_scales = np.nanstd(X, axis=0)
          column_scales[column_scales == 0] = 1.0
      else:
          column_scales = np.ones(n_cols, dtype=dtype)
  
      last_residual = self.residual(X)
      if self.verbose:
          print(
              "[BiScaler] Initial log residual value = %f"
              % (np.log(last_residual),)
          )
      for i in range(self.max_iters):
          if last_residual == 0:
              # already have a perfect fit, so let's get out of here
              print("[BiScaler] No room for improvement")
              break
  
          assert (
              len(column_means) == n_cols
          ), "Wrong number of column means, expected %d but got %d" % (
              n_cols,
              len(column_means),
          )
          assert len(column_scales) == n_cols, (
              "Wrong number of column scales, expected %d but got %d"
              % (n_cols, len(column_scales))
          )
          assert (
              len(row_means) == n_rows
          ), "Wrong number of row means, expected %d but got %d" % (
              n_rows,
              len(row_means),
          )
          assert (
              len(row_scales) == n_rows
          ), "Wrong number of row scales, expected %d but got %d" % (
              n_rows,
              len(row_scales),
          )
  
          if self.center_rows:
              row_means = self.estimate_row_means(
                  X=X_row_major,
                  observed=observed_row_major,
                  column_means=column_means,
                  column_scales=column_scales,
              )
          if self.center_columns:
              column_means = self.estimate_column_means(
                  X=X_column_major,
                  observed=observed_column_major,
                  row_means=row_means,
                  row_scales=row_scales,
              )
  
          X_centered = self.center(X, row_means, column_means)
          if self.scale_rows:
              row_scales = self.estimate_row_scales(
                  X_centered=X_centered, column_scales=column_scales
              )
          if self.scale_columns:
              column_scales = self.estimate_column_scales(
                  X_centered=X_centered, row_scales=row_scales
              )
  
          X_normalized = self.rescale(
              X_centered, row_scales, column_scales
          )
          residual = self.residual(X_normalized)
          change_in_residual = last_residual - residual
          if self.verbose:
              print(
                  "[BiScaler] Iter %d: log residual = %f, log improvement ratio=%f"
                  % (
                      i + 1,
                      np.log(residual),
                      np.log(last_residual / residual),
                  )
              )
          if change_in_residual / last_residual < self.tolerance:
              break
          last_residual = residual
      self.row_means = row_means
      self.row_scales = row_scales
      self.column_means = column_means
      self.column_scales = column_scales
  
imports: import numpy
label_desc: Use image node_id 10 for calling the BiScaler obj's underlying member method code with example usage: obj.fit(X) without return types