ContentsFoundations to FrontierBezhig

graduate_code/solutions/ethics.py

All lab code. Download this file.

"""G11: descriptive group error audit. No automatic fairness or deployment verdict."""
import math


def binary_report(y, probabilities, threshold=.5):
    if not y or len(y) != len(probabilities) or not 0 <= threshold <= 1:
        raise ValueError("nonempty matching vectors and threshold in [0,1] required")
    if any(type(t) is not int or t not in (0,1) for t in y):
        raise ValueError("strict binary labels required")
    if any(not math.isfinite(p) or not 0 <= p <= 1 for p in probabilities):
        raise ValueError("probabilities must be finite in [0,1]")
    pred = [int(p >= threshold) for p in probabilities]
    tp = sum(t == 1 and v == 1 for t,v in zip(y,pred))
    fp = sum(t == 0 and v == 1 for t,v in zip(y,pred))
    fn = sum(t == 1 and v == 0 for t,v in zip(y,pred))
    tn = len(y)-tp-fp-fn
    ratio = lambda a,b: a/b if b else None
    return {"n":len(y), "tp":tp, "fp":fp, "fn":fn, "tn":tn,
            "precision":ratio(tp,tp+fp), "recall":ratio(tp,tp+fn),
            "fpr":ratio(fp,fp+tn), "selection":(tp+fp)/len(y),
            "accuracy":(tp+tn)/len(y),
            "brier":math.fsum((p-t)**2 for p,t in zip(probabilities,y))/len(y)}


def audit(y, probabilities, groups, threshold=.5):
    whole = binary_report(y, probabilities, threshold)
    if len(groups) != len(y) or any(type(g) is not str or not g for g in groups):
        raise ValueError("one nonempty group label per observation required")
    reports = {}
    for g in sorted(set(groups)):
        idx = [i for i,v in enumerate(groups) if v == g]
        reports[g] = binary_report([y[i] for i in idx],
                                   [probabilities[i] for i in idx],threshold)
    return {"overall":whole, "groups":reports}


if __name__ == "__main__":
    print(audit([1,0,1,0],[.9,.6,.4,.2],["A","A","B","B"]))