From 5d2971f80b2e6e02c1e21577ce1513ed25e1d23d Mon Sep 17 00:00:00 2001 From: "Francesca L. Bleken" <48128015+francescalb@users.noreply.github.com> Date: Thu, 7 Apr 2022 11:26:50 +0200 Subject: [PATCH] create_from_excel/pandas return as list of concepts that are worngly defined in the excelfile (#396) * Added returning errors in excelparser * Added test to check that all concpets that are defined in the wrong manner are listed in the error dictionary returned. * Documented new output from excelparser --- ontopy/excelparser.py | 71 ++++++++++++++++++++++++--- tests/test_excelparser.py | 17 ++++++- tests/testonto/excelparser/onto.xlsx | Bin 17165 -> 17205 bytes 3 files changed, 81 insertions(+), 7 deletions(-) diff --git a/ontopy/excelparser.py b/ontopy/excelparser.py index 34a70a5b2..a5eb266f8 100755 --- a/ontopy/excelparser.py +++ b/ontopy/excelparser.py @@ -67,12 +67,38 @@ def create_ontology_from_excel( # pylint: disable=too-many-arguments base_iri_from_metadata: Whether to use base IRI defined from metadata. imports: List of imported ontologies. catalog: Imported ontologies with (name, full path) key/value-pairs. - force: Forcibly make an ontology by skipping concepts with a prefLabel - that is erroneously defined. + force: Forcibly make an ontology by skipping concepts + that are erroneously defined or other errors in the excel sheet. Returns: - A tuple of the created ontology and the associated catalog of ontology - names and resolvable path as dict. + A tuple with the + * created ontology + * associated catalog of ontology names and resolvable path as dict + * a dictionary with lists of concepts that raise errors, with the + following keys: + - "already_defined": These are concepts that are already in + the ontology, + either because they were already added in a + previous line of + the excelfile/pandas dataframe, + or because it is already defined + in the imported ontologies. + - "in_imported_ontologies": Concepts that are defined in the excel, + but already exist in the imported ontologies. + This is a subset of the 'already_defined' + - "wrongly_defined": Concepts that are given an invalid prefLabel + (e.g. with a space in the name). + - "missing_parents": Concepts that are missing parents. + These concepts are added directly + under owl:Thing. + - "invalid_parents": Concepts with invalidly defined parents. + These concepts are added directly + under owl:Thing. + - "nonadded_concepts": List of all concepts that are not added, + either because the prefLabel is invalid, + or because the concept has already been added + once or already exists in an imported + ontology. """ @@ -115,6 +141,8 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran ) -> Tuple[ontopy.ontology.Ontology, dict]: """ Create an ontology from a pandas DataFrame. + + Check 'create_ontology_from_excel' for complete documentation. """ # Remove lines with empty prefLabel @@ -130,6 +158,10 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran onto, catalog = get_metadata_from_dataframe( metadata, base_iri, imports=imports ) + # Get a set of imported concepts + imported_concepts = { + concept.prefLabel.first() for concept in onto.get_entities() + } # Set given or default base_iri if base_iri_from_metadata is False. if not base_iri_from_metadata: @@ -140,6 +172,16 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran if not altlabel == "nan": labels.update(altlabel.split(";")) + # Dictionary with lists of concepts that raise errors + concepts_with_errors = { + "already_defined": [], + "in_imported_ontologies": [], + "wrongly_defined": [], + "missing_parents": [], + "invalid_parents": [], + "nonadded_concepts": [], + } + onto.sync_python_names() with onto: remaining_rows = set(range(len(data))) @@ -158,6 +200,7 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran f'Ignoring concept "{name}" since it is already in ' "the ontology." ) + concepts_with_errors["already_defined"].append(name) # What to do if we want to add info to this concept? # Should that be not allowed? # If it should be allowed the index has to be added to @@ -168,14 +211,16 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran f'Ignoring concept "{name}". ' f'The following error was raised: "{err}"' ) + concepts_with_errors["wrongly_defined"].append(name) continue except NoSuchLabelError: pass - if pd.isna(row["subClassOf"]): + if row["subClassOf"] == "nan": if not force: raise ExcelError(f"{row[0]} has no subClassOf") parent_names = [] # Should be "owl:Thing" + concepts_with_errors["missing_parents"].append(name) else: parent_names = str(row["subClassOf"]).split(";") @@ -191,6 +236,9 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran f'Invalid parents for "{name}": ' f'"{parent_name}".' ) + concepts_with_errors["invalid_parents"].append( + name + ) break raise ExcelError( f'Invalid parents for "{name}": {exc}\n' @@ -276,6 +324,7 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran " Will continue without these." ) remaining_rows = False + concepts_with_errors["nonadded_concepts"] = unadded else: raise ExcelError( f"Not able to add the following concepts: {unadded}." @@ -303,6 +352,7 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran f"Property to be Evaluated: {prop}. " f"Error is {exc}." ) + concepts_with_errors["errors_in_properties"].append(name) except NoSuchLabelError as exc: msg = ( f"Error in Property assignment for: {concept}. " @@ -311,6 +361,9 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran ) if force is True: warnings.warn(msg) + concepts_with_errors["errors_in_properties"].append( + name + ) else: raise ExcelError(msg) from exc @@ -319,7 +372,13 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran name_policy="uuid", name_prefix="EMMO_", class_docstring="elucidation" ) onto.dir_label = False - return onto, catalog + concepts_with_errors = { + key: set(value) for key, value in concepts_with_errors.items() + } + concepts_with_errors["in_imported_ontologies"] = concepts_with_errors[ + "already_defined" + ].intersection(imported_concepts) + return onto, catalog, concepts_with_errors def get_metadata_from_dataframe( # pylint: disable=too-many-locals,too-many-branches,too-many-statements diff --git a/tests/test_excelparser.py b/tests/test_excelparser.py index 9cf1fd3ee..48ac5bce2 100644 --- a/tests/test_excelparser.py +++ b/tests/test_excelparser.py @@ -16,5 +16,20 @@ def test_excelparser(repo_dir: "Path") -> None: onto = get_ontology(str(ontopath)).load() xlspath = repo_dir / "tests" / "testonto" / "excelparser" / "onto.xlsx" - ontology, catalog = create_ontology_from_excel(xlspath, force=True) + ontology, catalog, errors = create_ontology_from_excel(xlspath, force=True) assert onto == ontology + + assert errors["already_defined"] == {"Atom", "Pattern"} + assert errors["in_imported_ontologies"] == {"Atom"} + assert errors["wrongly_defined"] == {"Temporal Boundary"} + assert errors["missing_parents"] == {"SpatioTemporalBoundary"} + assert errors["invalid_parents"] == { + "TemporalPattern", + "SubSubgrainBoundary", + "SubgrainBoundary", + } + assert errors["nonadded_concepts"] == { + "Atom", + "Pattern", + "Temporal Boundary", + } diff --git a/tests/testonto/excelparser/onto.xlsx b/tests/testonto/excelparser/onto.xlsx index 9e7f2bf6fa27baba857cfc57f0db79c14ad85253..9dab7c9a6345f18ef5980496386cbbdc103b3870 100755 GIT binary patch delta 6754 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 WQlKJO0ay~02ipKMp(05CH}GHKy5m;> delta 6840 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