修改为东南天坐标系

This commit is contained in:
2026-01-20 09:49:52 +08:00
parent 9538757047
commit 333fad40ac
7201 changed files with 1030888 additions and 85410 deletions

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
import pytest
from test_unstructured.unit_utils import example_doc_path
from unstructured.metrics.element_type import (
FrequencyDict,
calculate_element_type_percent_match,
get_element_type_frequency,
)
from unstructured.partition.auto import partition
from unstructured.staging.base import elements_to_json
@pytest.mark.parametrize(
("filename", "frequency"),
[
(
"fake-email.txt",
{
("NarrativeText", None): 1,
("UncategorizedText", None): 1,
("ListItem", 1): 2,
},
),
(
"sample-presentation.pptx",
{
("Title", 0): 4,
("Title", 1): 1,
("NarrativeText", 0): 3,
("PageBreak", None): 3,
("ListItem", 0): 6,
("ListItem", 1): 6,
("ListItem", 2): 3,
("Table", None): 1,
},
),
],
)
def test_get_element_type_frequency(filename: str, frequency: dict[tuple[str, int | None], int]):
elements = partition(example_doc_path(filename))
elements_freq = get_element_type_frequency(elements_to_json(elements))
assert elements_freq == frequency
@pytest.mark.parametrize(
("filename", "expected_frequency", "percent_matched"),
[
(
"fake-email.txt",
{
("UncategorizedText", None): 1,
("ListItem", 1): 2,
("NarrativeText", None): 2,
},
(0.8, 0.8, 0.80),
),
(
"sample-presentation.pptx",
{
("Title", 0): 3,
("Title", 1): 1,
("NarrativeText", None): 1,
("NarrativeText", 0): 3,
("ListItem", 0): 6,
("ListItem", 1): 6,
("ListItem", 2): 3,
("Table", None): 1,
},
(0.96, 0.96, 0.96),
),
(
"handbook-1p.docx",
{
("Header", None): 1,
("UncategorizedText", 0): 6,
("ListItem", 3): 3,
("NarrativeText", 0): 7,
("Footer", None): 1,
},
(0.78, 0.72, 0.81),
),
(
"handbook-1p.docx",
{
("Header", None): 1,
("UncategorizedText", 0): 6,
("NarrativeText", 0): 7,
("PageBreak", None): 1,
("Footer", None): 1,
},
(0.94, 0.88, 0.98),
),
],
)
def test_calculate_element_type_percent_match(
filename: str, expected_frequency: FrequencyDict, percent_matched: tuple[float, float, float]
):
elements = partition(example_doc_path(filename))
elements_frequency = get_element_type_frequency(elements_to_json(elements))
assert (
round(calculate_element_type_percent_match(elements_frequency, expected_frequency), 2)
== percent_matched[0]
)
assert (
round(calculate_element_type_percent_match(elements_frequency, expected_frequency, 0.0), 2)
== percent_matched[1]
)
assert (
round(calculate_element_type_percent_match(elements_frequency, expected_frequency, 0.8), 2)
== percent_matched[2]
)

View File

