///|
pub(all) struct BenchmarkResult {
name : String
documents : Int
repetitions : Int
input_characters : Int
findings : Int
output_characters : Int
} derive(Debug, Eq)
///|
pub fn exact_span_match(left : Span, right : Span) -> Bool {
left.start == right.start && left.end == right.end
}
///|
pub fn overlap_span_match(left : Span, right : Span) -> Bool {
left.overlaps(right)
}
///|
pub fn evaluate_one_case(
case : EvaluationCase,
predicted : Array[Finding],
) -> EvaluationResult {
let expected = case.expected
let predicted_spans = findings_to_spans(predicted)
let true_positive = expected
.filter(fn(item) {
predicted_spans.any(fn(candidate) { exact_span_match(item, candidate) })
})
.length()
let false_negative = expected.length() - true_positive
let false_positive = predicted_spans
.filter(fn(item) {
!expected.any(fn(candidate) { exact_span_match(item, candidate) })
})
.length()
{
cases: 1,
expected: expected.length(),
predicted: predicted_spans.length(),
true_positive,
false_positive,
false_negative,
}
}
///|
pub fn evaluate_cases(
cases : Array[EvaluationCase],
rules? : Array[Rule] = comprehensive_rules(),
) -> EvaluationResult raise DeidError {
let mut total = {
cases: 0,
expected: 0,
predicted: 0,
true_positive: 0,
false_positive: 0,
false_negative: 0,
}
for case in cases {
let predicted = scan(case.text, rules~)
let current = evaluate_one_case(case, predicted)
total = {
cases: total.cases + current.cases,
expected: total.expected + current.expected,
predicted: total.predicted + current.predicted,
true_positive: total.true_positive + current.true_positive,
false_positive: total.false_positive + current.false_positive,
false_negative: total.false_negative + current.false_negative,
}
}
total
}
///|
pub fn precision(result : EvaluationResult) -> Float {
result.precision()
}
///|
pub fn recall(result : EvaluationResult) -> Float {
result.recall()
}
///|
pub fn f1_score(result : EvaluationResult) -> Float {
result.f1()
}
///|
pub fn evaluation_summary(result : EvaluationResult) -> String {
[
"cases=\{result.cases}",
"expected=\{result.expected}",
"predicted=\{result.predicted}",
"tp=\{result.true_positive}",
"fp=\{result.false_positive}",
"fn=\{result.false_negative}",
"precision=\{result.precision()}",
"recall=\{result.recall()}",
"f1=\{result.f1()}",
].join(" ")
}
///|
pub fn evaluation_to_json(result : EvaluationResult) -> String {
"{" +
"\"cases\":\{result.cases}," +
"\"expected\":\{result.expected}," +
"\"predicted\":\{result.predicted}," +
"\"true_positive\":\{result.true_positive}," +
"\"false_positive\":\{result.false_positive}," +
"\"false_negative\":\{result.false_negative}," +
"\"precision\":\{result.precision()}," +
"\"recall\":\{result.recall()}," +
"\"f1\":\{result.f1()}" +
"}"
}
///|
pub fn benchmark_once(
name : String,
documents : Array[String],
repetitions : Int,
config : RedactionConfig,
) -> BenchmarkResult raise DeidError {
let rounds = if repetitions <= 0 { 1 } else { repetitions }
let mut input_characters = 0
let mut findings = 0
let mut output_characters = 0
for _ in 0.. String {
[
"name=\{result.name}",
"documents=\{result.documents}",
"repetitions=\{result.repetitions}",
"input_characters=\{result.input_characters}",
"findings=\{result.findings}",
"output_characters=\{result.output_characters}",
].join("\n")
}
///|
pub fn benchmark_to_json(result : BenchmarkResult) -> String {
"{" +
"\"name\":\{json_string(result.name)}," +
"\"documents\":\{result.documents}," +
"\"repetitions\":\{result.repetitions}," +
"\"input_characters\":\{result.input_characters}," +
"\"findings\":\{result.findings}," +
"\"output_characters\":\{result.output_characters}" +
"}"
}
///|
pub fn synthetic_evaluation_cases() -> Array[EvaluationCase] {
[
{
id: "mixed-identifiers",
text: "姓名:张三,电话13800138000,邮箱 zhang@example.com",
expected: [
{ start: 3, end: 5 },
{ start: 8, end: 19 },
{ start: 22, end: 38 },
],
},
{
id: "english-record",
text: "Patient: John Smith MRN: A1234567",
expected: [{ start: 9, end: 19 }, { start: 25, end: 33 }],
},
{
id: "structured-identifiers",
text: "DOB: 1990-01-02 IMEI: 490154203237518",
expected: [{ start: 5, end: 15 }, { start: 22, end: 37 }],
},
]
}
///|
pub fn evaluate_synthetic_cases() -> EvaluationResult raise DeidError {
evaluate_cases(synthetic_evaluation_cases())
}
///|
pub fn benchmark_fixture() -> Array[String] {
[
"姓名:张三;身份证:110101199003071234;电话:13800138000;邮箱:zhang@example.com;住址:北京市海淀区中关村1号。",
"Patient: John Smith; DOB: 1990-01-02; Phone: 415-555-1234; MRN: A1234567; Email: john@example.org.",
"就诊号:ENC-20260819-0001;检验号:LAB-778899;医生:李医生;地址:上海市徐汇区漕溪北路。",
]
}