///|
fn validation_issue(
code : String,
level : ValidationLevel,
message : String,
query_id? : String = "",
doc_id? : String = "",
) -> ValidationIssue {
{ code, level, message, query_id, doc_id }
}
///|
fn add_validation_issue(
issues : Array[ValidationIssue],
code : String,
level : ValidationLevel,
message : String,
query_id? : String = "",
doc_id? : String = "",
) -> Unit {
issues.push(validation_issue(code, level, message, query_id~, doc_id~))
}
///|
fn has_query(
qrels_by_query : Map[String, Array[JudgedDoc]],
query_id : String,
) -> Bool {
qrels_by_query.contains(query_id)
}
///|
pub fn validate_dataset(
qrels : Array[JudgedDoc],
run : Array[RetrievedDoc],
) -> ValidationSummary {
let qrels_by_query = group_qrels(qrels)
let runs_by_query = group_runs(run)
let issues : Array[ValidationIssue] = []
let judged_keys : Map[String, Unit] = Map([])
let duplicate_judgments : Map[String, Unit] = Map([])
for item in qrels {
if item.query_id.is_empty() {
add_validation_issue(
issues,
"empty_query_id",
Error,
"qrels query_id must not be empty",
doc_id=item.doc_id,
)
}
if item.doc_id.is_empty() {
add_validation_issue(
issues,
"empty_doc_id",
Error,
"qrels doc_id must not be empty",
query_id=item.query_id,
)
}
if item.relevance < 0 {
add_validation_issue(
issues,
"negative_relevance",
Error,
"relevance must be non-negative",
query_id=item.query_id,
doc_id=item.doc_id,
)
}
let key = "\{item.query_id}:\{item.doc_id}"
if judged_keys.contains(key) {
if !duplicate_judgments.contains(key) {
duplicate_judgments[key] = ()
add_validation_issue(
issues,
"duplicate_judgment",
Warning,
"qrels contains a duplicate query/document judgment",
query_id=item.query_id,
doc_id=item.doc_id,
)
}
} else {
judged_keys[key] = ()
}
}
let run_keys : Map[String, Unit] = Map([])
let duplicate_runs : Map[String, Unit] = Map([])
for item in run {
if item.query_id.is_empty() {
add_validation_issue(
issues,
"empty_query_id",
Error,
"run query_id must not be empty",
doc_id=item.doc_id,
)
}
if item.doc_id.is_empty() {
add_validation_issue(
issues,
"empty_doc_id",
Error,
"run doc_id must not be empty",
query_id=item.query_id,
)
}
if item.score.is_nan() || item.score.is_inf() {
add_validation_issue(
issues,
"non_finite_score",
Error,
"run score must be finite",
query_id=item.query_id,
doc_id=item.doc_id,
)
}
let key = "\{item.query_id}:\{item.doc_id}"
if run_keys.contains(key) {
if !duplicate_runs.contains(key) {
duplicate_runs[key] = ()
add_validation_issue(
issues,
"duplicate_run_doc",
Warning,
"run contains a duplicate query/document row",
query_id=item.query_id,
doc_id=item.doc_id,
)
}
} else {
run_keys[key] = ()
}
if !has_query(qrels_by_query, item.query_id) {
add_validation_issue(
issues,
"unknown_query",
Warning,
"run query_id does not occur in qrels",
query_id=item.query_id,
doc_id=item.doc_id,
)
}
}
for query_id, _ in qrels_by_query {
if !runs_by_query.contains(query_id) {
add_validation_issue(
issues,
"missing_run_query",
Warning,
"qrels query_id has no retrieved results",
query_id~,
)
}
}
if qrels.is_empty() {
add_validation_issue(
issues,
"empty_qrels",
Error,
"qrels must contain at least one row",
)
}
if run.is_empty() {
add_validation_issue(issues, "empty_run", Warning, "run contains no rows")
}
let mut error_count = 0
let mut warning_count = 0
for issue in issues {
match issue.level {
Error => error_count += 1
Warning => warning_count += 1
}
}
{
error_count,
warning_count,
query_count: sorted_query_ids(qrels_by_query, runs_by_query).length(),
judged_count: qrels.length(),
retrieved_count: run.length(),
issues,
}
}
///|
pub fn normalize_run(run : Array[RetrievedDoc]) -> Array[RetrievedDoc] {
let grouped = group_runs(run)
let normalized : Array[RetrievedDoc] = []
let query_ids : Array[String] = []
for query_id, _ in grouped {
query_ids.push(query_id)
}
query_ids.sort()
for query_id in query_ids {
let seen : Map[String, Unit] = Map([])
for item in grouped[query_id] {
if !seen.contains(item.doc_id) {
seen[item.doc_id] = ()
normalized.push(item)
}
}
}
normalized
}
///|
pub fn normalize_cutoffs(cutoffs : Array[Int]) -> Array[Int] {
unique_positive_cutoffs(cutoffs)
}