@@ -0,0 +1,595 @@
import os
import pathlib
import shutil
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pandas as pd
import pytest
from unstructured.metrics.evaluate import (
ElementTypeMetricsCalculator,
TableStructureMetricsCalculator,
TextExtractionMetricsCalculator,
filter_metrics,
get_mean_grouping,
)
is_in_docker = os.path.exists("/.dockerenv")
EXAMPLE_DOCS_DIRECTORY = os.path.join(
pathlib.Path(__file__).parent.resolve(), "..", "..", "example-docs"
)
TESTING_FILE_DIR = os.path.join(EXAMPLE_DOCS_DIRECTORY, "test_evaluate_files")
UNSTRUCTURED_OUTPUT_DIRNAME = "unstructured_output"
GOLD_CCT_DIRNAME = "gold_standard_cct"
GOLD_ELEMENT_TYPE_DIRNAME = "gold_standard_element_type"
GOLD_TABLE_STRUCTURE_DIRNAME = "gold_standard_table_structure"
UNSTRUCTURED_CCT_DIRNAME = "unstructured_output_cct"
UNSTRUCTURED_TABLE_STRUCTURE_DIRNAME = "unstructured_output_table_structure"
DUMMY_DF_CCT = pd.DataFrame(
{
"filename": [
"Bank Good Credit Loan.pptx",
"Performance-Audit-Discussion.pdf",
"currency.csv",
],
"doctype": ["pptx", "pdf", "csv"],
"connector": ["connector1", "connector1", "connector2"],
"cct-accuracy": [0.812, 0.994, 0.887],
"cct-%missing": [0.001, 0.002, 0.041],
}
)
DUMMY_DF_ELEMENT_TYPE = pd.DataFrame(
{
"filename": [
"Bank Good Credit Loan.pptx",
"Performance-Audit-Discussion.pdf",
"currency.csv",
],
"doctype": ["pptx", "pdf", "csv"],
"connector": ["connector1", "connector1", "connector2"],
"element-type-accuracy": [0.812, 0.994, 0.887],
}
)
@pytest.fixture
def mock_dependencies():
with patch(
"unstructured.metrics.evaluate.calculate_accuracy"
) as mock_calculate_accuracy, patch(
"unstructured.metrics.evaluate.calculate_percent_missing_text"
) as mock_calculate_percent_missing_text, patch.object(
TextExtractionMetricsCalculator, "_get_ccts"
) as mock_get_ccts, patch(
"unstructured.metrics.evaluate.get_element_type_frequency"
) as mock_get_element_type_frequency, patch(
"unstructured.metrics.evaluate.calculate_element_type_percent_match"
) as mock_calculate_element_type_percent_match, patch(
"unstructured.metrics.evaluate._read_text_file"
) as mock_read_text_file, patch.object(
Path, "exists"
) as mock_path_exists, patch(
"unstructured.metrics.evaluate.TableEvalProcessor.from_json_files"
) as mock_table_eval_processor_from_json_files, patch.object(
TableStructureMetricsCalculator, "supported_metric_names"
) as mock_supported_metric_names:
mocks = {
"mock_calculate_accuracy": mock_calculate_accuracy,
"mock_calculate_percent_missing_text": mock_calculate_percent_missing_text,
"mock_get_ccts": mock_get_ccts,
"mock_get_element_type_frequency": mock_get_element_type_frequency,
"mock_read_text_file": mock_read_text_file,
"mock_calculate_element_type_percent_match": mock_calculate_element_type_percent_match,
"mock_table_eval_processor_from_json_files": mock_table_eval_processor_from_json_files,
"mock_supported_metric_names": mock_supported_metric_names,
"mock_path_exists": mock_path_exists,
}
# setup mocks
mocks["mock_calculate_accuracy"].return_value = 0.5
mocks["mock_calculate_percent_missing_text"].return_value = 0.5
mocks["mock_get_ccts"].return_value = ["output_cct", "source_cct"]
mocks["mock_get_element_type_frequency"].side_effect = [{"ele1": 1}, {"ele2": 3}]
mocks["mock_calculate_element_type_percent_match"].return_value = 0.5
mocks["mock_supported_metric_names"].return_value = ["table_level_acc"]
mocks["mock_path_exists"].return_value = True
mocks["mock_read_text_file"].side_effect = ["output_text", "source_text"]
yield mocks
@pytest.fixture()
def _cleanup_after_test():
"""Fixture for removing side-effects of running tests in this file."""
def remove_generated_directories():
"""Remove directories created from running tests."""
# Directories to be removed:
target_dir_names = [
"test_evaluate_results_cct",
"test_evaluate_results_cct_txt",
"test_evaluate_results_element_type",
"test_evaluate_result_table_structure",
]
subdirs = (d for d in os.scandir(TESTING_FILE_DIR) if d.is_dir())
for d in subdirs:
if d.name in target_dir_names:
shutil.rmtree(d.path)
# Run test as normal
yield
remove_generated_directories()
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_evaluation():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir, ground_truths_dir=source_dir
).calculate(export_dir=export_dir, visualize_progress=False, display_agg_df=False)
assert os.path.isfile(os.path.join(export_dir, "all-docs-cct.tsv"))
df = pd.read_csv(os.path.join(export_dir, "all-docs-cct.tsv"), sep="\t")
assert len(df) == 3
assert len(df.columns) == 5
assert df.iloc[0].filename == "Bank Good Credit Loan.pptx"
@pytest.mark.parametrize(
("calculator_class", "output_dirname", "source_dirname", "path", "expected_length", "kwargs"),
[
(
TextExtractionMetricsCalculator,
UNSTRUCTURED_CCT_DIRNAME,
GOLD_CCT_DIRNAME,
Path("Bank Good Credit Loan.pptx.txt"),
5,
{"document_type": "txt"},
),
(
TableStructureMetricsCalculator,
UNSTRUCTURED_TABLE_STRUCTURE_DIRNAME,
GOLD_TABLE_STRUCTURE_DIRNAME,
Path("IRS-2023-Form-1095-A.pdf.json"),
14,
{},
),
(
ElementTypeMetricsCalculator,
UNSTRUCTURED_OUTPUT_DIRNAME,
GOLD_ELEMENT_TYPE_DIRNAME,
Path("IRS-form-1987.pdf.json"),
4,
{},
),
],
)
def test_process_document_returns_the_correct_amount_of_values(
calculator_class, output_dirname, source_dirname, path, expected_length, kwargs
):
output_dir = Path(TESTING_FILE_DIR) / output_dirname
source_dir = Path(TESTING_FILE_DIR) / source_dirname
calculator = calculator_class(documents_dir=output_dir, ground_truths_dir=source_dir, **kwargs)
output_list = calculator._process_document(path)
assert len(output_list) == expected_length
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test", "mock_dependencies")
@pytest.mark.parametrize(
("calculator_class", "output_dirname", "source_dirname", "path", "kwargs"),
[
(
TextExtractionMetricsCalculator,
UNSTRUCTURED_CCT_DIRNAME,
GOLD_CCT_DIRNAME,
Path("2310.03502text_to_image_synthesis1-7.pdf.txt"),
{"document_type": "txt"},
),
],
)
def test_TextExtractionMetricsCalculator_process_document_returns_the_correct_doctype(
mock_dependencies, calculator_class, output_dirname, source_dirname, path, kwargs
):
output_dir = Path(TESTING_FILE_DIR) / output_dirname
source_dir = Path(TESTING_FILE_DIR) / source_dirname
mock_calculate_accuracy = mock_dependencies["mock_calculate_accuracy"]
mock_calculate_percent_missing_text = mock_dependencies["mock_calculate_percent_missing_text"]
mock_get_ccts = mock_dependencies["mock_get_ccts"]
calculator = calculator_class(documents_dir=output_dir, ground_truths_dir=source_dir, **kwargs)
output_list = calculator._process_document(path)
assert output_list[1] == ".pdf"
assert mock_calculate_accuracy.call_count == 1
assert mock_calculate_percent_missing_text.call_count == 1
assert mock_get_ccts.call_count == 1
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test", "mock_dependencies")
@pytest.mark.parametrize(
("calculator_class", "output_dirname", "source_dirname", "path", "kwargs"),
[
(
TableStructureMetricsCalculator,
UNSTRUCTURED_TABLE_STRUCTURE_DIRNAME,
GOLD_TABLE_STRUCTURE_DIRNAME,
Path("tablib-627mTABLES-2310.07875-p7.pdf.json"),
{},
),
# (
# ElementTypeMetricsCalculator,
# UNSTRUCTURED_OUTPUT_DIRNAME,
# GOLD_ELEMENT_TYPE_DIRNAME,
# Path("IRS-form.1987.pdf.json"),
# {},
# ),
],
)
def test_TableStructureMetricsCalculator_process_document_returns_the_correct_doctype(
mock_dependencies, calculator_class, output_dirname, source_dirname, path, kwargs
):
output_dir = Path(TESTING_FILE_DIR) / output_dirname
source_dir = Path(TESTING_FILE_DIR) / source_dirname
calculator = calculator_class(documents_dir=output_dir, ground_truths_dir=source_dir, **kwargs)
calculator._ground_truths_dir = source_dir
calculator._documents_dir = output_dir
calculator._ground_truth_paths = [source_dir / path]
mock_report = MagicMock()
mock_report.total_predicted_tables = 3
mock_report.table_evel_acc = 0.83
mock_table_eval_processor_from_json_files = mock_dependencies[
"mock_table_eval_processor_from_json_files"
]
mock_table_eval_processor_from_json_files.return_value.process_file.return_value = mock_report
output_list = calculator._process_document(path)
assert output_list[1] == ".pdf"
assert mock_table_eval_processor_from_json_files.call_count == 1
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test", "mock_dependencies")
@pytest.mark.parametrize(
("calculator_class", "output_dirname", "source_dirname", "path", "kwargs"),
[
(
ElementTypeMetricsCalculator,
UNSTRUCTURED_OUTPUT_DIRNAME,
GOLD_ELEMENT_TYPE_DIRNAME,
Path("IRS-form.1987.pdf.json"),
{},
),
],
)
def test_ElementTypeMetricsCalculator_process_document_returns_the_correct_doctype(
mock_dependencies, calculator_class, output_dirname, source_dirname, path, kwargs
):
output_dir = Path(TESTING_FILE_DIR) / output_dirname
source_dir = Path(TESTING_FILE_DIR) / source_dirname
calculator = calculator_class(documents_dir=output_dir, ground_truths_dir=source_dir, **kwargs)
mock_element_type_frequency = mock_dependencies["mock_get_element_type_frequency"]
mock_read_text_file = mock_dependencies["mock_read_text_file"]
mock_calculate_element_type_percent_match = mock_dependencies[
"mock_calculate_element_type_percent_match"
]
output_list = calculator._process_document(path)
assert output_list[1] == ".pdf"
assert mock_read_text_file.call_count == 2
assert mock_element_type_frequency.call_count == 2
assert mock_calculate_element_type_percent_match.call_count == 1
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_evaluation_type_txt():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_CCT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir, ground_truths_dir=source_dir, document_type="txt"
).calculate(export_dir=export_dir)
df = pd.read_csv(os.path.join(export_dir, "all-docs-cct.tsv"), sep="\t")
assert len(df) == 3
assert len(df.columns) == 5
assert df.iloc[0].filename == "Bank Good Credit Loan.pptx"
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_element_type_evaluation():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_ELEMENT_TYPE_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
ElementTypeMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
).calculate(export_dir=export_dir, visualize_progress=False)
assert os.path.isfile(os.path.join(export_dir, "all-docs-element-type-frequency.tsv"))
df = pd.read_csv(os.path.join(export_dir, "all-docs-element-type-frequency.tsv"), sep="\t")
assert len(df) == 1
assert len(df.columns) == 4
assert df.iloc[0].filename == "IRS-form-1987.pdf"
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_table_structure_evaluation():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_TABLE_STRUCTURE_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_TABLE_STRUCTURE_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_result_table_structure")
TableStructureMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
).calculate(export_dir=export_dir, visualize_progress=False)
assert os.path.isfile(os.path.join(export_dir, "all-docs-table-structure-accuracy.tsv"))
assert os.path.isfile(os.path.join(export_dir, "aggregate-table-structure-accuracy.tsv"))
df = pd.read_csv(os.path.join(export_dir, "all-docs-table-structure-accuracy.tsv"), sep="\t")
agg_df = pd.read_csv(
os.path.join(export_dir, "aggregate-table-structure-accuracy.tsv"), sep="\t"
).set_index("metric")
assert len(df) == 2
assert len(df.columns) == 15
assert df.iloc[1].filename == "IRS-2023-Form-1095-A.pdf"
assert (
np.round(np.average(df["table_level_acc"], weights=df["total_tables"]), 3)
== agg_df.loc["table_level_acc", "average"]
)
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_takes_list():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
output_list = ["currency.csv.json"]
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
).on_files(document_paths=output_list).calculate(export_dir=export_dir)
# check that only the listed files are included
assert os.path.isfile(os.path.join(export_dir, "all-docs-cct.tsv"))
df = pd.read_csv(os.path.join(export_dir, "all-docs-cct.tsv"), sep="\t")
assert len(df) == len(output_list)
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_with_grouping():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
group_by="doctype",
).calculate(export_dir=export_dir)
df = pd.read_csv(os.path.join(export_dir, "all-doctype-agg-cct.tsv"), sep="\t")
assert len(df) == 4 # metrics row and doctype rows
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_wrong_type():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
with pytest.raises(ValueError):
TextExtractionMetricsCalculator(
documents_dir=output_dir, ground_truths_dir=source_dir, document_type="invalid type"
).calculate(export_dir=export_dir)
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
@pytest.mark.parametrize(("grouping", "count_row"), [("doctype", 3), ("connector", 2)])
def test_get_mean_grouping_df_input(grouping: str, count_row: int):
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
get_mean_grouping(
group_by=grouping,
data_input=DUMMY_DF_CCT,
export_dir=export_dir,
eval_name="text_extraction",
)
grouped_df = pd.read_csv(os.path.join(export_dir, f"all-{grouping}-agg-cct.tsv"), sep="\t")
assert grouped_df[grouping].dropna().nunique() == count_row
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_tsv_input():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
).calculate(export_dir=export_dir)
filename = os.path.join(export_dir, "all-docs-cct.tsv")
get_mean_grouping(
group_by="doctype",
data_input=filename,
export_dir=export_dir,
eval_name="text_extraction",
)
grouped_df = pd.read_csv(os.path.join(export_dir, "all-doctype-agg-cct.tsv"), sep="\t")
assert grouped_df["doctype"].dropna().nunique() == 3
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_invalid_group():
output_dir = os.path.join(TESTING_FILE_DIR, UNSTRUCTURED_OUTPUT_DIRNAME)
source_dir = os.path.join(TESTING_FILE_DIR, GOLD_CCT_DIRNAME)
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
TextExtractionMetricsCalculator(
documents_dir=output_dir,
ground_truths_dir=source_dir,
).calculate(export_dir=export_dir)
df = pd.read_csv(os.path.join(export_dir, "all-docs-cct.tsv"), sep="\t")
with pytest.raises(ValueError):
get_mean_grouping(
group_by="invalid",
data_input=df,
export_dir=export_dir,
eval_name="text_extraction",
)
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_text_extraction_grouping_empty_df():
empty_df = pd.DataFrame()
with pytest.raises(SystemExit):
get_mean_grouping("doctype", empty_df, "some_dir", eval_name="text_extraction")
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_missing_grouping_column():
df_with_no_grouping = pd.DataFrame({"some_column": [1, 2, 3]})
with pytest.raises(SystemExit):
get_mean_grouping("doctype", df_with_no_grouping, "some_dir", "text_extraction")
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_all_null_grouping_column():
df_with_null_grouping = pd.DataFrame({"doctype": [None, None, None]})
with pytest.raises(SystemExit):
get_mean_grouping("doctype", df_with_null_grouping, "some_dir", eval_name="text_extraction")
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_invalid_eval_name():
with pytest.raises(ValueError):
get_mean_grouping("doctype", DUMMY_DF_ELEMENT_TYPE, "some_dir", eval_name="invalid")
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
@pytest.mark.parametrize(("group_by", "count_row"), [("doctype", 3), ("connector", 2)])
def test_get_mean_grouping_element_type(group_by: str, count_row: int):
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_element_type")
get_mean_grouping(
group_by=group_by,
data_input=DUMMY_DF_ELEMENT_TYPE,
export_dir=export_dir,
eval_name="element_type",
)
grouped_df = pd.read_csv(
os.path.join(export_dir, f"all-{group_by}-agg-element-type.tsv"), sep="\t"
)
assert grouped_df[group_by].dropna().nunique() == count_row
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_filter_metrics():
with open(os.path.join(TESTING_FILE_DIR, "filter_list.txt"), "w") as file:
file.write("Bank Good Credit Loan.pptx\n")
file.write("Performance-Audit-Discussion.pdf\n")
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
filter_metrics(
data_input=DUMMY_DF_CCT,
filter_list=os.path.join(TESTING_FILE_DIR, "filter_list.txt"),
filter_by="filename",
export_filename="filtered_metrics.tsv",
export_dir=export_dir,
return_type="file",
)
filtered_df = pd.read_csv(os.path.join(export_dir, "filtered_metrics.tsv"), sep="\t")
assert len(filtered_df) == 2
assert filtered_df["filename"].iloc[0] == "Bank Good Credit Loan.pptx"
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_all_file():
with open(os.path.join(TESTING_FILE_DIR, "filter_list.txt"), "w") as file:
file.write("Bank Good Credit Loan.pptx\n")
file.write("Performance-Audit-Discussion.pdf\n")
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
filter_metrics(
data_input=DUMMY_DF_CCT,
filter_list=["Bank Good Credit Loan.pptx", "Performance-Audit-Discussion.pdf"],
filter_by="filename",
export_filename="filtered_metrics.tsv",
export_dir=export_dir,
return_type="file",
)
filtered_df = pd.read_csv(os.path.join(export_dir, "filtered_metrics.tsv"), sep="\t")
get_mean_grouping(
group_by="all",
data_input=filtered_df,
export_dir=export_dir,
eval_name="text_extraction",
export_filename="two-filename-agg-cct.tsv",
)
grouped_df = pd.read_csv(os.path.join(export_dir, "two-filename-agg-cct.tsv"), sep="\t")
assert np.isclose(float(grouped_df.iloc[1, 0]), 0.903)
assert np.isclose(float(grouped_df.iloc[1, 1]), 0.129)
assert np.isclose(float(grouped_df.iloc[1, 2]), 0.091)
@pytest.mark.skipif(is_in_docker, reason="Skipping this test in Docker container")
@pytest.mark.usefixtures("_cleanup_after_test")
def test_get_mean_grouping_all_file_txt():
with open(os.path.join(TESTING_FILE_DIR, "filter_list.txt"), "w") as file:
file.write("Bank Good Credit Loan.pptx\n")
file.write("Performance-Audit-Discussion.pdf\n")
export_dir = os.path.join(TESTING_FILE_DIR, "test_evaluate_results_cct")
filter_metrics(
data_input=DUMMY_DF_CCT,
filter_list=os.path.join(TESTING_FILE_DIR, "filter_list.txt"),
filter_by="filename",
export_filename="filtered_metrics.tsv",
export_dir=export_dir,
return_type="file",
)
filtered_df = pd.read_csv(os.path.join(export_dir, "filtered_metrics.tsv"), sep="\t")
get_mean_grouping(
group_by="all",
data_input=filtered_df,
export_dir=export_dir,
eval_name="text_extraction",
export_filename="two-filename-agg-cct.tsv",
)
grouped_df = pd.read_csv(os.path.join(export_dir, "two-filename-agg-cct.tsv"), sep="\t")
assert np.isclose(float(grouped_df.iloc[1, 0]), 0.903)
assert np.isclose(float(grouped_df.iloc[1, 1]), 0.129)
assert np.isclose(float(grouped_df.iloc[1, 2]), 0.091)

