예제 #1
0
def test_fit_error():
    """Test fit method errors out."""
    # case: s array not the same length as y array
    with pytest.raises(ValueError):
        Relabeller().fit(create_linear_X(), create_y(), np.array([1, 0, 0, 1]))
    # case: y targets are not a binary variable
    with pytest.raises(TypeError):
        targets = create_y()
        targets[0] = 100
        Relabeller().fit_transform(create_linear_X(), targets, create_s())
    # case: s protected classes are not a binary variable
    with pytest.raises(TypeError):
        s_classes = create_y()
        s_classes[0] = 100
        Relabeller().fit_transform(create_linear_X(), targets, s_classes)
예제 #2
0
def test_multiple_ro_clf_fit_predict_demote_true():
    """Test fit and predict methods when demote is True.

    The create_linear_X, create_y, and create_Y functions (see conftest.py)
    are set up such a linear classifier can find a neat decision boundary to
    perfectly classify examples.
    """
    X = create_linear_X()
    y = create_y()
    s = create_s()
    roc_clf = MultipleROClassifier(theta=0.2)
    roc_clf.fit(X, y)
    raw_pred_proba = roc_clf._raw_predict_proba(X, s)
    raw_pred = (raw_pred_proba[:, 1] > DECISION_THRESHOLD).astype(int)
    pred_proba = roc_clf.predict_proba(X, s)
    pred = roc_clf.predict(X, s)

    # all raw predictions should perfectly classify
    assert (raw_pred == y).all()
    assert (pred == (pred_proba[:, 1] > DECISION_THRESHOLD)).all()

    # probabilities should be the weighted mean of probabilities from the
    # specified estimators in the ensemble
    expected_probs = np.zeros_like(y, dtype="float64")
    for e, w in zip(roc_clf.estimators_, roc_clf.pred_weights_):
        expected_probs += e.predict_proba(X)[:, 1] * w
    expected_probs = expected_probs / roc_clf.pred_weights_.sum()
    assert (raw_pred_proba[:, 1] == expected_probs).all()
예제 #3
0
def test_single_ro_clf_fit_predict_demote_false():
    """Test fit and predict methods when demote is False.

    When demote=False, only disadvantage group examples should be promoted
    (i.e. no advantaged group demotions).
    """
    X = create_linear_X()
    y = create_y()
    s = create_s()

    roc_clf = SingleROClassifier(theta=0.2, demote=False)
    roc_clf.fit(X, y)
    raw_pred_proba = roc_clf._raw_predict_proba(X, s)
    raw_pred = (raw_pred_proba[:, 1] > DECISION_THRESHOLD).astype(int)
    pred_proba = roc_clf.predict_proba(X, s)
    pred = roc_clf.predict(X, s)
    # all raw predictions should perfectly classify
    assert (raw_pred == y).all()
    assert (pred == (pred_proba[:, 1] > DECISION_THRESHOLD)).all()

    # with the theta critical region threshold of 0.2, and demote=False,
    # only the disadvantaged observation will be promoted.
    midpoint = (X.shape[0] - 1) / 2.0
    promote_index = int(math.floor(midpoint))
    other_index = [i for i in range(X.shape[0]) if i not in [promote_index]]
    # check correctly flipped predicted labels
    assert pred[promote_index] == (1 - raw_pred[promote_index])
    assert (pred[other_index] == raw_pred[other_index]).all()
    # check correctly flipped predicted probability
    assert pred_proba[promote_index, 1] == \
        (1 - raw_pred_proba[promote_index, 1])
    assert (pred[other_index] == raw_pred[other_index]).all()
예제 #4
0
def test_fit_transform_error():
    """Test fit_transform method errors out.

    ValueError should occur when X input to transform method is not the same
    as the X input to fit method.
    """
    X_input = create_linear_X()
    with pytest.raises(ValueError):
        Relabeller().fit(X_input, create_y(), create_s()).transform(X_input.T)
