taxo 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
taxo-1.0.0/.gitignore ADDED
@@ -0,0 +1,27 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ *.egg
6
+ .eggs/
7
+ build/
8
+ dist/
9
+ .venv/
10
+
11
+ # uv
12
+ uv.lock
13
+
14
+ # Packaging
15
+ taxotagger.egg-info/
16
+
17
+ # Test / coverage
18
+ .pytest_cache/
19
+ .coverage
20
+ htmlcov/
21
+
22
+ # Editors / OS
23
+ .vscode/
24
+ .idea/
25
+ .DS_Store
26
+
27
+
@@ -0,0 +1,17 @@
1
+ # This CITATION.cff file was generated with cffinit.
2
+ # Visit https://bit.ly/cffinit to generate yours today!
3
+
4
+ cff-version: 1.2.0
5
+ title: taxo
6
+ message: >-
7
+ If you use this software, please cite it using the
8
+ metadata from this file.
9
+ type: software
10
+ authors:
11
+ - given-names: Andreas
12
+ family-names: Hamm
13
+ email: andreas.hamm@dlr.de
14
+ affiliation: German Aerospace Center (DLR)
15
+ orcid: 'https://orcid.org/0000-0001-5854-851X'
16
+ url: 'https://elib.dlr.de/190966/'
17
+ license: MIT
taxo-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 German Aerospace Center (DLR)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
taxo-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: taxo
3
+ Version: 1.0.0
4
+ Summary: Tag documents with taxonomy terms using embeddings and a concept hierarchy
5
+ Project-URL: Homepage, https://github.com/DLR-SC/taxo-tag
6
+ Project-URL: Repository, https://github.com/DLR-SC/taxo-tag
7
+ Author-email: Andreas Hamm <andreas.hamm@dlr.de>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: document-tagging,embeddings,hierarchy,sentence-transformers,taxonomy
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: networkx>=3.4.2
19
+ Requires-Dist: nltk>=3.9.1
20
+ Requires-Dist: numpy>=2.2.5
21
+ Requires-Dist: openpyxl>=3.1.5
22
+ Requires-Dist: pandas>=2.2.3
23
+ Requires-Dist: scipy>=1.14.1
24
+ Requires-Dist: sentence-transformers>=4.1.0
25
+ Requires-Dist: torch>=2.6.0
26
+ Requires-Dist: transformers>=4.46.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: build; extra == 'dev'
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: twine; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # taxo
34
+
35
+ Tag documents with terms from a concept hierarchy (taxonomy) using sentence
36
+ embeddings.
37
+
38
+ `taxo` reads in a hierarchy of concepts, embeds their descriptions in a shared
39
+ vector space, and then finds the concepts most related to a document. It can
40
+ also aggregate sentence-level evidence along hierarchical paths to produce a
41
+ consistent set of top-level tags.
42
+
43
+ For the main ideas used in `taxo`, see
44
+ ```
45
+ @inproceedings{dlr190966,
46
+ title = {Strategy Comparison for Semantic Zero-Shot Taxonomy Filters},
47
+ month = {October},
48
+ booktitle = {4th International Open Search Symposium (OSSYM 2022)},
49
+ author = {Hamm, Andreas},
50
+ year = {2022},
51
+ url = {https://elib.dlr.de/190966/}
52
+ }
53
+ ```
54
+
55
+ ## Installation
56
+
57
+ On Python >= 3.11:
58
+
59
+ ```bash
60
+ pip install taxo
61
+ ```
62
+
63
+ This is a uv-style package, so `uv` works too:
64
+
65
+ ```bash
66
+ uv add taxo
67
+ ```
68
+
69
+ ## Usage
70
+ The package is both installed and imported as `taxo`.
71
+
72
+ Given a taxonomy (by this we mean a hierarchy of concepts, ordered in the structure of a directed acyclic graph (DAG)) - presented in tabular form as described below - and the text of a document, this package can be used to tag the document with the best-fitting taxonomy concepts.
73
+
74
+ The table used to specify the taxonomy is expected to be a Pandas dataframe. Thus, it can be imported from several tabular sources like csv files or Excel sheets. An example is the file `media.xlsx` included in this repository. It is based on the [IPTC media topics taxonomy](https://iptc.org/standards/media-topics/), a taxonomy frequently used for subject classification of news articles.
75
+
76
+ The following short excerpt illustrates the structure:
77
+
78
+ | key | label_en | description_en | label_de | description_de | parents |
79
+ |-----------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
80
+ | M00000000 | [TOP] | | [TOP] | | [] |
81
+ | M05000000 | education | All aspects of furthering knowledge, formally or informally | Erziehung, Ausbildung | Alle Aspekte der Förderung von Wissen für den Menschen von der Geburt bis zum Tod. | ['M00000000'] |
82
+ | M06000000 | environment | All aspects of protection, damage, and condition of the ecosystem of the planet earth and its surroundings. | Umwelt | Alle Aspekte des Schutzes, der Zerstörung und die Bedingungen des Ökosystems der Erde und ihrer Umgebung | ['M00000000'] |
83
+ | M07000000 | health | All aspects pertaining to the physical and mental welfare of living beings | Medizin, Gesundheit | Alle Aspekte des körperlichen und geistigen Wohlbefindens von Menschen. | ['M00000000'] |
84
+ | M20000400 | school | A building or institution in which education is provided | Schule | Gebäude oder Einrichtung, wo verschiedenartige Ausbildung angeboten wird. | ['M05000000'] |
85
+ | M20000405 | college and university | Institutions of higher learning that provide degrees in a course of study at the undergraduate, postgraduate or doctorate level | Hochschule | Ausbildung oberhalb des zweiten Levels, meist erwartet von Fachhochschulen und Universitäten, die in einer professionellen Qualifikation oder einem akademischen Grad führt | ['M20000400'] |
86
+ | M20000406 | college | Institution of higher learning, its specific scope is set differently in different countries | Fachhochschule | Institution der Lehre auf höherer Ebene, spezielle Form variiert von Land zu Land | ['M20000405'] |
87
+ | M20000407 | university | Institutions of higher learning capable of providing doctorate degrees | Universität | Institution der Lehre auf höherer Ebene, geeignet um Doktoren- und Professorentitel etc. zu verleihen. | ['M20000405'] |
88
+ | M20000446 | diseases and conditions | Any harmful or destructive health condition affecting humans. | Krankheit | Jeder gesundheitsgefährdende oder zerstörerische Zustand | ['M07000000'] |
89
+
90
+
91
+ The table has one row for every node (concept) of the taxonomy. Formally, there is one special node needed, the so called root or top node, which is the ancestor of all other nodes in the hierarchy. (See the node with key `M00000000` in the media example.)
92
+
93
+ Each row of the table has to specify:
94
+ - the concept's unique `key`
95
+ - language dependent labels for the concept
96
+ - short language dependent descriptions of the concept
97
+ - the keys of the hierarchical `parents` of the concept
98
+
99
+ The parents' keys have to be given as a string that evaluates to a list in Python (e.g., `"['concept1', 'concept2']"`). In the case of a hierarchy that is a tree, every concept has of course only one parent.
100
+
101
+ Given a 2-letter ISO code `xy` for a language, the row headers for labels and descriptions are `label_xy` and `description_xy`, respectively. The table is expected to have English entries (`en`) as default, and more entries for the other languages needed. The code in this package is also meant to handle German (`de`) in addition to English; other languages need small obvious modifications.
102
+
103
+ Tagging is based on semantic similarity considerations as calculated by a Sentence Transformer model. Any model provided by the sentence_transformers package can be used. The small model `all-MiniLM-L6-v2` works well for English (but not so well for other languages).
104
+
105
+ The code fragment below shows the principles of usage, finally executing the tagging by calling the method `identify_concepts` which has several tuning parameters which can be adapted to specific constellations and use cases.
106
+
107
+ ```python
108
+ import networkx as nx
109
+ import pandas as pd
110
+ from sentence_transformers import SentenceTransformer
111
+
112
+ import taxo
113
+
114
+ # Build a taxonomy from a dataframe (one row per concept).
115
+ df = pd.read_excel("media.xlsx")
116
+ df.fillna("", inplace=True)
117
+
118
+ tax = taxo.ConceptTaxonomy(
119
+ nx.DiGraph(),
120
+ root="M00000000", # key of the formal top concept
121
+ transformer="all-MiniLM-L6-v2",
122
+ default_lang="en",
123
+ )
124
+ tax.from_df(df)
125
+
126
+ # Tag a document.
127
+ doc = taxo.TaxedDocument(tax, text)
128
+ result = doc.identify_concepts(
129
+ top_k=10, # concepts kept when checking similarity to text
130
+ min_similarity=0.25, # minimum sentence-level similarity to be kept
131
+ title_weight=2, # weight of the first sentence (the document title)
132
+ saturation=1.5, # saturation parameter for score aggregation
133
+ min_agg=0.25, # minimum aggregated score for a concept to be returned
134
+ top_percent=80, # percentage kept when truncating the hierarchy on the top level
135
+ second_percent=80, # percentage kept when truncating the hierarchy on the second level
136
+ max_baseconcepts=7, # maximum number of returned (leaf) concepts
137
+ max_paths=2, # number of paths to the root considered per concept
138
+ with_scores=True, # include aggregated scores in the results
139
+ )
140
+ ```
141
+
142
+ Each entry of `result` is a hierarchical path from the root to a concept, as a
143
+ list of `(name, key)` pairs (optionally with an aggregated score appended).
144
+
145
+ See also the included Jupyter notebook `usage.ipynb`.
146
+
147
+ ## API
148
+
149
+ ### `ConceptTaxonomy`
150
+
151
+ Holds the concept hierarchy and its embeddings.
152
+
153
+ - `__init__(taxgraph, root, long_descr=True, transformer=None, default_lang="en", classifier=None, token=None)`
154
+ - `from_df(df_concepts, rename_columns=None)` — add/update concepts from a dataframe
155
+ - `to_df()` — export the hierarchy to a dataframe
156
+ - `describe_concepts(lang=None, intro=None, long=True)` — prepare comparison sentences
157
+ - `embed_concepts(model=None)` — compute the concept embeddings
158
+ - `get_concepts_in_text(text, top_k=10, threshold=0.3, sim_func=None)` — similarity or zero-shot search
159
+ - `get_parent_concepts(child, top_parent=None, lang=None)` — paths from a concept to the root
160
+ - `show_concepts(concepts, lang=None, scores=None)` — format results for output
161
+
162
+ ### `TaxedDocument`
163
+
164
+ Prepares a document for concept identification.
165
+
166
+ - `set_parameters(top_k=10, threshold=0.3, weight_dev=None, kparm=1.0)`
167
+ - `score_concepts_overall()` / `score_concepts_sentencewise()`
168
+ - `aggregate_sentence_scores(min_agg_score=0)`
169
+ - `find_best_paths(concept, n)`
170
+ - `evaluate_hierarchy(max_paths=2)` / `restrict_hierarchy(level, percentage)`
171
+ - `get_relevant_concepts(min_score=None, max_number=100)`
172
+ - `identify_concepts(...)` — all of the above in one call
173
+
174
+
175
+ ## License
176
+
177
+ MIT — DLR. See LICENSE file.
taxo-1.0.0/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # taxo
2
+
3
+ Tag documents with terms from a concept hierarchy (taxonomy) using sentence
4
+ embeddings.
5
+
6
+ `taxo` reads in a hierarchy of concepts, embeds their descriptions in a shared
7
+ vector space, and then finds the concepts most related to a document. It can
8
+ also aggregate sentence-level evidence along hierarchical paths to produce a
9
+ consistent set of top-level tags.
10
+
11
+ For the main ideas used in `taxo`, see
12
+ ```
13
+ @inproceedings{dlr190966,
14
+ title = {Strategy Comparison for Semantic Zero-Shot Taxonomy Filters},
15
+ month = {October},
16
+ booktitle = {4th International Open Search Symposium (OSSYM 2022)},
17
+ author = {Hamm, Andreas},
18
+ year = {2022},
19
+ url = {https://elib.dlr.de/190966/}
20
+ }
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ On Python >= 3.11:
26
+
27
+ ```bash
28
+ pip install taxo
29
+ ```
30
+
31
+ This is a uv-style package, so `uv` works too:
32
+
33
+ ```bash
34
+ uv add taxo
35
+ ```
36
+
37
+ ## Usage
38
+ The package is both installed and imported as `taxo`.
39
+
40
+ Given a taxonomy (by this we mean a hierarchy of concepts, ordered in the structure of a directed acyclic graph (DAG)) - presented in tabular form as described below - and the text of a document, this package can be used to tag the document with the best-fitting taxonomy concepts.
41
+
42
+ The table used to specify the taxonomy is expected to be a Pandas dataframe. Thus, it can be imported from several tabular sources like csv files or Excel sheets. An example is the file `media.xlsx` included in this repository. It is based on the [IPTC media topics taxonomy](https://iptc.org/standards/media-topics/), a taxonomy frequently used for subject classification of news articles.
43
+
44
+ The following short excerpt illustrates the structure:
45
+
46
+ | key | label_en | description_en | label_de | description_de | parents |
47
+ |-----------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|
48
+ | M00000000 | [TOP] | | [TOP] | | [] |
49
+ | M05000000 | education | All aspects of furthering knowledge, formally or informally | Erziehung, Ausbildung | Alle Aspekte der Förderung von Wissen für den Menschen von der Geburt bis zum Tod. | ['M00000000'] |
50
+ | M06000000 | environment | All aspects of protection, damage, and condition of the ecosystem of the planet earth and its surroundings. | Umwelt | Alle Aspekte des Schutzes, der Zerstörung und die Bedingungen des Ökosystems der Erde und ihrer Umgebung | ['M00000000'] |
51
+ | M07000000 | health | All aspects pertaining to the physical and mental welfare of living beings | Medizin, Gesundheit | Alle Aspekte des körperlichen und geistigen Wohlbefindens von Menschen. | ['M00000000'] |
52
+ | M20000400 | school | A building or institution in which education is provided | Schule | Gebäude oder Einrichtung, wo verschiedenartige Ausbildung angeboten wird. | ['M05000000'] |
53
+ | M20000405 | college and university | Institutions of higher learning that provide degrees in a course of study at the undergraduate, postgraduate or doctorate level | Hochschule | Ausbildung oberhalb des zweiten Levels, meist erwartet von Fachhochschulen und Universitäten, die in einer professionellen Qualifikation oder einem akademischen Grad führt | ['M20000400'] |
54
+ | M20000406 | college | Institution of higher learning, its specific scope is set differently in different countries | Fachhochschule | Institution der Lehre auf höherer Ebene, spezielle Form variiert von Land zu Land | ['M20000405'] |
55
+ | M20000407 | university | Institutions of higher learning capable of providing doctorate degrees | Universität | Institution der Lehre auf höherer Ebene, geeignet um Doktoren- und Professorentitel etc. zu verleihen. | ['M20000405'] |
56
+ | M20000446 | diseases and conditions | Any harmful or destructive health condition affecting humans. | Krankheit | Jeder gesundheitsgefährdende oder zerstörerische Zustand | ['M07000000'] |
57
+
58
+
59
+ The table has one row for every node (concept) of the taxonomy. Formally, there is one special node needed, the so called root or top node, which is the ancestor of all other nodes in the hierarchy. (See the node with key `M00000000` in the media example.)
60
+
61
+ Each row of the table has to specify:
62
+ - the concept's unique `key`
63
+ - language dependent labels for the concept
64
+ - short language dependent descriptions of the concept
65
+ - the keys of the hierarchical `parents` of the concept
66
+
67
+ The parents' keys have to be given as a string that evaluates to a list in Python (e.g., `"['concept1', 'concept2']"`). In the case of a hierarchy that is a tree, every concept has of course only one parent.
68
+
69
+ Given a 2-letter ISO code `xy` for a language, the row headers for labels and descriptions are `label_xy` and `description_xy`, respectively. The table is expected to have English entries (`en`) as default, and more entries for the other languages needed. The code in this package is also meant to handle German (`de`) in addition to English; other languages need small obvious modifications.
70
+
71
+ Tagging is based on semantic similarity considerations as calculated by a Sentence Transformer model. Any model provided by the sentence_transformers package can be used. The small model `all-MiniLM-L6-v2` works well for English (but not so well for other languages).
72
+
73
+ The code fragment below shows the principles of usage, finally executing the tagging by calling the method `identify_concepts` which has several tuning parameters which can be adapted to specific constellations and use cases.
74
+
75
+ ```python
76
+ import networkx as nx
77
+ import pandas as pd
78
+ from sentence_transformers import SentenceTransformer
79
+
80
+ import taxo
81
+
82
+ # Build a taxonomy from a dataframe (one row per concept).
83
+ df = pd.read_excel("media.xlsx")
84
+ df.fillna("", inplace=True)
85
+
86
+ tax = taxo.ConceptTaxonomy(
87
+ nx.DiGraph(),
88
+ root="M00000000", # key of the formal top concept
89
+ transformer="all-MiniLM-L6-v2",
90
+ default_lang="en",
91
+ )
92
+ tax.from_df(df)
93
+
94
+ # Tag a document.
95
+ doc = taxo.TaxedDocument(tax, text)
96
+ result = doc.identify_concepts(
97
+ top_k=10, # concepts kept when checking similarity to text
98
+ min_similarity=0.25, # minimum sentence-level similarity to be kept
99
+ title_weight=2, # weight of the first sentence (the document title)
100
+ saturation=1.5, # saturation parameter for score aggregation
101
+ min_agg=0.25, # minimum aggregated score for a concept to be returned
102
+ top_percent=80, # percentage kept when truncating the hierarchy on the top level
103
+ second_percent=80, # percentage kept when truncating the hierarchy on the second level
104
+ max_baseconcepts=7, # maximum number of returned (leaf) concepts
105
+ max_paths=2, # number of paths to the root considered per concept
106
+ with_scores=True, # include aggregated scores in the results
107
+ )
108
+ ```
109
+
110
+ Each entry of `result` is a hierarchical path from the root to a concept, as a
111
+ list of `(name, key)` pairs (optionally with an aggregated score appended).
112
+
113
+ See also the included Jupyter notebook `usage.ipynb`.
114
+
115
+ ## API
116
+
117
+ ### `ConceptTaxonomy`
118
+
119
+ Holds the concept hierarchy and its embeddings.
120
+
121
+ - `__init__(taxgraph, root, long_descr=True, transformer=None, default_lang="en", classifier=None, token=None)`
122
+ - `from_df(df_concepts, rename_columns=None)` — add/update concepts from a dataframe
123
+ - `to_df()` — export the hierarchy to a dataframe
124
+ - `describe_concepts(lang=None, intro=None, long=True)` — prepare comparison sentences
125
+ - `embed_concepts(model=None)` — compute the concept embeddings
126
+ - `get_concepts_in_text(text, top_k=10, threshold=0.3, sim_func=None)` — similarity or zero-shot search
127
+ - `get_parent_concepts(child, top_parent=None, lang=None)` — paths from a concept to the root
128
+ - `show_concepts(concepts, lang=None, scores=None)` — format results for output
129
+
130
+ ### `TaxedDocument`
131
+
132
+ Prepares a document for concept identification.
133
+
134
+ - `set_parameters(top_k=10, threshold=0.3, weight_dev=None, kparm=1.0)`
135
+ - `score_concepts_overall()` / `score_concepts_sentencewise()`
136
+ - `aggregate_sentence_scores(min_agg_score=0)`
137
+ - `find_best_paths(concept, n)`
138
+ - `evaluate_hierarchy(max_paths=2)` / `restrict_hierarchy(level, percentage)`
139
+ - `get_relevant_concepts(min_score=None, max_number=100)`
140
+ - `identify_concepts(...)` — all of the above in one call
141
+
142
+
143
+ ## License
144
+
145
+ MIT — DLR. See LICENSE file.
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "taxo"
7
+ version = "1.0.0"
8
+ description = "Tag documents with taxonomy terms using embeddings and a concept hierarchy"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Andreas Hamm", email = "andreas.hamm@dlr.de" },
14
+ ]
15
+ keywords = ["taxonomy", "document-tagging", "embeddings", "sentence-transformers", "hierarchy"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "networkx>=3.4.2",
26
+ "nltk>=3.9.1",
27
+ "numpy>=2.2.5",
28
+ "pandas>=2.2.3",
29
+ "sentence_transformers>=4.1.0",
30
+ "scipy>=1.14.1",
31
+ "openpyxl>=3.1.5",
32
+ "torch>=2.6.0",
33
+ "transformers>=4.46.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "build",
39
+ "twine",
40
+ "pytest",
41
+ ]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/DLR-SC/taxo-tag"
45
+ Repository = "https://github.com/DLR-SC/taxo-tag"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/taxo"]
49
+
50
+ [tool.hatch.build.targets.sdist]
51
+ include = [
52
+ "src/",
53
+ "README.md",
54
+ "LICENSE",
55
+ "CITATION.cff",
56
+ "pyproject.toml",
57
+ "requirements.txt",
58
+ "usage.ipynb",
59
+ ]
@@ -0,0 +1,9 @@
1
+ networkx>=3.4.2
2
+ nltk>=3.9.1
3
+ numpy>=2.2.5
4
+ pandas>=2.2.3
5
+ sentence_transformers>=4.1.0
6
+ scipy>=1.14.1
7
+ openpyxl>=3.1.5
8
+ torch>=2.6.0
9
+ transformers>=4.46.0
@@ -0,0 +1,533 @@
1
+ """Python module for tagging documents with taxonomy terms
2
+ Author: Andreas Hamm (andreas.hamm@dlr.de)
3
+ Version: 1.0.0
4
+ Copyright: DLR
5
+ License: MIT
6
+ """
7
+
8
+
9
+ from collections import defaultdict
10
+ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
+ import nltk
12
+ from nltk.tokenize import sent_tokenize
13
+ import pandas as pd
14
+ import numpy as np
15
+ import networkx as nx
16
+ from sentence_transformers import SentenceTransformer, util
17
+ from transformers import pipeline
18
+ import ast
19
+
20
+ # Maps two-letter ISO codes to the punkt_tab language directory names NLTK's
21
+ # Punkt tokenizer loads. These must match the directory names on disk
22
+ # (e.g. "german", "english"): punkt.py does a plain ``find("tokenizers/punkt_tab/{lang}/")``
23
+ # with no case normalization, so capitalization must match the filesystem
24
+ # (which is fine on Windows but would fail on case-sensitive Linux/macOS).
25
+ # Only models actually installed as ``punkt_tab`` will work.
26
+ _ISO_TO_NLTK_LANGUAGE = {
27
+ "en": "english",
28
+ "de": "german",
29
+ }
30
+
31
+
32
+ def _ensure_punkt(language: str = "english") -> bool:
33
+ """Ensures the punkt_tab tokenizer for ``language`` is available.
34
+
35
+ NLTK only downloads punkt_tab lazily and raises ``LookupError`` otherwise.
36
+ On first use it attempts a silent download of ``punkt_tab`` and retries.
37
+
38
+ Args:
39
+ language (str): The punkt_tab language directory name, e.g. ``"english"`` or ``"german"``.
40
+
41
+ Returns:
42
+ bool: ``True`` if the tokenizer is available afterwards, ``False`` otherwise.
43
+ """
44
+ try:
45
+ nltk.data.find(f"tokenizers/punkt_tab/{language}")
46
+ return True
47
+ except LookupError:
48
+ try:
49
+ return bool(nltk.download("punkt_tab", quiet=True))
50
+ except Exception:
51
+ return False
52
+
53
+ class ConceptTaxonomy:
54
+ """Class for working with a concept hierarchy including concept embeddings
55
+
56
+ Attributes:
57
+ default_lang (str): Default language (two-letter ISO code of a supported language)
58
+ tg (networkx.DiGraph): Graph defining the concept hierarchy. The concepts are nodes with attributes
59
+ - ``key``
60
+ - ``label_{isolang}``
61
+ - ``description_{isolang}``
62
+ where ``isolang`` is the two-letter ISO code of each supported language (e.g., ``"en"``, ``"de"``).
63
+ The edges point from concept nodes to their parents.
64
+ root (str): Key of the formal root node.
65
+ transformer (sentence_transformers.SentenceTransformer): SentenceTransformer model used for initial embeddings (only set if ``transformer`` was passed).
66
+ classifier (None or transformers.pipeline): Optionally a Huggingface zero-shot-classification pipeline.
67
+ desc_lang (str): Current language of concept descriptions (two-letter ISO code of a supported language).
68
+ desc_concepts (List[Tuple[str, str, str]]): List of ``(key, label, sentence)`` triples for all concepts, with labels and
69
+ descriptive sentences in the current language.
70
+ model (sentence_transformers.SentenceTransformer): SentenceTransformer model used for current embeddings (only set if ``transformer`` was passed).
71
+ emb_concepts (Optional[numpy.ndarray]): Row-normalized embedding vectors for all concepts in ``desc_concepts``.
72
+ token (None or str): Optionally Huggingface Access token.
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ taxgraph: nx.DiGraph,
78
+ root: str,
79
+ long_descr: bool = True,
80
+ transformer: Optional[Union[str, SentenceTransformer]] = None,
81
+ default_lang: str = "en",
82
+ classifier: Optional[str] = None,
83
+ token: Optional[str] = None,
84
+ ):
85
+ """Constructs a ``ConceptTaxonomy``, optionally computing an initial embedding.
86
+
87
+ Args:
88
+ taxgraph (networkx.DiGraph): Graph defining the concept hierarchy. The concepts are nodes with attributes
89
+ - ``key``
90
+ - ``label_{isolang}``
91
+ - ``description_{isolang}``
92
+ where ``isolang`` is the two-letter ISO code of each supported language (e.g., ``"en"``, ``"de"``)
93
+ root (str): Key of the formal root node. Must be a node in ``taxgraph``.
94
+ long_descr (bool, optional): Whether to include the concept descriptions in the concept comparison sentences. Defaults to ``True``.
95
+ transformer (Union[str, SentenceTransformer], optional): SentenceTransformer model (or its name) used for initial embeddings. Defaults to ``None``.
96
+ default_lang (str, optional): Default language (two-letter ISO code of a supported language). Defaults to ``"en"``.
97
+ classifier (str, optional): Optionally a Huggingface zero-shot-classification model name. Defaults to ``None``.
98
+ token (str, optional): Huggingface access token. Defaults to ``None``.
99
+
100
+ Raises:
101
+ ValueError: If ``root`` is not a node in ``taxgraph``.
102
+ """
103
+ self.default_lang = default_lang
104
+ self.tg = taxgraph
105
+ self.root = root
106
+ self.token = token
107
+ # Ensure the punkt_tab sentence tokenizer for the default language is
108
+ # available, since score_concepts_sentencewise depends on it.
109
+ _ensure_punkt(_ISO_TO_NLTK_LANGUAGE.get(self.default_lang, "english"))
110
+ if long_descr:
111
+ self.describe_concepts()
112
+ else:
113
+ self.describe_concepts(long=False)
114
+ if transformer:
115
+ self.transformer = transformer
116
+ self.embed_concepts(transformer)
117
+ if classifier:
118
+ self.classifier = pipeline("zero-shot-classification", model=classifier)
119
+ else:
120
+ self.classifier = None
121
+
122
+ def from_df(
123
+ self,
124
+ df_concepts: pd.DataFrame,
125
+ rename_columns: Optional[Dict[str, str]] = None,
126
+ ) -> "ConceptTaxonomy":
127
+ """Reads in new concepts from a dataframe.
128
+
129
+ Args:
130
+ df_concepts (pandas.DataFrame): Dataframe. Each row contains a node to be added to the graph defining the concept hierarchy.
131
+ The dataframe has to have the columns
132
+ - ``key`` for the concept key
133
+ - ``parents``, a list with the parent keys
134
+ - ``label_xx`` with ``xx`` the default language for the concept label
135
+ - ``description_xx`` with ``xx`` the default language for the concept description (might be empty)
136
+ and optionally further ``label_yz``, ``description_yz`` for other supported languages, and also other attributes
137
+ rename_columns (dict, optional): Dictionary for optionally renaming column headings. Defaults to ``{}``.
138
+
139
+ Returns:
140
+ ConceptTaxonomy: This instance, to allow method chaining.
141
+ """
142
+ if rename_columns is None:
143
+ rename_columns = {}
144
+ if rename_columns:
145
+ df_concepts = df_concepts.rename(columns=rename_columns)
146
+ if len(set(df_concepts.keys()) & set(["key", f"label_{self.default_lang}", f"description_{self.default_lang}", "parents"])) < 4:
147
+ self.describe_concepts()
148
+ if self.transformer:
149
+ self.embed_concepts()
150
+ return self
151
+ for index, row in df_concepts.iterrows():
152
+ self.tg.add_node(row["key"], **{k: v for k, v in row.items() if k not in ["key", "parents"]})
153
+ for index, row in df_concepts.iterrows():
154
+ for p in ast.literal_eval(row["parents"]):
155
+ self.tg.add_edge(row["key"], p)
156
+ # Refresh the derived concept/embedding caches, since the nodes above
157
+ # changed but the caches from __init__ did not.
158
+ self.describe_concepts()
159
+ if self.transformer:
160
+ self.embed_concepts()
161
+ return self
162
+
163
+ def to_df(self) -> pd.DataFrame:
164
+ """Writes the concept hierarchy into a dataframe.
165
+
166
+ Returns:
167
+ pandas.DataFrame: Dataframe. Each row contains one concept. The columns are ``key``, all attributes of the concept nodes, and ``parents``.
168
+ """
169
+ records = []
170
+ for conc, attr in self.tg.nodes.items():
171
+ records.append({
172
+ "key": conc,
173
+ **attr,
174
+ "parents": [p for p in self.tg.predecessors(conc)]
175
+ })
176
+ return pd.DataFrame(records)
177
+
178
+
179
+ def describe_concepts(
180
+ self,
181
+ lang: Optional[str] = None,
182
+ intro: Optional[str] = None,
183
+ long: bool = True,
184
+ ) -> None:
185
+ """Fills the attributes :pyattr:`desc_concepts` (a list of ``(key, label, sentence)`` triples) and :pyattr:`desc_lang`.
186
+
187
+ Args:
188
+ lang (str, optional): Two-letter ISO code for language to use for concept names and descriptions. Defaults to ``None``, which means ``self.default_lang``.
189
+ intro (str, optional): Phrase for forming concept comparison sentences. Defaults to ``'This text is about '`` (which gets translated into ``'Dieser Text handelt von '`` in German).
190
+ long (bool, optional): Whether to include the concept descriptions in the concept comparison sentences. Defaults to ``True``.
191
+ """
192
+ if lang is None:
193
+ lang = self.default_lang
194
+ if intro is None:
195
+ if lang == "de":
196
+ intro = "Dieser Text handelt von "
197
+ elif lang == "en":
198
+ intro = "This text is about "
199
+ else:
200
+ intro = ""
201
+ self.desc_lang = lang
202
+ self.desc_concepts = []
203
+ for ckey, cattrib in self.tg.nodes.items():
204
+ label = cattrib.get(f"label_{lang}", cattrib.get("label_en"))
205
+ if label is None:
206
+ continue
207
+ concept_sent = intro + label
208
+ if long:
209
+ description = cattrib.get(f"description_{lang}", cattrib.get("description_en"))
210
+ if description:
211
+ concept_sent += " (" + description + ")"
212
+ self.desc_concepts.append((ckey, label, concept_sent))
213
+
214
+ def embed_concepts(self, model: Optional[SentenceTransformer] = None) -> None:
215
+ """Fills the attribute :pyattr:`emb_concepts` (the embedding of all concept comparison sentences) and sets :pyattr:`model`.
216
+ On large taxonomies, this method will have a considerable execution time.
217
+
218
+ Args:
219
+ model (sentence_transformers.SentenceTransformer, optional): SentenceTransformer model to be used for embeddings. Defaults to ``None``, which reuses ``self.transformer``.
220
+ """
221
+ if model is None:
222
+ model = self.transformer
223
+ self.model = model
224
+ if len(self.desc_concepts) == 0:
225
+ self.emb_concepts = []
226
+ return
227
+ emb_sents = model.encode([x[2] for x in self.desc_concepts], convert_to_numpy=True)
228
+ self.emb_concepts = emb_sents / np.linalg.norm(emb_sents, axis=1)[:, None]
229
+
230
+ def get_concepts_in_text(
231
+ self,
232
+ text: str,
233
+ top_k: int = 10,
234
+ threshold: float = 0.3,
235
+ sim_func: Optional[Callable[[Any, Any], Any]] = None,
236
+ ) -> List[Tuple[str, str, float]]:
237
+ """Similarity search for taxonomy concepts in a given text.
238
+ Uses either a zero-shot classification transformers pipeline (given as optional class attribute ``classifier``) or, by default, cosine similarity in the SentenceTransformer model's embedding space.
239
+
240
+ Args:
241
+ text (str): Text.
242
+ top_k (int, optional): Maximum number of concepts returned. Defaults to ``10``.
243
+ threshold (float, optional): Minimum similarity threshold. Defaults to ``0.3``.
244
+ sim_func (Callable[[Tensor, Tensor], Tensor], optional): Similarity function passed to ``util.semantic_search``. Defaults to ``None``, which means cosine similarity.
245
+
246
+ Returns:
247
+ list: List of found concepts as ``(concept key, concept name, similarity score)`` triples, sorted by descending score.
248
+ """
249
+ if len(self.desc_concepts) == 0:
250
+ return []
251
+ if self.classifier:
252
+ premises = [cd[2] for cd in self.desc_concepts]
253
+ clion = self.classifier(text, premises, hypothesis_template="{}", multi_class=True)
254
+ scored = [(cd[0], cd[1], score)
255
+ for cd, score in zip(self.desc_concepts, clion["scores"])
256
+ if score > threshold]
257
+ return sorted(scored, key=lambda z: -z[2])[:top_k]
258
+ emb_sent = self.model.encode(text, convert_to_numpy=True)
259
+ if sim_func is not None:
260
+ return [(self.desc_concepts[x["corpus_id"]][0], self.desc_concepts[x["corpus_id"]][1], x["score"])
261
+ for x in util.semantic_search(emb_sent, self.emb_concepts, top_k=top_k, score_function=sim_func)[0] if x["score"] > threshold]
262
+ return [(self.desc_concepts[x["corpus_id"]][0], self.desc_concepts[x["corpus_id"]][1], x["score"])
263
+ for x in util.semantic_search(emb_sent, self.emb_concepts, top_k=top_k)[0] if x["score"] > threshold]
264
+
265
+ def get_parent_concepts(
266
+ self,
267
+ child: str,
268
+ top_parent: Optional[str] = None,
269
+ lang: Optional[str] = None,
270
+ ) -> Iterator[List[Union[str, Tuple[str, str, str]]]]:
271
+ """Returns paths of predecessors of a given child concept, up back to a ``top_parent``.
272
+
273
+ Args:
274
+ child (str): Key of child concept.
275
+ top_parent (str, optional): Key of earliest ancestor. Defaults to ``None``, in which case it is the root node of the hierarchy.
276
+ lang (str, optional): Two-letter ISO code for language to use for concept names and descriptions. Defaults to ``None``, in which case just the concept keys are returned.
277
+
278
+ Returns:
279
+ list: An iterator of paths to ``top_parent``, each path being a list of concepts. If ``lang`` is ``None``, each path is a list of concept keys; otherwise each concept is a ``(key, name, description)`` triple.
280
+ """
281
+ if top_parent is None:
282
+ top_parent = self.root
283
+ paths = nx.all_simple_paths(self.tg, target=top_parent, source=child)
284
+ if lang is None:
285
+ return paths
286
+ return [[(k, self.tg.nodes[k][f"label_{lang}"], self.tg.nodes[k][f"description_{lang}"]) for k in p] for p in paths]
287
+
288
+ def show_concepts(
289
+ self,
290
+ concepts: List[List[str]],
291
+ lang: Optional[str] = None,
292
+ scores: Optional[List[float]] = None,
293
+ ) -> List[List[Union[Tuple[str, str], Tuple[str, str, float]]]]:
294
+ """Convenience function for producing output of concept results.
295
+
296
+ Args:
297
+ concepts (list): List of hierarchy paths leading to concepts, each path represented as a list of concept keys.
298
+ lang (str, optional): Two-letter ISO code for language to use for concept names and descriptions. Defaults to ``None``, which means ``self.default_lang``.
299
+ scores (list, optional): List of scores along a hierarchical concept path. Defaults to ``None``.
300
+
301
+ Returns:
302
+ list: List of lists of ``(concept name, concept key)`` pairs, or ``(concept name, concept key, aggregated score)`` triples when ``scores`` is given.
303
+ """
304
+ if lang is None:
305
+ lang = self.default_lang
306
+ if scores is None:
307
+ return [
308
+ [(self.tg.nodes[ck].get(f"label_{lang}", ck), ck) for ck in path]
309
+ for path in concepts
310
+ ]
311
+ return [
312
+ [(self.tg.nodes[ck].get(f"label_{lang}", ck), ck, sc) for ck in path]
313
+ for path, sc in zip(concepts, scores)
314
+ ]
315
+
316
+
317
+ class TaxedDocument:
318
+ """Class for working with a document to detect related concepts of a taxonomy in it
319
+
320
+ Attributes:
321
+ taxonomy (ConceptTaxonomy): The taxonomy from which the concept tags to the document are taken
322
+ doc_text (str): Text of the document
323
+ overall_concepts (Dict[str, float]): Concept scores for whole text (Key: Concept key, Value: Score)
324
+ sentences_concepts (Dict[str, List[float]]): Sentencewise concept scores (Key: Concept key, Value: List of sentence scores)
325
+ aggregated_concepts (Dict[str, float]): Aggregated concept scores (Key: Concept key, Value: Score)
326
+ hierarchy_distribution (Dict[int, Dict[str, float]]): Aggregated concept scores summed up along hierarchical upward paths.
327
+ Key: Hierarchy level, Value: Dict with Key: Concept key, Value: Summed-up score divided by path length
328
+ hierarchy_restriction (Dict[int, List[str]]): Consistency-restricted concepts on hierarchical levels.
329
+ Key: Hierarchical level, Value: Restricted list of concepts that make up the required percentage of scores
330
+ top_k (int): Current number of concepts retained when checking similarity to text
331
+ threshold (float): Current minimum similarity for considering concepts
332
+ weight_dev (Dict[int, float]): Dictionary for setting sentence weights different from 1 (Key: sentence number, Value: Sentence weight)
333
+ kparm (float): Current saturation parameter for aggregating similarities
334
+ """
335
+
336
+ def __init__(self, taxonomy: ConceptTaxonomy, doc_text: str):
337
+ """Constructor for TaxedDocument.
338
+
339
+ Args:
340
+ taxonomy (ConceptTaxonomy): The taxonomy to use.
341
+ doc_text (str): Text of the document.
342
+ """
343
+ self.taxonomy = taxonomy
344
+ self.doc_text = doc_text
345
+ self.overall_concepts = None
346
+ self.sentences_concepts = None
347
+ self.aggregated_concepts = None
348
+ self.hierarchy_distribution = None
349
+ self.hierarchy_restriction = {}
350
+ self.set_parameters()
351
+
352
+ def set_parameters(
353
+ self,
354
+ top_k: int = 10,
355
+ threshold: float = 0.3,
356
+ weight_dev: Optional[Dict[int, float]] = None,
357
+ kparm: float = 1.0,
358
+ ) -> None:
359
+ """Set the parameters for taxonomy concept assignment to document text.
360
+
361
+ Args:
362
+ top_k (int, optional): Number of concepts retained when checking similarity to text. Defaults to 10.
363
+ threshold (float, optional): Minimum similarity for considering concepts. Defaults to 0.3.
364
+ weight_dev (Dict[int, float], optional): Dictionary for setting sentence weights different from 1. Defaults to ``{0: 1.5}``.
365
+ kparm (float, optional): Saturation parameter for aggregating similarities. Defaults to 1.0.
366
+ """
367
+ if weight_dev is None:
368
+ weight_dev = {0: 1.5}
369
+ self.top_k = top_k
370
+ self.threshold = threshold
371
+ self.weight_dev = weight_dev
372
+ self.kparm = kparm
373
+
374
+ def score_concepts_overall(self) -> None:
375
+ """Finds concepts by simple similarity search for the whole text and stores them in attribute :pyattr:`overall_concepts`
376
+ as a dict with concept key as key and similarity score as value.
377
+ """
378
+ self.overall_concepts = {
379
+ x[0]: x[2]
380
+ for x in self.taxonomy.get_concepts_in_text(self.doc_text, top_k=self.top_k, threshold=self.threshold)
381
+ }
382
+
383
+ def score_concepts_sentencewise(self) -> None:
384
+ """Finds concepts by sentencewise similarity search and stores them in attribute :pyattr:`sentences_concepts`
385
+ as a dict with concept key as key and a list of similarity scores for affected sentences, taking ``weight_dev`` into consideration.
386
+ """
387
+ language = _ISO_TO_NLTK_LANGUAGE.get(self.taxonomy.default_lang, "english")
388
+ sentences = sent_tokenize(self.doc_text, language=language)
389
+ weights = [1] * len(sentences)
390
+ for i, g in self.weight_dev.items():
391
+ if i < len(sentences):
392
+ weights[i] = g
393
+ concepts_scores = defaultdict(list)
394
+ for w, s in zip(weights, sentences):
395
+ for c in self.taxonomy.get_concepts_in_text(s, top_k=self.top_k, threshold=self.threshold):
396
+ concepts_scores[c[0]].append(w * c[2])
397
+ self.sentences_concepts = dict(concepts_scores)
398
+
399
+ def aggregate_sentence_scores(self, min_agg_score: float = 0) -> None:
400
+ """Identifies those of the concepts found by sentencewise similarity search which exceed a certain score after score aggregation
401
+ and stores them in attribute :pyattr:`aggregated_concepts` as a dict with concept key as key and aggregated score as value.
402
+
403
+ Args:
404
+ min_agg_score (float, optional): Minimum score after sentence aggregation. Defaults to 0.
405
+ """
406
+ self.score_concepts_sentencewise()
407
+ a_concepts = {}
408
+ for c, v in self.sentences_concepts.items():
409
+ a_score = 0
410
+ for i, score in enumerate(sorted(v, reverse=True)):
411
+ a_score += score * self.kparm / (i + self.kparm) / (i + 1 + self.kparm)
412
+ if a_score >= min_agg_score:
413
+ a_concepts[c] = a_score
414
+ self.aggregated_concepts = a_concepts
415
+
416
+ def find_best_paths(self, concept: str, n: int) -> List[List[str]]:
417
+ """Finds paths from a given concept to the root of the taxonomy hierarchy along which the mean of aggregated concept scores is greatest.
418
+
419
+ Args:
420
+ concept (str): Key of concept.
421
+ n (int): Number of paths to be returned.
422
+
423
+ Returns:
424
+ list: List of paths, each path being a list of concept keys.
425
+ """
426
+ scored_paths = [(p, sum([self.aggregated_concepts.get(ci, 0) for ci in p]) / len(p))
427
+ for p in self.taxonomy.get_parent_concepts(concept)
428
+ ]
429
+ sorted_paths = sorted(scored_paths, key=lambda x: x[1], reverse=True)
430
+ return [x[0] for x in sorted_paths[:n]]
431
+
432
+ def evaluate_hierarchy(self, max_paths: int = 2) -> None:
433
+ """Shares aggregated scores of concepts with their hierarchical predecessors and stores the results in attribute :pyattr:`hierarchy_distribution` as
434
+ a dict of dicts, where the outer keys are hierarchy levels and the inner keys are concept keys, followed by the summed-up score shares.
435
+
436
+ Args:
437
+ max_paths (int, optional): Number of paths to the root considered per concept. Defaults to 2.
438
+ """
439
+ levels = defaultdict(lambda: defaultdict(float))
440
+ for c, asc in self.aggregated_concepts.items():
441
+ cps = self.find_best_paths(c, max_paths)
442
+ for cp in cps:
443
+ for i, ci in enumerate(reversed(cp[:-1])):
444
+ levels[i][ci] += asc / len(cps)
445
+ self.hierarchy_distribution = {i: {c: v for c, v in cs.items()} for i, cs in levels.items()}
446
+
447
+ def restrict_hierarchy(self, level: int, percentage: float) -> None:
448
+ """Forms truncated lists of highest-scored concepts of a given hierarchy level such that the summed-up hierarchical scores of the remaining
449
+ concepts exceed a given percentage of the total summed-up score, and stores the results in attribute :pyattr:`hierarchy_restriction` as a dict
450
+ with the level number as key and the truncated list of concept keys as value.
451
+
452
+ Args:
453
+ level (int): Hierarchy level.
454
+ percentage (float): Percentage of the total summed-up score that the remaining concepts must exceed.
455
+ """
456
+ if self.hierarchy_distribution.get(level):
457
+ sum_list = []
458
+ list_sum = 0
459
+ for elem in sorted(self.hierarchy_distribution[level].items(), key=lambda x: x[1], reverse=True):
460
+ sum_list.append((elem, list_sum))
461
+ list_sum += elem[1]
462
+ self.hierarchy_restriction[level] = [y[0][0] for y in sum_list if y[1] / list_sum * 100 < percentage]
463
+ else:
464
+ self.hierarchy_restriction[level] = []
465
+
466
+ def get_relevant_concepts(
467
+ self,
468
+ min_score: Optional[float] = None,
469
+ max_number: int = 100,
470
+ ) -> List[Tuple[List[str], float]]:
471
+ """Returns the hierarchical paths to all concepts exceeding a minimum aggregated score, as long as the steps of these paths are contained
472
+ in the truncated level lists of concepts in ``hierarchy_restriction``.
473
+
474
+ Args:
475
+ min_score (float, optional): Minimum aggregated score. Defaults to ``None``, in which case the class attribute ``threshold`` is used.
476
+ max_number (int, optional): Maximum number of concept paths returned. Defaults to 100.
477
+
478
+ Returns:
479
+ list: List of resulting concept paths, where each concept path is a pair of the list of concept keys constituting the path (root-first) and the aggregated score.
480
+ """
481
+ if min_score is None:
482
+ min_score = self.threshold
483
+ concept_paths = sorted(
484
+ [(self.taxonomy.get_parent_concepts(c), s) for c, s in self.aggregated_concepts.items() if s >= min_score],
485
+ key=lambda x: x[1], reverse=True
486
+ )[:max_number]
487
+ concept_paths = [(cp[-2::-1], cps[1]) for cps in concept_paths for cp in cps[0]]
488
+ for level, rest in self.hierarchy_restriction.items():
489
+ concept_paths = [cp for cp in concept_paths if set(cp[0][level:level + 1]).issubset(set(rest))]
490
+ return concept_paths
491
+
492
+ def identify_concepts(
493
+ self,
494
+ top_k: int,
495
+ min_similarity: float,
496
+ title_weight: float,
497
+ saturation: float,
498
+ min_agg: float,
499
+ top_percent: float,
500
+ second_percent: float,
501
+ max_baseconcepts: int,
502
+ max_paths: int,
503
+ with_scores: bool = False,
504
+ ) -> List[List[Union[Tuple[str, str], Tuple[str, str, float]]]]:
505
+ """Convenience function for identifying concepts, given the various parameters of the algorithm.
506
+
507
+ Args:
508
+ top_k (int): Number of concepts retained when checking similarity to text.
509
+ min_similarity (float): Minimum value for similarity on sentence level.
510
+ title_weight (float): Weight for first sentence (which is supposed to be the title of the document).
511
+ saturation (float): Saturation parameter for score aggregation.
512
+ min_agg (float): Minimum value for aggregated score.
513
+ top_percent (float): Percentage for hierarchy truncation on top level.
514
+ second_percent (float): Percentage for hierarchy truncation on second level.
515
+ max_baseconcepts (int): Maximum number of returned (leaf) concepts.
516
+ max_paths (int): Number of paths to the root considered per concept.
517
+ with_scores (bool, optional): Whether to include scores. Defaults to ``False``.
518
+
519
+ Returns:
520
+ list: List of lists of ``(concept name, concept key)`` pairs, or ``(concept name, concept key, aggregated score)`` triples when ``with_scores`` is ``True``.
521
+ """
522
+ self.set_parameters(top_k=top_k, threshold=min_similarity, weight_dev={0: title_weight}, kparm=saturation)
523
+ self.aggregate_sentence_scores(min_agg_score=min_agg)
524
+ self.evaluate_hierarchy(max_paths=max_paths)
525
+ self.restrict_hierarchy(0, top_percent)
526
+ self.restrict_hierarchy(1, second_percent)
527
+ relevant_concepts_and_scores = self.get_relevant_concepts(min_score=min_agg, max_number=max_baseconcepts)
528
+ relevant_concepts = [rc[0] for rc in relevant_concepts_and_scores]
529
+ relevant_scores = [rc[1] for rc in relevant_concepts_and_scores]
530
+ if not with_scores:
531
+ return self.taxonomy.show_concepts(relevant_concepts)
532
+ else:
533
+ return self.taxonomy.show_concepts(relevant_concepts, scores=relevant_scores)
taxo-1.0.0/usage.ipynb ADDED
@@ -0,0 +1,281 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "f2ccd734",
6
+ "metadata": {},
7
+ "source": "## taxo\n\nThis notebook demonstrates the use of taxo."
8
+ },
9
+ {
10
+ "cell_type": "markdown",
11
+ "id": "ae9b02d2",
12
+ "metadata": {},
13
+ "source": [
14
+ "First import the packages used."
15
+ ]
16
+ },
17
+ {
18
+ "cell_type": "code",
19
+ "execution_count": 3,
20
+ "id": "19b68fc3",
21
+ "metadata": {},
22
+ "outputs": [],
23
+ "source": [
24
+ "import pandas as pd\n",
25
+ "import networkx as nx\n",
26
+ "from sentence_transformers import SentenceTransformer\n",
27
+ "\n",
28
+ "import taxo"
29
+ ]
30
+ },
31
+ {
32
+ "cell_type": "markdown",
33
+ "id": "186799df",
34
+ "metadata": {},
35
+ "source": [
36
+ "Then fill a pandas dataframe with a taxonomy, for instance by loading it from an excel sheet.\n",
37
+ "\n",
38
+ "Each row corresponds to a taxonomy concept. Each concept has to have a key, a label_en, a description_en (optionally also label and description in other languages), and a parents attribute, which is a string that interprets as the list of its parent concepts (note that not only trees but also more general DAGs are allowed as taxonomic hierarchies).\n",
39
+ "\n",
40
+ "There has to be on special formal concept which is the top parent of all other concepts."
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": 4,
46
+ "id": "4707d994",
47
+ "metadata": {},
48
+ "outputs": [],
49
+ "source": [
50
+ "df_media_tax = pd.read_excel(\"media.xlsx\")\n",
51
+ "df_media_tax.fillna(\"\", inplace=True)"
52
+ ]
53
+ },
54
+ {
55
+ "cell_type": "markdown",
56
+ "id": "b5bf204a",
57
+ "metadata": {},
58
+ "source": [
59
+ "The example contained in the file `media.xlsx` is extracted from the IPTC media topics taxonomy, a taxonomy often used for subject classification of news articles.\n",
60
+ "\n",
61
+ "Note that the top parent node has the key \"M00000000\".\n",
62
+ "\n",
63
+ "In the following, we use the embedding model [all-MiniLM-L6-v2](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2)."
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": 5,
69
+ "id": "49c013c5",
70
+ "metadata": {},
71
+ "outputs": [
72
+ {
73
+ "data": {
74
+ "text/plain": [
75
+ "<taxo.ConceptTaxonomy at 0x21ce1ad3770>"
76
+ ]
77
+ },
78
+ "execution_count": 5,
79
+ "metadata": {},
80
+ "output_type": "execute_result"
81
+ }
82
+ ],
83
+ "source": [
84
+ "media_tax = taxo.ConceptTaxonomy(nx.DiGraph(), \"M00000000\", transformer=SentenceTransformer(\"all-MiniLM-L6-v2\"))\n",
85
+ "media_tax.from_df(df_media_tax)"
86
+ ]
87
+ },
88
+ {
89
+ "cell_type": "markdown",
90
+ "id": "a4cc827d",
91
+ "metadata": {},
92
+ "source": [
93
+ "Now, given an example text ..."
94
+ ]
95
+ },
96
+ {
97
+ "cell_type": "code",
98
+ "execution_count": 6,
99
+ "id": "fbd75c2f",
100
+ "metadata": {},
101
+ "outputs": [],
102
+ "source": [
103
+ "text = \"\"\"\n",
104
+ "AI opens up new possibilities for air traffic control and the cockpit.\n",
105
+ "\n",
106
+ "How will artificial intelligence (AI) support air traffic controllers and pilots in the future? Over the past four years, \n",
107
+ "the German Aerospace Center (Deutsches Zentrum für Luft- und Raumfahrt; DLR) has been investigating this question as part \n",
108
+ "of the LOKI project (Kollaboration von Luftfahrt-OperateurInnen und KI-Systemen, or 'Collaboration between Aviation \n",
109
+ "Operators and AI Systems' in English). In collaboration with national and international companies and research \n",
110
+ "institutions from the air traffic control sector, as well as public authorities, the project also developed relevant \n",
111
+ "recommendations for introducing AI into aviation.\n",
112
+ "\n",
113
+ "The aviation industry is currently working intensively on introducing AI systems. Unlike conventional assistance systems, \n",
114
+ "these can process large volumes of data, be trained on the basis of this data and so adapt flexibly to new situations. \n",
115
+ "AI systems offer the potential to increase the efficiency of air traffic and relieve the workload on highly qualified \n",
116
+ "specialists, while at the same time ensuring safety, transparency and human control at all times.\n",
117
+ "\n",
118
+ "\"In the DLR LOKI project, we investigated how humans and AI systems can cooperate successfully in aviation. Our aim was to \n",
119
+ "make this collaboration trustworthy, transparent and user-centred,\" says the project’s scientific lead, Carmen Bruder from \n",
120
+ "the DLR Institute of Aerospace Medicine.\n",
121
+ "\n",
122
+ "The researchers developed a digital air traffic controller as an AI-supported team member for air traffic control and an \n",
123
+ "intelligent assistance system for pilots, and made recommendations for the safe introduction of trustworthy AI into aviation.\n",
124
+ "\n",
125
+ "One focus of the research was the development of the digital air traffic controller DIRC (Digital Interactive Reliable \n",
126
+ "Controller). Unlike traditional assistance systems, the DIRC not only supports human controllers with information or \n",
127
+ "recommendations but can also take on certain tasks independently. This allows for dynamic coordination with human air traffic \n",
128
+ "controllers regarding which tasks are to be undertaken.\n",
129
+ "\n",
130
+ "Simulations showed that human and digital air traffic controllers can divide their tasks efficiently. Together, they managed \n",
131
+ "up to 25 percent more than the current maximum traffic volume specified for an airspace sector. The research thus addresses \n",
132
+ "a key challenge facing European air traffic: rising traffic volumes coupled with the increasing challenge of maintaining \n",
133
+ "staffing levels in air traffic control.\n",
134
+ "\n",
135
+ "In addition to air traffic control, the researchers developed the Intelligent Pilot Assistance System (IPAS), a demonstrator \n",
136
+ "designed to support pilots in the cockpit.\n",
137
+ "\n",
138
+ "The system is designed to support crews particularly in time-critical situations, for example when selecting alternative \n",
139
+ "flight routes or suitable alternate airports. The AI analyses large amounts of relevant information and processes it to aid \n",
140
+ "decision-making. At the same time, the team investigated what information pilots need in order to understand and reliably \n",
141
+ "assess the recommendations of an AI system.\n",
142
+ "\n",
143
+ "In addition to technical developments, the project investigated the conditions necessary for trustworthy collaboration \n",
144
+ "between humans and AI systems. The researchers analysed the expectations and requirements of aviation personnel and assessed \n",
145
+ "how collaboration between humans and AI in aviation should be structured.\n",
146
+ "\n",
147
+ "A key finding is that trust is not built solely on a system’s performance. Crucial factors include transparent information \n",
148
+ "about the AI's capabilities and limitations, suitable training opportunities and the gradual introduction of new functions. \n",
149
+ "Based on the project findings, important recommendations were formulated; taking these into account will help ensure the \n",
150
+ "successful development and introduction of AI systems in aviation.\n",
151
+ "\n",
152
+ "The findings form a basis for the further development of AI systems in aviation and will feed into the European debate on the \n",
153
+ "safe introduction of such systems.\n",
154
+ "\"\"\""
155
+ ]
156
+ },
157
+ {
158
+ "cell_type": "markdown",
159
+ "id": "4432982c",
160
+ "metadata": {},
161
+ "source": [
162
+ "... we can prepare it for tagging."
163
+ ]
164
+ },
165
+ {
166
+ "cell_type": "code",
167
+ "execution_count": 7,
168
+ "id": "10997545",
169
+ "metadata": {},
170
+ "outputs": [],
171
+ "source": [
172
+ "taxed_document = taxo.TaxedDocument(media_tax, text)"
173
+ ]
174
+ },
175
+ {
176
+ "cell_type": "markdown",
177
+ "id": "6024862e",
178
+ "metadata": {},
179
+ "source": [
180
+ "The following set of hyperparameters typically works well with this taxonomy. Other taxonomies might require tuning of these parameters."
181
+ ]
182
+ },
183
+ {
184
+ "cell_type": "code",
185
+ "execution_count": 8,
186
+ "id": "3dc3a401",
187
+ "metadata": {},
188
+ "outputs": [],
189
+ "source": [
190
+ "tagging_parameters = (\n",
191
+ " 10, # Max number of concepts initially assigned to each sentence\n",
192
+ " 0.25, # Minimum similarity value on sentence level\n",
193
+ " 2, # Weight for title sentence\n",
194
+ " 1.5, # Saturation parameter for score aggregation\n",
195
+ " 0.25, # Minimum value for aggregated score \n",
196
+ " 80, # Percentage for hierarchy truncation on top level\n",
197
+ " 80, # Percentage for hierarchy truncation on second level\n",
198
+ " 7, # Maximum number of returned leaf concepts \n",
199
+ " 2, # Number of paths to the root considered per concept\n",
200
+ ")"
201
+ ]
202
+ },
203
+ {
204
+ "cell_type": "markdown",
205
+ "id": "4c00e4f1",
206
+ "metadata": {},
207
+ "source": [
208
+ "Now we are ready to identify the taxonomy tags for the given text. The result shows the highest scored concepts including their hierarchical anchestry."
209
+ ]
210
+ },
211
+ {
212
+ "cell_type": "code",
213
+ "execution_count": 9,
214
+ "id": "24e9f432",
215
+ "metadata": {},
216
+ "outputs": [
217
+ {
218
+ "data": {
219
+ "text/plain": [
220
+ "[[('economy, business and finance', 'M04000000', 0.572107398702342),\n",
221
+ " ('economic sector', 'M20000209', 0.572107398702342),\n",
222
+ " ('transport', 'M20000337', 0.572107398702342),\n",
223
+ " ('air transport', 'M20000338', 0.572107398702342)],\n",
224
+ " [('science and technology', 'M13000000', 0.4709945980217274),\n",
225
+ " ('technology and engineering', 'M20000756', 0.4709945980217274),\n",
226
+ " ('information technology and computer science',\n",
227
+ " 'M20000763',\n",
228
+ " 0.4709945980217274),\n",
229
+ " ('artificial intelligence', 'M20001298', 0.4709945980217274)],\n",
230
+ " [('disaster, accident and emergency incident',\n",
231
+ " 'M03000000',\n",
232
+ " 0.4322114299130465),\n",
233
+ " ('accident and emergency incident', 'M20000139', 0.4322114299130465),\n",
234
+ " ('transportation accident and incident', 'M20000143', 0.4322114299130465),\n",
235
+ " ('air and space accident and incident', 'M20000144', 0.4322114299130465)],\n",
236
+ " [('science and technology', 'M13000000', 0.4314064954667891),\n",
237
+ " ('technology and engineering', 'M20000756', 0.4314064954667891),\n",
238
+ " ('aerospace engineering', 'M20000757', 0.4314064954667891)],\n",
239
+ " [('science and technology', 'M13000000', 0.42194015078626435),\n",
240
+ " ('scientific research', 'M20000735', 0.42194015078626435),\n",
241
+ " ('scientific innovation', 'M20000736', 0.42194015078626435)],\n",
242
+ " [('disaster, accident and emergency incident',\n",
243
+ " 'M03000000',\n",
244
+ " 0.3640833247315276),\n",
245
+ " ('emergency incident', 'M20000160', 0.3640833247315276),\n",
246
+ " ('transport incident', 'M20000162', 0.3640833247315276),\n",
247
+ " ('air and space incident', 'M20000163', 0.3640833247315276)]]"
248
+ ]
249
+ },
250
+ "execution_count": 9,
251
+ "metadata": {},
252
+ "output_type": "execute_result"
253
+ }
254
+ ],
255
+ "source": [
256
+ "taxed_document.identify_concepts(*tagging_parameters, with_scores=True)"
257
+ ]
258
+ }
259
+ ],
260
+ "metadata": {
261
+ "kernelspec": {
262
+ "display_name": ".venv",
263
+ "language": "python",
264
+ "name": "python3"
265
+ },
266
+ "language_info": {
267
+ "codemirror_mode": {
268
+ "name": "ipython",
269
+ "version": 3
270
+ },
271
+ "file_extension": ".py",
272
+ "mimetype": "text/x-python",
273
+ "name": "python",
274
+ "nbconvert_exporter": "python",
275
+ "pygments_lexer": "ipython3",
276
+ "version": "3.13.3"
277
+ }
278
+ },
279
+ "nbformat": 4,
280
+ "nbformat_minor": 5
281
+ }