<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P157-fraud-alerts/ | updated: 2026-10-18 -->

# P157 Fraud alerts

A detector right 99% of the time over a million payments a month: accuracy that stays put while precision follows the fraud rate, the rate at which alerts are right as often as not, and a second check on the alerts.

EML-P project `projects/fraud-alerts` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/fraud-alerts` runs it and `eml project verify projects/fraud-alerts` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: base-rate-and-precision (https://efficientnewlanguage.org/cases/261-base-rate-and-precision/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
2

2
1000001
2
x
3

3
1001
3
990

3
990
x
3
0
10
4
1
3
5
0
4
5
6
```

Screen:

```text
== Fraud alerts ==
A detector right 99% of the time, a million payments a month, and how often an alert is fraud.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 0
Pick a number from 1 to 6.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> x
Pick a number from 1 to 6.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 2
frauds in a month of 1000000 payments> 
Cancelled.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 2
frauds in a month of 1000000 payments> 1000001
Type a whole number from 0 to 1000000.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 2
frauds in a month of 1000000 payments> x
Type a whole number from 0 to 1000000.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 
Cancelled.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 1001
Type a whole number from 0 to 1000.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 990
honest payments flagged, in tenths of a percent (0 to 1000)> 
Cancelled.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 990
honest payments flagged, in tenths of a percent (0 to 1000)> x
Type a whole number from 0 to 1000.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 0
honest payments flagged, in tenths of a percent (0 to 1000)> 10
The detector now catches 0.0% of frauds and flags 1.0% of honest payments.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 4
The same detector, 0.0% caught and 1.0% false alarms, at every fraud rate:
  frauds a month  accuracy  alerts   precision
  1               99.0%     9999     0.0%
  10              99.0%     9999     0.0%
  100             99.0%     9999     0.0%
  1000            98.9%     9990     0.0%
  10000           98.0%     9900     0.0%
  100000          89.1%     9000     0.0%
Accuracy runs from 89.1% to 99.0%; precision is 0.0% wherever there are alerts.
A detector that catches nothing makes no alert right, at any fraud rate.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 1
A month of 1000000 payments, 200 of them fraud; the detector catches 0.0% of frauds and flags 1.0% of honest payments:
              flagged    passed
  fraud             0       200
  honest         9998    989802
Accuracy 99.0%: the detector is right about that share of payments.
Of the 9998 alerts, 0 are fraud: precision 0.0%. Act on an alert and you are wrong 100.0% of the time.
A detector that catches nothing makes no alert right, at any fraud rate.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 5
honest payments flagged, in tenths of a percent (0 to 1000)> 0
The detector now catches 0.5% of frauds and flags 0.0% of honest payments.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 4
The same detector, 0.5% caught and 0.0% false alarms, at every fraud rate:
  frauds a month  accuracy  alerts   precision
  1               100.0%    0        -
  10              100.0%    0        -
  100             100.0%    0        -
  1000            99.9%     5        100.0%
  10000           99.0%     50       100.0%
  100000          90.1%     500      100.0%
Accuracy runs from 90.1% to 100.0%; precision is 100.0% wherever there are alerts.
An alert is fraud at least as often as not only from 200 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 5
The 1 alert checked again by an independent detector just as good:
  first check:   1 fraud of 1, precision 100.0%
  second check:  no alert left
  frauds missed by the two together: 200

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
4
5
2
20000
1
5
3
999
1
1
4
6
```

Screen:

