# Copyright (C) 2017-2023 Cleanlab Inc.
# This file is part of cleanlab.
#
# cleanlab is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cleanlab is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with cleanlab. If not, see <https://www.gnu.org/licenses/>.
"""
Methods for analysis of classification data labeled by multiple annotators.
To analyze a fixed dataset labeled by multiple annotators, use the
:py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>` function which estimates:
* A consensus label for each example that aggregates the individual annotations more accurately than alternative aggregation via majority-vote or other algorithms used in crowdsourcing like Dawid-Skene.
* A quality score for each consensus label which measures our confidence that this label is correct.
* An analogous label quality score for each individual label chosen by one annotator for a particular example.
* An overall quality score for each annotator which measures our confidence in the overall correctness of labels obtained from this annotator.
The algorithms to compute these estimates are described in `the CROWDLAB paper <https://arxiv.org/abs/2210.06812>`_.
If you have some labeled and unlabeled data (with multiple annotators for some labeled examples) and want to decide what data to collect additional labels for,
use the :py:func:`get_active_learning_scores <cleanlab.multiannotator.get_active_learning_scores>` function, which is intended for active learning.
This function estimates an ActiveLab quality score for each example,
which can be used to prioritize which examples are most informative to collect additional labels for.
This function is effective for settings where some examples have been labeled by one or more annotators and other examples can have no labels at all so far,
as well as settings where new labels are collected either in batches of examples or one at a time.
Here is an `example notebook <https://github.com/cleanlab/examples/blob/master/active_learning_multiannotator/active_learning.ipynb>`_ showcasing the use of this ActiveLab method for active learning with data re-labeling.
The algorithms to compute these active learning scores are described in `the ActiveLab paper <https://arxiv.org/abs/2301.11856>`_.
Each of the main functions in this module utilizes any trained classifier model.
Variants of these functions are provided for settings where you have trained an ensemble of multiple models.
"""
import warnings
import numpy as np
import pandas as pd
from typing import List, Dict, Any, Union, Tuple, Optional
from cleanlab.rank import get_label_quality_scores
from cleanlab.internal.util import get_num_classes, value_counts
from cleanlab.internal.constants import CLIPPING_LOWER_BOUND
from cleanlab.internal.multiannotator_utils import (
assert_valid_inputs_multiannotator,
assert_valid_pred_probs,
check_consensus_label_classes,
find_best_temp_scaler,
temp_scale_pred_probs,
)
[docs]def get_label_quality_multiannotator(
labels_multiannotator: Union[pd.DataFrame, np.ndarray],
pred_probs: np.ndarray,
*,
consensus_method: Union[str, List[str]] = "best_quality",
quality_method: str = "crowdlab",
calibrate_probs: bool = False,
return_detailed_quality: bool = True,
return_annotator_stats: bool = True,
return_weights: bool = False,
verbose: bool = True,
label_quality_score_kwargs: dict = {},
) -> Dict[str, Any]:
"""Returns label quality scores for each example and for each annotator in a dataset labeled by multiple annotators.
This function is for multiclass classification datasets where examples have been labeled by
multiple annotators (not necessarily the same number of annotators per example).
It computes one consensus label for each example that best accounts for the labels chosen by each
annotator (and their quality), as well as a consensus quality score for how confident we are that this consensus label is actually correct.
It also computes similar quality scores for each annotator's individual labels, and the quality of each annotator.
Scores are between 0 and 1 (estimated via methods like CROWDLAB); lower scores indicate labels/annotators less likely to be correct.
To decide what data to collect additional labels for, try the :py:func:`get_active_learning_scores <cleanlab.multiannotator.get_active_learning_scores>`
(ActiveLab) function, which is intended for active learning with multiple annotators.
Parameters
----------
labels_multiannotator : pd.DataFrame of np.ndarray
2D pandas DataFrame or array of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
``labels_multiannotator[n][m]`` = label for n-th example given by m-th annotator.
For a dataset with K classes, each given label must be an integer in 0, 1, ..., K-1 or ``NaN`` if this annotator did not label a particular example.
If you have string or other differently formatted labels, you can convert them to the proper format using :py:func:`format_multiannotator_labels <cleanlab.internal.multiannotator_utils.format_multiannotator_labels>`.
If pd.DataFrame, column names should correspond to each annotator's ID.
pred_probs : np.ndarray
An array of shape ``(N, K)`` of predicted class probabilities from a trained classifier model.
Predicted probabilities in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
consensus_method : str or List[str], default = "majority_vote"
Specifies the method used to aggregate labels from multiple annotators into a single consensus label.
Options include:
* ``majority_vote``: consensus obtained using a simple majority vote among annotators, with ties broken via ``pred_probs``.
* ``best_quality``: consensus obtained by selecting the label with highest label quality (quality determined by method specified in ``quality_method``).
A List may be passed if you want to consider multiple methods for producing consensus labels.
If a List is passed, then the 0th element of the list is the method used to produce columns `consensus_label`, `consensus_quality_score`, `annotator_agreement` in the returned DataFrame.
The remaning (1st, 2nd, 3rd, etc.) elements of this list are output as extra columns in the returned pandas DataFrame with names formatted as:
`consensus_label_SUFFIX`, `consensus_quality_score_SUFFIX` where `SUFFIX` = each element of this
list, which must correspond to a valid method for computing consensus labels.
quality_method : str, default = "crowdlab"
Specifies the method used to calculate the quality of the consensus label.
Options include:
* ``crowdlab``: an emsemble method that weighs both the annotators' labels as well as the model's prediction.
* ``agreement``: the fraction of annotators that agree with the consensus label.
calibrate_probs : bool, default = False
Boolean value that specifies whether the provided `pred_probs` should be re-calibrated to better match the annotators' empirical label distribution.
We recommend setting this to True in active learning applications, in order to prevent overconfident models from suggesting the wrong examples to collect labels for.
return_detailed_quality: bool, default = True
Boolean to specify if `detailed_label_quality` is returned.
return_annotator_stats : bool, default = True
Boolean to specify if `annotator_stats` is returned.
return_weights : bool, default = False
Boolean to specify if `model_weight` and `annotator_weight` is returned.
Model and annotator weights are applicable for ``quality_method == crowdlab``, will return ``None`` for any other quality methods.
verbose : bool, default = True
Important warnings and other printed statements may be suppressed if ``verbose`` is set to ``False``.
label_quality_score_kwargs : dict, optional
Keyword arguments to pass into :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
Returns
-------
labels_info : dict
Dictionary containing up to 5 pandas DataFrame with keys as below:
``label_quality`` : pandas.DataFrame
pandas DataFrame in which each row corresponds to one example, with columns:
* ``num_annotations``: the number of annotators that have labeled each example.
* ``consensus_label``: the single label that is best for each example (you can control how it is derived from all annotators' labels via the argument: ``consensus_method``).
* ``annotator_agreement``: the fraction of annotators that agree with the consensus label (only consider the annotators that labeled that particular example).
* ``consensus_quality_score``: label quality score for consensus label, calculated by the method specified in ``quality_method``.
``detailed_label_quality`` : pandas.DataFrame
Only returned if `return_detailed_quality=True`.
Returns a pandas DataFrame with columns `quality_annotator_1`, `quality_annotator_2`, ..., `quality_annotator_M` where each entry is
the label quality score for the labels provided by each annotator (is ``NaN`` for examples which this annotator did not label).
``annotator_stats`` : pandas.DataFrame
Only returned if `return_annotator_stats=True`.
Returns overall statistics about each annotator, sorted by lowest annotator_quality first.
pandas DataFrame in which each row corresponds to one annotator (the row IDs correspond to annotator IDs), with columns:
* ``annotator_quality``: overall quality of a given annotator's labels, calculated by the method specified in ``quality_method``.
* ``num_examples_labeled``: number of examples annotated by a given annotator.
* ``agreement_with_consensus``: fraction of examples where a given annotator agrees with the consensus label.
* ``worst_class``: the class that is most frequently mislabeled by a given annotator.
``model_weight`` : float
Only returned if `return_weights=True`. It is only applicable for ``quality_method == crowdlab``.
The model weight specifies the weight of classifier model in weighted averages used to estimate label quality
This number is an estimate of how trustworthy the model is relative the annotators.
``annotator_weight`` : np.ndarray
Only returned if `return_weights=True`. It is only applicable for ``quality_method == crowdlab``.
An array of shape ``(M,)`` where M is the number of annotators, specifying the weight of each annotator in weighted averages used to estimate label quality.
These weights are estimates of how trustworthy each annotator is relative to the other annotators.
"""
if isinstance(labels_multiannotator, np.ndarray):
labels_multiannotator = pd.DataFrame(labels_multiannotator)
if return_weights == True and quality_method != "crowdlab":
raise ValueError(
"Model and annotator weights are only applicable to the crowdlab quality method. "
"Either set return_weights=False or quality_method='crowdlab'."
)
assert_valid_inputs_multiannotator(labels_multiannotator, pred_probs)
# Count number of non-NaN values for each example
num_annotations = labels_multiannotator.count(axis=1).to_numpy()
# calibrate pred_probs
if calibrate_probs:
optimal_temp = find_best_temp_scaler(labels_multiannotator, pred_probs)
pred_probs = temp_scale_pred_probs(pred_probs, optimal_temp)
if not isinstance(consensus_method, list):
consensus_method = [consensus_method]
if "best_quality" in consensus_method or "majority_vote" in consensus_method:
majority_vote_label = get_majority_vote_label(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
verbose=False,
)
(
MV_annotator_agreement,
MV_consensus_quality_score,
MV_post_pred_probs,
MV_model_weight,
MV_annotator_weight,
) = _get_consensus_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
num_annotations=num_annotations,
consensus_label=majority_vote_label,
quality_method=quality_method,
verbose=verbose,
label_quality_score_kwargs=label_quality_score_kwargs,
)
label_quality = pd.DataFrame({"num_annotations": num_annotations})
valid_methods = ["majority_vote", "best_quality"]
main_method = True
for curr_method in consensus_method:
# geting consensus label and stats
if curr_method == "majority_vote":
consensus_label = majority_vote_label
annotator_agreement = MV_annotator_agreement
consensus_quality_score = MV_consensus_quality_score
post_pred_probs = MV_post_pred_probs
model_weight = MV_model_weight
annotator_weight = MV_annotator_weight
elif curr_method == "best_quality":
consensus_label = np.full(len(majority_vote_label), np.nan)
for i in range(len(consensus_label)):
max_pred_probs_ind = np.where(
MV_post_pred_probs[i] == np.max(MV_post_pred_probs[i])
)[0]
if len(max_pred_probs_ind) == 1:
consensus_label[i] = max_pred_probs_ind[0]
else:
consensus_label[i] = majority_vote_label[i]
consensus_label = consensus_label.astype("int64") # convert all label types to int
(
annotator_agreement,
consensus_quality_score,
post_pred_probs,
model_weight,
annotator_weight,
) = _get_consensus_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
num_annotations=num_annotations,
consensus_label=consensus_label,
quality_method=quality_method,
verbose=verbose,
label_quality_score_kwargs=label_quality_score_kwargs,
)
else:
raise ValueError(
f"""
{curr_method} is not a valid consensus method!
Please choose a valid consensus_method: {valid_methods}
"""
)
if verbose:
# check if any classes no longer appear in the set of consensus labels
check_consensus_label_classes(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
consensus_method=curr_method,
)
# saving stats into dataframe, computing additional stats if specified
if main_method:
(
label_quality["consensus_label"],
label_quality["consensus_quality_score"],
label_quality["annotator_agreement"],
) = (
consensus_label,
consensus_quality_score,
annotator_agreement,
)
label_quality = label_quality.reindex(
columns=[
"consensus_label",
"consensus_quality_score",
"annotator_agreement",
"num_annotations",
]
)
if return_detailed_quality:
# Compute the label quality scores for each annotators' labels
detailed_label_quality = labels_multiannotator.apply(
_get_annotator_label_quality_score,
pred_probs=post_pred_probs,
label_quality_score_kwargs=label_quality_score_kwargs,
)
detailed_label_quality = detailed_label_quality.add_prefix("quality_annotator_")
if return_annotator_stats:
annotator_stats = _get_annotator_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=post_pred_probs,
consensus_label=consensus_label,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
model_weight=model_weight,
annotator_weight=annotator_weight,
consensus_quality_score=consensus_quality_score,
quality_method=quality_method,
)
main_method = False
else:
(
label_quality[f"consensus_label_{curr_method}"],
label_quality[f"consensus_quality_score_{curr_method}"],
label_quality[f"annotator_agreement_{curr_method}"],
) = (
consensus_label,
consensus_quality_score,
annotator_agreement,
)
labels_info = {
"label_quality": label_quality,
}
if return_detailed_quality:
labels_info["detailed_label_quality"] = detailed_label_quality
if return_annotator_stats:
labels_info["annotator_stats"] = annotator_stats
if return_weights:
labels_info["model_weight"] = model_weight
labels_info["annotator_weight"] = annotator_weight
return labels_info
[docs]def get_label_quality_multiannotator_ensemble(
labels_multiannotator: Union[pd.DataFrame, np.ndarray],
pred_probs: np.ndarray,
*,
calibrate_probs: bool = False,
return_detailed_quality: bool = True,
return_annotator_stats: bool = True,
return_weights: bool = False,
verbose: bool = True,
label_quality_score_kwargs: dict = {},
) -> Dict[str, Any]:
"""Returns label quality scores for each example and for each annotator, based on predictions from an ensemble of models.
This function is similar to :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>` but for settings where
you have trained an ensemble of multiple classifier models rather than a single model.
Parameters
----------
labels_multiannotator : pd.DataFrame of np.ndarray
Multiannotator labels in the same format expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
pred_probs : np.ndarray
An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from the ensemble models.
Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
calibrate_probs : bool, default = False
Boolean value as expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
return_detailed_quality: bool, default = True
Boolean value as expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
return_annotator_stats : bool, default = True
Boolean value as expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
return_weights : bool, default = False
Boolean value as expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
verbose : bool, default = True
Boolean value as expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
label_quality_score_kwargs : dict, optional
Keyword arguments in the same format expected by py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
Returns
-------
labels_info : dict
Dictionary containing up to 5 pandas DataFrame with keys as below:
``label_quality`` : pandas.DataFrame
Similar to output as :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
``detailed_label_quality`` : pandas.DataFrame
Similar to output as :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
``annotator_stats`` : pandas.DataFrame
Similar to output as :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
``model_weight`` : np.ndarray
Only returned if `return_weights=True`.
An array of shape ``(P,)`` where is the number of models in the ensemble, specifying the weight of each classifier model in weighted averages used to estimate label quality.
These weigthts is an estimate of how trustworthy the model is relative the annotators.
An array of shape ``(P,)`` where is the number of models in the ensemble, specifying the model weight used in weighted averages.
``annotator_weight`` : np.ndarray
Only returned if `return_weights=True`.
Similar to output as :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
See Also
--------
get_label_quality_multiannotator
"""
if isinstance(labels_multiannotator, np.ndarray):
labels_multiannotator = pd.DataFrame(labels_multiannotator)
assert_valid_inputs_multiannotator(labels_multiannotator, pred_probs, ensemble=True)
# Count number of non-NaN values for each example
num_annotations = labels_multiannotator.count(axis=1).to_numpy()
# temp scale pred_probs
if calibrate_probs:
for i in range(len(pred_probs)):
curr_pred_probs = pred_probs[i]
optimal_temp = find_best_temp_scaler(labels_multiannotator, curr_pred_probs)
pred_probs[i] = temp_scale_pred_probs(curr_pred_probs, optimal_temp)
label_quality = pd.DataFrame({"num_annotations": num_annotations})
# get majority vote stats
avg_pred_probs = np.mean(pred_probs, axis=0)
majority_vote_label = get_majority_vote_label(
labels_multiannotator=labels_multiannotator,
pred_probs=avg_pred_probs,
verbose=False,
)
(
MV_annotator_agreement,
MV_consensus_quality_score,
MV_post_pred_probs,
MV_model_weight,
MV_annotator_weight,
) = _get_consensus_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
num_annotations=num_annotations,
consensus_label=majority_vote_label,
verbose=verbose,
ensemble=True,
**label_quality_score_kwargs,
)
# get crowdlab stats
consensus_label = np.full(len(majority_vote_label), np.nan)
for i in range(len(consensus_label)):
max_pred_probs_ind = np.where(MV_post_pred_probs[i] == np.max(MV_post_pred_probs[i]))[0]
if len(max_pred_probs_ind) == 1:
consensus_label[i] = max_pred_probs_ind[0]
else:
consensus_label[i] = majority_vote_label[i]
consensus_label = consensus_label.astype("int64") # convert all label types to int
(
annotator_agreement,
consensus_quality_score,
post_pred_probs,
model_weight,
annotator_weight,
) = _get_consensus_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
num_annotations=num_annotations,
consensus_label=consensus_label,
verbose=verbose,
ensemble=True,
**label_quality_score_kwargs,
)
if verbose:
# check if any classes no longer appear in the set of consensus labels
check_consensus_label_classes(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
consensus_method="crowdlab",
)
(
label_quality["consensus_label"],
label_quality["consensus_quality_score"],
label_quality["annotator_agreement"],
) = (
consensus_label,
consensus_quality_score,
annotator_agreement,
)
label_quality = label_quality.reindex(
columns=[
"consensus_label",
"consensus_quality_score",
"annotator_agreement",
"num_annotations",
]
)
if return_detailed_quality:
# Compute the label quality scores for each annotators' labels
detailed_label_quality = labels_multiannotator.apply(
_get_annotator_label_quality_score,
pred_probs=post_pred_probs,
**label_quality_score_kwargs,
)
detailed_label_quality = detailed_label_quality.add_prefix("quality_annotator_")
if return_annotator_stats:
annotator_stats = _get_annotator_stats(
labels_multiannotator=labels_multiannotator,
pred_probs=post_pred_probs,
consensus_label=consensus_label,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
model_weight=np.mean(model_weight), # use average model weight when scoring annotators
annotator_weight=annotator_weight,
consensus_quality_score=consensus_quality_score,
)
labels_info = {
"label_quality": label_quality,
}
if return_detailed_quality:
labels_info["detailed_label_quality"] = detailed_label_quality
if return_annotator_stats:
labels_info["annotator_stats"] = annotator_stats
if return_weights:
labels_info["model_weight"] = model_weight
labels_info["annotator_weight"] = annotator_weight
return labels_info
[docs]def get_active_learning_scores(
labels_multiannotator: Optional[Union[pd.DataFrame, np.ndarray]] = None,
pred_probs: Optional[np.ndarray] = None,
pred_probs_unlabeled: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, np.ndarray]:
"""Returns an ActiveLab quality score for each example in the dataset, to estimate which examples are most informative to (re)label next in active learning.
We consider settings where one example can be labeled by one or more annotators and some examples have no labels at all so far.
The score is in between 0 and 1, and can be used to prioritize what data to collect additional labels for.
Lower scores indicate examples whose true label we are least confident about based on the current data;
collecting additional labels for these low-scoring examples will be more informative than collecting labels for other examples.
To use an annotation budget most efficiently, select a batch of examples with the lowest scores and collect one additional label for each example,
and repeat this process after retraining your classifier.
You can use this function to get active learning scores for: examples that already have one or more labels (specify ``labels_multiannotator`` and ``pred_probs``
as arguments), or for unlabeled examples (specify ``pred_probs_unlabeled``), or for both types of examples (specify all of the above arguments).
To analyze a fixed dataset labeled by multiple annotators rather than collecting additional labels, try the
:py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>` (CROWDLAB) function instead.
Parameters
----------
labels_multiannotator : pd.DataFrame of np.ndarray, optional
2D pandas DataFrame or array of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators. Note that this function also works with
datasets where there is only one annotator (M=1).
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
Note that examples that have no annotator labels should not be included in this DataFrame/array.
This argument is optional if ``pred_probs`` is not provided (you might only provide ``pred_probs_unlabeled`` to only get active learning scores for the unlabeled examples).
pred_probs : np.ndarray, optional
An array of shape ``(N, K)`` of predicted class probabilities from a trained classifier model.
Predicted probabilities in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
This argument is optional if you only want to get active learning scores for unlabeled examples (specify only ``pred_probs_unlabeled`` instead).
pred_probs_unlabeled : np.ndarray, optional
An array of shape ``(N, K)`` of predicted class probabilities from a trained classifier model for examples that have no annotator labels.
Predicted probabilities in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
This argument is optional if you only want to get active learning scores for already-labeled examples (specify only ``pred_probs`` instead).
Returns
-------
active_learning_scores : np.ndarray
Array of shape ``(N,)`` indicating the ActiveLab quality scores for each example.
This array is empty if no already-labeled data was provided via ``labels_multiannotator``.
Examples with the lowest scores are those we should label next in order to maximally improve our classifier model.
active_learning_scores_unlabeled : np.ndarray
Array of shape ``(N,)`` indicating the active learning quality scores for each unlabeled example.
Returns an empty array if no unlabeled data is provided.
Examples with the lowest scores are those we should label next in order to maximally improve our classifier model
(scores for unlabeled data are directly comparable with the `active_learning_scores` for labeled data).
"""
assert_valid_pred_probs(pred_probs=pred_probs, pred_probs_unlabeled=pred_probs_unlabeled)
# compute multiannotator stats if labeled data is provided
if pred_probs is not None:
if labels_multiannotator is None:
raise ValueError(
"labels_multiannotator cannot be None when passing in pred_probs. ",
"Either provide labels_multiannotator to obtain active learning scores for the labeled examples, "
"or just pass in pred_probs_unlabeled to get active learning scores for unlabeled examples.",
)
if isinstance(labels_multiannotator, np.ndarray):
labels_multiannotator = pd.DataFrame(labels_multiannotator)
num_classes = get_num_classes(pred_probs=pred_probs)
# if all examples are only labeled by a single annotator
if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all():
optimal_temp = 1.0 # do not temp scale for single annotator case, temperature is defined here for later use
assert_valid_inputs_multiannotator(
labels_multiannotator, pred_probs, allow_single_label=True
)
consensus_label = get_majority_vote_label(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
verbose=False,
)
quality_of_consensus_labeled = get_label_quality_scores(consensus_label, pred_probs)
model_weight = 1
annotator_weight = np.full(labels_multiannotator.shape[1], 1)
avg_annotator_weight = np.mean(annotator_weight)
# examples are annotated by multiple annotators
else:
optimal_temp = find_best_temp_scaler(labels_multiannotator, pred_probs)
pred_probs = temp_scale_pred_probs(pred_probs, optimal_temp)
multiannotator_info = get_label_quality_multiannotator(
labels_multiannotator,
pred_probs,
return_annotator_stats=False,
return_detailed_quality=False,
return_weights=True,
)
quality_of_consensus_labeled = multiannotator_info["label_quality"][
"consensus_quality_score"
]
model_weight = multiannotator_info["model_weight"]
annotator_weight = multiannotator_info["annotator_weight"]
avg_annotator_weight = np.mean(annotator_weight)
# compute scores for labeled data
active_learning_scores = np.full(len(labels_multiannotator), np.nan)
for i in range(len(active_learning_scores)):
annotator_labels = labels_multiannotator.iloc[i]
active_learning_scores[i] = np.average(
(quality_of_consensus_labeled[i], 1 / num_classes),
weights=(
np.sum(annotator_weight[annotator_labels.notna()]) + model_weight,
avg_annotator_weight,
),
)
# no labeled data provided so do not estimate temperature and model/annotator weights
elif pred_probs_unlabeled is not None:
num_classes = get_num_classes(pred_probs=pred_probs_unlabeled)
optimal_temp = 1
model_weight = 1
avg_annotator_weight = 1
active_learning_scores = np.array([])
else:
raise ValueError(
"pred_probs and pred_probs_unlabeled cannot both be None, specify at least one of the two."
)
# compute scores for unlabeled data
if pred_probs_unlabeled is not None:
pred_probs_unlabeled = temp_scale_pred_probs(pred_probs_unlabeled, optimal_temp)
quality_of_consensus_unlabeled = np.max(pred_probs_unlabeled, axis=1)
active_learning_scores_unlabeled = np.average(
np.stack(
[
quality_of_consensus_unlabeled,
np.full(len(quality_of_consensus_unlabeled), 1 / num_classes),
]
),
weights=[model_weight, avg_annotator_weight],
axis=0,
)
else:
active_learning_scores_unlabeled = np.array([])
return active_learning_scores, active_learning_scores_unlabeled
[docs]def get_active_learning_scores_ensemble(
labels_multiannotator: Optional[Union[pd.DataFrame, np.ndarray]] = None,
pred_probs: Optional[np.ndarray] = None,
pred_probs_unlabeled: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, np.ndarray]:
"""Returns an ActiveLab quality score for each example in the dataset, based on predictions from an ensemble of models.
This function is similar to :py:func:`get_active_learning_scores <cleanlab.multiannotator.get_active_learning_scores>` but allows for an
ensemble of multiple classifier models to be trained and will aggregate predictions from the models to compute the ActiveLab quality score.
Parameters
----------
labels_multiannotator : pd.DataFrame or np.ndarray
Multiannotator labels in the same format expected by :py:func:`get_active_learning_scores <cleanlab.multiannotator.get_active_learning_scores>`.
This argument is optional if ``pred_probs`` is not provided (in cases where you only provide ``pred_probs_unlabeled`` to get active learning scores for unlabeled examples).
pred_probs : np.ndarray
An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from the ensemble models.
Note that this function also works with datasets where there is only one annotator (M=1).
Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
This argument is optional if you only want to get active learning scores for unlabeled examples (pass in ``pred_probs_unlabeled`` instead).
pred_probs_unlabeled : np.ndarray, optional
An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from a trained classifier model
for examples that have no annotated labels so far (but which we may want to label in the future, and hence compute active learning quality scores for).
Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
This argument is optional if you only want to get active learning scores for labeled examples (pass in ``pred_probs`` instead).
Returns
-------
active_learning_scores : np.ndarray
Similar to output as :py:func:`get_label_quality_scores <cleanlab.multiannotator.get_label_quality_scores>`.
active_learning_scores_unlabeled : np.ndarray
Similar to output as :py:func:`get_label_quality_scores <cleanlab.multiannotator.get_label_quality_scores>`.
See Also
--------
get_active_learning_scores
"""
assert_valid_pred_probs(
pred_probs=pred_probs, pred_probs_unlabeled=pred_probs_unlabeled, ensemble=True
)
# compute multiannotator stats if labeled data is provided
if pred_probs is not None:
if labels_multiannotator is None:
raise ValueError(
"labels_multiannotator cannot be None when passing in pred_probs. ",
"You can either provide labels_multiannotator to obtain active learning scores for the labeled examples, "
"or just pass in pred_probs_unlabeled to get active learning scores for unlabeled examples.",
)
if isinstance(labels_multiannotator, np.ndarray):
labels_multiannotator = pd.DataFrame(labels_multiannotator)
num_classes = get_num_classes(pred_probs=pred_probs[0])
# if all examples are only labeled by a single annotator
if labels_multiannotator.apply(lambda s: len(s.dropna()) == 1, axis=1).all():
# do not temp scale for single annotator case, temperature is defined here for later use
optimal_temp = np.full(len(pred_probs), 1.0)
assert_valid_inputs_multiannotator(
labels_multiannotator, pred_probs, ensemble=True, allow_single_label=True
)
avg_pred_probs = np.mean(pred_probs, axis=0)
consensus_label = get_majority_vote_label(
labels_multiannotator=labels_multiannotator,
pred_probs=avg_pred_probs,
verbose=False,
)
quality_of_consensus_labeled = get_label_quality_scores(consensus_label, avg_pred_probs)
model_weight = np.full(len(pred_probs), 1)
annotator_weight = np.full(labels_multiannotator.shape[1], 1)
avg_annotator_weight = np.mean(annotator_weight)
# examples are annotated by multiple annotators
else:
optimal_temp = np.full(len(pred_probs), np.NaN)
for i in range(len(pred_probs)):
curr_pred_probs = pred_probs[i]
curr_optimal_temp = find_best_temp_scaler(labels_multiannotator, curr_pred_probs)
pred_probs[i] = temp_scale_pred_probs(curr_pred_probs, curr_optimal_temp)
optimal_temp[i] = curr_optimal_temp
multiannotator_info = get_label_quality_multiannotator_ensemble(
labels_multiannotator,
pred_probs,
return_annotator_stats=False,
return_detailed_quality=False,
return_weights=True,
)
quality_of_consensus_labeled = multiannotator_info["label_quality"][
"consensus_quality_score"
]
model_weight = multiannotator_info["model_weight"]
annotator_weight = multiannotator_info["annotator_weight"]
avg_annotator_weight = np.mean(annotator_weight)
# compute scores for labeled data
active_learning_scores = np.full(len(labels_multiannotator), np.nan)
for i in range(len(active_learning_scores)):
annotator_labels = labels_multiannotator.iloc[i]
active_learning_scores[i] = np.average(
(quality_of_consensus_labeled[i], 1 / num_classes),
weights=(
np.sum(annotator_weight[annotator_labels.notna()]) + np.sum(model_weight),
avg_annotator_weight,
),
)
# no labeled data provided so do not estimate temperature and model/annotator weights
elif pred_probs_unlabeled is not None:
num_classes = get_num_classes(pred_probs=pred_probs_unlabeled[0])
optimal_temp = np.full(len(pred_probs_unlabeled), 1.0)
model_weight = np.full(len(pred_probs_unlabeled), 1)
avg_annotator_weight = 1
active_learning_scores = np.array([])
else:
raise ValueError(
"pred_probs and pred_probs_unlabeled cannot both be None, specify at least one of the two."
)
# compute scores for unlabeled data
if pred_probs_unlabeled is not None:
for i in range(len(pred_probs_unlabeled)):
pred_probs_unlabeled[i] = temp_scale_pred_probs(
pred_probs_unlabeled[i], optimal_temp[i]
)
avg_pred_probs_unlabeled = np.mean(pred_probs_unlabeled, axis=0)
consensus_label_unlabeled = get_majority_vote_label(
np.argmax(pred_probs_unlabeled, axis=2).T,
avg_pred_probs_unlabeled,
)
modified_pred_probs_unlabeled = np.average(
np.concatenate(
(
pred_probs_unlabeled,
np.full(pred_probs_unlabeled.shape[1:], 1 / num_classes)[np.newaxis, :, :],
)
),
weights=np.concatenate((model_weight, np.array([avg_annotator_weight]))),
axis=0,
)
active_learning_scores_unlabeled = get_label_quality_scores(
consensus_label_unlabeled, modified_pred_probs_unlabeled
)
else:
active_learning_scores_unlabeled = np.array([])
return active_learning_scores, active_learning_scores_unlabeled
[docs]def get_majority_vote_label(
labels_multiannotator: Union[pd.DataFrame, np.ndarray],
pred_probs: Optional[np.ndarray] = None,
verbose: bool = True,
) -> np.ndarray:
"""Returns the majority vote label for each example, aggregated from the labels given by multiple annotators.
Parameters
----------
labels_multiannotator : pd.DataFrame or np.ndarray
2D pandas DataFrame or array of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
pred_probs : np.ndarray, optional
An array of shape ``(N, K)`` of model-predicted probabilities, ``P(label=k|x)``.
For details, predicted probabilities in the same format expected by :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
verbose : bool, optional
Important warnings and other printed statements may be suppressed if ``verbose`` is set to ``False``.
Returns
-------
consensus_label: np.ndarray
An array of shape ``(N,)`` with the majority vote label aggregated from all annotators.
In the event of majority vote ties, ties are broken in the following order:
using the model ``pred_probs`` (if provided) and selecting the class with highest predicted probability,
using the empirical class frequencies and selecting the class with highest frequency,
using an initial annotator quality score and selecting the class that has been labeled by annotators with higher quality,
and lastly by random selection.
"""
if isinstance(labels_multiannotator, np.ndarray):
labels_multiannotator = pd.DataFrame(labels_multiannotator)
if verbose:
assert_valid_inputs_multiannotator(
labels_multiannotator, pred_probs, allow_single_label=True
)
majority_vote_label = np.full(len(labels_multiannotator), np.nan)
mode_labels_multiannotator = labels_multiannotator.mode(axis=1)
nontied_idx = []
tied_idx = dict()
# obtaining consensus using annotator majority vote
for idx in range(len(mode_labels_multiannotator)):
label_mode = mode_labels_multiannotator.iloc[idx].dropna().astype(int).to_numpy()
if len(label_mode) == 1:
majority_vote_label[idx] = label_mode[0]
nontied_idx.append(idx)
else:
tied_idx[idx] = label_mode
# tiebreak 1: using pred_probs (if provided)
if pred_probs is not None and len(tied_idx) > 0:
for idx, label_mode in tied_idx.copy().items():
max_pred_probs = np.where(
pred_probs[idx, label_mode] == np.max(pred_probs[idx, label_mode])
)[0]
if len(max_pred_probs) == 1:
majority_vote_label[idx] = label_mode[max_pred_probs[0]]
del tied_idx[idx]
else:
tied_idx[idx] = label_mode[max_pred_probs]
# tiebreak 2: using empirical class frequencies
# current tiebreak will select the minority class (to prevent larger class imbalance)
if len(tied_idx) > 0:
if pred_probs is not None:
num_classes = pred_probs.shape[1]
else:
num_classes = int(
np.nanmax(labels_multiannotator.replace({pd.NA: np.NaN}).astype(float).values) + 1
)
class_frequencies = labels_multiannotator.apply(
lambda s: pd.Series(np.bincount(s[s.notna()], minlength=num_classes)), axis=1
).sum()
for idx, label_mode in tied_idx.copy().items():
min_frequency = np.where(
class_frequencies[label_mode] == np.min(class_frequencies[label_mode])
)[0]
if len(min_frequency) == 1:
majority_vote_label[idx] = label_mode[min_frequency[0]]
del tied_idx[idx]
else:
tied_idx[idx] = label_mode[min_frequency]
# tiebreak 3: using initial annotator quality scores
if len(tied_idx) > 0:
nontied_majority_vote_label = majority_vote_label[nontied_idx]
nontied_labels_multiannotator = labels_multiannotator.iloc[nontied_idx]
annotator_agreement_with_consensus = nontied_labels_multiannotator.apply(
lambda s: np.mean(s[pd.notna(s)] == nontied_majority_vote_label[pd.notna(s)]),
axis=0,
)
# impute average annotator accuracy for any annotator that do not overlap with consensus
mask = annotator_agreement_with_consensus.isna()
avg_annotator_agreement = np.mean(annotator_agreement_with_consensus[~mask])
annotator_agreement_with_consensus[mask] = avg_annotator_agreement
for idx, label_mode in tied_idx.copy().items():
label_quality_score = np.array(
[
np.mean(
annotator_agreement_with_consensus[
labels_multiannotator.iloc[idx][
labels_multiannotator.iloc[idx] == label
].index.values
]
)
for label in label_mode
]
)
max_score = np.where(label_quality_score == label_quality_score.max())[0]
if len(max_score) == 1:
majority_vote_label[idx] = label_mode[max_score[0]]
del tied_idx[idx]
else:
tied_idx[idx] = label_mode[max_score]
# if still tied, break by random selection
if len(tied_idx) > 0:
warnings.warn(
f"breaking ties of examples {list(tied_idx.keys())} by random selection, you may want to set seed for reproducability"
)
for idx, label_mode in tied_idx.items():
majority_vote_label[idx] = np.random.choice(label_mode)
if verbose:
# check if any classes no longer appear in the set of consensus labels
check_consensus_label_classes(
labels_multiannotator=labels_multiannotator,
consensus_label=majority_vote_label,
consensus_method="majority_vote",
)
return majority_vote_label.astype("int64")
[docs]def convert_long_to_wide_dataset(
labels_multiannotator_long: pd.DataFrame,
) -> pd.DataFrame:
"""Converts a long format dataset to wide format which is suitable for passing into
:py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
Dataframe must contain three columns named:
#. ``task`` representing each example labeled by the annotators
#. ``annotator`` representing each annotator
#. ``label`` representing the label given by an annotator for the corresponding task (i.e. example)
Parameters
----------
labels_multiannotator_long : pd.DataFrame
pandas DataFrame in long format with three columns named ``task``, ``annotator`` and ``label``
Returns
-------
labels_multiannotator_wide : pd.DataFrame
pandas DataFrame of the proper format to be passed as ``labels_multiannotator`` for the other ``cleanlab.multiannotator`` functions.
"""
labels_multiannotator_wide = labels_multiannotator_long.pivot(
index="task", columns="annotator", values="label"
)
labels_multiannotator_wide.index.name = None
labels_multiannotator_wide.columns.name = None
return labels_multiannotator_wide
def _get_consensus_stats(
labels_multiannotator: pd.DataFrame,
pred_probs: np.ndarray,
num_annotations: np.ndarray,
consensus_label: np.ndarray,
quality_method: str = "crowdlab",
verbose: bool = True,
ensemble: bool = False,
label_quality_score_kwargs: dict = {},
) -> tuple:
"""Returns a tuple containing the consensus labels, annotator agreement scores, and quality of consensus
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
pred_probs : np.ndarray
An array of shape ``(N, K)`` of model-predicted probabilities, ``P(label=k|x)``.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the consensus label.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
label_quality_score_kwargs : dict, optional
Keyword arguments to pass into ``get_label_quality_scores()``.
verbose : bool, default = True
Certain warnings and notes will be printed if ``verbose`` is set to ``True``.
ensemble : bool, default = False
Boolean flag to indicate whether the pred_probs passed are from ensemble models.
Returns
------
stats : tuple
A tuple of (consensus_label, annotator_agreement, consensus_quality_score, post_pred_probs).
"""
# compute the fraction of annotator agreeing with the consensus labels
annotator_agreement = _get_annotator_agreement_with_consensus(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
)
# compute posterior predicted probabilites
if ensemble:
post_pred_probs, model_weight, annotator_weight = _get_post_pred_probs_and_weights_ensemble(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
prior_pred_probs=pred_probs,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
quality_method=quality_method,
verbose=verbose,
)
else:
post_pred_probs, model_weight, annotator_weight = _get_post_pred_probs_and_weights(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
prior_pred_probs=pred_probs,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
quality_method=quality_method,
verbose=verbose,
)
# compute quality of the consensus labels
consensus_quality_score = _get_consensus_quality_score(
consensus_label=consensus_label,
pred_probs=post_pred_probs,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
quality_method=quality_method,
label_quality_score_kwargs=label_quality_score_kwargs,
)
return (
annotator_agreement,
consensus_quality_score,
post_pred_probs,
model_weight,
annotator_weight,
)
def _get_annotator_stats(
labels_multiannotator: pd.DataFrame,
pred_probs: np.ndarray,
consensus_label: np.ndarray,
num_annotations: np.ndarray,
annotator_agreement: np.ndarray,
model_weight: np.ndarray,
annotator_weight: np.ndarray,
consensus_quality_score: np.ndarray,
quality_method: str = "crowdlab",
) -> pd.DataFrame:
"""Returns a dictionary containing overall statistics about each annotator.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
pred_probs : np.ndarray
An array of shape ``(N, K)`` of model-predicted probabilities, ``P(label=k|x)``.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
model_weight : float
float specifying the model weight used in weighted averages,
None if model weight is not used to compute quality scores
annotator_weight : np.ndarray
An array of shape ``(M,)`` where M is the number of annotators, specifying the annotator weights used in weighted averages,
None if annotator weights are not used to compute quality scores
consensus_quality_score : np.ndarray
An array of shape ``(N,)`` with the quality score of the consensus.
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the consensus label.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
Returns
-------
annotator_stats : pd.DataFrame
Overall statistics about each annotator.
For details, see the documentation of :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
"""
annotator_quality = _get_annotator_quality(
labels_multiannotator=labels_multiannotator,
pred_probs=pred_probs,
consensus_label=consensus_label,
num_annotations=num_annotations,
annotator_agreement=annotator_agreement,
model_weight=model_weight,
annotator_weight=annotator_weight,
quality_method=quality_method,
)
# Compute the number of labels labeled/ by each annotator
num_examples_labeled = labels_multiannotator.count()
# Compute the fraction of labels annotated by each annotator that agrees with the consensus label
# TODO: check if we should drop singleton labels here
agreement_with_consensus = labels_multiannotator.apply(
lambda s: np.mean(s[pd.notna(s)] == consensus_label[pd.notna(s)]),
axis=0,
).to_numpy()
# Find the worst labeled class for each annotator
worst_class = _get_annotator_worst_class(
labels_multiannotator=labels_multiannotator,
consensus_label=consensus_label,
consensus_quality_score=consensus_quality_score,
)
# Create multi-annotator stats DataFrame from its columns
annotator_stats = pd.DataFrame(
{
"annotator_quality": annotator_quality,
"agreement_with_consensus": agreement_with_consensus,
"worst_class": worst_class,
"num_examples_labeled": num_examples_labeled,
}
)
return annotator_stats.sort_values(by=["annotator_quality", "agreement_with_consensus"])
def _get_annotator_agreement_with_consensus(
labels_multiannotator: pd.DataFrame,
consensus_label: np.ndarray,
) -> np.ndarray:
"""Returns the fractions of annotators that agree with the consensus label per example. Note that the
fraction for each example only considers the annotators that labeled that particular example.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
Returns
-------
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
"""
annotator_agreement = labels_multiannotator.assign(consensus_label=consensus_label).apply(
lambda s: np.mean(s.drop("consensus_label").dropna() == s["consensus_label"]),
axis=1,
)
return annotator_agreement.to_numpy()
def _get_annotator_agreement_with_annotators(
labels_multiannotator: pd.DataFrame,
num_annotations: np.ndarray,
verbose: bool = True,
) -> np.ndarray:
"""Returns the average agreement of each annotator with other annotators that label the same example.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
verbose : bool, default = True
Certain warnings and notes will be printed if ``verbose`` is set to ``True``.
Returns
-------
annotator_agreement : np.ndarray
An array of shape ``(M,)`` where M is the number of annotators, with the agreement of each annotator with other
annotators that labeled the same examples.
"""
def get_single_annotator_agreement(
labels_multiannotator: pd.DataFrame,
num_annotations: np.ndarray,
annotator_id: Union[int, str],
):
annotator_agreement_per_example = labels_multiannotator.apply(
lambda s: np.mean(s[pd.notna(s)].drop(annotator_id) == s[annotator_id]), axis=1
)
np.nan_to_num(annotator_agreement_per_example, copy=False, nan=0)
try:
annotator_agreement = np.average(
annotator_agreement_per_example, weights=num_annotations - 1
)
except:
annotator_agreement = np.NaN
return annotator_agreement
annotator_agreement_with_annotators = labels_multiannotator.apply(
lambda s: get_single_annotator_agreement(
labels_multiannotator[pd.notna(s)], num_annotations[pd.notna(s)], s.name
)
)
# impute average annotator accuracy for any annotator that do not overlap with other annotators
mask = annotator_agreement_with_annotators.isna()
if np.sum(mask) > 0:
if verbose:
print(
f"Annotator(s) {annotator_agreement_with_annotators[mask].index.values} did not annotate any examples that overlap with other annotators, \
\nusing the average annotator agreeement among other annotators as this annotator's agreement."
)
avg_annotator_agreement = np.mean(annotator_agreement_with_annotators[~mask])
annotator_agreement_with_annotators[mask] = avg_annotator_agreement
return annotator_agreement_with_annotators.to_numpy()
def _get_post_pred_probs_and_weights(
labels_multiannotator: pd.DataFrame,
consensus_label: np.ndarray,
prior_pred_probs: np.ndarray,
num_annotations: np.ndarray,
annotator_agreement: np.ndarray,
quality_method: str = "crowdlab",
verbose: bool = True,
) -> Tuple[np.ndarray, Any, Any]:
"""Return the posterior predicted probabilities of each example given a specified quality method.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
prior_pred_probs : np.ndarray
An array of shape ``(N, K)`` of prior predicted probabilities, ``P(label=k|x)``, usually the out-of-sample predicted probability computed by a model.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the consensus label.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
verbose : bool, default = True
Certain warnings and notes will be printed if ``verbose`` is set to ``True``.
Returns
-------
post_pred_probs : np.ndarray
An array of shape ``(N, K)`` with the posterior predicted probabilities.
model_weight : float
float specifying the model weight used in weighted averages,
None if model weight is not used to compute quality scores
annotator_weight : np.ndarray
An array of shape ``(M,)`` where M is the number of annotators, specifying the annotator weights used in weighted averages,
None if annotator weights are not used to compute quality scores
"""
valid_methods = [
"crowdlab",
"agreement",
]
# setting dummy variables for model and annotator weights that will be returned
# only relevant for quality_method == crowdlab, return None for all other methods
return_model_weight = None
return_annotator_weight = None
if quality_method == "crowdlab":
num_classes = get_num_classes(pred_probs=prior_pred_probs)
likelihood = np.mean(
annotator_agreement[num_annotations != 1]
) # likelihood that any annotator will annotate the consensus label for any example
# subsetting the dataset to only includes examples with more than one annotation
mask = num_annotations != 1
consensus_label_subset = consensus_label[mask]
prior_pred_probs_subset = prior_pred_probs[mask]
# compute most likely class error
most_likely_class_error = np.clip(
np.mean(
consensus_label_subset
!= np.argmax(np.bincount(consensus_label_subset, minlength=num_classes))
),
a_min=CLIPPING_LOWER_BOUND,
a_max=None,
)
# compute adjusted annotator agreement (used as annotator weights)
annotator_agreement_with_annotators = _get_annotator_agreement_with_annotators(
labels_multiannotator, num_annotations, verbose
)
annotator_error = 1 - annotator_agreement_with_annotators
adjusted_annotator_agreement = np.clip(
1 - (annotator_error / most_likely_class_error), a_min=CLIPPING_LOWER_BOUND, a_max=None
)
# compute model weight
model_error = np.mean(np.argmax(prior_pred_probs_subset, axis=1) != consensus_label_subset)
model_weight = np.max(
[(1 - (model_error / most_likely_class_error)), CLIPPING_LOWER_BOUND]
) * np.sqrt(np.mean(num_annotations))
# compute weighted average
post_pred_probs = np.full(prior_pred_probs.shape, np.nan)
for i in range(len(labels_multiannotator)):
example_mask = labels_multiannotator.iloc[i].notna()
example = labels_multiannotator.iloc[i][example_mask]
post_pred_probs[i] = [
np.average(
[prior_pred_probs[i, true_label]]
+ [
likelihood
if annotator_label == true_label
else (1 - likelihood) / (num_classes - 1)
for annotator_label in example
],
weights=np.concatenate(
([model_weight], adjusted_annotator_agreement[example_mask])
),
)
for true_label in range(num_classes)
]
return_model_weight = model_weight
return_annotator_weight = adjusted_annotator_agreement
elif quality_method == "agreement":
num_classes = get_num_classes(pred_probs=prior_pred_probs)
label_counts = np.full((len(labels_multiannotator), num_classes), np.NaN)
for i in range(len(labels_multiannotator)):
s = labels_multiannotator.iloc[i]
label_counts[i, :] = value_counts(s.dropna(), num_classes=num_classes)
post_pred_probs = label_counts / num_annotations.reshape(-1, 1)
else:
raise ValueError(
f"""
{quality_method} is not a valid quality method!
Please choose a valid quality_method: {valid_methods}
"""
)
return post_pred_probs, return_model_weight, return_annotator_weight
def _get_post_pred_probs_and_weights_ensemble(
labels_multiannotator: pd.DataFrame,
consensus_label: np.ndarray,
prior_pred_probs: np.ndarray,
num_annotations: np.ndarray,
annotator_agreement: np.ndarray,
quality_method: str = "crowdlab",
verbose: bool = True,
) -> Tuple[np.ndarray, Any, Any]:
"""Return the posterior predicted class probabilites of each example given a specified quality method and prior predicted class probabilities from an ensemble of multiple classifier models.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(P, N, K)`` where P is the number of models, consisting of predicted class probabilities from the ensemble models.
Each set of predicted probabilities with shape ``(N, K)`` is in the same format expected by the :py:func:`get_label_quality_scores <cleanlab.rank.get_label_quality_scores>`.
prior_pred_probs : np.ndarray
An array of shape ``(N, K)`` of prior predicted probabilities, ``P(label=k|x)``, usually the out-of-sample predicted probability computed by a model.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the consensus label.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
verbose : bool, default = True
Certain warnings and notes will be printed if ``verbose`` is set to ``True``.
Returns
-------
post_pred_probs : np.ndarray
An array of shape ``(N, K)`` with the posterior predicted probabilities.
model_weight : np.ndarray
An array of shape ``(P,)`` where P is the number of models in this ensemble, specifying the model weight used in weighted averages,
``None`` if model weight is not used to compute quality scores
annotator_weight : np.ndarray
An array of shape ``(M,)`` where M is the number of annotators, specifying the annotator weights used in weighted averages,
``None`` if annotator weights are not used to compute quality scores
"""
num_classes = get_num_classes(pred_probs=prior_pred_probs[0])
likelihood = np.mean(
annotator_agreement[num_annotations != 1]
) # likelihood that any annotator will annotate the consensus label for any example
# subsetting the dataset to only includes examples with more than one annotation
mask = num_annotations != 1
consensus_label_subset = consensus_label[mask]
# compute most likely class error
most_likely_class_error = np.clip(
np.mean(
consensus_label_subset
!= np.argmax(np.bincount(consensus_label_subset, minlength=num_classes))
),
a_min=CLIPPING_LOWER_BOUND,
a_max=None,
)
# compute adjusted annotator agreement (used as annotator weights)
annotator_agreement_with_annotators = _get_annotator_agreement_with_annotators(
labels_multiannotator, num_annotations, verbose
)
annotator_error = 1 - annotator_agreement_with_annotators
adjusted_annotator_agreement = np.clip(
1 - (annotator_error / most_likely_class_error), a_min=CLIPPING_LOWER_BOUND, a_max=None
)
# compute model weight
model_weight = np.full(prior_pred_probs.shape[0], np.nan)
for idx in range(prior_pred_probs.shape[0]):
prior_pred_probs_subset = prior_pred_probs[idx][mask]
model_error = np.mean(np.argmax(prior_pred_probs_subset, axis=1) != consensus_label_subset)
model_weight[idx] = np.max(
[(1 - (model_error / most_likely_class_error)), CLIPPING_LOWER_BOUND]
) * np.sqrt(np.mean(num_annotations))
# compute weighted average
post_pred_probs = np.full(prior_pred_probs[0].shape, np.nan)
for i in range(len(labels_multiannotator)):
example_mask = labels_multiannotator.iloc[i].notna()
example = labels_multiannotator.iloc[i][example_mask]
post_pred_probs[i] = [
np.average(
[prior_pred_probs[ind][i, true_label] for ind in range(prior_pred_probs.shape[0])]
+ [
likelihood
if annotator_label == true_label
else (1 - likelihood) / (num_classes - 1)
for annotator_label in example
],
weights=np.concatenate((model_weight, adjusted_annotator_agreement[example_mask])),
)
for true_label in range(num_classes)
]
return_model_weight = model_weight
return_annotator_weight = adjusted_annotator_agreement
return post_pred_probs, return_model_weight, return_annotator_weight
def _get_consensus_quality_score(
consensus_label: np.ndarray,
pred_probs: np.ndarray,
num_annotations: np.ndarray,
annotator_agreement: np.ndarray,
quality_method: str = "crowdlab",
label_quality_score_kwargs: dict = {},
) -> np.ndarray:
"""Return scores representing quality of the consensus label for each example.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
pred_probs : np.ndarray
An array of shape ``(N, K)`` of posterior predicted probabilities, ``P(label=k|x)``.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the consensus label.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
Returns
-------
consensus_quality_score : np.ndarray
An array of shape ``(N,)`` with the quality score of the consensus.
"""
valid_methods = [
"crowdlab",
"agreement",
]
if quality_method == "crowdlab":
consensus_quality_score = get_label_quality_scores(
consensus_label, pred_probs, **label_quality_score_kwargs
)
elif quality_method == "agreement":
consensus_quality_score = annotator_agreement
else:
raise ValueError(
f"""
{quality_method} is not a valid consensus quality method!
Please choose a valid quality_method: {valid_methods}
"""
)
return consensus_quality_score
def _get_annotator_label_quality_score(
annotator_label: pd.Series,
pred_probs: np.ndarray,
label_quality_score_kwargs: dict = {},
) -> pd.Series:
"""Returns quality scores for each datapoint.
Very similar functionality as ``_get_consensus_quality_score`` with additional support for annotator labels that contain NaN values.
For more info about parameters and returns, see the docstring of :py:func:`_get_consensus_quality_score <cleanlab.multiannotator._get_consensus_quality_score>`.
"""
mask = pd.notna(annotator_label)
annotator_label_quality_score_subset = get_label_quality_scores(
labels=annotator_label[mask].to_numpy().astype("int64"),
pred_probs=pred_probs[mask],
**label_quality_score_kwargs,
)
annotator_label_quality_score = np.full(len(annotator_label), np.nan)
annotator_label_quality_score[mask] = annotator_label_quality_score_subset
return pd.Series(annotator_label_quality_score)
def _get_annotator_quality(
labels_multiannotator: pd.DataFrame,
pred_probs: np.ndarray,
consensus_label: np.ndarray,
num_annotations: np.ndarray,
annotator_agreement: np.ndarray,
model_weight: np.ndarray,
annotator_weight: np.ndarray,
quality_method: str = "crowdlab",
) -> pd.DataFrame:
"""Returns annotator quality score for each annotator.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
pred_probs : np.ndarray
An array of shape ``(N, K)`` of model-predicted probabilities, ``P(label=k|x)``.
For details, predicted probabilities in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
num_annotations : np.ndarray
An array of shape ``(N,)`` with the number of annotators that have labeled each example.
annotator_agreement : np.ndarray
An array of shape ``(N,)`` with the fraction of annotators that agree with each consensus label.
model_weight : float
An array of shape ``(P,)`` where P is the number of models in this ensemble, specifying the model weight used in weighted averages,
``None`` if model weight is not used to compute quality scores
annotator_weight : np.ndarray
An array of shape ``(M,)`` where M is the number of annotators, specifying the annotator weights used in weighted averages,
``None`` if annotator weights are not used to compute quality scores
quality_method : str, default = "crowdlab" (Options: ["crowdlab", "agreement"])
Specifies the method used to calculate the quality of the annotators.
For valid quality methods, view :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`
Returns
-------
annotator_quality : np.ndarray
Quality scores of a given annotator's labels
"""
valid_methods = [
"crowdlab",
"agreement",
]
if quality_method == "crowdlab":
annotator_lqs = labels_multiannotator.apply(
lambda s: np.mean(
get_label_quality_scores(s[pd.notna(s)].astype("int64"), pred_probs[pd.notna(s)])
)
)
mask = num_annotations != 1
labels_multiannotator_subset = labels_multiannotator[mask]
consensus_label_subset = consensus_label[mask]
annotator_agreement = labels_multiannotator_subset.apply(
lambda s: np.mean(s[pd.notna(s)] == consensus_label_subset[pd.notna(s)]),
axis=0,
)
avg_num_annotations_frac = np.mean(num_annotations) / len(annotator_weight)
annotator_weight_adjusted = np.sum(annotator_weight) * avg_num_annotations_frac
w = model_weight / (model_weight + annotator_weight_adjusted)
annotator_quality = w * annotator_lqs + (1 - w) * annotator_agreement
elif quality_method == "agreement":
mask = num_annotations != 1
labels_multiannotator_subset = labels_multiannotator[mask]
consensus_label_subset = consensus_label[mask]
annotator_quality = labels_multiannotator_subset.apply(
lambda s: np.mean(s[pd.notna(s)] == consensus_label_subset[pd.notna(s)]),
axis=0,
)
else:
raise ValueError(
f"""
{quality_method} is not a valid annotator quality method!
Please choose a valid quality_method: {valid_methods}
"""
)
return annotator_quality
def _get_annotator_worst_class(
labels_multiannotator: pd.DataFrame,
consensus_label: np.ndarray,
consensus_quality_score: np.ndarray,
) -> np.ndarray:
"""Returns the class which each annotator makes the most errors in.
Parameters
----------
labels_multiannotator : pd.DataFrame
2D pandas DataFrame of multiple given labels for each example with shape ``(N, M)``,
where N is the number of examples and M is the number of annotators.
For more details, labels in the same format expected by the :py:func:`get_label_quality_multiannotator <cleanlab.multiannotator.get_label_quality_multiannotator>`.
consensus_label : np.ndarray
An array of shape ``(N,)`` with the consensus labels aggregated from all annotators.
consensus_quality_score : np.ndarray
An array of shape ``(N,)`` with the quality score of the consensus.
Returns
-------
worst_class : np.ndarray
The class that is most frequently mislabeled by a given annotator
"""
def get_single_annotator_worst_class(s, consensus_quality_score):
mask = pd.notna(s)
class_accuracies = (s[mask] == consensus_label[mask]).groupby(s).mean()
accuracy_min_idx = class_accuracies[class_accuracies == class_accuracies.min()].index.values
if len(accuracy_min_idx) == 1:
return accuracy_min_idx[0]
# tiebreak 1: class counts
class_count = s[mask].groupby(s).count()[accuracy_min_idx]
count_max_idx = class_count[class_count == class_count.max()].index.values
if len(count_max_idx) == 1:
return count_max_idx[0]
# tiebreak 2: consensus quality scores
avg_consensus_quality = (
pd.DataFrame(
{"annotator_label": s, "consensus_quality_score": consensus_quality_score}
)[mask]
.groupby("annotator_label")
.mean()["consensus_quality_score"][count_max_idx]
)
quality_max_ind = avg_consensus_quality[
avg_consensus_quality == avg_consensus_quality.max()
].index.values
# return first item even if there are ties - no better methods to tiebreak
return quality_max_ind[0]
worst_class = (
labels_multiannotator.apply(
lambda s: get_single_annotator_worst_class(s, consensus_quality_score)
)
.to_numpy()
.astype("int64")
)
return worst_class