View File

@@ -0,0 +1,14 @@
from unstructured.metrics.table.table_alignment import TableAlignment
def test_get_element_level_alignment_when_no_match():
example_table = [{"row_index": 0, "col_index": 0, "content": "a"}]
metrics = TableAlignment.get_element_level_alignment(
predicted_table_data=[example_table],
ground_truth_table_data=[example_table],
matched_indices=[-1],
)
assert metrics["col_index_acc"] == 0
assert metrics["row_index_acc"] == 0
assert metrics["row_content_acc"] == 0
assert metrics["col_content_acc"] == 0

View File

@@ -0,0 +1,33 @@
import pytest
from unstructured.metrics.table.table_eval import calculate_table_detection_metrics
@pytest.mark.parametrize(
("matched_indices", "ground_truth_tables_number", "expected_metrics"),
[
([0, 1, 2], 3, (1, 1, 1)), # everything was predicted correctly
([2, 1, 0], 3, (1, 1, 1)), # everything was predicted correctly
(
[-1, 2, -1, 1, 0, -1],
3,
(1, 0.5, 0.66),
), # some false positives, all tables matched, too many predictions
([2, 2, 1, 1], 8, (0.25, 0.5, 0.33)),
# Some false negatives, all predictions matched with gt, not enough predictions
# The precision here is not 1 as only one from tables matched with '1' index can be correct
([1, -1], 2, (0.5, 0.5, 0.5)), # typical case with false positive and false negative
([-1, -1, -1], 2, (0, 0, 0)), # nothing was matched
([-1, -1, -1], 0, (0, 0, 0)), # there was no table in ground truth
([], 0, (0, 0, 0)), # just zeros to account for errors
],
)
def test_calculate_table_metrics(matched_indices, ground_truth_tables_number, expected_metrics):
expected_recall, expected_precision, expected_f1 = expected_metrics
pred_recall, pred_precision, pred_f1 = calculate_table_detection_metrics(
matched_indices=matched_indices, ground_truth_tables_number=ground_truth_tables_number
)
assert pred_recall == expected_recall
assert pred_precision == expected_precision
assert pred_f1 == pytest.approx(expected_f1, abs=0.01)