```text
== Fraud alerts ==
A detector right 99% of the time, a million payments a month, and how often an alert is fraud.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 1
A month of 1000000 payments, 200 of them fraud; the detector catches 99.0% of frauds and flags 1.0% of honest payments:
              flagged    passed
  fraud           198         2
  honest         9998    989802
Accuracy 99.0%: the detector is right about that share of payments.
Of the 10196 alerts, 198 are fraud: precision 1.9%. Act on an alert and you are wrong 98.1% of the time.
An alert is fraud at least as often as not only from 10000 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 4
The same detector, 99.0% caught and 1.0% false alarms, at every fraud rate:
  frauds a month  accuracy  alerts   precision
  1               99.0%     9999     0.0%
  10              99.0%     10008    0.1%
  100             99.0%     10098    1.0%
  1000            99.0%     10980    9.0%
  10000           99.0%     19800    50.0%
  100000          99.0%     108000   91.7%
Accuracy is 99.0% at every rate; precision runs from 0.0% to 91.7%, following the fraud rate, which the detector does not know.
An alert is fraud at least as often as not only from 10000 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 5
The 10196 alerts checked again by an independent detector just as good:
  first check:   198 fraud of 10196, precision 1.9%
  second check:  196 fraud of 295, precision 66.4%
The alerts are a population of their own, with a fraud rate equal to the first precision; the second check starts from there.
  frauds missed by the two together: 4

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 2
frauds in a month of 1000000 payments> 20000
Fraud set to 20000 a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 1
A month of 1000000 payments, 20000 of them fraud; the detector catches 99.0% of frauds and flags 1.0% of honest payments:
              flagged    passed
  fraud         19800       200
  honest         9800    970200
Accuracy 99.0%: the detector is right about that share of payments.
Of the 29600 alerts, 19800 are fraud: precision 66.9%. Act on an alert and you are wrong 33.1% of the time.
An alert is fraud at least as often as not only from 10000 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 5
The 29600 alerts checked again by an independent detector just as good:
  first check:   19800 fraud of 29600, precision 66.9%
  second check:  19602 fraud of 19700, precision 99.5%
The alerts are a population of their own, with a fraud rate equal to the first precision; the second check starts from there.
  frauds missed by the two together: 398

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 3
frauds caught, in tenths of a percent (0 to 1000)> 999
honest payments flagged, in tenths of a percent (0 to 1000)> 1
The detector now catches 99.9% of frauds and flags 0.1% of honest payments.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 1
A month of 1000000 payments, 20000 of them fraud; the detector catches 99.9% of frauds and flags 0.1% of honest payments:
              flagged    passed
  fraud         19980        20
  honest          980    979020
Accuracy 99.9%: the detector is right about that share of payments.
Of the 20960 alerts, 19980 are fraud: precision 95.3%. Act on an alert and you are wrong 4.7% of the time.
An alert is fraud at least as often as not only from 1000 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 4
The same detector, 99.9% caught and 0.1% false alarms, at every fraud rate:
  frauds a month  accuracy  alerts   precision
  1               99.9%     999      0.0%
  10              99.9%     1008     0.9%
  100             99.9%     1098     9.0%
  1000            99.9%     1998     50.0%
  10000           99.9%     10980    91.0%
  100000          99.9%     100800   99.1%
Accuracy is 99.9% at every rate; precision runs from 0.0% to 99.1%, following the fraud rate, which the detector does not know.
An alert is fraud at least as often as not only from 1000 frauds a month.

1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P157 fraud alerts: a month of payments through the detector, the fraud
# rate and the detector set by hand, every fraud rate side by side, and the
# alerts checked a second time.
import fraud

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def lpad(s, n):
    s => out
    while len(out) < n:
        " " + out => out
    return out

def pct(t):
    t % 10 => d
    return str(int((t - d) / 10)) + "." + str(d) + "%"

def number(prompt, high):
    # a whole number from 0 to high, -1 for a cancel, -2 when not valid
    plain(input(prompt)) => t
    if t == "":
        return 0 - 1
    if len(t) > 7:
        return 0 - 2
    for c in t:
        if c < "0" or c > "9":
            return 0 - 2
    if int(t) > high:
        return 0 - 2
    return int(t)

def even_line(catch, alarm):
    fraud.even(catch, alarm) => b
    if b < 0:
        "A detector that catches nothing makes no alert right, at any fraud rate." ^0
    else:
        if b == 1:
            "An alert is fraud at least as often as not from the first fraud of the month." ^0
        else:
            ("An alert is fraud at least as often as not only from " + str(b) + " frauds a month.") ^0

def show_month(f, catch, alarm):
    fraud.outcomes(f, catch, alarm) => r
    ("A month of " + str(fraud.month()) + " payments, " + str(f) + " of them fraud; the detector catches " + pct(catch) + " of frauds and flags " + pct(alarm) + " of honest payments:") ^0
    ("  " + pad("", 9) + lpad("flagged", 10) + lpad("passed", 10)) ^0
    ("  " + pad("fraud", 9) + lpad(str(r[0]), 10) + lpad(str(r[1]), 10)) ^0
    ("  " + pad("honest", 9) + lpad(str(r[2]), 10) + lpad(str(r[3]), 10)) ^0
    ("Accuracy " + pct(fraud.accuracy(r)) + ": the detector is right about that share of payments.") ^0
    r[0] + r[2] => alerts
    if alerts == 0:
        "No alerts this month." ^0
    else:
        if alerts == 1:
            "Of the 1 alert, " + str(r[0]) + " is fraud" => head
        else:
            "Of the " + str(alerts) + " alerts, " + str(r[0]) + " are fraud" => head
        (head + ": precision " + pct(fraud.precision(r)) + ". Act on an alert and you are wrong " + pct(1000 - fraud.precision(r)) + " of the time.") ^0
    even_line(catch, alarm)

def sweep(catch, alarm):
    ("The same detector, " + pct(catch) + " caught and " + pct(alarm) + " false alarms, at every fraud rate:") ^0
    ("  " + pad("frauds a month", 16) + pad("accuracy", 10) + pad("alerts", 9) + "precision") ^0
    1001 => alo
    0 - 1 => ahi
    1001 => plo
    0 - 1 => phi
    for f in [1, 10, 100, 1000, 10000, 100000]:
        fraud.outcomes(f, catch, alarm) => r
        fraud.accuracy(r) => a
        fraud.precision(r) => p
        if r[0] + r[2] == 0:
            ("  " + pad(str(f), 16) + pad(pct(a), 10) + pad("0", 9) + "-") ^0
        else:
            ("  " + pad(str(f), 16) + pad(pct(a), 10) + pad(str(r[0] + r[2]), 9) + pct(p)) ^0
            if p < plo:
                p => plo
            if p > phi:
                p => phi
        if a < alo:
            a => alo
        if a > ahi:
            a => ahi
    if alo == ahi:
        "Accuracy is " + pct(alo) + " at every rate" => acc
    else:
        "Accuracy runs from " + pct(alo) + " to " + pct(ahi) => acc
    if phi < 0:
        (acc + "; at none of these rates is there an alert.") ^0
    elif plo == phi:
        (acc + "; precision is " + pct(plo) + " wherever there are alerts.") ^0
    else:
        (acc + "; precision runs from " + pct(plo) + " to " + pct(phi) + ", following the fraud rate, which the detector does not know.") ^0
    even_line(catch, alarm)

def again(f, catch, alarm):
    fraud.outcomes(f, catch, alarm) => r
    fraud.second(r, catch, alarm) => s
    r[0] + r[2] => alerts
    if alerts == 0:
        "No alerts this month, so nothing to check again." ^0
        return
    if alerts == 1:
        "The 1 alert checked again by an independent detector just as good:" ^0
    else:
        ("The " + str(alerts) + " alerts checked again by an independent detector just as good:") ^0
    ("  first check:   " + str(r[0]) + " fraud of " + str(alerts) + ", precision " + pct(fraud.precision(r))) ^0
    if s[0] + s[1] == 0:
        "  second check:  no alert left" ^0
    else:
        ("  second check:  " + str(s[0]) + " fraud of " + str(s[0] + s[1]) + ", precision " + pct(fraud.tenths(s[0], s[0] + s[1]))) ^0
        "The alerts are a population of their own, with a fraud rate equal to the first precision; the second check starts from there." ^0
    ("  frauds missed by the two together: " + str(f - s[0])) ^0

200 => f
990 => catch
10 => alarm

"== Fraud alerts ==" ^0
"A detector right 99% of the time, a million payments a month, and how often an alert is fraud." ^0
True => running
while running:
    "" ^0
    "1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_month(f, catch, alarm)
    elif choice == "2":
        number("frauds in a month of 1000000 payments> ", fraud.month()) => n
        if n == 0 - 1:
            "Cancelled." ^0
        elif n < 0:
            "Type a whole number from 0 to 1000000." ^0
        else:
            n => f
            ("Fraud set to " + str(f) + " a month.") ^0
    elif choice == "3":
        number("frauds caught, in tenths of a percent (0 to 1000)> ", 1000) => a
        if a == 0 - 1:
            "Cancelled." ^0
        elif a < 0:
            "Type a whole number from 0 to 1000." ^0
        else:
            number("honest payments flagged, in tenths of a percent (0 to 1000)> ", 1000) => b
            if b == 0 - 1:
                "Cancelled." ^0
            elif b < 0:
                "Type a whole number from 0 to 1000." ^0
            else:
                a => catch
                b => alarm
                ("The detector now catches " + pct(catch) + " of frauds and flags " + pct(alarm) + " of honest payments.") ^0
    elif choice == "4":
        sweep(catch, alarm)
    elif choice == "5":
        again(f, catch, alarm)
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import fraud

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def lpad(s, n):
    out = s
    while len(out) < n:
        out = " " + out
    return out

def pct(t):
    d = t % 10
    return str(int((t - d) / 10)) + "." + str(d) + "%"

def number(prompt, high):
    t = plain(input(prompt))
    if t == "":
        return 0 - 1
    if len(t) > 7:
        return 0 - 2
    for c in t:
        if c < "0" or c > "9":
            return 0 - 2
    if int(t) > high:
        return 0 - 2
    return int(t)

def even_line(catch, alarm):
    b = fraud.even(catch, alarm)
    if b < 0:
        print("A detector that catches nothing makes no alert right, at any fraud rate.")
    elif b == 1:
        print("An alert is fraud at least as often as not from the first fraud of the month.")
    else:
        print("An alert is fraud at least as often as not only from " + str(b) + " frauds a month.")

def show_month(f, catch, alarm):
    r = fraud.outcomes(f, catch, alarm)
    print("A month of " + str(fraud.month()) + " payments, " + str(f) + " of them fraud; the detector catches " + pct(catch) + " of frauds and flags " + pct(alarm) + " of honest payments:")
    print("  " + pad("", 9) + lpad("flagged", 10) + lpad("passed", 10))
    print("  " + pad("fraud", 9) + lpad(str(r[0]), 10) + lpad(str(r[1]), 10))
    print("  " + pad("honest", 9) + lpad(str(r[2]), 10) + lpad(str(r[3]), 10))
    print("Accuracy " + pct(fraud.accuracy(r)) + ": the detector is right about that share of payments.")
    alerts = r[0] + r[2]
    if alerts == 0:
        print("No alerts this month.")
    else:
        if alerts == 1:
            head = "Of the 1 alert, " + str(r[0]) + " is fraud"
        else:
            head = "Of the " + str(alerts) + " alerts, " + str(r[0]) + " are fraud"
        print(head + ": precision " + pct(fraud.precision(r)) + ". Act on an alert and you are wrong " + pct(1000 - fraud.precision(r)) + " of the time.")
    even_line(catch, alarm)

def sweep(catch, alarm):
    print("The same detector, " + pct(catch) + " caught and " + pct(alarm) + " false alarms, at every fraud rate:")
    print("  " + pad("frauds a month", 16) + pad("accuracy", 10) + pad("alerts", 9) + "precision")
    alo = 1001
    ahi = 0 - 1
    plo = 1001
    phi = 0 - 1
    for f in [1, 10, 100, 1000, 10000, 100000]:
        r = fraud.outcomes(f, catch, alarm)
        a = fraud.accuracy(r)
        p = fraud.precision(r)
        if r[0] + r[2] == 0:
            print("  " + pad(str(f), 16) + pad(pct(a), 10) + pad("0", 9) + "-")
        else:
            print("  " + pad(str(f), 16) + pad(pct(a), 10) + pad(str(r[0] + r[2]), 9) + pct(p))
            if p < plo:
                plo = p
            if p > phi:
                phi = p
        if a < alo:
            alo = a
        if a > ahi:
            ahi = a
    if alo == ahi:
        acc = "Accuracy is " + pct(alo) + " at every rate"
    else:
        acc = "Accuracy runs from " + pct(alo) + " to " + pct(ahi)
    if phi < 0:
        print(acc + "; at none of these rates is there an alert.")
    elif plo == phi:
        print(acc + "; precision is " + pct(plo) + " wherever there are alerts.")
    else:
        print(acc + "; precision runs from " + pct(plo) + " to " + pct(phi) + ", following the fraud rate, which the detector does not know.")
    even_line(catch, alarm)

def again(f, catch, alarm):
    r = fraud.outcomes(f, catch, alarm)
    s = fraud.second(r, catch, alarm)
    alerts = r[0] + r[2]
    if alerts == 0:
        print("No alerts this month, so nothing to check again.")
        return
    if alerts == 1:
        print("The 1 alert checked again by an independent detector just as good:")
    else:
        print("The " + str(alerts) + " alerts checked again by an independent detector just as good:")
    print("  first check:   " + str(r[0]) + " fraud of " + str(alerts) + ", precision " + pct(fraud.precision(r)))
    if s[0] + s[1] == 0:
        print("  second check:  no alert left")
    else:
        print("  second check:  " + str(s[0]) + " fraud of " + str(s[0] + s[1]) + ", precision " + pct(fraud.tenths(s[0], s[0] + s[1])))
        print("The alerts are a population of their own, with a fraud rate equal to the first precision; the second check starts from there.")
    print("  frauds missed by the two together: " + str(f - s[0]))

f = 200
catch = 990
alarm = 10
print("== Fraud alerts ==")
print("A detector right 99% of the time, a million payments a month, and how often an alert is fraud.")
running = True
while running:
    print("")
    print("1) a month  2) the fraud rate  3) the detector  4) every fraud rate  5) check the alerts again  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_month(f, catch, alarm)
    elif choice == "2":
        n = number("frauds in a month of 1000000 payments> ", fraud.month())
        if n == 0 - 1:
            print("Cancelled.")
        elif n < 0:
            print("Type a whole number from 0 to 1000000.")
        else:
            f = n
            print("Fraud set to " + str(f) + " a month.")
    elif choice == "3":
        a = number("frauds caught, in tenths of a percent (0 to 1000)> ", 1000)
        if a == 0 - 1:
            print("Cancelled.")
        elif a < 0:
            print("Type a whole number from 0 to 1000.")
        else:
            b = number("honest payments flagged, in tenths of a percent (0 to 1000)> ", 1000)
            if b == 0 - 1:
                print("Cancelled.")
            elif b < 0:
                print("Type a whole number from 0 to 1000.")
            else:
                catch = a
                alarm = b
                print("The detector now catches " + pct(catch) + " of frauds and flags " + pct(alarm) + " of honest payments.")
    elif choice == "4":
        sweep(catch, alarm)
    elif choice == "5":
        again(f, catch, alarm)
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### fraud.eml

```eml
# P157 fraud alerts - a detector that is right 99% of the time, run over a
# million card payments a month. As in the corpus case
# base-rate-and-precision, accuracy is about the detector and precision -
# how often an alert is fraud - is about the world: when fraud is rare, 1%
# of the honest payments outnumbers 99% of the frauds, and most alerts are
# wrong. Counts are whole payments over a fixed month; a rate is in tenths
# of a percent.

