Merged in feature/test-1-to-n-fields (pull request #426)
Feature/test 1 to n fields * Add function to evaluate one-to-n field comparisons and calculate metrics * Add fuzzy matching for one-to-n field comparisons and enhance evaluation metrics * Refactor similarity calculation to use token sort ratio for improved field comparison * Add matched labels to evaluation metrics in one-to-n fuzzy matching Approved-by: Katon Minhas
This commit is contained in:
committed by
Katon Minhas
parent
576ea95b6d
commit
08ebf92109
@@ -0,0 +1,115 @@
|
||||
import pandas as pd
|
||||
|
||||
from rapidfuzz import fuzz
|
||||
|
||||
|
||||
def are_entries_similar_field_by_field(entry1: tuple, entry2: tuple, thresholds=None):
|
||||
"""Compare two entries for similarity based on a given threshold using rapidfuzz
|
||||
|
||||
Args:
|
||||
entry1 (tuple): The first entry to compare.
|
||||
entry2 (tuple): The second entry to compare.
|
||||
thresholds (int, optional): Dictionary or list of thresholds for each field (defaults to 95 for all)
|
||||
"""
|
||||
if thresholds is None:
|
||||
thresholds = [95] * len(entry1) # Default threshold to 95 for all fields
|
||||
|
||||
# Check if threshold is a single value
|
||||
if isinstance(thresholds, (int, float)):
|
||||
thresholds = [thresholds] * len(entry1)
|
||||
|
||||
# Compare each field with its corresponding threshold
|
||||
for i, (field1, field2) in enumerate(zip(entry1, entry2)):
|
||||
field1_str = str(field1)
|
||||
field2_str = str(field2)
|
||||
|
||||
# Calculate similarity for this field
|
||||
similarity = fuzz.token_sort_ratio(field1_str, field2_str)
|
||||
|
||||
# If any field fails the threshold check, entries are not similar
|
||||
if similarity < thresholds[i]:
|
||||
return False
|
||||
|
||||
# All fields passed their threshold checks
|
||||
return True
|
||||
|
||||
|
||||
def evaluate_one_to_n_fields_fuzzy_matching_field_by_field(
|
||||
labels_df: pd.DataFrame,
|
||||
predictions_df: pd.DataFrame,
|
||||
key_field: str,
|
||||
comparison_fields: list,
|
||||
thresholds=None,
|
||||
):
|
||||
"""Evaluate 1:n field predictions against labels using field-by-field fuzzy matching
|
||||
|
||||
Args:
|
||||
labels_df (pd.DataFrame): DataFrame with ground truth labels
|
||||
predictions_df (pd.DataFrame): DataFrame with predictions
|
||||
key_field (str): Field that identifies the contract (probably "CONTRACT_FILE_NAME")
|
||||
comparison_fields (list): A list of fields to compare against the labels
|
||||
thresholds: Thresholds for each comparison field (same length as comparison_fields); defaults to 95 for all
|
||||
"""
|
||||
# Track metrics
|
||||
total_true_positives = 0
|
||||
total_false_positives = 0
|
||||
total_false_negatives = 0
|
||||
|
||||
# group by contract
|
||||
contract_ids = set(labels_df[key_field].unique()).union(
|
||||
set(predictions_df[key_field].unique())
|
||||
)
|
||||
|
||||
for contract_id in contract_ids:
|
||||
# Get labeled and predicted values for this contract
|
||||
contract_labels = labels_df[labels_df[key_field] == contract_id]
|
||||
contract_predictions = predictions_df[predictions_df[key_field] == contract_id]
|
||||
|
||||
# Create lists of entries as tuples for comparison
|
||||
labeled_entries = [tuple(row) for row in contract_labels[comparison_fields].values]
|
||||
predicted_entries = [tuple(row) for row in contract_predictions[comparison_fields].values]
|
||||
|
||||
# Track matches to avoid double counting
|
||||
matched_labels = set()
|
||||
matched_predictions = set()
|
||||
|
||||
# Find matches using field-by-field fuzzy comparison
|
||||
for i, labeled_entry in enumerate(labeled_entries):
|
||||
for j, predicted_entry in enumerate(predicted_entries):
|
||||
if j not in matched_predictions and are_entries_similar_field_by_field(labeled_entry, predicted_entry, thresholds):
|
||||
# If a match is found, increment true positives and mark entries as matched
|
||||
total_true_positives += 1
|
||||
matched_labels.add(i)
|
||||
matched_predictions.add(j)
|
||||
break
|
||||
# Calculate unmatched entries
|
||||
total_false_negatives += len(labeled_entries) - len(matched_labels)
|
||||
total_false_positives += len(predicted_entries) - len(matched_predictions)
|
||||
|
||||
|
||||
# Calculate overall metrics
|
||||
precision = (
|
||||
total_true_positives / (total_true_positives + total_false_positives)
|
||||
if (total_true_positives + total_false_positives) > 0
|
||||
else 0
|
||||
)
|
||||
recall = (
|
||||
total_true_positives / (total_true_positives + total_false_negatives)
|
||||
if (total_true_positives + total_false_negatives) > 0
|
||||
else 0
|
||||
)
|
||||
f1_score = (
|
||||
2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
|
||||
)
|
||||
|
||||
return {
|
||||
"precision": precision,
|
||||
"recall": recall,
|
||||
"f1_score": f1_score,
|
||||
"true_positives": total_true_positives,
|
||||
"false_positives": total_false_positives,
|
||||
"false_negatives": total_false_negatives,
|
||||
"matched_labels": {contract_id: [labeled_entries[i] for i in matched_labels] for contract_id in contract_ids},
|
||||
"unmatched_labels": {contract_id: [labeled_entries[i] for i in range(len(labeled_entries)) if i not in matched_labels] for contract_id in contract_ids},
|
||||
"unmatched_predictions": {contract_id: [predicted_entries[j] for j in range(len(predicted_entries)) if j not in matched_predictions] for contract_id in contract_ids}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user