View File

@@ -0,0 +1,33 @@
import pytest
from unstructured.metrics.table.table_formats import SimpleTableCell
@pytest.mark.parametrize(
("row_nums", "column_nums", "x", "y", "w", "h"),
[
([3, 2, 1], [6, 7], 6, 1, 2, 3),
([2], [6, 7], 6, 2, 2, 1),
([1, 2, 3], [20], 20, 1, 1, 3),
([5], [5], 5, 5, 1, 1),
],
)
def test_simple_table_cell_parsing_from_table_transformer_when_expected_input(
row_nums, column_nums, x, y, w, h
):
table_transformer_cell = {"row_nums": row_nums, "column_nums": column_nums, "cell text": "text"}
transformed_cell = SimpleTableCell.from_table_transformer_cell(table_transformer_cell)
expected_cell = SimpleTableCell(x=x, y=y, w=w, h=h, content="text")
assert expected_cell == transformed_cell
def test_simple_table_cell_parsing_from_table_transformer_when_missing_row_nums():
cell = {"row_nums": [], "column_nums": [1], "cell text": "text"}
with pytest.raises(ValueError, match='has missing values under "row_nums" key'):
SimpleTableCell.from_table_transformer_cell(cell)
def test_simple_table_cell_parsing_from_table_transformer_when_missing_column_nums():
cell = {"row_nums": [1], "column_nums": [], "cell text": "text"}
with pytest.raises(ValueError, match='has missing values under "column_nums" key'):
SimpleTableCell.from_table_transformer_cell(cell)

View File