def month():
    return 1000000

def outcomes(fraud, catch, alarm):
    # [frauds caught, frauds missed, honest flagged, honest passed] for a
    # month with fraud frauds, catching catch and falsely flagging alarm
    # tenths of a percent, each count rounded down
    int(fraud * catch / 1000) => tp
    month() - fraud => honest
    int(honest * alarm / 1000) => fp
    return [tp, fraud - tp, fp, honest - fp]

def tenths(n, d):
    # n / d as a percentage in tenths, rounded half up; 0 when d is 0
    if d == 0:
        return 0
    int((2000 * n + d) / (2 * d)) => q
    while q * 2 * d > 2000 * n + d:
        q - 1 => q
    while (q + 1) * 2 * d <= 2000 * n + d:
        q + 1 => q
    return q

def precision(r):
    return tenths(r[0], r[0] + r[2])

def accuracy(r):
    return tenths(r[0] + r[3], month())

def right_half(b, catch, alarm):
    # some fraud is caught, and the alerts are fraud at least as often as not
    outcomes(b, catch, alarm) => r
    return r[0] > 0 and r[0] >= r[2]

def even(catch, alarm):
    # the fewest frauds a month at which an alert is fraud at least as
    # often as not; the formula's answer, then nudged past the rounding
    if catch == 0:
        return 0 - 1
    int(month() * alarm / (catch + alarm)) => b
    if b < 1:
        1 => b
    while b > 1 and right_half(b - 1, catch, alarm):
        b - 1 => b
    while not right_half(b, catch, alarm):
        b + 1 => b
    return b

