def apply_scoring_rules(
  noteStats: pd.DataFrame,
  rules: List[ScoringRule],
  statusColumn: str,
  ruleColumn: str,
  decidedByColumn: Optional[str] = None,
) -> pd.DataFrame:
  """Apply a list of ScoringRules to note inputs and return noteStats augmented with scoring results.

  This function applies a list of ScoringRules in order.  Once each rule has run
  a final ratingStatus is set for each note. An additional column is added to capture
  which rules acted on the note and any additional columns generated by the ScoringRules
  are merged with the scored notes to generate the final return value.

  Args:
    noteStats: attributes, aggregates and raw scoring signals for each note.
    rules: ScoringRules which will be applied in the order given.
    statusColumn: str indicating column where status should be assigned.
    ruleColumn: str indicating column where active rules should be stored.
    decidedByColumn: None or str indicating column where the last rule to act on a note is stored.

  Returns:
    noteStats with additional columns representing scoring results.
  """
  # Initialize empty dataframes to store labels for each note and which rules impacted
  # scoring for each note.
  noteLabels = pd.DataFrame.from_dict(
    {c.noteIdKey: pd.Series([], dtype=np.int64), statusColumn: pd.Series([], dtype=object)}
  )
  noteRules = pd.DataFrame.from_dict(
    {c.noteIdKey: pd.Series([], dtype=np.int64), ruleColumn: pd.Series([], dtype=object)}
  )
  noteColumns = pd.DataFrame.from_dict({c.noteIdKey: pd.Series([], dtype=np.int64)})

  # Establish state to enforce rule dependencies.
  ruleIDs: Set[RuleID] = set()

  # Successively apply each rule
  for rule in rules:
    with c.time_block(f"Applying scoring rule: {rule.get_name()}"):
      logger.info(f"Applying scoring rule: {rule.get_name()}")
      rule.check_dependencies(ruleIDs)
      assert rule.get_rule_id() not in ruleIDs, f"repeat ruleID: {rule.get_name()}"
      ruleIDs.add(rule.get_rule_id())
      with c.time_block(f"Calling score_notes: {rule.get_name()}"):
        noteStatusUpdates, additionalColumns = rule.score_notes(noteStats, noteLabels, statusColumn)
      if (
        additionalColumns is not None
        # This rule updates both status and NmrDueToStableCrhTime (in additional column), they can
        # be on different rows.
        and rule.get_rule_id() != RuleID.NMR_DUE_TO_MIN_STABLE_CRH_TIME
      ):
        assert set(noteStatusUpdates[c.noteIdKey]) == set(additionalColumns[c.noteIdKey])

      # Update noteLabels, which will always hold at most one label per note.
      unsafeAllowed = {c.internalRatingStatusKey, c.finalRatingStatusKey, c.defaultIndexKey}
      noteLabels = (
        pd.concat([noteLabels, noteStatusUpdates], unsafeAllowed=unsafeAllowed)
        .groupby(c.noteIdKey)
        .tail(1)
      )
      # Update note rules to have one row per rule which was active for a note
      noteRules = pd.concat(
        [
          noteRules,
          pd.DataFrame.from_dict(
            {c.noteIdKey: noteStatusUpdates[c.noteIdKey], ruleColumn: rule.get_name()}
          ),
        ],
        unsafeAllowed={c.internalActiveRulesKey, c.defaultIndexKey, c.metaScorerActiveRulesKey},
      )
      if additionalColumns is not None:
        # Merge any additional columns into current set of new columns
        assert {c.noteIdKey} == (set(noteColumns.columns) & set(additionalColumns.columns))
        noteColumns = noteColumns.merge(
          additionalColumns, on=c.noteIdKey, how="outer", unsafeAllowed=c.defaultIndexKey
        )

  with c.time_block("Condense noteRules after applying all scoring rules"):
    # Having applied all scoring rules, condense noteRules to have one row per note representing
    # all of the ScoringRuless which were active for the note.
    noteRules = noteRules.groupby(c.noteIdKey).aggregate(list).reset_index()
    if decidedByColumn:
      noteRules[decidedByColumn] = [rules[-1] for rules in noteRules[ruleColumn]]
    noteRules[ruleColumn] = [",".join(activeRules) for activeRules in noteRules[ruleColumn]]
    # Validate that there are labels and assigned rules for each note
    assert set(noteStats[c.noteIdKey]) == set(noteLabels[c.noteIdKey])
    assert set(noteStats[c.noteIdKey]) == set(noteRules[c.noteIdKey])
    assert len(set(noteColumns[c.noteIdKey]) - set(noteStats[c.noteIdKey])) == 0
    # Merge note labels, active rules and new columns into noteStats to form scoredNotes
    scoredNotes = noteStats.merge(noteLabels, on=c.noteIdKey, how="inner")
    scoredNotes = scoredNotes.merge(noteRules, on=c.noteIdKey, how="inner")
    scoredNotes = scoredNotes.merge(noteColumns, on=c.noteIdKey, how="left")
    # Add all of the individual model rules to the active rules column
    assert len(scoredNotes) == len(noteStats)
    # Set boolean columns indicating scoring outcomes
    scoredNotes[c.currentlyRatedHelpfulBoolKey] = (
      scoredNotes[statusColumn] == c.currentlyRatedHelpful
    )
    scoredNotes[c.currentlyRatedNotHelpfulBoolKey] = (
      scoredNotes[statusColumn] == c.currentlyRatedNotHelpful
    )
    scoredNotes[c.awaitingMoreRatingsBoolKey] = scoredNotes[statusColumn] == c.needsMoreRatings

  # Return completed DF including original noteStats signals merged wtih scoring results
  return scoredNotes
Apply a list of ScoringRules to note inputs and return noteStats augmented with scoring results.

  This function applies a list of ScoringRules in order.  Once each rule has run
  a final ratingStatus is set for each note. An additional column is added to capture
  which rules acted on the note and any additional columns generated by the ScoringRules
  are merged with the scored notes to generate the final return value.

  Args:
    noteStats: attributes, aggregates and raw scoring signals for each note.
    rules: ScoringRules which will be applied in the order given.
    statusColumn: str indicating column where status should be assigned.
    ruleColumn: str indicating column where active rules should be stored.
    decidedByColumn: None or str indicating column where the last rule to act on a note is stored.

  Returns:
    noteStats with additional columns representing scoring results.
  
sourcecode/scoring/scoring_rules.py