def _get_note_counts(
    self,
    scoredNotes: pd.DataFrame,
    notes: pd.DataFrame,
    noteStatusHistory: pd.DataFrame,
    prepareForTraining: bool,
  ) -> pd.DataFrame:
    """Return counts of relevant events on peer notes.

    Counts should include: number of misleading and non-misleading notes created on the same post,
    number of misleading notes on the same post that went CRH and ratios of the above values.

    Note that aggregation works differently during training and in production, since training
    effectively has access to data from the future.


    Args:
      scoredNotes: DF containing {note, tweet} associations and effective scoring times for training.
      notes: DF containing set of all notes, not just those that are being scored.
      noteStatusHistory: DF specifying key scoring timestamps for peer notes.
      prepareForTraining: whether notes should be pruned to discard future data.
    """
    # Merge inputs into a single dataframe with relevant info
    assert scoredNotes[c.tweetIdKey].min() > 0  # tweet should be set for all notes being scored
    peerNotes = scoredNotes[[c.noteIdKey, c.tweetIdKey]].merge(
      notes[[c.tweetIdKey, c.noteIdKey, c.classificationKey]].rename(
        columns={c.noteIdKey: "peerNoteId"}
      )
    )
    assert len(scoredNotes) == peerNotes[c.noteIdKey].nunique()
    assert peerNotes[c.classificationKey].isna().sum() == 0
    peerNotes = peerNotes.merge(noteStatusHistory[[c.noteIdKey, c.createdAtMillisKey]])
    peerNotes = peerNotes.merge(
      noteStatusHistory[
        [
          c.noteIdKey,
          c.createdAtMillisKey,
          c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey,
          c.timestampMillisOfNoteFirstNonNMRLabelKey,
          c.firstNonNMRLabelKey,
        ]
      ].rename(columns={c.noteIdKey: "peerNoteId", c.createdAtMillisKey: "peerCreatedAtMillis"})
    )
    assert len(scoredNotes) == peerNotes[c.noteIdKey].nunique()

    # If we are in training, prune scope of what info should be available
    assert (_SCORING_CUTOFF_MTS in scoredNotes) == prepareForTraining
    if _SCORING_CUTOFF_MTS in scoredNotes:
      peerNotes = peerNotes.merge(scoredNotes[[c.noteIdKey, _SCORING_CUTOFF_MTS]])
      # Prune any notes created after the cutoff
      peerNotes = peerNotes[peerNotes["peerCreatedAtMillis"] <= peerNotes[_SCORING_CUTOFF_MTS]]
      # Null any scoring events that happened after the cutoff
      peerNotes.loc[
        peerNotes[c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey]
        > peerNotes[_SCORING_CUTOFF_MTS],
        c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey,
      ] = np.nan
      peerNotes.loc[
        peerNotes[c.timestampMillisOfNoteFirstNonNMRLabelKey] > peerNotes[_SCORING_CUTOFF_MTS],
        [c.timestampMillisOfNoteFirstNonNMRLabelKey, c.firstNonNMRLabelKey],
      ] = np.nan

    # Compute totals
    peerNotes = peerNotes[peerNotes[c.noteIdKey] != peerNotes["peerNoteId"]]
    totalPeerNotes = (
      peerNotes[c.noteIdKey]
      .value_counts()
      .to_frame()
      .reset_index(drop=False)
      .rename(columns={"count": _TOTAL_PEER_NOTES})
    )
    totalPeerMisleadingNotes = (
      peerNotes[peerNotes[c.classificationKey] == c.notesSaysTweetIsMisleadingKey][c.noteIdKey]
      .value_counts()
      .to_frame()
      .reset_index(drop=False)
      .rename(columns={"count": _TOTAL_PEER_MISLEADING_NOTES})
    )
    totalPeerNotMisleadingNotes = (
      peerNotes[peerNotes[c.classificationKey] == c.noteSaysTweetIsNotMisleadingKey][c.noteIdKey]
      .value_counts()
      .to_frame()
      .reset_index(drop=False)
      .rename(columns={"count": _TOTAL_PEER_NON_MISLEADING_NOTES})
    )
    totalPeerStabilizationNotes = (
      peerNotes[
        (peerNotes[c.classificationKey] == c.notesSaysTweetIsMisleadingKey)
        & (peerNotes[c.timestampMillisOfFirstNmrDueToMinStableCrhTimeKey].notna())
      ][c.noteIdKey]
      .value_counts()
      .to_frame()
      .reset_index(drop=False)
      .rename(columns={"count": _TOTAL_PEER_STABILIZATION_NOTES})
    )
    totalPeerCrhNotes = (
      peerNotes[
        (peerNotes[c.classificationKey] == c.notesSaysTweetIsMisleadingKey)
        & (peerNotes[c.firstNonNMRLabelKey] == c.currentlyRatedHelpful)
      ][c.noteIdKey]
      .value_counts()
      .to_frame()
      .reset_index(drop=False)
      .rename(columns={"count": _TOTAL_PEER_CRH_NOTES})
    )
    return (
      scoredNotes[[c.noteIdKey]]
      .merge(totalPeerNotes, how="left")
      .merge(totalPeerMisleadingNotes, how="left")
      .merge(totalPeerNotMisleadingNotes, how="left")
      .merge(totalPeerStabilizationNotes, how="left")
      .merge(totalPeerCrhNotes, how="left")
      .fillna(0)
      .astype(pd.Int64Dtype())
    )
Return counts of relevant events on peer notes.

    Counts should include: number of misleading and non-misleading notes created on the same post,
    number of misleading notes on the same post that went CRH and ratios of the above values.

    Note that aggregation works differently during training and in production, since training
    effectively has access to data from the future.


    Args:
      scoredNotes: DF containing {note, tweet} associations and effective scoring times for training.
      notes: DF containing set of all notes, not just those that are being scored.
      noteStatusHistory: DF specifying key scoring timestamps for peer notes.
      prepareForTraining: whether notes should be pruned to discard future data.
    
sourcecode/scoring/pflip_plus_model.py