def second(r, catch, alarm):
    # the alerts checked again by an independent detector just as good:
    # [frauds still flagged, honest still flagged]
    return [int(r[0] * catch / 1000), int(r[2] * alarm / 1000)]
```

Python projection of fraud.eml:

```python
def month():
    return 1000000

def outcomes(fraud, catch, alarm):
    tp = int(fraud * catch / 1000)
    honest = month() - fraud
    fp = int(honest * alarm / 1000)
    return [tp, fraud - tp, fp, honest - fp]

def tenths(n, d):
    if d == 0:
        return 0
    q = int((2000 * n + d) / (2 * d))
    while q * 2 * d > 2000 * n + d:
        q = q - 1
    while (q + 1) * 2 * d <= 2000 * n + d:
        q = q + 1
    return q

def precision(r):
    return tenths(r[0], r[0] + r[2])

def accuracy(r):
    return tenths(r[0] + r[3], month())

def right_half(b, catch, alarm):
    r = outcomes(b, catch, alarm)
    return r[0] > 0 and r[0] >= r[2]

def even(catch, alarm):
    if catch == 0:
        return 0 - 1
    b = int(month() * alarm / (catch + alarm))
    if b < 1:
        b = 1
    while b > 1 and right_half(b - 1, catch, alarm):
        b = b - 1
    while not right_half(b, catch, alarm):
        b = b + 1
    return b

