def compute_scored_notes(
  ratings: pd.DataFrame,
  noteParams: pd.DataFrame,
  raterParams: Optional[pd.DataFrame],
  noteStatusHistory: pd.DataFrame,
  minRatingsNeeded: int,
  crhThreshold: float,
  crnhThresholdIntercept: float,
  crnhThresholdNoteFactorMultiplier: float,
  crnhThresholdNMIntercept: float,
  crnhThresholdUCBIntercept: float,
  crhSuperThreshold: Optional[float],
  inertiaDelta: float,
  tagFilterThresholds: Optional[Dict[str, float]],
  incorrectFilterThreshold: float,
  finalRound: bool = False,
  # TODO: We might want to consider inputing only the series here, instead of the whole callable
  is_crh_function: Callable[..., pd.Series] = is_crh,
  is_crnh_diamond_function: Callable[..., pd.Series] = is_crnh_diamond,
  is_crnh_ucb_function: Callable[..., pd.Series] = is_crnh_ucb,
  is_crnh_ratio_function: Callable[..., pd.Series] = is_crnh_ratio,
  lowDiligenceThreshold: float = 0.263,
  factorThreshold: float = 0.5,
  firmRejectThreshold: Optional[float] = None,
  minMinorityNetHelpfulRatings: Optional[int] = None,
  minMinorityNetHelpfulRatio: Optional[float] = None,
  crhThresholdNoHighVol: Optional[float] = None,
) -> pd.DataFrame:
  """
  Merges note status history, ratings, and model output. It annotes the data frame with
  different note statuses, and features needed to calculate contributor stats.

  Args:
      ratings: All ratings from Community Notes contributors.
      noteParams: Note intercepts and factors returned from matrix factorization.
      raterParams: Rater intercepts and factors returned from matrix factorization.
      noteStatusHistory: History of note statuses.
      minRatingsNeeded: Minimum number of ratings for a note to achieve status.
      crhThrehsold: Minimum intercept for most notes to achieve CRH status.
      crnhThresholdIntercept: Minimum intercept for most notes to achieve CRNH status.
      crnhThresholdNoteFactorMultiplier: Scaling factor making controlling the relationship between
        CRNH threshold and note intercept.  Note that this constant is set negative so that notes with
        larger (magnitude) factors must have proportionally lower intercepts to become CRNH.
      crnhThresholdNMIntercept: Minimum intercept for notes which do not claim a tweet is misleading
        to achieve CRNH status.
      crnhThresholdUCBIntercept: Maximum UCB of the intercept (determined with pseudoraters) for
        notes to achieve CRNH status.
        notes to achieve CRH status.
      crhSuperThreshold: Minimum intercept for notes which have consistent and common patterns of
        repeated reason tags in not-helpful ratings to achieve CRH status.
      inertiaDelta: Minimum amount which a note that has achieve CRH status must drop below the
        applicable threshold to lose CRH status.
      finalRound: If true, enable additional status assignment logic which is only applied when
        determining final status.  Given that these mechanisms add complexity we don't apply them
        in earlier rounds.
      is_crh_function: Function specifying default CRH critierai.
      is_crnh_diamond_function: Function specifying default CRNH critierai.
      is_crnh_ucb_function: Function specifying default CRNH critierai, ORed together with previous.
      is_crnh_ratio_function:
  Returns:
      pd.DataFrame: scoredNotes The scored notes
  """
  # Compute noteStats and drop columns which won't be needed
  noteStats = compute_note_stats(ratings, noteStatusHistory)
  noteStats = noteStats.drop(
    columns=[
      c.numRatingsLast28DaysKey,
      c.createdAtMillisKey,
    ]
  )

  # Get meanHelpfulNum per side and minSignCount for each note
  noteStats = noteStats.merge(
    get_note_counts_by_rater_sign(raterParams, ratings),
    how="left",
    on=c.noteIdKey,
    unsafeAllowed={c.minSignCountKey, c.netMinHelpfulKey},
  )
  noteStats[[c.minSignCountKey]] = noteStats[[c.minSignCountKey]].fillna(0).astype(np.int32)
  noteStats[[c.netMinHelpfulKey]] = noteStats[[c.netMinHelpfulKey]].fillna(0).astype(np.int32)
  noteStats[[c.netMinHelpfulRatioKey]] = (
    noteStats[[c.netMinHelpfulRatioKey]].fillna(0.0).astype(np.float32)
  )

  # Merge with noteParams as necessary
  noteParamsColsToKeep = [c.noteIdKey, c.internalNoteInterceptKey, c.internalNoteFactor1Key]
  if finalRound:
    noteParamsColsToKeep += [c.lowDiligenceNoteInterceptKey]
  for col in c.noteParameterUncertaintyTSVColumns:
    if col in noteParams.columns:
      noteParamsColsToKeep.append(col)
  if crhThresholdNoHighVol is not None:
    noteParamsColsToKeep.append(c.internalNoteInterceptNoHighVolKey)
  noteStats = noteStats.merge(
    noteParams[noteParamsColsToKeep],
    on=c.noteIdKey,
    how="left",
    unsafeAllowed={"ratingCount_all", "ratingCount_neg_fac", "ratingCount_pos_fac"},
  )

  rules = [
    scoring_rules.DefaultRule(RuleID.INITIAL_NMR, set(), c.needsMoreRatings),
    scoring_rules.RuleFromFunction(
      RuleID.GENERAL_CRH,
      {RuleID.INITIAL_NMR},
      c.currentlyRatedHelpful,
      lambda noteStats: is_crh_function(noteStats, minRatingsNeeded, crhThreshold),
      onlyApplyToNotesThatSayTweetIsMisleading=True,
    ),
    scoring_rules.RuleFromFunction(
      RuleID.GENERAL_CRNH,
      {RuleID.INITIAL_NMR},
      c.currentlyRatedNotHelpful,
      lambda noteStats: is_crnh_diamond_function(
        noteStats, minRatingsNeeded, crnhThresholdIntercept, crnhThresholdNoteFactorMultiplier
      ),
      onlyApplyToNotesThatSayTweetIsMisleading=False,
    ),
    scoring_rules.RuleFromFunction(
      RuleID.UCB_CRNH,
      {RuleID.INITIAL_NMR},
      c.currentlyRatedNotHelpful,
      lambda noteStats: is_crnh_ucb_function(
        noteStats, minRatingsNeeded, crnhThresholdUCBIntercept
      ),
      onlyApplyToNotesThatSayTweetIsMisleading=False,
    ),
    scoring_rules.RuleFromFunction(
      RuleID.RATIO_CRNH,
      {RuleID.INITIAL_NMR},
      c.currentlyRatedNotHelpful,
      lambda noteStats: is_crnh_ratio_function(noteStats, minRatingsNeeded, do=finalRound),
      onlyApplyToNotesThatSayTweetIsMisleading=False,
    ),
    scoring_rules.NMtoCRNH(
      RuleID.NM_CRNH, {RuleID.INITIAL_NMR}, c.currentlyRatedNotHelpful, crnhThresholdNMIntercept
    ),
  ]
  if finalRound:
    with c.time_block("compute_scored_notes: compute tag aggregates"):
      # Compute tag aggregates only if they are required for tag filtering.
      tagAggregates = tag_filter.get_note_tag_aggregates(ratings, noteParams, raterParams)

      # set pandas option to display all columns
      pd.set_option("display.max_columns", None)
      assert len(tagAggregates) == len(noteParams), f"""there should be one aggregate per scored note
      len(noteParams) == {len(noteParams)}; len(np.unique(noteParams[c.noteIdKey])) == {len(np.unique(noteParams[c.noteIdKey]))}
      len(tagAggregates) == {len(tagAggregates)}; len(np.unique(tagAggregates[c.noteIdKey])) == {len(np.unique(tagAggregates[c.noteIdKey]))}

      The first 30 notes that appear in noteParams but not in tagAggregates are:
      {noteParams[~noteParams[c.noteIdKey].isin(tagAggregates[c.noteIdKey])].head(30)}

      The first 30 notes that appear in tagAggregates but not in noteParams are:
      {tagAggregates[~tagAggregates[c.noteIdKey].isin(noteParams[c.noteIdKey])].head(30)}
      """

      noteStats = tagAggregates.merge(noteStats, on=c.noteIdKey, how="outer")
    with c.time_block("compute_scored_notes: compute incorrect aggregates"):
      incorrectAggregates = incorrect_filter.get_incorrect_aggregates_final_scoring(
        ratings, noteParams, raterParams
      )
      noteStats = noteStats.merge(
        incorrectAggregates,
        on=c.noteIdKey,
        how="outer",
        unsafeAllowed={
          c.notHelpfulIncorrectIntervalKey,
          c.numVotersIntervalKey,
        },
      )
    assert tagFilterThresholds is not None

    # Add tag filtering and sticky scoring logic.
    rules.extend(
      [
        scoring_rules.AddCRHInertia(
          RuleID.GENERAL_CRH_INERTIA,
          {RuleID.GENERAL_CRH},
          c.currentlyRatedHelpful,
          crhThreshold - inertiaDelta,
          crhThreshold,
          minRatingsNeeded,
        ),
        scoring_rules.FilterTagOutliers(
          RuleID.TAG_OUTLIER,
          {RuleID.GENERAL_CRH},
          c.firmReject if firmRejectThreshold is not None else c.needsMoreRatings,
          tagFilterThresholds=tagFilterThresholds,
        ),
      ]
    )
    if crhSuperThreshold is not None:
      rules.extend(
        [
          scoring_rules.RuleFromFunction(
            RuleID.ELEVATED_CRH,
            {RuleID.INITIAL_NMR},
            c.currentlyRatedHelpful,
            lambda noteStats: is_crh_function(noteStats, minRatingsNeeded, crhSuperThreshold),
            onlyApplyToNotesThatSayTweetIsMisleading=True,
          ),
          scoring_rules.AddCRHInertia(
            RuleID.ELEVATED_CRH_INERTIA,
            {RuleID.TAG_OUTLIER},
            c.currentlyRatedHelpful,
            crhSuperThreshold - inertiaDelta,
            crhSuperThreshold,
            minRatingsNeeded,
          ),
        ]
      )
    rules.extend(
      [
        scoring_rules.FilterIncorrect(
          RuleID.INCORRECT_OUTLIER,
          {RuleID.TAG_OUTLIER},
          c.firmReject if firmRejectThreshold is not None else c.needsMoreRatings,
          tagThreshold=2,
          voteThreshold=3,
          weightedTotalVotes=incorrectFilterThreshold,
        ),
        scoring_rules.FilterLowDiligence(
          RuleID.LOW_DILIGENCE,
          {RuleID.INCORRECT_OUTLIER},
          c.firmReject if firmRejectThreshold is not None else c.needsMoreRatings,
          interceptThreshold=lowDiligenceThreshold,
        ),
        scoring_rules.FilterLargeFactor(
          RuleID.LARGE_FACTOR,
          {RuleID.LOW_DILIGENCE},
          c.firmReject if firmRejectThreshold is not None else c.needsMoreRatings,
          factorThreshold=factorThreshold,
        ),
      ]
    )
    if firmRejectThreshold is not None:
      rules.append(
        scoring_rules.RejectLowIntercept(
          RuleID.LOW_INTERCEPT,
          {RuleID.LARGE_FACTOR},
          c.firmReject,
          firmRejectThreshold,
        )
      )
    if minMinorityNetHelpfulRatings is not None:
      rules.append(
        scoring_rules.RequireMinMinorityRaters(
          RuleID.MIN_MINORITY_RATERS,
          {RuleID.LARGE_FACTOR},
          c.needsYourHelp,
          minMinorityNetHelpfulRatings,
        )
      )
    if minMinorityNetHelpfulRatio is not None:
      rules.append(
        scoring_rules.RequireRaterBalance(
          RuleID.RATER_BALANCE,
          {RuleID.MIN_MINORITY_RATERS},
          c.needsYourHelp,
          minMinorityNetHelpfulRatio,
        )
      )
    if crhThresholdNoHighVol is not None:
      rules.append(
        scoring_rules.NoHighVolIntercept(
          RuleID.NO_HIGH_VOL_INTERCEPT,
          {RuleID.LARGE_FACTOR},
          c.needsYourHelp,
          crhThresholdNoHighVol,
        )
      )
  scoredNotes = scoring_rules.apply_scoring_rules(
    noteStats, rules, c.internalRatingStatusKey, c.internalActiveRulesKey
  )
  # Discard the locked status column since it is captured in noteStatusHistory and
  # not necessary for the rest of scoring.
  scoredNotes = scoredNotes.drop(columns=[c.lockedStatusKey])
  # Discard extra columns related to the RatioCRNH rule that are no longer needed for the rest of scoring.
  scoredNotes = scoredNotes.drop(
    columns=[
      c.minSignCountKey,
      c.negFactorMeanHelpfulNumKey,
      c.posFactorMeanHelpfulNumKey,
      c.netMinHelpfulKey,
      c.netMinHelpfulRatioKey,
    ]
  )

  return scoredNotes

  Merges note status history, ratings, and model output. It annotes the data frame with
  different note statuses, and features needed to calculate contributor stats.

  Args:
      ratings: All ratings from Community Notes contributors.
      noteParams: Note intercepts and factors returned from matrix factorization.
      raterParams: Rater intercepts and factors returned from matrix factorization.
      noteStatusHistory: History of note statuses.
      minRatingsNeeded: Minimum number of ratings for a note to achieve status.
      crhThrehsold: Minimum intercept for most notes to achieve CRH status.
      crnhThresholdIntercept: Minimum intercept for most notes to achieve CRNH status.
      crnhThresholdNoteFactorMultiplier: Scaling factor making controlling the relationship between
        CRNH threshold and note intercept.  Note that this constant is set negative so that notes with
        larger (magnitude) factors must have proportionally lower intercepts to become CRNH.
      crnhThresholdNMIntercept: Minimum intercept for notes which do not claim a tweet is misleading
        to achieve CRNH status.
      crnhThresholdUCBIntercept: Maximum UCB of the intercept (determined with pseudoraters) for
        notes to achieve CRNH status.
        notes to achieve CRH status.
      crhSuperThreshold: Minimum intercept for notes which have consistent and common patterns of
        repeated reason tags in not-helpful ratings to achieve CRH status.
      inertiaDelta: Minimum amount which a note that has achieve CRH status must drop below the
        applicable threshold to lose CRH status.
      finalRound: If true, enable additional status assignment logic which is only applied when
        determining final status.  Given that these mechanisms add complexity we don't apply them
        in earlier rounds.
      is_crh_function: Function specifying default CRH critierai.
      is_crnh_diamond_function: Function specifying default CRNH critierai.
      is_crnh_ucb_function: Function specifying default CRNH critierai, ORed together with previous.
      is_crnh_ratio_function:
  Returns:
      pd.DataFrame: scoredNotes The scored notes
  
sourcecode/scoring/note_ratings.py