@@ -0,0 +1,700 @@
from unittest import mock
import numpy as np
import pytest
from test_unstructured.unit_utils import example_doc_path
from unstructured.metrics.table.table_alignment import TableAlignment
from unstructured.metrics.table.table_eval import TableEvalProcessor
from unstructured.metrics.table_structure import (
eval_table_transformer_for_file,
image_or_pdf_to_dataframe,
)
@pytest.mark.parametrize(
"filename",
[
example_doc_path("img/table-multi-row-column-cells.png"),
example_doc_path("pdf/table-multi-row-column-cells.pdf"),
],
)
def test_image_or_pdf_to_dataframe(filename):
df = image_or_pdf_to_dataframe(filename)
assert ["Blind", "5", "1", "4", "34.5%, n=1", "1199 sec, n=1"] in df.values
def test_eval_table_transformer_for_file():
score = eval_table_transformer_for_file(
example_doc_path("img/table-multi-row-column-cells.png"),
example_doc_path("table-multi-row-column-cells-actual.csv"),
)
# avoid severe degradation of performance
assert 0.8 < score < 1
def test_table_eval_processor_simple():
prediction = [
{
"type": "Table",
"metadata": {
"text_as_html": """<table><thead><tr><th>r1c1</th><th>r1c2</th></tr></thead>
<tbody><tr><td>r2c1</td><td>r2c2</td></tr></tbody></table>"""
},
}
]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c1",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c1",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c2",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c2",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 1.0
assert result.element_row_level_index_acc == 1.0
assert result.element_col_level_index_acc == 1.0
assert result.element_row_level_content_acc == 1.0
assert result.element_col_level_content_acc == 1.0
def test_table_eval_processor_simple_when_input_as_cells():
prediction = [
{
"type": "Table",
"metadata": {
"table_as_cells": [
{
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c2",
},
{
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c1",
},
{
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c1",
},
{
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c2",
},
]
},
}
]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c1",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c1",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c2",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c2",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth, source_type="cells")
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 1.0
assert result.element_row_level_index_acc == 1.0
assert result.element_col_level_index_acc == 1.0
assert result.element_row_level_content_acc == 1.0
assert result.element_col_level_content_acc == 1.0
def test_table_eval_processor_when_wrong_source_type():
prediction = [
{
"type": "Table",
"metadata": {"table_as_cells": []},
}
]
ground_truth = [
{
"type": "Table",
"text": [],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth, source_type="wrong_type")
with pytest.raises(ValueError):
te_processor.process_file()
@pytest.mark.parametrize(
"text_as_html",
[
"""
<table>
<thead>
<tr>
<th>r1c1</th>
<th>r1c2</th>
</tr>
</thead>
<tbody>
<tr>
<td>r2c1</td>
<td>r2c2</td>
</tr>
<tr>
<td>r3c1</td>
<td>r3c2</td>
</tr>
</tbody>
</table>
""",
"""
<table>
<tr>
<th>r1c1</th>
<th>r1c2</th>
</tr>
<tbody>
<tr>
<td>r2c1</td>
<td>r2c2</td>
</tr>
<tr>
<td>r3c1</td>
<td>r3c2</td>
</tr>
</tbody>
</table>
""",
"""
<table>
</tbody>
<tr>
<td>r1c1</td>
<td>r1c2</td>
</tr>
<tr>
<td>r2c1</td>
<td>r2c2</td>
</tr>
<tr>
<td>r3c1</td>
<td>r3c2</td>
</tr>
</tbody>
</table>
""",
],
)
def test_table_eval_processor_various_table_html_structures(text_as_html):
prediction = [{"type": "Table", "metadata": {"text_as_html": text_as_html}}]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c1",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c1",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c2",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c2",
},
{
"id": "364f4a17-2979-4506-ae77-e8adf8e3f554",
"x": 0,
"y": 2,
"w": 1,
"h": 1,
"content": "r3c1",
},
{
"id": "30f87503-ac1f-4db1-b924-b316af585702",
"x": 1,
"y": 2,
"w": 1,
"h": 1,
"content": "r3c2",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 1.0
assert result.element_row_level_index_acc == 1.0
assert result.element_col_level_index_acc == 1.0
assert result.element_row_level_content_acc == 1.0
assert result.element_col_level_content_acc == 1.0
def test_table_eval_processor_non_str_values_in_table():
prediction = [
{
"type": "Table",
"metadata": {
"text_as_html": """
<table>
<thead>
<tr>
<th>11</th>
<th>12</th>
</tr>
</thead>
<tbody>
<tr>
<td>21</td>
<td>22</td>
</tr>
</tbody>
</table>"""
},
}
]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "11",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "21",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "12",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "22",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 1.0
assert result.element_row_level_index_acc == 1.0
assert result.element_col_level_index_acc == 1.0
assert result.element_row_level_content_acc == 1.0
assert result.element_col_level_content_acc == 1.0
def test_table_eval_processor_merged_cells():
prediction = [
{
"type": "Table",
"metadata": {
"text_as_html": """
<table>
<thead>
<tr>
<th rowspan="2">r1c1</th>
<th>r1c2</th>
<th colspan="2">r1c3</th>
</tr>
<tr>
<th>r2c2</th>
<th>r2c3</th>
<th>r2c4</th>
</tr>
</thead>
<tbody>
<tr>
<td>r3c1</td>
<td>r3c2</td>
<td colspan="2" rowspan="2">r3c3</td>
</tr>
<tr>
<td>r4c1</td>
<td>r4c2</td>
</tr>
</tbody>
</table>
"""
},
}
]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "f399ef57-5b88-4509-8971-9cb63246866e",
"x": 0,
"y": 0,
"w": 1,
"h": 2,
"content": "r1c1",
},
{
"id": "2dfdec2f-e8f3-4be7-a6ac-8ff21c4e8556",
"x": 0,
"y": 2,
"w": 1,
"h": 1,
"content": "r3c1",
},
{
"id": "9c771c58-88c7-49d8-9c12-85d0e44b920e",
"x": 0,
"y": 3,
"w": 1,
"h": 1,
"content": "r4c1",
},
{
"id": "5bd6f3f0-34c5-495b-8a28-c4ac96989ef8",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "r1c2",
},
{
"id": "7b8e6bc2-a310-4dd6-997c-313f951e7f96",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c2",
},
{
"id": "1c152ad4-12fa-4a7b-90de-a992aa6410a4",
"x": 1,
"y": 2,
"w": 1,
"h": 1,
"content": "r3c2",
},
{
"id": "55063f64-0003-4217-b6ca-aff5914793ff",
"x": 1,
"y": 3,
"w": 1,
"h": 1,
"content": "r4c2",
},
{
"id": "22852e86-0e22-4d32-b63a-9ba7dd4118a2",
"x": 2,
"y": 0,
"w": 2,
"h": 1,
"content": "r1c3",
},
{
"id": "eae013c5-5597-4a8b-9771-82e28c5c5cba",
"x": 2,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c3",
},
{
"id": "0dea3a42-8523-4d6e-9e70-d65cc2314678",
"x": 2,
"y": 2,
"w": 2,
"h": 2,
"content": "r3c3",
},
{
"id": "60093e2c-d3e2-4146-92b5-97a2fc16c061",
"x": 3,
"y": 1,
"w": 1,
"h": 1,
"content": "r2c4",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 1.0
assert result.element_row_level_index_acc == 1.0
assert result.element_col_level_index_acc == 1.0
assert result.element_row_level_content_acc == 1.0
assert result.element_col_level_content_acc == 1.0
def test_table_eval_processor_when_no_match_with_pred():
prediction = [
{
"type": "Table",
"metadata": {"text_as_html": """<table><tr><td>Some cell</td></tr></table>"""},
}
]
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "11",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "21",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "12",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "22",
},
],
}
]
with mock.patch.object(TableAlignment, "get_table_level_alignment") as align_fn:
align_fn.return_value = [-1]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 0
assert result.element_row_level_index_acc == 0
assert result.element_col_level_index_acc == 0
assert result.element_row_level_content_acc == 0
assert result.element_col_level_content_acc == 0
def test_table_eval_processor_when_no_tables():
prediction = [{}]
ground_truth = [{}]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 0
assert result.table_level_acc == 1
assert np.isnan(result.element_row_level_index_acc)
assert np.isnan(result.element_col_level_index_acc)
assert np.isnan(result.element_row_level_content_acc)
assert np.isnan(result.element_col_level_content_acc)
def test_table_eval_processor_when_only_gt():
prediction = []
ground_truth = [
{
"type": "Table",
"text": [
{
"id": "ee862c7a-d27e-4484-92de-4faa42a63f3b",
"x": 0,
"y": 0,
"w": 1,
"h": 1,
"content": "11",
},
{
"id": "6237ac7b-bfc8-40d2-92f2-d138277205e2",
"x": 0,
"y": 1,
"w": 1,
"h": 1,
"content": "21",
},
{
"id": "9d0933a9-5984-4cad-80d9-6752bf9bc4df",
"x": 1,
"y": 0,
"w": 1,
"h": 1,
"content": "12",
},
{
"id": "1152d043-5ead-4ab8-8b88-888d48831ac2",
"x": 1,
"y": 1,
"w": 1,
"h": 1,
"content": "22",
},
],
}
]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 1
assert result.table_level_acc == 0
assert result.element_row_level_index_acc == 0
assert result.element_col_level_index_acc == 0
assert result.element_row_level_content_acc == 0
assert result.element_col_level_content_acc == 0
def test_table_eval_processor_when_only_pred():
prediction = [
{
"type": "Table",
"metadata": {"text_as_html": """<table><tr><td>Some cell</td></tr></table>"""},
}
]
ground_truth = [{}]
te_processor = TableEvalProcessor(prediction, ground_truth)
result = te_processor.process_file()
assert result.total_tables == 0
assert result.table_level_acc == 0
assert result.element_row_level_index_acc == 0
assert result.element_col_level_index_acc == 0
assert result.element_row_level_content_acc == 0
assert result.element_col_level_content_acc == 0

View File