def second(r, catch, alarm):
    return [int(r[0] * catch / 1000), int(r[2] * alarm / 1000)]
```

## README

# P157 - Fraud alerts

A detector right 99% of the time, run over a million card payments a
month, and how often one of its alerts is fraud.

- `main.eml` - the menu, a month through the detector, the fraud rate and
  the detector set by hand, every fraud rate side by side, and the alerts
  checked a second time
- `fraud.eml` - the month, the four counts, percentages in tenths rounded
  half up, precision and accuracy, the fraud rate at which alerts are right
  as often as not, and a second check

How each part works:

- The detector catches 99.0% of frauds and flags 1.0% of honest payments.
  With 200 frauds in a million payments it is right about 99.0% of them,
  and of its 10196 alerts 198 are fraud: precision 1.9%.
- As in the corpus case `base-rate-and-precision`, accuracy is about the
  detector and precision is about the world: at every fraud rate from 1 to
  100000 a month accuracy stays 99.0%, while precision runs from 0.0% to
  91.7%. Alerts are fraud as often as not only from 10000 frauds a month -
  found from the formula and then checked past the rounding.
- The alerts are a population of their own, with a fraud rate equal to
  the first precision. Checked again by an independent detector just as
  good, 196 of the 295 left are fraud: 66.4%.
- With the detector set by hand: catching nothing makes no alert right at
  any rate; flagging nothing honest makes every alert right from the first
  caught fraud.

What is checked: menu choices 1 to 6; frauds a month from 0 to 1000000;
the catch rate and the false alarm rate from 0 to 1000 tenths of a
percent. An empty answer cancels. Every count is rounded down.

Sessions: `sessions/basic.in` gives:
- a month, every fraud rate, and the alerts checked again;
- 20000 frauds a month, a month and the alerts checked again;
- the detector at 99.9% and 0.1%, a month and every fraud rate.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- fraud rates: a cancel, 1000001 and x;
- detectors: a cancel and 1001, then 990 with a cancel and x; a detector
  that catches nothing, every rate and a month; one that catches 0.5% and
  flags nothing honest, every rate and the alerts checked again.

Built on the verified corpus case `base-rate-and-precision` (a fixed test
swept across base rates, precision against accuracy, and the rate at which
a positive is more likely right than wrong).