예제 #5
0
def test_predict_value_error(random_X_data):
    """Raise ValueError if X doesn't have expected number of variables."""
    X = create_random_X(random_X_data)
    s = create_s()
    lin_acf = counterfactually_fair_models.LinearACFClassifier()
    lin_acf.fit(X, create_y(), s)
    with pytest.raises(ValueError):
        # pass in just a subset of the input variables
        lin_acf.predict(X[:, 5], s)
        lin_acf.predict_proba(X[:, 5], s)
예제 #6
0
def test_fit_predict(random_X_data):
    """Test happy path of LinearACFClassifier `fit` and `predict` methods."""
    X = create_random_X(random_X_data)
    y = create_y()
    s = create_s()
    for residual_type in ["pearson", "deviance", "absolute"]:
        lin_acf = counterfactually_fair_models.LinearACFClassifier(
            binary_residual_type=residual_type)
        lin_acf.fit(X, y, s)
        lin_acf_pred_proba = lin_acf.predict_proba(X, s)[:, 1]
        assert (
            lin_acf.fit_residuals_ == lin_acf._compute_residuals_on_predict(
                X, s)).all()
        assert is_binary(lin_acf.predict(X, s))
        assert is_continuous(lin_acf_pred_proba)
        assert max(lin_acf_pred_proba) < 1
        assert min(lin_acf_pred_proba) > 0
예제 #7
0
def test_relabeller_fit():
    """Test that relabeller fitting """
    relabeller = Relabeller()
    X_input = create_linear_X()
    targets = create_y()
    protected_class = create_s()
    # The formula to determine how many observations to promote/demote is
    # the number needed to make the proportion of positive labels equal
    # between the two groups. This proportion is rounded up.
    expected_n = 3
    # Given data specified in X function, the default LogisticRegression
    # estimator should be able to draw a perfect decision boundary to seperate
    # the y target.
    relabeller.fit(X_input, targets, protected_class)
    assert relabeller.n_relabels_ == expected_n
    assert (relabeller.X_ == X_input).all()
    assert (relabeller.y_ == targets).all()
    assert (relabeller.s_ == protected_class).all()
예제 #8
0
def test_binary_single_class(random_X_data):
    """Linear ACF can handle training data with single-valued column."""
    X = create_random_X(random_X_data)
    X = np.concatenate([X, np.ones((X.shape[0], 1))], axis=1)
    s = create_s()
    y = create_y()
    lin_acf = counterfactually_fair_models.LinearACFClassifier()
    for residual_type in ["pearson", "deviance", "absolute"]:
        lin_acf = counterfactually_fair_models.LinearACFClassifier(
            binary_residual_type=residual_type)
        lin_acf.fit(X, y, s)
        lin_acf_pred_proba = lin_acf.predict_proba(X, s)[:, 1]
        assert (
            lin_acf.fit_residuals_ == lin_acf._compute_residuals_on_predict(
                X, s)).all()
        assert is_binary(lin_acf.predict(X, s))
        assert is_continuous(lin_acf_pred_proba)
        assert max(lin_acf_pred_proba) < 1
        assert min(lin_acf_pred_proba) > 0
예제 #9
0
def test_single_ro_clf_fit_predict_demote_true():
    """Test fit and predict methods when demote is True.

    The create_linear_X, create_y, and create_Y functions (see conftest.py)
    are set up such a linear classifier can find a neat decision boundary to
    perfectly classify examples.
    """
    X = create_linear_X()
    y = create_y()
    s = create_s()
    roc_clf = SingleROClassifier(theta=0.2)
    roc_clf.fit(X, y)
    raw_pred_proba = roc_clf._raw_predict_proba(X, s)
    raw_pred = (raw_pred_proba[:, 1] > DECISION_THRESHOLD).astype(int)
    pred_proba = roc_clf.predict_proba(X, s)
    pred = roc_clf.predict(X, s)
    # all raw predictions should perfectly classify
    assert (raw_pred == y).all()
    assert (pred == (pred_proba[:, 1] > DECISION_THRESHOLD)).all()

    # with the theta critical region threshold of 0.2, the middle two
    # observations in create_linear_X should be flipped in label.
    midpoint = (X.shape[0] - 1) / 2.0
    promote_index = int(math.floor(midpoint))
    demote_index = int(math.ceil(midpoint))
    other_index = [
        i for i in range(X.shape[0]) if i not in [promote_index, demote_index]
    ]
    # check correctly flipped predicted labels
    assert pred[promote_index] == (1 - raw_pred[promote_index])
    assert pred[demote_index] == (1 - raw_pred[demote_index])
    assert (pred[other_index] == raw_pred[other_index]).all()
    # check correctly flipped predicted probability
    assert pred_proba[promote_index, 1] == \
        (1 - raw_pred_proba[promote_index, 1])
    assert pred_proba[demote_index, 1] == (1 - raw_pred_proba[demote_index, 1])
    assert (pred[other_index] == raw_pred[other_index]).all()