@@ -0,0 +1,859 @@
import re
import pytest
from unstructured.metrics import text_extraction
from unstructured.metrics.table.table_extraction import (
deckerd_table_to_html,
extract_cells_from_table_as_cells,
extract_cells_from_text_as_html,
html_table_to_deckerd,
)
from unstructured.partition.auto import partition
def test_calculate_edit_distance():
source_cct = "I like pizza. I like bagels."
source_cct_word_space = "I like p i z z a . I like bagles."
source_cct_spaces = re.sub(r"\s+", " ", " ".join(source_cct))
source_cct_no_space = source_cct.replace(" ", "")
source_cct_one_sentence = "I like pizza."
source_cct_missing_word = "I like pizza. I like ."
source_cct_addn_char = "I like pizza. I like beagles."
source_cct_dup_word = "I like pizza pizza. I like bagels."
assert (
round(text_extraction.calculate_edit_distance(source_cct, source_cct, return_as="score"), 2)
== 1.0
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_word_space,
source_cct,
return_as="score",
),
2,
)
== 0.75
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_spaces,
source_cct,
return_as="score",
),
2,
)
== 0.39
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_no_space,
source_cct,
return_as="score",
),
2,
)
== 0.64
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_one_sentence,
source_cct,
return_as="score",
),
2,
)
== 0.0
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_missing_word,
source_cct,
return_as="score",
),
2,
)
== 0.57
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_addn_char,
source_cct,
return_as="score",
),
2,
)
== 0.89
)
assert (
round(
text_extraction.calculate_edit_distance(
source_cct_dup_word,
source_cct,
return_as="score",
),
2,
)
== 0.79
)
@pytest.mark.parametrize(
("filename", "standardize_whitespaces", "expected_score", "expected_distance"),
[
("fake-text.txt", False, 0.78, 38),
("fake-text.txt", True, 0.92, 12),
],
)
def test_calculate_edit_distance_with_filename(
filename, standardize_whitespaces, expected_score, expected_distance
):
with open("example-docs/fake-text.txt") as f:
source_cct = f.read()
elements = partition(filename=f"example-docs/{filename}")
output_cct = "\n".join([str(el) for el in elements])
score = text_extraction.calculate_edit_distance(
output_cct, source_cct, return_as="score", standardize_whitespaces=standardize_whitespaces
)
distance = text_extraction.calculate_edit_distance(
output_cct,
source_cct,
return_as="distance",
standardize_whitespaces=standardize_whitespaces,
)
assert score >= 0
assert score <= 1.0
assert distance >= 0
assert round(score, 2) == expected_score
assert distance == expected_distance
@pytest.mark.parametrize(
("text1", "text2"),
[
(
"The dog\rloved the cat, but\t\n the cat\tloved the\n cow",
"The dog loved the cat, but the cat loved the cow",
),
(
"Hello my\tname\tis H a r p e r, \nwhat's your\vname?",
"Hello my name is H a r p e r, what's your name?",
),
(
"I have a\t\n\tdog and a\tcat,\fI love my\n\n\n\ndog.",
"I have a dog and a cat, I love my dog.",
),
(
"""
Name Age City Occupation
Alice 30 New York Engineer
Bob 25 Los Angeles Designer
Charlie 35 Chicago Teacher
David 40 San Francisco Developer
""",
"""
Name\tAge\tCity\tOccupation
Alice\t30\tNew York\tEngineer
Bob\t25\tLos Angeles\tDesigner
Charlie\t35\tChicago\tTeacher
David\t40\tSan Francisco\tDeveloper
""",
),
(
"""
Name\tAge\tCity\tOccupation
Alice\t30\tNew York\tEngineer
Bob\t25\tLos Angeles\tDesigner
Charlie\t35\tChicago\tTeacher
David\t40\tSan Francisco\tDeveloper
""",
"Name\tAge\tCity\tOccupation\n\n \nAlice\t30\tNew York\tEngineer\nBob\t25\tLos Angeles\tDesigner\nCharlie\t35\tChicago\tTeacher\nDavid\t40\tSan Francisco\tDeveloper", # noqa: E501
),
],
)
def test_calculate_edit_distance_with_various_whitespace_1(text1, text2):
assert (
text_extraction.calculate_edit_distance(
text1, text2, return_as="score", standardize_whitespaces=True
)
== 1.0
)
assert (
text_extraction.calculate_edit_distance(
text1, text2, return_as="distance", standardize_whitespaces=True
)
== 0
)
assert (
text_extraction.calculate_edit_distance(
text1, text2, return_as="score", standardize_whitespaces=False
)
< 1.0
)
assert (
text_extraction.calculate_edit_distance(
text1, text2, return_as="distance", standardize_whitespaces=False
)
> 0
)
def test_calculate_edit_distance_with_various_whitespace_2():
source_cct_tabs = """
Name\tAge\tCity\tOccupation
Alice\t30\tNew York\tEngineer
Bob\t25\tLos Angeles\tDesigner
Charlie\t35\tChicago\tTeacher
David\t40\tSan Francisco\tDeveloper
"""
source_cct_with_borders = """
| Name | Age | City | Occupation |
|---------|-----|--------------|----------------|
| Alice | 30 | New York | Engineer |
| Bob | 25 | Los Angeles | Designer |
| Charlie | 35 | Chicago | Teacher |
| David | 40 | San Francisco| Developer |
"""
assert text_extraction.calculate_edit_distance(
source_cct_tabs, source_cct_with_borders, return_as="score", standardize_whitespaces=True
) > text_extraction.calculate_edit_distance(
source_cct_tabs, source_cct_with_borders, return_as="score", standardize_whitespaces=False
)
assert text_extraction.calculate_edit_distance(
source_cct_tabs, source_cct_with_borders, return_as="distance", standardize_whitespaces=True
) < text_extraction.calculate_edit_distance(
source_cct_tabs,
source_cct_with_borders,
return_as="distance",
standardize_whitespaces=False,
)
@pytest.mark.parametrize(
("text", "expected"),
[
(
"The dog loved the cat, but the cat loved the cow",
{"the": 4, "cat": 2, "loved": 2, "dog": 1, "but": 1, "cow": 1},
),
(
"Hello my name is H a r p e r, what's your name?",
{"hello": 1, "my": 1, "name": 2, "is": 1, "what's": 1, "your": 1},
),
(
"I have a dog and a cat, I love my dog.",
{"i": 2, "have": 1, "a": 2, "dog": 2, "and": 1, "cat": 1, "love": 1, "my": 1},
),
(
"My dog's hair is red, but the dogs' houses are blue.",
{
"my": 1,
"dog's": 1,
"hair": 1,
"is": 1,
"red": 1,
"but": 1,
"the": 1,
"dogs'": 1,
"houses": 1,
"are": 1,
"blue": 1,
},
),
(
"""Sometimes sentences have a dash - like this one!
A hyphen connects 2 words with no gap: easy-peasy.""",
{
"sometimes": 1,
"sentences": 1,
"have": 1,
"a": 2,
"dash": 1,
"like": 1,
"this": 1,
"one": 1,
"hyphen": 1,
"connects": 1,
"2": 1,
"words": 1,
"with": 1,
"no": 1,
"gap": 1,
"easy-peasy": 1,
},
),
],
)
def test_bag_of_words(text, expected):
assert text_extraction.bag_of_words(text) == expected
@pytest.mark.parametrize(
("text", "expected"),
[
(
"The dog\rloved the cat, but\t\n the cat\tloved the\n cow\n\n",
"The dog loved the cat, but the cat loved the cow",
),
(
"\n\nHello my\tname\tis H a r p e r, \nwhat's your\vname?",
"Hello my name is H a r p e r, what's your name?",
),
(
"I have a\t\n\tdog and a\tcat,\fI love my\n\n\n\ndog.",
"I have a dog and a cat, I love my dog.",
),
(
"""L is for the way you look at me
O is for the only one I see
V is very, very extraordinary
E is even more than anyone that you adore can""",
"L is for the way you look at me O is for the only one I see V is very, very extraordinary E is even more than anyone that you adore can", # noqa: E501
),
(
"""
| Name | Age | City | Occupation |
|---------|-----|--------------|----------------|
| Alice | 30 | New York | Engineer |
| Bob | 25 | Los Angeles | Designer |
| Charlie | 35 | Chicago | Teacher |
| David | 40 | San Francisco| Developer |
""",
"| Name | Age | City | Occupation | |---------|-----|--------------|----------------| | Alice | 30 | New York | Engineer | | Bob | 25 | Los Angeles | Designer | | Charlie | 35 | Chicago | Teacher | | David | 40 | San Francisco| Developer |", # noqa: E501
),
],
)
def test_prepare_string(text, expected):
assert text_extraction.prepare_str(text, standardize_whitespaces=True) == expected
assert text_extraction.prepare_str(text) == text
@pytest.mark.parametrize(
("input_text", "expected_output"),
[
# Mixed quotes in longer sentences
(
"She said \"Hello\" and then whispered 'Goodbye' before leaving.",
"She said \"Hello\" and then whispered 'Goodbye' before leaving.",
),
# Double low-9 quotes with complex content
(
"„To be, or not to be, that is the question\" - Shakespeare's famous quote.",
'"To be, or not to be, that is the question" - Shakespeare\'s famous quote.',
),
# Angle quotes with nested quotes
(
'«When he said "life is beautiful," I believed him» wrote Maria.',
'"When he said "life is beautiful," I believed him" wrote Maria.',
),
# Heavy ornament quotes in dialogue
(
"❝Do you remember when we first met?❞ she asked with a smile.",
'"Do you remember when we first met?" she asked with a smile.',
),
# Double prime quotes with punctuation
(
"〝The meeting starts at 10:00, don't be late!〟 announced the manager.",
'"The meeting starts at 10:00, don\'t be late!" announced the manager.',
),
# Corner brackets with nested quotes
(
'「He told me "This is important" yesterday」, she explained.',
"'He told me \"This is important\" yesterday', she explained.",
),
# White corner brackets with multiple sentences
(
"『The sun was setting. The birds were singing. It was peaceful.』",
"'The sun was setting. The birds were singing. It was peaceful.'",
),
# Vertical corner brackets with numbers and special characters
("﹂Meeting #123 @ 15:00 - Don't forget!﹁", "'Meeting #123 @ 15:00 - Don't forget!'"),
# Complex mixed quote types
(
'「Hello」, ❝World❞, "Test", \'Example\', „Quote", «Final»',
'\'Hello\', "World", "Test", \'Example\', "Quote", "Final"',
),
# Quotes with multiple apostrophes
("It's John's book, isn't it?", "It's John's book, isn't it?"),
# Single angle quotes with nested content
(
'Testing the system\'s capability for "quoted" text',
"'Testing the system's capability for \"quoted\" text'",
),
# Heavy single ornament quotes with multiple sentences
(
"❛First sentence. Second sentence. Third sentence.❜",
"'First sentence. Second sentence. Third sentence.'",
),
# Mix of various quote types in complex text
(
'「Chapter 1」: ❝The Beginning❞ - „A new story" begins «today».',
'\'Chapter 1\': "The Beginning" - "A new story" begins "today".',
),
],
)
def test_standardize_quotes(input_text, expected_output):
assert text_extraction.standardize_quotes(input_text) == expected_output
@pytest.mark.parametrize(
("output_text", "source_text", "expected_percentage"),
[
(
"extra",
"",
0,
),
(
"",
"Source text has a sentence.",
1,
),
(
"The original s e n t e n c e is normal.",
"The original sentence is normal...",
0.2,
),
(
"We saw 23% improvement in this quarter.",
"We saw 23% improvement in sales this quarter.",
0.125,
),
(
"no",
"Is it possible to have more than everything missing?",
1,
),
],
)
def test_calculate_percent_missing_text(output_text, source_text, expected_percentage):
assert (
text_extraction.calculate_percent_missing_text(output_text, source_text)
== expected_percentage
)
@pytest.mark.parametrize(
("table_as_cells", "expected_extraction"),
[
pytest.param(
[
{"x": 0, "y": 0, "w": 1, "h": 1, "content": "Month A."},
{"x": 0, "y": 1, "w": 1, "h": 1, "content": "22"},
],
[
{"row_index": 0, "col_index": 0, "content": "Month A."},
{"row_index": 1, "col_index": 0, "content": "22"},
],
id="Simple table, 1 head cell, 1 body cell, no spans",
),
pytest.param(
[
{"x": 0, "y": 0, "w": 1, "h": 1, "content": "Month A."},
{"x": 1, "y": 0, "w": 1, "h": 1, "content": "Month B."},
{"x": 2, "y": 0, "w": 1, "h": 1, "content": "Month C."},
{"x": 0, "y": 1, "w": 1, "h": 1, "content": "11"},
{"x": 1, "y": 1, "w": 1, "h": 1, "content": "12"},
{"x": 2, "y": 1, "w": 1, "h": 1, "content": "13"},
{"x": 0, "y": 2, "w": 1, "h": 1, "content": "21"},
{"x": 1, "y": 2, "w": 1, "h": 1, "content": "22"},
{"x": 2, "y": 2, "w": 1, "h": 1, "content": "23"},
],
[
{"row_index": 0, "col_index": 0, "content": "Month A."},
{"row_index": 0, "col_index": 1, "content": "Month B."},
{"row_index": 0, "col_index": 2, "content": "Month C."},
{"row_index": 1, "col_index": 0, "content": "11"},
{"row_index": 1, "col_index": 1, "content": "12"},
{"row_index": 1, "col_index": 2, "content": "13"},
{"row_index": 2, "col_index": 0, "content": "21"},
{"row_index": 2, "col_index": 1, "content": "22"},
{"row_index": 2, "col_index": 2, "content": "23"},
],
id="Simple table, 3 head cell, 5 body cell, no spans",
),
# +----------+---------------------+----------+
# | | h1col23 | h1col4 |
# | h12col1 |----------+----------+----------|
# | | h2col2 | h2col34 |
# |----------|----------+----------+----------+
# | r3col1 | r3col2 | |
# |----------+----------| r34col34 |
# | r4col12 | |
# +----------+----------+----------+----------+
pytest.param(
[
{
"y": 0,
"x": 0,
"w": 2,
"h": 1,
"content": "h12col1",
},
{
"y": 0,
"x": 1,
"w": 1,
"h": 2,
"content": "h1col23",
},
{
"y": 0,
"x": 3,
"w": 1,
"h": 1,
"content": "h1col4",
},
{
"y": 1,
"x": 1,
"w": 1,
"h": 1,
"content": "h2col2",
},
{
"y": 1,
"x": 2,
"w": 1,
"h": 2,
"content": "h2col34",
},
{
"y": 2,
"x": 0,
"w": 1,
"h": 1,
"content": "r3col1",
},
{
"y": 2,
"x": 1,
"w": 1,
"h": 1,
"content": "r3col2",
},
{
"y": 2,
"x": 2,
"w": 2,
"h": 2,
"content": "r34col34",
},
{
"y": 3,
"x": 0,
"w": 1,
"h": 2,
"content": "r4col12",
},
],
[
{
"row_index": 0,
"col_index": 0,
"content": "h12col1",
},
{
"row_index": 0,
"col_index": 1,
"content": "h1col23",
},
{
"row_index": 0,
"col_index": 3,
"content": "h1col4",
},
{
"row_index": 1,
"col_index": 1,
"content": "h2col2",
},
{
"row_index": 1,
"col_index": 2,
"content": "h2col34",
},
{
"row_index": 2,
"col_index": 0,
"content": "r3col1",
},
{
"row_index": 2,
"col_index": 1,
"content": "r3col2",
},
{
"row_index": 2,
"col_index": 2,
"content": "r34col34",
},
{
"row_index": 3,
"col_index": 0,
"content": "r4col12",
},
],
id="various spans, with 2 row header",
),
],
)
def test_cells_table_extraction_from_prediction(table_as_cells, expected_extraction):
example_element = {
"type": "Table",
"metadata": {"table_as_cells": table_as_cells},
}
assert extract_cells_from_table_as_cells(example_element) == expected_extraction
@pytest.mark.parametrize(
("text_as_html", "expected_extraction"),
[
pytest.param(
"""
<table>
<thead>
<tr>
<th>Month A.</th>
</tr>
</thead>
<tbody>
<tr>
<td>22</td>
</tr>
</tbody>
</table>"
""",
[
{"row_index": 0, "col_index": 0, "content": "Month A."},
{"row_index": 1, "col_index": 0, "content": "22"},
],
id="Simple table, 1 head cell, 1 body cell, no spans",
),
pytest.param(
"""
<table>
<thead>
<tr>
<th>Month A.</th>
<th>Month B.</th>
<th>Month C.</th>
</tr>
</thead>
<tbody>
<tr>
<td>11</td>
<td>12</td>
<td>13</td>
</tr>
<tr>
<td>21</td>
<td>22</td>
<td>23</td>
</tr>
</tbody>
</table>"
""",
[
{"row_index": 0, "col_index": 0, "content": "Month A."},
{"row_index": 0, "col_index": 1, "content": "Month B."},
{"row_index": 0, "col_index": 2, "content": "Month C."},
{"row_index": 1, "col_index": 0, "content": "11"},
{"row_index": 1, "col_index": 1, "content": "12"},
{"row_index": 1, "col_index": 2, "content": "13"},
{"row_index": 2, "col_index": 0, "content": "21"},
{"row_index": 2, "col_index": 1, "content": "22"},
{"row_index": 2, "col_index": 2, "content": "23"},
],
id="Simple table, 3 head cell, 5 body cell, no spans",
),
# +----------+---------------------+----------+
# | | h1col23 | h1col4 |
# | h12col1 |----------+----------+----------|
# | | h2col2 | h2col34 |
# |----------|----------+----------+----------+
# | r3col1 | r3col2 | |
# |----------+----------| r34col34 |
# | r4col12 | |
# +----------+----------+----------+----------+
pytest.param(
"""
<table>
<thead>
<tr>
<th rowspan="2">h12col1</th>
<th colspan="2">h1col23</th>
<th>h1col4</th>
</tr>
<tr>
<th>h2col2</th>
<th colspan="2">h2col34</th>
</tr>
</thead>
<tbody>
<tr>
<td>r3col1</td>
<td>r3col2</td>
<td colspan="2" rowspan="2">r34col34</td>
</tr>
<tr>
<td colspan="2">r4col12</td>
</tr>
</tbody>
</table>
""",
[
{
"row_index": 0,
"col_index": 0,
"content": "h12col1",
},
{
"row_index": 0,
"col_index": 1,
"content": "h1col23",
},
{
"row_index": 0,
"col_index": 3,
"content": "h1col4",
},
{
"row_index": 1,
"col_index": 1,
"content": "h2col2",
},
{
"row_index": 1,
"col_index": 2,
"content": "h2col34",
},
{
"row_index": 2,
"col_index": 0,
"content": "r3col1",
},
{
"row_index": 2,
"col_index": 1,
"content": "r3col2",
},
{
"row_index": 2,
"col_index": 2,
"content": "r34col34",
},
{
"row_index": 3,
"col_index": 0,
"content": "r4col12",
},
],
id="various spans, with 2 row header",
),
],
)
def test_html_table_extraction_from_prediction(text_as_html, expected_extraction):
example_element = {
"type": "Table",
"metadata": {
"text_as_html": text_as_html,
},
}
assert extract_cells_from_text_as_html(example_element) == expected_extraction
def test_cells_extraction_from_prediction_when_missing_prediction():
example_element = {"type": "Table", "metadata": {"text_as_html": "", "table_as_cells": []}}
assert extract_cells_from_text_as_html(example_element) is None
assert extract_cells_from_table_as_cells(example_element) is None
def _trim_html(html: str) -> str:
html_lines = [line.strip() for line in html.split("\n") if line]
return "".join(html_lines)
@pytest.mark.parametrize(
"html_to_test",
[
"""
<table>
<thead>
<tr>
<th>Month A.</th>
</tr>
</thead>
<tbody>
<tr>
<td>22</td>
</tr>
</tbody>
</table>
""",
"""
<table>
<thead>
<tr>
<th>Month A.</th>
<th>Month B.</th>
<th>Month C.</th>
</tr>
</thead>
<tbody>
<tr>
<td>11</td>
<td>12</td>
<td>13</td>
</tr>
<tr>
<td>21</td>
<td>22</td>
<td>23</td>
</tr>
</tbody>
</table>
""",
"""
<table>
<thead>
<tr>
<th rowspan="2">h12col1</th>
<th colspan="2">h1col23</th>
<th>h1col4</th>
</tr>
<tr>
<th>h2col2</th>
<th colspan="2">h2col34</th>
</tr>
</thead>
<tbody>
<tr>
<td>r3col1</td>
<td>r3col2</td>
<td colspan="2" rowspan="2">r34col34</td>
</tr>
<tr>
<td colspan="2">r4col12</td>
</tr>
</tbody>
</table>
""",
],
)
def test_deckerd_html_converter(html_to_test):
deckerd_table = html_table_to_deckerd(html_to_test)
html_table = deckerd_table_to_html(deckerd_table)
assert _trim_html(html_to_test) == html_table

View File

@@ -0,0 +1,35 @@
import pytest
from unstructured.metrics.utils import (
_mean,
_pstdev,
_stdev,
_uniquity_file,
)
@pytest.mark.parametrize(
("numbers", "expected_mean", "expected_stdev", "expected_pstdev"),
[
([2, 5, 6, 7], 5, 2.16, 1.871),
([1, 100], 50.5, 70.004, 49.5),
([1], 1, None, None),
([], None, None, None),
],
)
def test_stats(numbers, expected_mean, expected_stdev, expected_pstdev):
mean = _mean(numbers)
stdev = _stdev(numbers)
pstdev = _pstdev(numbers)
assert mean == expected_mean
assert stdev == expected_stdev
assert pstdev == expected_pstdev
@pytest.mark.parametrize(
("filenames"),
[("filename.ext", "filename (1).ext", "randomfile.ext", "filename.txt", "filename (5).txt")],
)
def test_uniquity_file(filenames):
final_filename = _uniquity_file(filenames, "filename.ext")
assert final_filename == "filename (2).ext"