예제 #10
0
def test_fairness_aware_meta_estimator():
    X = create_linear_X()
    y = create_y()
    s = create_s()

    # use sklearn estimator
    lr = FairnessAwareMetaEstimator(LogisticRegression())
    lr.fit(X, y)
    lr.predict(X)
    lr.predict_proba(X)

    # use themis_ml LinearACFClassifier estimator
    linear_acf = FairnessAwareMetaEstimator(
        counterfactually_fair_models.LinearACFClassifier(
            binary_residual_type="absolute"))
    linear_acf.fit(X, y, s)
    linear_acf.predict(X, s)
    linear_acf.predict_proba(X, s)

    # when fit/predict methods need s, raise ValueError
    with pytest.raises(ValueError):
        linear_acf.fit(X, y, s=None)
    with pytest.raises(ValueError):
        linear_acf.predict(X, s=None)
    with pytest.raises(ValueError):
        linear_acf.predict_proba(X, s=None)

    # use themis_ml relabeller preprocessor
    relabel_clf = FairnessAwareMetaEstimator(
        estimator=LogisticRegression(), relabeller=relabelling.Relabeller())
    relabel_clf.fit(X, y, s)
    relabel_clf.predict(X)
    relabel_clf.predict_proba(X)

    # when estimator method does not need `s` on predict, raise ValueError if
    # it is provided
    with pytest.raises(ValueError):
        relabel_clf.predict(X, s)
        relabel_clf.predict_proba(X, s)

    # use themis_ml RejectOption Classifier
    single_reject_option_clf = FairnessAwareMetaEstimator(
        reject_option_classification.SingleROClassifier())
    single_reject_option_clf.fit(X, y)
    single_reject_option_clf.predict(X, s)
    single_reject_option_clf.predict_proba(X, s)

    with pytest.raises(ValueError):
        single_reject_option_clf.fit(X, y, s)
    with pytest.raises(ValueError):
        single_reject_option_clf.predict(X, s=None)
    with pytest.raises(ValueError):
        single_reject_option_clf.predict_proba(X, s=None)

    multi_reject_option_clf = FairnessAwareMetaEstimator(
        reject_option_classification.MultipleROClassifier())
    multi_reject_option_clf.fit(X, y)
    multi_reject_option_clf.predict(X, s)
    multi_reject_option_clf.predict_proba(X, s)

    with pytest.raises(ValueError):
        multi_reject_option_clf.fit(X, y, s)
    with pytest.raises(ValueError):
        multi_reject_option_clf.predict(X, s=None)
    with pytest.raises(ValueError):
        multi_reject_option_clf.predict_proba(X, s=None)
예제 #11
0
def test_not_fitted_error():
    """Test raises not fitted error if predict before fit."""
    with pytest.raises(NotFittedError):
        SingleROClassifier().predict(create_linear_X(), create_y())
예제 #12
0
def test_relabeller_transform():
    """Test that relabeller correctly relabels targets."""
    expected = np.array([[0, 0, 1, 1, 1, 0, 0, 0, 1, 1]])
    assert (Relabeller().fit_transform(create_linear_X(),
                                       create_y(),
                                       s=create_s()) == expected).all()