From e69a8309435101e767d6a1a969d8d119792fc602 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 12 Mar 2025 17:40:06 +0100 Subject: [PATCH 01/59] [metrics] First draft on mkdocs-version --- .gitignore | 1 + README.bkp.md | 173 ++++++++++++++++ README.md | 184 ++++-------------- docs/index.md | 21 ++ docs/macros/__pycache__/main.cpython-312.pyc | Bin 0 -> 1290 bytes docs/macros/main.py | 15 ++ docs/metrics/04_outgoing_edges.md | 37 ++++ docs/metrics/05_incoming_edges.md | 37 ++++ docs/metrics/index.md | 10 + mkdocs.yml | 18 ++ .../examples}/Educational resources.rq | 0 {examples => queries/examples}/Metadata.rq | 0 .../examples}/Services in the NFDI4Earth.rq | 0 queries/metrics/GM0004_1.rq | 8 + queries/metrics/GM0004_2.rq | 6 + queries/metrics/GM0004_3.rq | 6 + queries/metrics/GM0004_4.rq | 14 ++ queries/metrics/GM0005_1.rq | 7 + queries/metrics/GM0005_2.rq | 6 + queries/metrics/GM0005_3.rq | 6 + queries/metrics/GM0005_4.rq | 14 ++ queries/metrics/GM001.rq | 6 + queries/metrics/GM002_1.rq | 13 ++ queries/metrics/GM002_2.rq | 12 ++ queries/metrics/GM002_3.rq | 12 ++ queries/metrics/GM003_1.rq | 34 ++++ queries/metrics/GM003_2.rq | 37 ++++ queries/metrics/GM003_3.rq | 25 +++ queries/metrics/GM003_4.rq | 26 +++ queries/{ => questions}/AG001.rq | 0 queries/{ => questions}/AG001_2.rq | 0 queries/{ => questions}/AG002_1.rq | 0 queries/{ => questions}/AG002_2.rq | 0 queries/{ => questions}/AT001.rq | 0 queries/{ => questions}/AT002_1.rq | 0 queries/{ => questions}/AT002_2.rq | 0 queries/{ => questions}/DA001.rq | 0 queries/{ => questions}/DA002_1.rq | 0 queries/{ => questions}/DA002_2.rq | 0 queries/{ => questions}/DA003_1.rq | 0 queries/{ => questions}/LH001.rq | 0 queries/{ => questions}/LH002_1.rq | 0 queries/{ => questions}/LH002_2.rq | 0 queries/{ => questions}/LR001.rq | 0 queries/{ => questions}/LR002_1.rq | 0 queries/{ => questions}/LR002_2.rq | 0 queries/{ => questions}/MS001.rq | 0 queries/{ => questions}/MS002_1.rq | 0 queries/{ => questions}/MS002_2.rq | 0 queries/{ => questions}/OG001.rq | 0 queries/{ => questions}/OG002_1.rq | 0 queries/{ => questions}/OG002_2.rq | 0 queries/{ => questions}/PE001.rq | 0 queries/{ => questions}/PE002_1.rq | 0 queries/{ => questions}/PE002_2.rq | 0 queries/{ => questions}/REG001.rq | 0 queries/{ => questions}/REG002_1.rq | 0 queries/{ => questions}/REG002_2.rq | 0 queries/{ => questions}/REP001.rq | 0 queries/{ => questions}/REP002_1.rq | 0 queries/{ => questions}/REP002_2.rq | 0 queries/{ => questions}/RP001.rq | 0 queries/{ => questions}/RP002_1.rq | 0 queries/{ => questions}/RP002_2.rq | 0 queries/{ => questions}/SC001.rq | 0 queries/{ => questions}/SC002_1.rq | 0 queries/{ => questions}/SC002_2.rq | 0 queries/{ => questions}/TY001.rq | 0 queries/{ => questions}/old/DR001_1.rq | 0 queries/{ => questions}/old/OR001_1.rq | 0 queries/{ => questions}/old/OR001_2.rq | 0 queries/{ => questions}/old/OR002_1.rq | 0 queries/{ => questions}/old/OR003_1.rq | 0 queries/{ => questions}/old/OR004_1.rq | 0 queries/{ => questions}/old/OR005_1.rq | 0 queries/{ => questions}/old/OR006_1.rq | 0 reports/metrics/0004.txt | 4 + reports/metrics/0005.txt | 4 + requirements.txt | 3 + 79 files changed, 592 insertions(+), 147 deletions(-) create mode 100644 .gitignore create mode 100644 README.bkp.md create mode 100644 docs/index.md create mode 100644 docs/macros/__pycache__/main.cpython-312.pyc create mode 100644 docs/macros/main.py create mode 100644 docs/metrics/04_outgoing_edges.md create mode 100644 docs/metrics/05_incoming_edges.md create mode 100644 docs/metrics/index.md create mode 100644 mkdocs.yml rename {examples => queries/examples}/Educational resources.rq (100%) rename {examples => queries/examples}/Metadata.rq (100%) rename {examples => queries/examples}/Services in the NFDI4Earth.rq (100%) create mode 100644 queries/metrics/GM0004_1.rq create mode 100644 queries/metrics/GM0004_2.rq create mode 100644 queries/metrics/GM0004_3.rq create mode 100644 queries/metrics/GM0004_4.rq create mode 100644 queries/metrics/GM0005_1.rq create mode 100644 queries/metrics/GM0005_2.rq create mode 100644 queries/metrics/GM0005_3.rq create mode 100644 queries/metrics/GM0005_4.rq create mode 100644 queries/metrics/GM001.rq create mode 100644 queries/metrics/GM002_1.rq create mode 100644 queries/metrics/GM002_2.rq create mode 100644 queries/metrics/GM002_3.rq create mode 100644 queries/metrics/GM003_1.rq create mode 100644 queries/metrics/GM003_2.rq create mode 100644 queries/metrics/GM003_3.rq create mode 100644 queries/metrics/GM003_4.rq rename queries/{ => questions}/AG001.rq (100%) rename queries/{ => questions}/AG001_2.rq (100%) rename queries/{ => questions}/AG002_1.rq (100%) rename queries/{ => questions}/AG002_2.rq (100%) rename queries/{ => questions}/AT001.rq (100%) rename queries/{ => questions}/AT002_1.rq (100%) rename queries/{ => questions}/AT002_2.rq (100%) rename queries/{ => questions}/DA001.rq (100%) rename queries/{ => questions}/DA002_1.rq (100%) rename queries/{ => questions}/DA002_2.rq (100%) rename queries/{ => questions}/DA003_1.rq (100%) rename queries/{ => questions}/LH001.rq (100%) rename queries/{ => questions}/LH002_1.rq (100%) rename queries/{ => questions}/LH002_2.rq (100%) rename queries/{ => questions}/LR001.rq (100%) rename queries/{ => questions}/LR002_1.rq (100%) rename queries/{ => questions}/LR002_2.rq (100%) rename queries/{ => questions}/MS001.rq (100%) rename queries/{ => questions}/MS002_1.rq (100%) rename queries/{ => questions}/MS002_2.rq (100%) rename queries/{ => questions}/OG001.rq (100%) rename queries/{ => questions}/OG002_1.rq (100%) rename queries/{ => questions}/OG002_2.rq (100%) rename queries/{ => questions}/PE001.rq (100%) rename queries/{ => questions}/PE002_1.rq (100%) rename queries/{ => questions}/PE002_2.rq (100%) rename queries/{ => questions}/REG001.rq (100%) rename queries/{ => questions}/REG002_1.rq (100%) rename queries/{ => questions}/REG002_2.rq (100%) rename queries/{ => questions}/REP001.rq (100%) rename queries/{ => questions}/REP002_1.rq (100%) rename queries/{ => questions}/REP002_2.rq (100%) rename queries/{ => questions}/RP001.rq (100%) rename queries/{ => questions}/RP002_1.rq (100%) rename queries/{ => questions}/RP002_2.rq (100%) rename queries/{ => questions}/SC001.rq (100%) rename queries/{ => questions}/SC002_1.rq (100%) rename queries/{ => questions}/SC002_2.rq (100%) rename queries/{ => questions}/TY001.rq (100%) rename queries/{ => questions}/old/DR001_1.rq (100%) rename queries/{ => questions}/old/OR001_1.rq (100%) rename queries/{ => questions}/old/OR001_2.rq (100%) rename queries/{ => questions}/old/OR002_1.rq (100%) rename queries/{ => questions}/old/OR003_1.rq (100%) rename queries/{ => questions}/old/OR004_1.rq (100%) rename queries/{ => questions}/old/OR005_1.rq (100%) rename queries/{ => questions}/old/OR006_1.rq (100%) create mode 100644 reports/metrics/0004.txt create mode 100644 reports/metrics/0005.txt create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..854d509 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*venv* diff --git a/README.bkp.md b/README.bkp.md new file mode 100644 index 0000000..6f8681c --- /dev/null +++ b/README.bkp.md @@ -0,0 +1,173 @@ +# KnowledgeHub - Domain Coverage + +This is collection of relevant questions and corresponding SPARQL-Queries, that answer those questions. The questions are grouped according to the different entities of interest (datasets, organizations, ...). The entities appear in alphabetical order. The first query is useful to get an overview of all entities available in the Knowledge Hub. The questions listed below form, altogether, the domain coverage of the Knowledge Hub. For details, see the [NFDI4Earth Deliverable D4.3.2](https://zenodo.org/records/7950860). + +***Overview of the types of entities*** +| ID | Question | Query/ies | +|---|---|---| +| TY001 | What are the types of entities available in the knowledge graph? | [TY001](queries/TY001.rq)| + + + + +### Aggregator + +| ID | Question | Query/ies | +|---|---|---| +| AG001 | What are all entities of type Aggregator? | [AG001](queries/AG001.rq)| +| AG001_2 | What are name and geometry of Aggregator? | [AG001_2](queries/AG001_2.rq)| +| AG002_1 | What are all attributes available for the type "Aggregator"? | [AG002_1](queries/AG002_1.rq)| +| AG002_2 | How many attributes are available for the type "Aggregator"? | [AG002_2](queries/AG002_2.rq)| + +### Article + +| ID | Question | Query/ies | +|---|---|---| +| AT001 | What are all entities of type schema:Article? | [AT001](queries/AT001.rq)| +| AT002_1 | What are all attributes available for the type "schema:Article"? | [AT002_1](queries/AT002_1.rq)| +| AT002_2 | How many attributes are available for the type "schema:Article"? | [AT002_2](queries/AT002_2.rq)| + +### Dataset + +| ID | Question | Query/ies | +|---|---|---| +| DA001 | What are all entities of type dcat:Dataset? | [DA001](queries/DA001.rq)| +| DA002_1 | What are all attributes available for the type "dcat:Dataset"? | [DA002_1](queries/DA002_1.rq)| +| DA002_2 | How many attributes are available for the type "dcat:Dataset"? | [DA002_2](queries/DA002_2.rq)| +| DA003_1 | What are the datasets having the string 'world settlement footprint' in title or description? | [DA003_1](queries/DA003_1.rq)| + +### LHBArticle + +| ID | Question | Query/ies | +|---|---|---| +| LH001 | What are all entities of type LHBArticle? | [LH001](queries/LH001.rq)| +| LH002_1 | What are all attributes available for the type "LHBArticle"? | [LH002_1](queries/LH002_1.rq)| +| LH002_2 | How many attributes are available for the type "LHBArticle"? | [LH002_2](queries/LH002_2.rq)| + +### LearningResource + +| ID | Question | Query/ies | +|---|---|---| +| LR001 | What are all entities of type LearningResource? | [LR001](queries/LR001.rq)| +| LR002_1 | What are all attributes available for the type "LearningResource"? | [LR002_1](queries/LR002_1.rq)| +| LR002_2 | How many attributes are available for the type "LearningResource"? | [LR002_2](queries/LR002_2.rq)| + +### MetadataStandard + +| ID | Question | Query/ies | +|---|---|---| +| MS001 | What are all entities of type MetadataStandard? | [MS001](queries/MS001.rq)| +| MS002_1 | What are all attributes available for the type "MetadataStandard"? | [MS002_1](queries/MS002_1.rq)| +| MS002_2 | How many attributes are available for the type "MetadataStandard"? | [MS002_2](queries/MS002_2.rq)| + +### Organization + +| ID | Question | Query/ies | +|---|---|---| +| OG001 | What are all entities of type Organization? | [OG001](queries/OG001.rq)| +| OG002_1 | What are all attributes available for the type "Organization"? | [OG002_1](queries/OG002_1.rq)| +| OG002_2 | How many attributes are available for the type "Organization"? | [OG002_2](queries/OG002_2.rq)| + +### Person + +| ID | Question | Query/ies | +|---|---|---| +| PE001 | What are all entities of type Person? | [PE001](queries/PE001.rq)| +| PE002_1 | What are all attributes available for the type "Person"? | [PE002_1](queries/PE002_1.rq)| +| PE002_2 | How many attributes are available for the type "Person"? | [PE002_2](queries/PE002_2.rq)| + +### Registry + +| ID | Question | Query/ies | +|---|---|---| +| REG001 | What are all entities of type Registry? | [REG001](queries/REG001.rq)| +| REG002_1 | What are all attributes available for the type "Registry"? | [REG002_1](queries/REG002_1.rq)| +| REG002_2 | How many attributes are available for the type "Registry"? | [REG002_2](queries/REG002_2.rq)| + +### Repository + +| ID | Question | Query/ies | +|---|---|---| +| REP001 | What are all entities of type Repository? | [REP001](queries/REP001.rq)| +| REP002_1 | What are all attributes available for the type "Repository"? | [REP002_1](queries/REP002_1.rq)| +| REP002_2 | How many attributes are available for the type "Repository"? | [REP002_2](queries/REP002_2.rq)| + +### ResearchProject + +| ID | Question | Query/ies | +|---|---|---| +| RP001 | What are all entities of type ResearchProject? | [RP001](queries/RP001.rq)| +| RP002_1 | What are all attributes available for the type "ResearchProject"? | [RP002_1](queries/RP002_1.rq)| +| RP002_2 | How many attributes are available for the type "ResearchProject"? | [RP002_2](queries/RP002_2.rq)| + + +### SoftwareSourceCode + +| ID | Question | Query/ies | +|---|---|---| +| SC001 | What are all entities of type SoftwareSourceCode? | [SC001](queries/SC001.rq)| +| SC002_1 | What are all attributes available for the type "SoftwareSourceCode"? | [SC002_1](queries/SC002_1.rq)| +| SC002_2 | How many attributes are available for the type "SoftwareSourceCode"? | [SC002_2](queries/SC002_2.rq)| + +### Graph Metrics + +| ID | Question | Query/ies | +|---|---|---| +|GM001|The number of instances in a graph|[GM001](queries/GM001.rq)| +|GM002|The number of assertions (or edges between entities)|[GM002](queries/GM002.rq)| +|||| +|||| +|||| +|||| + + +<!--- Template for a new table (including first line) + +### EntityType + +| ID | Question | Query/ies | +|---|---|---| +| XX001 | What are all entities of type EntityType? | [XX001](queries/XX001.rq)| + +--> + + +<!--- + + +### Organizations + +| ID | Question | Query/ies | +|---|---|---| +| OR001 | What is the URL of the homepage for the organization with the following name: 'Karlsruhe Institute of Technology'? | [OR001_1](queries/OR001_1.rq),[OR001_2](queries/OR001_2.rq) | +| OR002 | What is the URL of the homepage for the organization with the following ID: 'https://nfdi4earth-knowledgehub.geo.tu-dresden.de/api/objects/n4ekh/a38143be5e15bed94a20' | [OR003_1](queries/OR003_1.rq) | +| OR003 | Which organizations have not defined any homepage? | [OR003_1](queries/OR003_1.rq) | +| OR004 | Which services are published by the organization? | [OR004_1](queries/OR004_1.rq) | +| OR005 | What is the geolocation of the organization called 'TU Dresden'? | [OR005_1](queries/OR005_1.rq) | +| OR006 | What is the geolocation of all organizations, that are members of the NFDI4Earth consortium? | [OR006_1](queries/OR006_1.rq) | + +### Repositories + +| ID | Question | Query/ies | +|----|----------|-----------| +| DR1 | At which repository can I archive my [geophysical] data of [2] GB?| [OR004_1](queries/OR004_1.rq) | +| DR2 | What is the temporal coverage of a data repository?|| +| DR3 | What is the spatial coverage of a data repository?|| +| DR4 | What is the curation policy of the data repository?|| +| DR5 | Which licences are supported by the data repository?|| +| DR6 | Does the repository give identifiers for its ressources?|| +| DR7 | Which metadata harversting interface is supported by the repository?|| +| DR8 | Which type of (persistent) identifiers are used by the repository?|| +| DR9 | What is the thematic area/subject of a repository?|| +| DR10 | Limitations of data deposit at the repository?|| +| DR11 | When was the medatada for a given repository first collected/last updated?|| +| DR12 | Is the repository still available?|| +| DR13 | Which repository allows long term archiving?|| + +--> + +# Notes + +This question-based approach takes inspiration from the [GeoSPARQLBenchmark](https://github.com/OpenLinkSoftware/GeoSPARQLBenchmark). + +It is directly linked to the [Knowledge Hub landing page project](https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_landingpage) as all the questions and examples are taken to explain the basic idea and demonstrate usage of the [Knowledge Hub](https://knowledgehub.nfdi4earth.de). diff --git a/README.md b/README.md index afdcdbd..d750a1c 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,52 @@ -# KnowledgeHub - Domain Coverage +# NFDI4Earth - KnowledgeHub - KnowledgeGraph Analysis -This is collection of relevant questions and corresponding SPARQL-Queries, that answer those questions. The questions are grouped according to the different entities of interest (datasets, organizations, ...). The entities appear in alphabetical order. The first query is useful to get an overview of all entities available in the Knowledge Hub. The questions listed below form, altogether, the domain coverage of the Knowledge Hub. For details, see the [NFDI4Earth Deliverable D4.3.2](https://zenodo.org/records/7950860). +A collection of SPARQL queries for analyzing the KnowledgeGraph of NFDI4Earth's KnowledgeHub. -***Overview of the types of entities*** -| ID | Question | Query/ies | -|---|---|---| -| TY001 | What are the types of entities available in the knowledge graph? | [TY001](queries/TY001.rq)| +## Repository Structure - - +``` +queries/ +├── examples/ # Basic SPARQL examples +├── questions/ # Domain-specific queries +└── metrics/ # Graph analysis queries -### Aggregator +docs/ +├── examples.md # Documentation for basic examples +├── questions.md # Documentation for domain questions +└── metrics.md # Documentation for graph metrics +``` -| ID | Question | Query/ies | -|---|---|---| -| AG001 | What are all entities of type Aggregator? | [AG001](queries/AG001.rq)| -| AG001_2 | What are name and geometry of Aggregator? | [AG001_2](queries/AG001_2.rq)| -| AG002_1 | What are all attributes available for the type "Aggregator"? | [AG002_1](queries/AG002_1.rq)| -| AG002_2 | How many attributes are available for the type "Aggregator"? | [AG002_2](queries/AG002_2.rq)| +## Usage -### Article +All queries are stored in `.rq` files and can be executed against the NFDI4Earth KnowledgeGraph endpoint. -| ID | Question | Query/ies | -|---|---|---| -| AT001 | What are all entities of type schema:Article? | [AT001](queries/AT001.rq)| -| AT002_1 | What are all attributes available for the type "schema:Article"? | [AT002_1](queries/AT002_1.rq)| -| AT002_2 | How many attributes are available for the type "schema:Article"? | [AT002_2](queries/AT002_2.rq)| +### Local Development -### Dataset +0. Setup virtual environment +```bash +python3 -m venv venv +. venv/bin/activate +``` -| ID | Question | Query/ies | -|---|---|---| -| DA001 | What are all entities of type dcat:Dataset? | [DA001](queries/DA001.rq)| -| DA002_1 | What are all attributes available for the type "dcat:Dataset"? | [DA002_1](queries/DA002_1.rq)| -| DA002_2 | How many attributes are available for the type "dcat:Dataset"? | [DA002_2](queries/DA002_2.rq)| -| DA003_1 | What are the datasets having the string 'world settlement footprint' in title or description? | [DA003_1](queries/DA003_1.rq)| +1. Install dependencies: +```bash +pip install -r requirements.txt +``` -### LHBArticle +2. Start local documentation server: +```bash +mkdocs serve +``` -| ID | Question | Query/ies | -|---|---|---| -| LH001 | What are all entities of type LHBArticle? | [LH001](queries/LH001.rq)| -| LH002_1 | What are all attributes available for the type "LHBArticle"? | [LH002_1](queries/LH002_1.rq)| -| LH002_2 | How many attributes are available for the type "LHBArticle"? | [LH002_2](queries/LH002_2.rq)| +3. Build documentation: +```bash +mkdocs build +``` -### LearningResource +## Contributing -| ID | Question | Query/ies | -|---|---|---| -| LR001 | What are all entities of type LearningResource? | [LR001](queries/LR001.rq)| -| LR002_1 | What are all attributes available for the type "LearningResource"? | [LR002_1](queries/LR002_1.rq)| -| LR002_2 | How many attributes are available for the type "LearningResource"? | [LR002_2](queries/LR002_2.rq)| +We welcome contributions! Please check our contribution guidelines for adding new queries. -### MetadataStandard +## Contact -| ID | Question | Query/ies | -|---|---|---| -| MS001 | What are all entities of type MetadataStandard? | [MS001](queries/MS001.rq)| -| MS002_1 | What are all attributes available for the type "MetadataStandard"? | [MS002_1](queries/MS002_1.rq)| -| MS002_2 | How many attributes are available for the type "MetadataStandard"? | [MS002_2](queries/MS002_2.rq)| - -### Organization - -| ID | Question | Query/ies | -|---|---|---| -| OG001 | What are all entities of type Organization? | [OG001](queries/OG001.rq)| -| OG002_1 | What are all attributes available for the type "Organization"? | [OG002_1](queries/OG002_1.rq)| -| OG002_2 | How many attributes are available for the type "Organization"? | [OG002_2](queries/OG002_2.rq)| - -### Person - -| ID | Question | Query/ies | -|---|---|---| -| PE001 | What are all entities of type Person? | [PE001](queries/PE001.rq)| -| PE002_1 | What are all attributes available for the type "Person"? | [PE002_1](queries/PE002_1.rq)| -| PE002_2 | How many attributes are available for the type "Person"? | [PE002_2](queries/PE002_2.rq)| - -### Registry - -| ID | Question | Query/ies | -|---|---|---| -| REG001 | What are all entities of type Registry? | [REG001](queries/REG001.rq)| -| REG002_1 | What are all attributes available for the type "Registry"? | [REG002_1](queries/REG002_1.rq)| -| REG002_2 | How many attributes are available for the type "Registry"? | [REG002_2](queries/REG002_2.rq)| - -### Repository - -| ID | Question | Query/ies | -|---|---|---| -| REP001 | What are all entities of type Repository? | [REP001](queries/REP001.rq)| -| REP002_1 | What are all attributes available for the type "Repository"? | [REP002_1](queries/REP002_1.rq)| -| REP002_2 | How many attributes are available for the type "Repository"? | [REP002_2](queries/REP002_2.rq)| - -### ResearchProject - -| ID | Question | Query/ies | -|---|---|---| -| RP001 | What are all entities of type ResearchProject? | [RP001](queries/RP001.rq)| -| RP002_1 | What are all attributes available for the type "ResearchProject"? | [RP002_1](queries/RP002_1.rq)| -| RP002_2 | How many attributes are available for the type "ResearchProject"? | [RP002_2](queries/RP002_2.rq)| - - -### SoftwareSourceCode - -| ID | Question | Query/ies | -|---|---|---| -| SC001 | What are all entities of type SoftwareSourceCode? | [SC001](queries/SC001.rq)| -| SC002_1 | What are all attributes available for the type "SoftwareSourceCode"? | [SC002_1](queries/SC002_1.rq)| -| SC002_2 | How many attributes are available for the type "SoftwareSourceCode"? | [SC002_2](queries/SC002_2.rq)| - - -<!--- Template for a new table (including first line) - -### EntityType - -| ID | Question | Query/ies | -|---|---|---| -| XX001 | What are all entities of type EntityType? | [XX001](queries/XX001.rq)| - ---> - - -<!--- - - -### Organizations - -| ID | Question | Query/ies | -|---|---|---| -| OR001 | What is the URL of the homepage for the organization with the following name: 'Karlsruhe Institute of Technology'? | [OR001_1](queries/OR001_1.rq),[OR001_2](queries/OR001_2.rq) | -| OR002 | What is the URL of the homepage for the organization with the following ID: 'https://nfdi4earth-knowledgehub.geo.tu-dresden.de/api/objects/n4ekh/a38143be5e15bed94a20' | [OR003_1](queries/OR003_1.rq) | -| OR003 | Which organizations have not defined any homepage? | [OR003_1](queries/OR003_1.rq) | -| OR004 | Which services are published by the organization? | [OR004_1](queries/OR004_1.rq) | -| OR005 | What is the geolocation of the organization called 'TU Dresden'? | [OR005_1](queries/OR005_1.rq) | -| OR006 | What is the geolocation of all organizations, that are members of the NFDI4Earth consortium? | [OR006_1](queries/OR006_1.rq) | - -### Repositories - -| ID | Question | Query/ies | -|----|----------|-----------| -| DR1 | At which repository can I archive my [geophysical] data of [2] GB?| [OR004_1](queries/OR004_1.rq) | -| DR2 | What is the temporal coverage of a data repository?|| -| DR3 | What is the spatial coverage of a data repository?|| -| DR4 | What is the curation policy of the data repository?|| -| DR5 | Which licences are supported by the data repository?|| -| DR6 | Does the repository give identifiers for its ressources?|| -| DR7 | Which metadata harversting interface is supported by the repository?|| -| DR8 | Which type of (persistent) identifiers are used by the repository?|| -| DR9 | What is the thematic area/subject of a repository?|| -| DR10 | Limitations of data deposit at the repository?|| -| DR11 | When was the medatada for a given repository first collected/last updated?|| -| DR12 | Is the repository still available?|| -| DR13 | Which repository allows long term archiving?|| - ---> - -# Notes - -This question-based approach takes inspiration from the [GeoSPARQLBenchmark](https://github.com/OpenLinkSoftware/GeoSPARQLBenchmark). - -It is directly linked to the [Knowledge Hub landing page project](https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_landingpage) as all the questions and examples are taken to explain the basic idea and demonstrate usage of the [Knowledge Hub](https://knowledgehub.nfdi4earth.de). +For questions about the NFDI4Earth KnowledgeHub Graph, contact [helpdesk@nfdi4earth.de](mailto:helpdesk@nfdi4earth.de?subject=[NFDI4Earth][KnowlegeGraph]). diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..5d30e05 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,21 @@ +# {{ config.site_name }} + +Welcome to the NFDI4Earth KnowledgeGraph Query Collection. This documentation provides insights into the NFDI4Earth KnowledgeHub Graph through SPARQL queries and their analysis. + +## Purpose + +The NFDI4Earth KnowledgeGraph represents a comprehensive network of earth science research data, connecting various domains, datasets, and research artifacts. Through SPARQL queries, we explore: + +- **Data Discovery**: Finding relevant research data across earth science domains +- **Domain Coverage**: Understanding the breadth and depth of represented research areas +- **Graph Structure**: Analyzing the knowledge graph's characteristics and connectivity + +## Exploration Areas + +We collect queries for three main purposes: + +1. **Basic Examples** to demonstrate common query patterns and graph exploration +2. **Domain Questions** addressing specific research data discovery needs +3. **Graph Metrics** providing insights into the knowledge graph's structure + +Each query is documented with its purpose, expected results, and practical implications for understanding and utilizing the NFDI4Earth KnowledgeHub. diff --git a/docs/macros/__pycache__/main.cpython-312.pyc b/docs/macros/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae0eaebbb09fdee392a5b5d374f2647080079500 GIT binary patch literal 1290 zcmX@j%ge>Uz`#(t?PB@{CI*JbAPx*OLK&YyN~SZUFtjj4F;p^YG9|-gKnj={7#KkK zvlasb!*qrkhAfaM8ctzKVQgVs4Hd0rtYpw+_Dcq7g9<UQF)%O)GcYiGwqOJq!^uzr zx1@$4g>f|#M4>4|EprWX7P3x=1Owb~mKw%J*%YQMR;UCLRl`!ln8F~%uo`N1GE)iz z2*ONfWC&)cVQS>7VX0w~WME*xCXdTxd}b;$7%~(ymNPLjL^3ckFfvp!R4_*}lrt(b zlz{vVcA*MG5ho)<3Udv^Y=#t;xy;E-kqp5MB^+Q$1_lNdh9VwDh7{Hs#@P%hY+zOF zb6Fv(%Aw|Ia`@e1EV{)|s>#5>pyi#KnU|{IT9lrel$TjtoT^ZkT9kHpPkK^fk)DD} zW~zcqVo7SILPC{9f<k#|QA(;pUS@Jei9&j6T4`QNYM!1}m7JDaYDP|Kkrh~dVqSV` zdTLT?UaCS=YGzKVLY1gOYDH#oNoHzM2~=}to<c$ubAq0hChIMh{DRcHTbxCyi77dm zd8x&>ShDgn^KJ>cW#**%<(IhSm*%Co78T_e-Qx1hOG&K&vo%?7ainGDq~;~&rrzQz zE=epZi3jPv#a*14m!6Xf;V`D%Vg-p77lWc%0R(>e>1X8Urs@|Z=A`Mn<Y$-WrskET z>ifC5c$&B-7L{bgd*|hs=cJ~mq^Ek6Cg}&3rWTiE=I1457NzQ^<R=&F=O!i><%6iq zJiUU-TY{N+$vLGdsqvX<@nCNjSE;6?re)@(#;4|$>DlDuCnx3<+v#B_1EtmCLktWI z4GbSxxH*#<?+A)c$XLL9Sy1Hz0~@c}3dS1}lJmJ|a?j?Sz<NVi_JXkH4Qbi=<}=L~ zn68joA$>vF^s=<scXl>jwT~e6A2=Aq6fcPC+>lmSAbn9<_d7RShKE5yYI^y^@)@O* z>%Q}{@v40XY4`*pet+R(5K#QYz{r`zctg}~N5zh;3myp<gcC3EB;Mhd1W6<_-Vin3 z;I+Z#f_=z^(AWzhaTnqeF9;`H;z|0z%*dI{_^a59fq|ik$I*nLPt8$-c?+kb3Bx`% zM-AqKEFk7VJ^>J04$RhIbu?r@sLSkV!f?<KOq%dJNirW|6$FtIj82lwnoNFvnvAzt z!6B{5bc;DPudGOrfq|hI6lMwvMFI>A47a!tp<E=wz`y{?t;JH{@Vvpw(H_|t`H6{# zHJR}<GXop{M+g&?P5d+&!Rl8s6p1o0Fn|sF#bJ}1pHiBWYF8u<Gm4vmf#Cx)BO~Ky LCO*bwMzCT41cp1D literal 0 HcmV?d00001 diff --git a/docs/macros/main.py b/docs/macros/main.py new file mode 100644 index 0000000..4287ee7 --- /dev/null +++ b/docs/macros/main.py @@ -0,0 +1,15 @@ +def define_env(env): + @env.macro + def include_if_exists(filename, start_line=None, single_line=None): + try: + with open(filename, "r") as f: + lines = f.readlines() + if start_line is not None: + return "".join(lines[start_line:]) + elif single_line is not None: + return lines[single_line] + return "".join(lines) + except FileNotFoundError: + return f"*Keine Ergebnisse verfügbar. Die Datei `{filename}` wurde nicht gefunden.*" + except IndexError: + return f"*Fehler: Die angegebene Zeile {start_line} existiert nicht in `{filename}`.*" diff --git a/docs/metrics/04_outgoing_edges.md b/docs/metrics/04_outgoing_edges.md new file mode 100644 index 0000000..8634ee7 --- /dev/null +++ b/docs/metrics/04_outgoing_edges.md @@ -0,0 +1,37 @@ +# Outgoing Edges + +This metric determines the median number of outgoing edges across all nodes in the graph. The calculation requires multiple steps. + +## Queries + +### Step 1: Count Outgoing Edges Per Node + +```sparql +{{ include_if_exists("queries/metrics/GM0004_1.rq") }} +``` + +### Step 2: Get Total Number of Nodes + +```sparql +{{ include_if_exists("queries/metrics/GM0004_2.rq") }} +``` + +### Step 3: Calculate Median Position + +Using the total node count (n), median position is: position = (n+1)/2 + +```sparql +{{ include_if_exists("queries/metrics/GM0004_3.rq") }} +``` + +### Step 4: Get Median Value(s) + +```sparql +{{ include_if_exists("queries/metrics/GM0004_4.rq") }} +``` + +## Results +{{ include_if_exists("reports/metrics/0004.txt", start_line=1) }} + +Last execution: +{{ include_if_exists("reports/metrics/0004.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/05_incoming_edges.md b/docs/metrics/05_incoming_edges.md new file mode 100644 index 0000000..f7670f9 --- /dev/null +++ b/docs/metrics/05_incoming_edges.md @@ -0,0 +1,37 @@ +# Incoming Edges + +This metric determines the median number of incoming edges across all nodes in the graph. The calculation requires multiple steps. + +## Queries + +### Step 1: Count Incoming Edges Per Node + +```sparql +{{ include_if_exists("queries/metrics/GM0005_1.rq") }} +``` + +### Step 2: Get Total Number of Nodes + +```sparql +{{ include_if_exists("queries/metrics/GM0005_2.rq") }} +``` + +### Step 3: Calculate Median Position + +Using the total node count (n), median position is: position = (n+1)/2 + +```sparql +{{ include_if_exists("queries/metrics/GM0005_3.rq") }} +``` + +### Step 4: Get Median Value(s) + +```sparql +{{ include_if_exists("queries/metrics/GM0005_4.rq") }} +``` + +## Results +{{ include_if_exists("reports/metrics/0005.txt", start_line=1) }} + +Last execution: +{{ include_if_exists("reports/metrics/0005.txt", single_line=0) }} diff --git a/docs/metrics/index.md b/docs/metrics/index.md new file mode 100644 index 0000000..380ab61 --- /dev/null +++ b/docs/metrics/index.md @@ -0,0 +1,10 @@ +# Metrics + +The KnowledgeGraph metrics provide quantitative insights into the structure and content of our knowledge graph. These measurements help us to: + +- Understand the graph's size and complexity +- Evaluate the coverage of earth science domains +- Identify areas for potential improvement +- Monitor the graph's growth and development + +The queries are stored in separate `.rq` files and can be executed against the NFDI4Earth [KnowledgeGraph endpoint](https://sparql.knowledgehub.nfdi4earth.de). diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..f20a844 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,18 @@ +site_name: NFDI4Earth - KnowledgeGraph - Questions & Metrics + +nav: + - Home: index.md + - Questions: questions.md + - Metrics: metrics/ + +plugins: + - search + - awesome-nav + - macros: + module_name: docs/macros/main + +# markdown_extensions: + # - pymdownx.superfences + # - pymdownx.snippets: + # base_path: ['.'] + # check_paths: true \ No newline at end of file diff --git a/examples/Educational resources.rq b/queries/examples/Educational resources.rq similarity index 100% rename from examples/Educational resources.rq rename to queries/examples/Educational resources.rq diff --git a/examples/Metadata.rq b/queries/examples/Metadata.rq similarity index 100% rename from examples/Metadata.rq rename to queries/examples/Metadata.rq diff --git a/examples/Services in the NFDI4Earth.rq b/queries/examples/Services in the NFDI4Earth.rq similarity index 100% rename from examples/Services in the NFDI4Earth.rq rename to queries/examples/Services in the NFDI4Earth.rq diff --git a/queries/metrics/GM0004_1.rq b/queries/metrics/GM0004_1.rq new file mode 100644 index 0000000..8a457d5 --- /dev/null +++ b/queries/metrics/GM0004_1.rq @@ -0,0 +1,8 @@ +# Count Outgoing Edges Per Node + +SELECT ?source (COUNT(?outgoing) as ?outEdges) +WHERE { + ?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY DESC(?outEdges) diff --git a/queries/metrics/GM0004_2.rq b/queries/metrics/GM0004_2.rq new file mode 100644 index 0000000..d7ff687 --- /dev/null +++ b/queries/metrics/GM0004_2.rq @@ -0,0 +1,6 @@ +# Returns a single number representing the cleaned count of unique outging nodes in the graph. + +SELECT (COUNT(DISTINCT ?source) as ?uniqueEdges) +WHERE { + ?source ?p ?outgoing . +} \ No newline at end of file diff --git a/queries/metrics/GM0004_3.rq b/queries/metrics/GM0004_3.rq new file mode 100644 index 0000000..165999b --- /dev/null +++ b/queries/metrics/GM0004_3.rq @@ -0,0 +1,6 @@ +# Returns a single number representing the median position of outgoing edges per node + +SELECT (COUNT(DISTINCT ?source) as ?uniqueNodes) / 2 +WHERE { + ?source ?p ?outgoing . +} \ No newline at end of file diff --git a/queries/metrics/GM0004_4.rq b/queries/metrics/GM0004_4.rq new file mode 100644 index 0000000..4249889 --- /dev/null +++ b/queries/metrics/GM0004_4.rq @@ -0,0 +1,14 @@ +# This SPARQL query calculates the median out-edge in a graph. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?outEdges as ?outMedian +WHERE { + SELECT ?source (COUNT(?outgoing) as ?outEdges) + WHERE { + ?source ?p ?outgoing . + } + GROUP BY ?source + ORDER BY ASC(?outEdges) +} +OFFSET {median_position} +LIMIT 1 diff --git a/queries/metrics/GM0005_1.rq b/queries/metrics/GM0005_1.rq new file mode 100644 index 0000000..86d531f --- /dev/null +++ b/queries/metrics/GM0005_1.rq @@ -0,0 +1,7 @@ +# Count Incoming Edges Per Node + +SELECT ?target (COUNT(?incoming) as ?inEdges) +WHERE { + ?incoming ?p ?target . +} +GROUP BY ?target diff --git a/queries/metrics/GM0005_2.rq b/queries/metrics/GM0005_2.rq new file mode 100644 index 0000000..3c319e1 --- /dev/null +++ b/queries/metrics/GM0005_2.rq @@ -0,0 +1,6 @@ +# Returns a single number representing the cleaned count of unique incoming nodes in the graph. + +SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) +WHERE { + ?incoming ?p ?target . +} \ No newline at end of file diff --git a/queries/metrics/GM0005_3.rq b/queries/metrics/GM0005_3.rq new file mode 100644 index 0000000..3c386ee --- /dev/null +++ b/queries/metrics/GM0005_3.rq @@ -0,0 +1,6 @@ +# Returns a single number representing the median position of incoming edges + +SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) / 2 +WHERE { + ?incoming ?p ?target . +} \ No newline at end of file diff --git a/queries/metrics/GM0005_4.rq b/queries/metrics/GM0005_4.rq new file mode 100644 index 0000000..952626e --- /dev/null +++ b/queries/metrics/GM0005_4.rq @@ -0,0 +1,14 @@ +# This SPARQL query calculates the median in-edge a graph. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?inEdges as ?inMedian +WHERE { + SELECT ?target (COUNT(?incoming) as ?inEdges) + WHERE { + ?incoming ?p ?target . + } + GROUP BY ?target + ORDER BY ASC(?inEdges) +} +OFFSET {median_position} +LIMIT 1 diff --git a/queries/metrics/GM001.rq b/queries/metrics/GM001.rq new file mode 100644 index 0000000..95b0c14 --- /dev/null +++ b/queries/metrics/GM001.rq @@ -0,0 +1,6 @@ +# The number of instances in a graph + +SELECT (COUNT(?instance) AS ?numInstances) +WHERE { + ?instance a [] . +} diff --git a/queries/metrics/GM002_1.rq b/queries/metrics/GM002_1.rq new file mode 100644 index 0000000..0bdd96a --- /dev/null +++ b/queries/metrics/GM002_1.rq @@ -0,0 +1,13 @@ +# Total number of assertions (between entities & literals) +# +# Explanation: +# ?subject ?predicate ?object searches through all triples (assertions) in the dataset. +# COUNT(*) AS ?numAssertions counts the number of triples. +# +# This query returns the total number of edges in the graph, regardless of +# whether they exist between entities or between entities and literals. + +SELECT (COUNT(*) AS ?numAssertions) +WHERE { + ?subject ?predicate ?object . +} diff --git a/queries/metrics/GM002_2.rq b/queries/metrics/GM002_2.rq new file mode 100644 index 0000000..a20aee9 --- /dev/null +++ b/queries/metrics/GM002_2.rq @@ -0,0 +1,12 @@ +# The number of assertions between entities (without literals) +# +# Explanation: +# ?subject ?predicate ?object searches through all triples. +# FILTER(isIRI(?object)) checks if the object is an IRI (i.e. an entity) and not a literal (e.g. not a string, date, or number). +# COUNT(*) AS ?numEntityEdges counts the number of corresponding edges. + +SELECT (COUNT(*) AS ?numEntityEdges) +WHERE { + ?subject ?predicate ?object . + FILTER(isIRI(?object)) # Only objects that are URIs (i.e. entities) +} diff --git a/queries/metrics/GM002_3.rq b/queries/metrics/GM002_3.rq new file mode 100644 index 0000000..ec6ab14 --- /dev/null +++ b/queries/metrics/GM002_3.rq @@ -0,0 +1,12 @@ +# The number of assertions between literals (without entities) +# +# Explanation: +# ?subject ?predicate ?object searches through all triples. +# FILTER(isLiteral(?object)) ensures that only edges are counted where the object is a literal (not an IRI). +# COUNT(*) AS ?numLiteralAssertions counts the number of corresponding edges. + +SELECT (COUNT(*) AS ?numLiteralAssertions) +WHERE { + ?subject ?predicate ?object . + FILTER(isLiteral(?object)) # Only objects that are literals +} diff --git a/queries/metrics/GM003_1.rq b/queries/metrics/GM003_1.rq new file mode 100644 index 0000000..db52f5b --- /dev/null +++ b/queries/metrics/GM003_1.rq @@ -0,0 +1,34 @@ +# The average linkage degree +# (i.e.: how many assertions per entity does the graph contain) +# +# WARNING: This query can be very resource-intensive and slow on large datasets! +# It might even timeout or crash for graphs with millions of triples. +# Consider using batch processing with LIMIT and OFFSET for large datasets. +# +# Explanation: +# First count outgoing edges per entity +# Then count incoming edges per entity +# Add both counts and calculate average + +SELECT (AVG(?totalDegree) as ?avgLinkageDegree) +WHERE { + { + SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) + WHERE { + { + SELECT ?entity (COUNT(*) as ?outDegree) + WHERE { + ?entity ?p ?o . + } + GROUP BY ?entity + } + { + SELECT ?entity (COUNT(*) as ?inDegree) + WHERE { + ?s ?p ?entity . + } + GROUP BY ?entity + } + } + } +} diff --git a/queries/metrics/GM003_2.rq b/queries/metrics/GM003_2.rq new file mode 100644 index 0000000..de8c8e1 --- /dev/null +++ b/queries/metrics/GM003_2.rq @@ -0,0 +1,37 @@ +# The average linkage degree with batch processing +# (i.e.: how many assertions per entity does the graph contain) +# +# Explanation: +# This is an alternative version (esp. for testing purposes) +# as it enables a limitation of analysed entities + +SELECT (AVG(?totalDegree) as ?avgLinkageDegree) +WHERE { + { + SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) + WHERE { + { + SELECT DISTINCT ?entity + WHERE { + { ?entity ?p ?o } UNION { ?s ?p ?entity } + } + LIMIT 1000 # Process 1000 entities at a time + OFFSET 0 # Start with first batch, increment for next batches + } + { + SELECT ?entity (COUNT(*) as ?outDegree) + WHERE { + ?entity ?p ?o . + } + GROUP BY ?entity + } + { + SELECT ?entity (COUNT(*) as ?inDegree) + WHERE { + ?s ?p ?entity . + } + GROUP BY ?entity + } + } + } +} \ No newline at end of file diff --git a/queries/metrics/GM003_3.rq b/queries/metrics/GM003_3.rq new file mode 100644 index 0000000..3f1028a --- /dev/null +++ b/queries/metrics/GM003_3.rq @@ -0,0 +1,25 @@ +# The average outgoing linkage degree +# (i.e.: how many outgoing assertions per entity does the graph contain) +# +# Explanation: +# This is a simplified version that only counts outgoing edges +# Limited to 1000 entities for testing purposes + +SELECT (AVG(?outDegree) as ?avgOutgoingLinkageDegree) +WHERE { + { + SELECT ?entity (COUNT(*) as ?outDegree) + WHERE { + { + SELECT DISTINCT ?entity + WHERE { + ?entity ?p ?o . + } + LIMIT 1000 # Process 1000 entities at a time + OFFSET 0 # Start with first batch + } + ?entity ?p ?o . + } + GROUP BY ?entity + } +} \ No newline at end of file diff --git a/queries/metrics/GM003_4.rq b/queries/metrics/GM003_4.rq new file mode 100644 index 0000000..1a61203 --- /dev/null +++ b/queries/metrics/GM003_4.rq @@ -0,0 +1,26 @@ +# The average incoming linkage degree +# (i.e.: how many incoming assertions per entity does the graph contain) +# +# Explanation: +# Simple version that only counts incoming edges per entity +# No complex sub-selects needed for this case +# Limited to 1000 entities for testing purposes + +SELECT (AVG(?inDegree) as ?avgIncomingLinkageDegree) +WHERE { + { + SELECT ?entity (COUNT(*) as ?inDegree) + WHERE { + { + SELECT DISTINCT ?entity + WHERE { + ?s ?p ?entity . + } + LIMIT 1000 # Process 1000 entities at a time + OFFSET 0 # Start with first batch + } + ?s ?p ?entity . + } + GROUP BY ?entity + } +} \ No newline at end of file diff --git a/queries/AG001.rq b/queries/questions/AG001.rq similarity index 100% rename from queries/AG001.rq rename to queries/questions/AG001.rq diff --git a/queries/AG001_2.rq b/queries/questions/AG001_2.rq similarity index 100% rename from queries/AG001_2.rq rename to queries/questions/AG001_2.rq diff --git a/queries/AG002_1.rq b/queries/questions/AG002_1.rq similarity index 100% rename from queries/AG002_1.rq rename to queries/questions/AG002_1.rq diff --git a/queries/AG002_2.rq b/queries/questions/AG002_2.rq similarity index 100% rename from queries/AG002_2.rq rename to queries/questions/AG002_2.rq diff --git a/queries/AT001.rq b/queries/questions/AT001.rq similarity index 100% rename from queries/AT001.rq rename to queries/questions/AT001.rq diff --git a/queries/AT002_1.rq b/queries/questions/AT002_1.rq similarity index 100% rename from queries/AT002_1.rq rename to queries/questions/AT002_1.rq diff --git a/queries/AT002_2.rq b/queries/questions/AT002_2.rq similarity index 100% rename from queries/AT002_2.rq rename to queries/questions/AT002_2.rq diff --git a/queries/DA001.rq b/queries/questions/DA001.rq similarity index 100% rename from queries/DA001.rq rename to queries/questions/DA001.rq diff --git a/queries/DA002_1.rq b/queries/questions/DA002_1.rq similarity index 100% rename from queries/DA002_1.rq rename to queries/questions/DA002_1.rq diff --git a/queries/DA002_2.rq b/queries/questions/DA002_2.rq similarity index 100% rename from queries/DA002_2.rq rename to queries/questions/DA002_2.rq diff --git a/queries/DA003_1.rq b/queries/questions/DA003_1.rq similarity index 100% rename from queries/DA003_1.rq rename to queries/questions/DA003_1.rq diff --git a/queries/LH001.rq b/queries/questions/LH001.rq similarity index 100% rename from queries/LH001.rq rename to queries/questions/LH001.rq diff --git a/queries/LH002_1.rq b/queries/questions/LH002_1.rq similarity index 100% rename from queries/LH002_1.rq rename to queries/questions/LH002_1.rq diff --git a/queries/LH002_2.rq b/queries/questions/LH002_2.rq similarity index 100% rename from queries/LH002_2.rq rename to queries/questions/LH002_2.rq diff --git a/queries/LR001.rq b/queries/questions/LR001.rq similarity index 100% rename from queries/LR001.rq rename to queries/questions/LR001.rq diff --git a/queries/LR002_1.rq b/queries/questions/LR002_1.rq similarity index 100% rename from queries/LR002_1.rq rename to queries/questions/LR002_1.rq diff --git a/queries/LR002_2.rq b/queries/questions/LR002_2.rq similarity index 100% rename from queries/LR002_2.rq rename to queries/questions/LR002_2.rq diff --git a/queries/MS001.rq b/queries/questions/MS001.rq similarity index 100% rename from queries/MS001.rq rename to queries/questions/MS001.rq diff --git a/queries/MS002_1.rq b/queries/questions/MS002_1.rq similarity index 100% rename from queries/MS002_1.rq rename to queries/questions/MS002_1.rq diff --git a/queries/MS002_2.rq b/queries/questions/MS002_2.rq similarity index 100% rename from queries/MS002_2.rq rename to queries/questions/MS002_2.rq diff --git a/queries/OG001.rq b/queries/questions/OG001.rq similarity index 100% rename from queries/OG001.rq rename to queries/questions/OG001.rq diff --git a/queries/OG002_1.rq b/queries/questions/OG002_1.rq similarity index 100% rename from queries/OG002_1.rq rename to queries/questions/OG002_1.rq diff --git a/queries/OG002_2.rq b/queries/questions/OG002_2.rq similarity index 100% rename from queries/OG002_2.rq rename to queries/questions/OG002_2.rq diff --git a/queries/PE001.rq b/queries/questions/PE001.rq similarity index 100% rename from queries/PE001.rq rename to queries/questions/PE001.rq diff --git a/queries/PE002_1.rq b/queries/questions/PE002_1.rq similarity index 100% rename from queries/PE002_1.rq rename to queries/questions/PE002_1.rq diff --git a/queries/PE002_2.rq b/queries/questions/PE002_2.rq similarity index 100% rename from queries/PE002_2.rq rename to queries/questions/PE002_2.rq diff --git a/queries/REG001.rq b/queries/questions/REG001.rq similarity index 100% rename from queries/REG001.rq rename to queries/questions/REG001.rq diff --git a/queries/REG002_1.rq b/queries/questions/REG002_1.rq similarity index 100% rename from queries/REG002_1.rq rename to queries/questions/REG002_1.rq diff --git a/queries/REG002_2.rq b/queries/questions/REG002_2.rq similarity index 100% rename from queries/REG002_2.rq rename to queries/questions/REG002_2.rq diff --git a/queries/REP001.rq b/queries/questions/REP001.rq similarity index 100% rename from queries/REP001.rq rename to queries/questions/REP001.rq diff --git a/queries/REP002_1.rq b/queries/questions/REP002_1.rq similarity index 100% rename from queries/REP002_1.rq rename to queries/questions/REP002_1.rq diff --git a/queries/REP002_2.rq b/queries/questions/REP002_2.rq similarity index 100% rename from queries/REP002_2.rq rename to queries/questions/REP002_2.rq diff --git a/queries/RP001.rq b/queries/questions/RP001.rq similarity index 100% rename from queries/RP001.rq rename to queries/questions/RP001.rq diff --git a/queries/RP002_1.rq b/queries/questions/RP002_1.rq similarity index 100% rename from queries/RP002_1.rq rename to queries/questions/RP002_1.rq diff --git a/queries/RP002_2.rq b/queries/questions/RP002_2.rq similarity index 100% rename from queries/RP002_2.rq rename to queries/questions/RP002_2.rq diff --git a/queries/SC001.rq b/queries/questions/SC001.rq similarity index 100% rename from queries/SC001.rq rename to queries/questions/SC001.rq diff --git a/queries/SC002_1.rq b/queries/questions/SC002_1.rq similarity index 100% rename from queries/SC002_1.rq rename to queries/questions/SC002_1.rq diff --git a/queries/SC002_2.rq b/queries/questions/SC002_2.rq similarity index 100% rename from queries/SC002_2.rq rename to queries/questions/SC002_2.rq diff --git a/queries/TY001.rq b/queries/questions/TY001.rq similarity index 100% rename from queries/TY001.rq rename to queries/questions/TY001.rq diff --git a/queries/old/DR001_1.rq b/queries/questions/old/DR001_1.rq similarity index 100% rename from queries/old/DR001_1.rq rename to queries/questions/old/DR001_1.rq diff --git a/queries/old/OR001_1.rq b/queries/questions/old/OR001_1.rq similarity index 100% rename from queries/old/OR001_1.rq rename to queries/questions/old/OR001_1.rq diff --git a/queries/old/OR001_2.rq b/queries/questions/old/OR001_2.rq similarity index 100% rename from queries/old/OR001_2.rq rename to queries/questions/old/OR001_2.rq diff --git a/queries/old/OR002_1.rq b/queries/questions/old/OR002_1.rq similarity index 100% rename from queries/old/OR002_1.rq rename to queries/questions/old/OR002_1.rq diff --git a/queries/old/OR003_1.rq b/queries/questions/old/OR003_1.rq similarity index 100% rename from queries/old/OR003_1.rq rename to queries/questions/old/OR003_1.rq diff --git a/queries/old/OR004_1.rq b/queries/questions/old/OR004_1.rq similarity index 100% rename from queries/old/OR004_1.rq rename to queries/questions/old/OR004_1.rq diff --git a/queries/old/OR005_1.rq b/queries/questions/old/OR005_1.rq similarity index 100% rename from queries/old/OR005_1.rq rename to queries/questions/old/OR005_1.rq diff --git a/queries/old/OR006_1.rq b/queries/questions/old/OR006_1.rq similarity index 100% rename from queries/old/OR006_1.rq rename to queries/questions/old/OR006_1.rq diff --git a/reports/metrics/0004.txt b/reports/metrics/0004.txt new file mode 100644 index 0000000..6b57c75 --- /dev/null +++ b/reports/metrics/0004.txt @@ -0,0 +1,4 @@ +2024-03-12 15:20 +- Total Edges: 6705836 +- Median Position: 3352918 +- Median Outgoing Edges: 2 \ No newline at end of file diff --git a/reports/metrics/0005.txt b/reports/metrics/0005.txt new file mode 100644 index 0000000..d337df0 --- /dev/null +++ b/reports/metrics/0005.txt @@ -0,0 +1,4 @@ +2024-03-12 15:20 +- Total Edges: 15111836 +- Median Position: 7555918 +- Median Outgoing Edges: 1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0017228 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +mkdocs +mkdocs-macros-plugin +mkdocs-awesome-nav -- GitLab From 0b061530cb787c079c06cf8dd9660b2cbb54c35e Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 09:29:09 +0100 Subject: [PATCH 02/59] [metrics] Switch to materials-theme --- mkdocs.yml | 24 +++++++++++++++++++----- requirements.txt | 1 + 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index f20a844..7184e25 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,14 +5,28 @@ nav: - Questions: questions.md - Metrics: metrics/ +theme: + name: material + language: de + features: + - search.highlight + - content.code.copy + plugins: - search - awesome-nav - macros: module_name: docs/macros/main -# markdown_extensions: - # - pymdownx.superfences - # - pymdownx.snippets: - # base_path: ['.'] - # check_paths: true \ No newline at end of file +markdown_extensions: + - admonition + - pymdownx.details +# - pymdownx.superfences +# - pymdownx.snippets: +# base_path: ['.'] +# check_paths: true +# - pymdownx.highlight: +# anchor_linenums: true +# line_spans: __span +# pygments_lang_class: true +# - pymdownx.inlinehilite \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0017228..b7e9d53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ mkdocs +mkdocs-material mkdocs-macros-plugin mkdocs-awesome-nav -- GitLab From 89a9af989d21854407c4e53b860916f91afe2342 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 09:29:43 +0100 Subject: [PATCH 03/59] [metrics] Introduce "general metrics" section --- docs/metrics/general metrics/01_instances.md | 24 ++++++++ docs/metrics/general metrics/02_assertions.md | 36 ++++++++++++ .../general metrics/03_linkage_degree.md | 57 +++++++++++++++++++ .../04_outgoing_edges.md | 14 ++--- .../05_incoming_edges.md | 14 ++--- docs/metrics/{ => general metrics}/index.md | 2 +- queries/metrics/{GM0004_1.rq => GM004_1.rq} | 1 - queries/metrics/{GM0004_2.rq => GM004_2.rq} | 0 queries/metrics/{GM0004_3.rq => GM004_3.rq} | 2 +- queries/metrics/{GM0004_4.rq => GM004_4.rq} | 0 queries/metrics/{GM0005_1.rq => GM005_1.rq} | 1 + queries/metrics/{GM0005_2.rq => GM005_2.rq} | 0 queries/metrics/{GM0005_3.rq => GM005_3.rq} | 0 queries/metrics/{GM0005_4.rq => GM005_4.rq} | 0 reports/metrics/{0004.txt => 004.txt} | 0 reports/metrics/{0005.txt => 005.txt} | 0 16 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 docs/metrics/general metrics/01_instances.md create mode 100644 docs/metrics/general metrics/02_assertions.md create mode 100644 docs/metrics/general metrics/03_linkage_degree.md rename docs/metrics/{ => general metrics}/04_outgoing_edges.md (55%) rename docs/metrics/{ => general metrics}/05_incoming_edges.md (55%) rename docs/metrics/{ => general metrics}/index.md (97%) rename queries/metrics/{GM0004_1.rq => GM004_1.rq} (84%) rename queries/metrics/{GM0004_2.rq => GM004_2.rq} (100%) rename queries/metrics/{GM0004_3.rq => GM004_3.rq} (87%) rename queries/metrics/{GM0004_4.rq => GM004_4.rq} (100%) rename queries/metrics/{GM0005_1.rq => GM005_1.rq} (85%) rename queries/metrics/{GM0005_2.rq => GM005_2.rq} (100%) rename queries/metrics/{GM0005_3.rq => GM005_3.rq} (100%) rename queries/metrics/{GM0005_4.rq => GM005_4.rq} (100%) rename reports/metrics/{0004.txt => 004.txt} (100%) rename reports/metrics/{0005.txt => 005.txt} (100%) diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md new file mode 100644 index 0000000..001bbf2 --- /dev/null +++ b/docs/metrics/general metrics/01_instances.md @@ -0,0 +1,24 @@ +# 01 - Instances in a graph + +This metric counts the total number of instances (nodes) in an RDF graph. An instance is counted as any node that appears as either a subject or object in a triple. The instance count provides a fundamental measure of the graph's size and gives a first indication of its complexity. + +The metric helps to: + +- Understand the overall scale of the knowledge graph +- Track growth over time +- Compare different graph versions or datasets +- Establish a baseline for other metrics + +## Queries + +### Count Number of instances in a graph + +```sparql +{{ include_if_exists("queries/metrics/GM001.rq") }} +``` + +## Results +{{ include_if_exists("reports/metrics/001.txt", start_line=1) }} + +Last execution: +{{ include_if_exists("reports/metrics/001.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md new file mode 100644 index 0000000..dad41a5 --- /dev/null +++ b/docs/metrics/general metrics/02_assertions.md @@ -0,0 +1,36 @@ +# 02 - Assertions in a graph + +This metric counts the total number of assertions (triples/edges) in an RDF graph. An assertion is any statement in the form of subject-predicate-object that exists in the graph. The assertion count provides a fundamental measure of the graph's connectivity and density. + +The metric helps to: + +- Measure the total number of relationships in the graph +- Understand the graph's density +- Track the growth of relationships over time +- Compare connectivity between different graph versions + +## Queries + +### Count Total Number of Assertions + +```sparql +{{ include_if_exists("queries/metrics/GM002_1.rq") }} +``` + +### Count Number of Entity-to-Entity Assertions + +```sparql +{{ include_if_exists("queries/metrics/GM002_2.rq") }} +``` + +### Count Number of Entity-to-Literal Assertions + +```sparql +{{ include_if_exists("queries/metrics/GM002_3.rq") }} +``` + +## Results +{{ include_if_exists("reports/metrics/002_1.txt", start_line=1) }} + +Last execution: +{{ include_if_exists("reports/metrics/002_1.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md new file mode 100644 index 0000000..e526e1e --- /dev/null +++ b/docs/metrics/general metrics/03_linkage_degree.md @@ -0,0 +1,57 @@ +# 03 - Linkage Degree Analysis + +This metric analyzes the connectivity patterns in the graph by measuring the linkage degree of entities. The linkage degree represents how well entities are connected to other entities through relationships. It helps understand the graph's structural characteristics and identifies patterns of connectivity. + +The metric provides insights into: + +- Average number of relationships per entity +- Distribution of connections across the graph +- Identification of highly connected or isolated entities +- Overall graph connectivity patterns + +## Queries + +### Average Linkage Degree + +This query calculates the average number of relationships (both incoming and outgoing) per entity in the graph. + +```sparql +{{ include_if_exists("queries/metrics/GM003_1.rq") }} +``` + +??? warning "Batch Processing Limitation" + The following queries use batch processing with a limitation of 1000 entities. While this enables quick results, it leads to approximate values. The results only approach the actual average value when using batch sizes in the six-figure range. + + For precise analysis, either adjust the LIMIT value accordingly or use the non-batch version. + +### Average Linkage Degree (Batch Processing) + +This is an optimized version of the linkage degree calculation that uses batch processing. It's particularly useful for large datasets where the standard query might timeout or consume too many resources. The query processes a limited number of entities at a time. + +```sparql +{{ include_if_exists("queries/metrics/GM003_2.rq") }} +``` + +### Average Outgoing Linkage Degree (Batch Processing) + +This query focuses specifically on outgoing relationships, calculating the average number of outgoing edges per entity. It uses batch processing for efficient execution on larger datasets. + +```sparql +{{ include_if_exists("queries/metrics/GM003_3.rq") }} +``` + +### Average Incoming Linkage Degree (Batch Processing) + +This query calculates the average number of incoming relationships per entity, providing insights into how frequently entities are referenced by others in the graph. It also uses batch processing for efficiency. + +```sparql +{{ include_if_exists("queries/metrics/GM003_4.rq") }} +``` + +## Results + +### Average Linkage Degree +{{ include_if_exists("reports/metrics/003_1.txt", start_line=1) }} + +Last execution: +{{ include_if_exists("reports/metrics/003_1.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/04_outgoing_edges.md b/docs/metrics/general metrics/04_outgoing_edges.md similarity index 55% rename from docs/metrics/04_outgoing_edges.md rename to docs/metrics/general metrics/04_outgoing_edges.md index 8634ee7..42dc5f4 100644 --- a/docs/metrics/04_outgoing_edges.md +++ b/docs/metrics/general metrics/04_outgoing_edges.md @@ -1,4 +1,4 @@ -# Outgoing Edges +# 04 - Outgoing Edges This metric determines the median number of outgoing edges across all nodes in the graph. The calculation requires multiple steps. @@ -7,13 +7,13 @@ This metric determines the median number of outgoing edges across all nodes in t ### Step 1: Count Outgoing Edges Per Node ```sparql -{{ include_if_exists("queries/metrics/GM0004_1.rq") }} +{{ include_if_exists("queries/metrics/GM004_1.rq") }} ``` ### Step 2: Get Total Number of Nodes ```sparql -{{ include_if_exists("queries/metrics/GM0004_2.rq") }} +{{ include_if_exists("queries/metrics/GM004_2.rq") }} ``` ### Step 3: Calculate Median Position @@ -21,17 +21,17 @@ This metric determines the median number of outgoing edges across all nodes in t Using the total node count (n), median position is: position = (n+1)/2 ```sparql -{{ include_if_exists("queries/metrics/GM0004_3.rq") }} +{{ include_if_exists("queries/metrics/GM004_3.rq") }} ``` ### Step 4: Get Median Value(s) ```sparql -{{ include_if_exists("queries/metrics/GM0004_4.rq") }} +{{ include_if_exists("queries/metrics/GM004_4.rq") }} ``` ## Results -{{ include_if_exists("reports/metrics/0004.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/004.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/0004.txt", single_line=0) }} \ No newline at end of file +{{ include_if_exists("reports/metrics/004.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/05_incoming_edges.md b/docs/metrics/general metrics/05_incoming_edges.md similarity index 55% rename from docs/metrics/05_incoming_edges.md rename to docs/metrics/general metrics/05_incoming_edges.md index f7670f9..a815f4c 100644 --- a/docs/metrics/05_incoming_edges.md +++ b/docs/metrics/general metrics/05_incoming_edges.md @@ -1,4 +1,4 @@ -# Incoming Edges +# 05 - Incoming Edges This metric determines the median number of incoming edges across all nodes in the graph. The calculation requires multiple steps. @@ -7,13 +7,13 @@ This metric determines the median number of incoming edges across all nodes in t ### Step 1: Count Incoming Edges Per Node ```sparql -{{ include_if_exists("queries/metrics/GM0005_1.rq") }} +{{ include_if_exists("queries/metrics/GM005_1.rq") }} ``` ### Step 2: Get Total Number of Nodes ```sparql -{{ include_if_exists("queries/metrics/GM0005_2.rq") }} +{{ include_if_exists("queries/metrics/GM005_2.rq") }} ``` ### Step 3: Calculate Median Position @@ -21,17 +21,17 @@ This metric determines the median number of incoming edges across all nodes in t Using the total node count (n), median position is: position = (n+1)/2 ```sparql -{{ include_if_exists("queries/metrics/GM0005_3.rq") }} +{{ include_if_exists("queries/metrics/GM005_3.rq") }} ``` ### Step 4: Get Median Value(s) ```sparql -{{ include_if_exists("queries/metrics/GM0005_4.rq") }} +{{ include_if_exists("queries/metrics/GM005_4.rq") }} ``` ## Results -{{ include_if_exists("reports/metrics/0005.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/005.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/0005.txt", single_line=0) }} +{{ include_if_exists("reports/metrics/005.txt", single_line=0) }} diff --git a/docs/metrics/index.md b/docs/metrics/general metrics/index.md similarity index 97% rename from docs/metrics/index.md rename to docs/metrics/general metrics/index.md index 380ab61..b97d544 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/general metrics/index.md @@ -1,4 +1,4 @@ -# Metrics +# Overview The KnowledgeGraph metrics provide quantitative insights into the structure and content of our knowledge graph. These measurements help us to: diff --git a/queries/metrics/GM0004_1.rq b/queries/metrics/GM004_1.rq similarity index 84% rename from queries/metrics/GM0004_1.rq rename to queries/metrics/GM004_1.rq index 8a457d5..5c2ffa7 100644 --- a/queries/metrics/GM0004_1.rq +++ b/queries/metrics/GM004_1.rq @@ -5,4 +5,3 @@ WHERE { ?source ?p ?outgoing . } GROUP BY ?source -ORDER BY DESC(?outEdges) diff --git a/queries/metrics/GM0004_2.rq b/queries/metrics/GM004_2.rq similarity index 100% rename from queries/metrics/GM0004_2.rq rename to queries/metrics/GM004_2.rq diff --git a/queries/metrics/GM0004_3.rq b/queries/metrics/GM004_3.rq similarity index 87% rename from queries/metrics/GM0004_3.rq rename to queries/metrics/GM004_3.rq index 165999b..65391cc 100644 --- a/queries/metrics/GM0004_3.rq +++ b/queries/metrics/GM004_3.rq @@ -1,4 +1,4 @@ -# Returns a single number representing the median position of outgoing edges per node +# Returns a single number representing the median position of outgoing edges SELECT (COUNT(DISTINCT ?source) as ?uniqueNodes) / 2 WHERE { diff --git a/queries/metrics/GM0004_4.rq b/queries/metrics/GM004_4.rq similarity index 100% rename from queries/metrics/GM0004_4.rq rename to queries/metrics/GM004_4.rq diff --git a/queries/metrics/GM0005_1.rq b/queries/metrics/GM005_1.rq similarity index 85% rename from queries/metrics/GM0005_1.rq rename to queries/metrics/GM005_1.rq index 86d531f..e4440f0 100644 --- a/queries/metrics/GM0005_1.rq +++ b/queries/metrics/GM005_1.rq @@ -5,3 +5,4 @@ WHERE { ?incoming ?p ?target . } GROUP BY ?target +ORDER BY ASC(?inEdges) diff --git a/queries/metrics/GM0005_2.rq b/queries/metrics/GM005_2.rq similarity index 100% rename from queries/metrics/GM0005_2.rq rename to queries/metrics/GM005_2.rq diff --git a/queries/metrics/GM0005_3.rq b/queries/metrics/GM005_3.rq similarity index 100% rename from queries/metrics/GM0005_3.rq rename to queries/metrics/GM005_3.rq diff --git a/queries/metrics/GM0005_4.rq b/queries/metrics/GM005_4.rq similarity index 100% rename from queries/metrics/GM0005_4.rq rename to queries/metrics/GM005_4.rq diff --git a/reports/metrics/0004.txt b/reports/metrics/004.txt similarity index 100% rename from reports/metrics/0004.txt rename to reports/metrics/004.txt diff --git a/reports/metrics/0005.txt b/reports/metrics/005.txt similarity index 100% rename from reports/metrics/0005.txt rename to reports/metrics/005.txt -- GitLab From 598bc9c8033ec1bdab2541498c6284b598d4c99b Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 10:42:08 +0100 Subject: [PATCH 04/59] [metrics] Integrate python-package 'kg_analysis' --- scripts/.flake8 | 2 + scripts/.gitignore | 6 + scripts/LICENSE | 201 ++++++++++++++++++++++++++++ scripts/MANIFEST.in | 0 scripts/README.MD | 44 ++++++ scripts/kg_analysis/__init__.py | 3 + scripts/kg_analysis/cli.py | 72 ++++++++++ scripts/kg_analysis/query_runner.py | 43 ++++++ scripts/kg_analysis/util.py | 18 +++ scripts/pyproject.toml | 28 ++++ 10 files changed, 417 insertions(+) create mode 100644 scripts/.flake8 create mode 100644 scripts/.gitignore create mode 100644 scripts/LICENSE create mode 100644 scripts/MANIFEST.in create mode 100644 scripts/README.MD create mode 100644 scripts/kg_analysis/__init__.py create mode 100644 scripts/kg_analysis/cli.py create mode 100644 scripts/kg_analysis/query_runner.py create mode 100644 scripts/kg_analysis/util.py create mode 100644 scripts/pyproject.toml diff --git a/scripts/.flake8 b/scripts/.flake8 new file mode 100644 index 0000000..bfead2c --- /dev/null +++ b/scripts/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 79 diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..154d56a --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,6 @@ +**/__pycache__ +**/dist + +*.egg-info +*.pyc +*.ini diff --git a/scripts/LICENSE b/scripts/LICENSE new file mode 100644 index 0000000..d577725 --- /dev/null +++ b/scripts/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 NFDI4Earth / SoftwareAndArchitecture + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/scripts/MANIFEST.in b/scripts/MANIFEST.in new file mode 100644 index 0000000..e69de29 diff --git a/scripts/README.MD b/scripts/README.MD new file mode 100644 index 0000000..bc7066b --- /dev/null +++ b/scripts/README.MD @@ -0,0 +1,44 @@ +# KG Analysis Tool + +A command-line tool for analyzing SPARQL endpoints, specifically designed for the NFDI4Earth Knowledge Graph. + +## Features + +- Run SPARQL queries from files +- Save query results to output files +- Configurable SPARQL endpoint via environment variable + +## Installation + +```bash +# Create and activate virtual environment +python3 -m venv venv +source venv/bin/activate + +# Install in development mode +pip install -e . +``` + +## Usage + +Set the SPARQL endpoint (optional): +```bash +export KG_SPARQL_ENDPOINT="https://your-sparql-endpoint/sparql" +``` + +Run a query: +```bash +kg_analysis run-query -q path/to/query.rq -o path/to/output.txt +``` + +## Requirements + +- Python >= 3.10 +- click +- SPARQLWrapper + +## License + +This project is published under the Apache License 2.0, see file `LICENSE`. + +Contributors: Ralf Klammer diff --git a/scripts/kg_analysis/__init__.py b/scripts/kg_analysis/__init__.py new file mode 100644 index 0000000..a9463af --- /dev/null +++ b/scripts/kg_analysis/__init__.py @@ -0,0 +1,3 @@ +from rich.console import Console + +console = Console() diff --git a/scripts/kg_analysis/cli.py b/scripts/kg_analysis/cli.py new file mode 100644 index 0000000..1808306 --- /dev/null +++ b/scripts/kg_analysis/cli.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +import click +import os + +from pathlib import Path +from rich import print_json + +from .util import cli_startup +from .query_runner import QueryRunner # type: ignore + +log = logging.getLogger(__name__) + +SPARQL_ENDPOINT = os.getenv( + "SPARQL_ENDPOINT", "https://sparql.knowledgehub.nfdi4earth.de" +) + + +@click.group() +@click.option("--debug/--no-debug", "-d", is_flag=True, default=False) +@click.pass_context +def main(ctx, debug): + cli_startup(log_level=debug and logging.DEBUG or logging.INFO) + ctx.ensure_object(dict) + ctx.obj["DEBUG"] = debug + + +@main.command() +@click.option( + "--query", + "-q", + type=click.Path(exists=True), + help="Path to SPARQL query file", + required=True, +) +@click.option("--output", "-o", type=click.Path(), help="Path to save results") +@click.pass_context +def run_query(ctx, query, output): + """Run analysis on the KnowledgeGraph.""" + runner = QueryRunner(SPARQL_ENDPOINT) + + query_path = Path(query) + # if output: + # output_path = Path(output) + # else: + # # Create default output path: queries/metrics/GM00X.rq + # # -> reports/metrics/00X.txt + # relative_path = query_path.relative_to("queries") + # output_path = Path("reports").joinpath( + # relative_path.with_suffix(".txt") + # ) + try: + results = runner.run_metric( + query_path, output_path=Path(output) if output else None + ) + click.echo( + click.style("Query executed successfully: ", fg="green") + + click.style(query_path.name, fg="blue") + ) + if not output: + print_json(data=results) + except Exception as e: + click.echo( + click.style(f"Error executing query: {e}", fg="red"), err=True + ) + + +if __name__ == "__main__": + main(obj={}) diff --git a/scripts/kg_analysis/query_runner.py b/scripts/kg_analysis/query_runner.py new file mode 100644 index 0000000..d09812f --- /dev/null +++ b/scripts/kg_analysis/query_runner.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +from pathlib import Path +from typing import Optional + +from SPARQLWrapper import JSON, SPARQLWrapper # type: ignore + +log = logging.getLogger(__name__) + + +class QueryRunner: + def __init__(self, endpoint_url: str): + self.sparql = SPARQLWrapper(endpoint_url) + self.sparql.setReturnFormat(JSON) + + def execute_query(self, query_path: Path) -> dict: + """Execute a SPARQL query from a file and return the results.""" + with open(query_path, "r") as f: + query = f.read() + + self.sparql.setQuery(query) + return self.sparql.query().convert() + + def run_metric( + self, query_path: Path, output_path: Optional[Path] = None + ) -> dict: + """Run a metric query and optionally save the results.""" + results = self.execute_query(query_path) + + if output_path: + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w") as f: + # Extract first value from results as metrics are usually + # single values + value = list(results["results"]["bindings"][0].values())[0][ + "value" + ] + f.write(str(value)) + + return results diff --git a/scripts/kg_analysis/util.py b/scripts/kg_analysis/util.py new file mode 100644 index 0000000..722ac16 --- /dev/null +++ b/scripts/kg_analysis/util.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +log = logging.getLogger(__name__) + + +def cli_startup(log_level=logging.INFO, log_file=None): + log_config = dict( + level=log_level, + format="%(asctime)s %(name)-10s %(levelname)-4s %(message)s", + ) + if log_file: + log_config["filename"] = log_file + + logging.basicConfig(**log_config) + logging.getLogger("").setLevel(log_level) diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml new file mode 100644 index 0000000..181b2ec --- /dev/null +++ b/scripts/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "kg_analysis" +version = "1.0.0b1" +description = "Knowledge Graph Analysis" +authors = [ + { name = "Ralf Klammer", email = "ralf.klammer@tu-dresden.de" } +] +requires-python = ">=3.10" +license = { text = "Copyright (C) 2025 TU-Dresden, ZIH" } +dependencies = [ + "rdflib", + "SPARQLWrapper", + "click", + "rich", +] + +[project.scripts] +kg_analysis = "kg_analysis.cli:main" + +[tool.black] +line-length = 79 + +[tool.mypy] +warn_no_return = false -- GitLab From c403969fc5cb675618d77c357341cf9b861e31c1 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 14:01:21 +0100 Subject: [PATCH 05/59] [metrics] First version of fully automated generation of metrics --- docs/metrics/general metrics/01_instances.md | 4 +- docs/metrics/general metrics/02_assertions.md | 4 +- .../general metrics/03_linkage_degree.md | 4 +- .../general metrics/04_outgoing_edges.md | 4 +- .../general metrics/05_incoming_edges.md | 4 +- reports/metrics/GM001.txt | 3 + reports/metrics/GM002.txt | 3 + reports/metrics/GM003.txt | 3 + reports/metrics/GM004.txt | 4 + reports/metrics/GM005.txt | 4 + scripts/kg_analysis/__init__.py | 9 + scripts/kg_analysis/cli.py | 24 +-- scripts/kg_analysis/metrics_runner.py | 169 ++++++++++++++++++ scripts/kg_analysis/query_runner.py | 57 +++--- 14 files changed, 247 insertions(+), 49 deletions(-) create mode 100644 reports/metrics/GM001.txt create mode 100644 reports/metrics/GM002.txt create mode 100644 reports/metrics/GM003.txt create mode 100644 reports/metrics/GM004.txt create mode 100644 reports/metrics/GM005.txt create mode 100644 scripts/kg_analysis/metrics_runner.py diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md index 001bbf2..a869138 100644 --- a/docs/metrics/general metrics/01_instances.md +++ b/docs/metrics/general metrics/01_instances.md @@ -18,7 +18,7 @@ The metric helps to: ``` ## Results -{{ include_if_exists("reports/metrics/001.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/GM001.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/001.txt", single_line=0) }} \ No newline at end of file +{{ include_if_exists("reports/metrics/GM001.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md index dad41a5..c2d3927 100644 --- a/docs/metrics/general metrics/02_assertions.md +++ b/docs/metrics/general metrics/02_assertions.md @@ -30,7 +30,7 @@ The metric helps to: ``` ## Results -{{ include_if_exists("reports/metrics/002_1.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/GM002.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/002_1.txt", single_line=0) }} \ No newline at end of file +{{ include_if_exists("reports/metrics/GM002.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md index e526e1e..ebfd0ee 100644 --- a/docs/metrics/general metrics/03_linkage_degree.md +++ b/docs/metrics/general metrics/03_linkage_degree.md @@ -51,7 +51,7 @@ This query calculates the average number of incoming relationships per entity, p ## Results ### Average Linkage Degree -{{ include_if_exists("reports/metrics/003_1.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/GM003.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/003_1.txt", single_line=0) }} \ No newline at end of file +{{ include_if_exists("reports/metrics/GM003.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/04_outgoing_edges.md b/docs/metrics/general metrics/04_outgoing_edges.md index 42dc5f4..0a80101 100644 --- a/docs/metrics/general metrics/04_outgoing_edges.md +++ b/docs/metrics/general metrics/04_outgoing_edges.md @@ -31,7 +31,7 @@ Using the total node count (n), median position is: position = (n+1)/2 ``` ## Results -{{ include_if_exists("reports/metrics/004.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/GM004.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/004.txt", single_line=0) }} \ No newline at end of file +{{ include_if_exists("reports/metrics/GM004.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/05_incoming_edges.md b/docs/metrics/general metrics/05_incoming_edges.md index a815f4c..63363aa 100644 --- a/docs/metrics/general metrics/05_incoming_edges.md +++ b/docs/metrics/general metrics/05_incoming_edges.md @@ -31,7 +31,7 @@ Using the total node count (n), median position is: position = (n+1)/2 ``` ## Results -{{ include_if_exists("reports/metrics/005.txt", start_line=1) }} +{{ include_if_exists("reports/metrics/GM005.txt", start_line=1) }} Last execution: -{{ include_if_exists("reports/metrics/005.txt", single_line=0) }} +{{ include_if_exists("reports/metrics/GM005.txt", single_line=0) }} diff --git a/reports/metrics/GM001.txt b/reports/metrics/GM001.txt new file mode 100644 index 0000000..4778d12 --- /dev/null +++ b/reports/metrics/GM001.txt @@ -0,0 +1,3 @@ +2025-03-13T13:57 +- Instance count: 1252089 +- Execution time: 0.05 seconds \ No newline at end of file diff --git a/reports/metrics/GM002.txt b/reports/metrics/GM002.txt new file mode 100644 index 0000000..f83346c --- /dev/null +++ b/reports/metrics/GM002.txt @@ -0,0 +1,3 @@ +2025-03-13T13:57 +- Assertions count: 40786353 +- Execution time: 0.04 seconds \ No newline at end of file diff --git a/reports/metrics/GM003.txt b/reports/metrics/GM003.txt new file mode 100644 index 0000000..4680c25 --- /dev/null +++ b/reports/metrics/GM003.txt @@ -0,0 +1,3 @@ +2025-03-13T13:57 +- Assertions count: 3.894342748611638 +- Execution time: 0.01 seconds \ No newline at end of file diff --git a/reports/metrics/GM004.txt b/reports/metrics/GM004.txt new file mode 100644 index 0000000..7dc2b50 --- /dev/null +++ b/reports/metrics/GM004.txt @@ -0,0 +1,4 @@ +2025-03-13T13:57 +- Total count: 6705836 +- Median: None +- Execution time: 12.72 seconds \ No newline at end of file diff --git a/reports/metrics/GM005.txt b/reports/metrics/GM005.txt new file mode 100644 index 0000000..2c28f94 --- /dev/null +++ b/reports/metrics/GM005.txt @@ -0,0 +1,4 @@ +2025-03-13T13:57 +- Total count: 15111836 +- Median: 1 +- Execution time: 0.02 seconds \ No newline at end of file diff --git a/scripts/kg_analysis/__init__.py b/scripts/kg_analysis/__init__.py index a9463af..ae88787 100644 --- a/scripts/kg_analysis/__init__.py +++ b/scripts/kg_analysis/__init__.py @@ -1,3 +1,12 @@ +import os + from rich.console import Console +from rich import inspect +from rich import print as rprint console = Console() + +SPARQL_ENDPOINT = os.getenv( + "SPARQL_ENDPOINT", "https://sparql.knowledgehub.nfdi4earth.de" +) +SPARQL_TIMEOUT = int(os.getenv("SPARQL_TIMEOUT", 120)) diff --git a/scripts/kg_analysis/cli.py b/scripts/kg_analysis/cli.py index 1808306..989caf5 100644 --- a/scripts/kg_analysis/cli.py +++ b/scripts/kg_analysis/cli.py @@ -4,20 +4,16 @@ import logging import click -import os from pathlib import Path from rich import print_json from .util import cli_startup from .query_runner import QueryRunner # type: ignore +from .metrics_runner import MetricsRunner # type: ignore log = logging.getLogger(__name__) -SPARQL_ENDPOINT = os.getenv( - "SPARQL_ENDPOINT", "https://sparql.knowledgehub.nfdi4earth.de" -) - @click.group() @click.option("--debug/--no-debug", "-d", is_flag=True, default=False) @@ -38,20 +34,11 @@ def main(ctx, debug): ) @click.option("--output", "-o", type=click.Path(), help="Path to save results") @click.pass_context -def run_query(ctx, query, output): +def query(ctx, query, output): """Run analysis on the KnowledgeGraph.""" - runner = QueryRunner(SPARQL_ENDPOINT) + runner = QueryRunner() query_path = Path(query) - # if output: - # output_path = Path(output) - # else: - # # Create default output path: queries/metrics/GM00X.rq - # # -> reports/metrics/00X.txt - # relative_path = query_path.relative_to("queries") - # output_path = Path("reports").joinpath( - # relative_path.with_suffix(".txt") - # ) try: results = runner.run_metric( query_path, output_path=Path(output) if output else None @@ -68,5 +55,10 @@ def run_query(ctx, query, output): ) +@main.command() +def metrics(): + MetricsRunner().run() + + if __name__ == "__main__": main(obj={}) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py new file mode 100644 index 0000000..bd602e9 --- /dev/null +++ b/scripts/kg_analysis/metrics_runner.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +from time import time + +from abc import ABC, abstractmethod +from datetime import datetime +from pathlib import Path + +from typing import Optional + +from .query_runner import QueryRunner + +log = logging.getLogger(__name__) + + +class MetricsRunnerBase(ABC): + """Base class for metric runners.""" + + _query_file: Optional[str] = None + _output_file: Optional[str] = None + _execution_time: Optional[float] = None + fail: bool = False + + def __init__(self): + self.runner = QueryRunner() + self.base_query_path = Path("queries/metrics") + self.base_output_path = Path("reports/metrics") + log.info(f"Running metric: {self.__class__.__name__}") + + @property + def query_file(self): + return Path(self._query_file) + + def get_query_path(self, query_file: str) -> Path: + return self.base_query_path.joinpath(query_file) + + @property + def query_path(self): + return self.get_query_path(self.query_file) + + @property + def output_path(self): + filename = self._output_file or self.query_file.with_suffix(".txt") + return self.base_output_path.joinpath(filename) + + def query_metric(self, query_path: Path, **kwargs) -> Optional[int]: + """Run a metric query and the result""" + start_time = time() + result = None + try: + result = self.runner.execute_query(query_path, **kwargs) + except Exception as e: + log.error(f"Failed to run metric: {e}") + self.fail = True + self._execution_time = time() - start_time + if result and result["results"]["bindings"]: + return list(result["results"]["bindings"][0].values())[0]["value"] + return None + + @abstractmethod + def run(self) -> dict: + """Run this specific metric.""" + pass + + def save(self, result) -> None: + """Run a metric query and optionally save the results.""" + + if self.fail: + log.error(f"Failed to run metric: {self.__class__.__name__}") + + with open(self.output_path, "w") as f: + f.write(f"{datetime.now().isoformat(timespec="minutes")}\n") + f.write(str(result)) + if self._execution_time: + f.write( + f"\n- Execution time: {self._execution_time:.2f} seconds" + ) + log.info(f"Results saved to {self.output_path}") + + +class MetricsRunner_001(MetricsRunnerBase): + """Runner for Metric 001: Instance Count""" + + _query_file = "GM001.rq" + + def run(self): + result = self.query_metric(self.query_path) + result_text = f"- Instance count: {result}" + self.save(result_text) + + +class MetricsRunner_002(MetricsRunnerBase): + """Runner for Metric 002: Assertions Count""" + + _query_file = "GM002_1.rq" + _output_file = "GM002.txt" + + def run(self): + result = self.query_metric(self.query_path) + result_text = f"- Assertions count: {result}" + self.save(result_text) + + +class MetricsRunner_003(MetricsRunnerBase): + """Runner for Metric 003: Average Linkage Degree""" + + _query_file = "GM003_1.rq" + _output_file = "GM003.txt" + + def run(self): + result = self.query_metric(self.query_path) + result_text = f"- Assertions count: {result}" + self.save(result_text) + + +class MetricsRunner_Edges(MetricsRunnerBase): + """Runner for Metric 004 & 005: Outgoing & Incoming Edges""" + + _files = { + "outgoing": { + "outfile": "GM004.txt", + "total_query": "GM004_2.rq", + "median_query": "GM004_4.rq", + }, + "incoming": { + "outfile": "GM005.txt", + "total_query": "GM005_2.rq", + "median_query": "GM005_4.rq", + }, + } + + def __init__(self, _type: str, *args, **kwargs): + super().__init__(*args, **kwargs) + self._output_file = self._files[_type]["outfile"] + self.total_query = self._files[_type]["total_query"] + self.median_query = self._files[_type]["median_query"] + + def run(self): + edges_total = self.query_metric(self.get_query_path(self.total_query)) + + if not edges_total: + self.fail = True + return + + edges_total = int(edges_total) + if edges_total % 2: + log.warning("Odd number of edges, median is not unique") + median_position = int(edges_total / 2) + median = self.query_metric( + self.get_query_path(self.median_query), + replace_dict={"{median_position}": median_position}, + ) + + result_text = f"- Total count: {edges_total}" + result_text += f"\n- Median: {median}" + self.save(result_text) + + +class MetricsRunner(ABC): + + def run(self): + MetricsRunner_001().run() + MetricsRunner_002().run() + MetricsRunner_003().run() + MetricsRunner_Edges("outgoing").run() + MetricsRunner_Edges("incoming").run() diff --git a/scripts/kg_analysis/query_runner.py b/scripts/kg_analysis/query_runner.py index d09812f..9978138 100644 --- a/scripts/kg_analysis/query_runner.py +++ b/scripts/kg_analysis/query_runner.py @@ -3,41 +3,52 @@ # ralf.klammer@tu-dresden.de import logging +import time + from pathlib import Path -from typing import Optional +from typing import Optional, Tuple from SPARQLWrapper import JSON, SPARQLWrapper # type: ignore +from . import SPARQL_ENDPOINT, SPARQL_TIMEOUT + log = logging.getLogger(__name__) class QueryRunner: - def __init__(self, endpoint_url: str): - self.sparql = SPARQLWrapper(endpoint_url) + def __init__(self, timeout: int = SPARQL_TIMEOUT): + self.sparql = SPARQLWrapper(SPARQL_ENDPOINT) self.sparql.setReturnFormat(JSON) + # Set timeout in seconds and convert to milliseconds + # as required by SPARQLWrapper.setTimout() + self.sparql.setTimeout(timeout * 1000) - def execute_query(self, query_path: Path) -> dict: - """Execute a SPARQL query from a file and return the results.""" + def execute_query( + self, query_path: Path, replace_dict: Optional[dict] = None + ) -> dict: + """Execute a SPARQL query from a file and return the json results""" + # Read the query from the file with open(query_path, "r") as f: query = f.read() + # Replace placeholders in the query + if replace_dict: + for r_key, r_value in replace_dict.items(): + query = query.replace(r_key, str(r_value)) + + # Set the query and execute self.sparql.setQuery(query) - return self.sparql.query().convert() + result = self.sparql.query().convert() - def run_metric( - self, query_path: Path, output_path: Optional[Path] = None - ) -> dict: - """Run a metric query and optionally save the results.""" - results = self.execute_query(query_path) - - if output_path: - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, "w") as f: - # Extract first value from results as metrics are usually - # single values - value = list(results["results"]["bindings"][0].values())[0][ - "value" - ] - f.write(str(value)) - - return results + # Check if the result is a dictionary + if isinstance(result, dict): + return result + raise ValueError("Query did not return a dictionary") + + def run_metric(self, query_path: Path) -> Optional[int]: + """Run a metric query and the result""" + result = self.execute_query(query_path) + if result and result["results"]["bindings"]: + return list(result["results"]["bindings"][0].values())[0]["value"] + + return None -- GitLab From 1d8a37b6f27f89fac17de710cf5bd5b475658b73 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 15:26:28 +0100 Subject: [PATCH 06/59] [metrics] Add min/max for edges --- .../general metrics/04_outgoing_edges.md | 12 +++++++ .../general metrics/05_incoming_edges.md | 14 +++++++- queries/metrics/GM004_5.rq | 9 +++++ queries/metrics/GM004_6.rq | 9 +++++ queries/metrics/GM005_5.rq | 9 +++++ queries/metrics/GM005_6.rq | 9 +++++ scripts/kg_analysis/metrics_runner.py | 33 +++++++++++++------ 7 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 queries/metrics/GM004_5.rq create mode 100644 queries/metrics/GM004_6.rq create mode 100644 queries/metrics/GM005_5.rq create mode 100644 queries/metrics/GM005_6.rq diff --git a/docs/metrics/general metrics/04_outgoing_edges.md b/docs/metrics/general metrics/04_outgoing_edges.md index 0a80101..5a4afb4 100644 --- a/docs/metrics/general metrics/04_outgoing_edges.md +++ b/docs/metrics/general metrics/04_outgoing_edges.md @@ -30,6 +30,18 @@ Using the total node count (n), median position is: position = (n+1)/2 {{ include_if_exists("queries/metrics/GM004_4.rq") }} ``` +### Step 5: Get Minimum Value + +```sparql +{{ include_if_exists("queries/metrics/GM004_5.rq") }} +``` + +### Step 5: Get Maximum Value + +```sparql +{{ include_if_exists("queries/metrics/GM004_6.rq") }} +``` + ## Results {{ include_if_exists("reports/metrics/GM004.txt", start_line=1) }} diff --git a/docs/metrics/general metrics/05_incoming_edges.md b/docs/metrics/general metrics/05_incoming_edges.md index 63363aa..79fc98b 100644 --- a/docs/metrics/general metrics/05_incoming_edges.md +++ b/docs/metrics/general metrics/05_incoming_edges.md @@ -24,12 +24,24 @@ Using the total node count (n), median position is: position = (n+1)/2 {{ include_if_exists("queries/metrics/GM005_3.rq") }} ``` -### Step 4: Get Median Value(s) +### Step 4: Get Median Value ```sparql {{ include_if_exists("queries/metrics/GM005_4.rq") }} ``` +### Step 5: Get Minimum Value + +```sparql +{{ include_if_exists("queries/metrics/GM005_5.rq") }} +``` + +### Step 5: Get Maximum Value + +```sparql +{{ include_if_exists("queries/metrics/GM005_6.rq") }} +``` + ## Results {{ include_if_exists("reports/metrics/GM005.txt", start_line=1) }} diff --git a/queries/metrics/GM004_5.rq b/queries/metrics/GM004_5.rq new file mode 100644 index 0000000..1dcb762 --- /dev/null +++ b/queries/metrics/GM004_5.rq @@ -0,0 +1,9 @@ +# Query to find the node with the minimum of outgoing edges in the graph + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { + ?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY ASC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/GM004_6.rq b/queries/metrics/GM004_6.rq new file mode 100644 index 0000000..6105231 --- /dev/null +++ b/queries/metrics/GM004_6.rq @@ -0,0 +1,9 @@ +# Query to find the node with the maximum of outgoing edges in the graph + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { + ?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY DESC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/GM005_5.rq b/queries/metrics/GM005_5.rq new file mode 100644 index 0000000..7465b95 --- /dev/null +++ b/queries/metrics/GM005_5.rq @@ -0,0 +1,9 @@ +# Query to find the node with the minimum of incoming edges in the graph + +SELECT COUNT(?incoming) as ?inEdges +WHERE { + ?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY ASC(?inEdges) +LIMIT 1 diff --git a/queries/metrics/GM005_6.rq b/queries/metrics/GM005_6.rq new file mode 100644 index 0000000..f01a1dc --- /dev/null +++ b/queries/metrics/GM005_6.rq @@ -0,0 +1,9 @@ +# Query to find the node with the maximum of incoming edges in the graph + +SELECT COUNT(?incoming) as ?inEdges +WHERE { + ?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY DESC(?inEdges) +LIMIT 1 diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index bd602e9..4fd7144 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -21,7 +21,7 @@ class MetricsRunnerBase(ABC): _query_file: Optional[str] = None _output_file: Optional[str] = None - _execution_time: Optional[float] = None + _execution_time: float = 0 fail: bool = False def __init__(self): @@ -55,7 +55,7 @@ class MetricsRunnerBase(ABC): except Exception as e: log.error(f"Failed to run metric: {e}") self.fail = True - self._execution_time = time() - start_time + self._execution_time += time() - start_time if result and result["results"]["bindings"]: return list(result["results"]["bindings"][0].values())[0]["value"] return None @@ -124,22 +124,33 @@ class MetricsRunner_Edges(MetricsRunnerBase): "outfile": "GM004.txt", "total_query": "GM004_2.rq", "median_query": "GM004_4.rq", + "min_query": "GM004_5.rq", + "max_query": "GM004_6.rq", }, "incoming": { "outfile": "GM005.txt", "total_query": "GM005_2.rq", "median_query": "GM005_4.rq", + "min_query": "GM005_5.rq", + "max_query": "GM005_6.rq", }, } def __init__(self, _type: str, *args, **kwargs): super().__init__(*args, **kwargs) + self.type = _type self._output_file = self._files[_type]["outfile"] - self.total_query = self._files[_type]["total_query"] - self.median_query = self._files[_type]["median_query"] def run(self): - edges_total = self.query_metric(self.get_query_path(self.total_query)) + edges_total = self.query_metric( + self.get_query_path(self._files[self.type]["total_query"]) + ) + minimum = self.query_metric( + self.get_query_path(self._files[self.type]["min_query"]) + ) + maximum = self.query_metric( + self.get_query_path(self._files[self.type]["max_query"]) + ) if not edges_total: self.fail = True @@ -150,20 +161,22 @@ class MetricsRunner_Edges(MetricsRunnerBase): log.warning("Odd number of edges, median is not unique") median_position = int(edges_total / 2) median = self.query_metric( - self.get_query_path(self.median_query), + self.get_query_path(self._files[self.type]["median_query"]), replace_dict={"{median_position}": median_position}, ) result_text = f"- Total count: {edges_total}" + result_text += f"\n- Minimum: {minimum}" result_text += f"\n- Median: {median}" + result_text += f"\n- Maximum: {maximum}" self.save(result_text) class MetricsRunner(ABC): def run(self): - MetricsRunner_001().run() - MetricsRunner_002().run() - MetricsRunner_003().run() - MetricsRunner_Edges("outgoing").run() + # MetricsRunner_001().run() + # MetricsRunner_002().run() + # MetricsRunner_003().run() + # MetricsRunner_Edges("outgoing").run() MetricsRunner_Edges("incoming").run() -- GitLab From 96a8427c9068d739e65a4ae55f40a88ebad00ef0 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 13 Mar 2025 15:26:50 +0100 Subject: [PATCH 07/59] [metrics] Move metrics/index.md --- docs/metrics/{general metrics => }/index.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/metrics/{general metrics => }/index.md (100%) diff --git a/docs/metrics/general metrics/index.md b/docs/metrics/index.md similarity index 100% rename from docs/metrics/general metrics/index.md rename to docs/metrics/index.md -- GitLab From 1132458dac60200653f7c196d015b0bd0498d854 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 09:58:22 +0100 Subject: [PATCH 08/59] [metrics] Queries for resource-type 'Dataset' --- .../resource specific metrics/06_dataset.md | 91 +++++++++++++++++++ queries/metrics/RM001_assertions.rq | 8 ++ queries/metrics/RM001_connectivity.rq | 19 ++++ queries/metrics/RM001_in_edges_max.rq | 10 ++ queries/metrics/RM001_in_edges_median.rq | 15 +++ queries/metrics/RM001_in_edges_min.rq | 10 ++ queries/metrics/RM001_in_edges_total.rq | 7 ++ queries/metrics/RM001_instances.rq | 13 +++ queries/metrics/RM001_linkage.rq | 30 ++++++ queries/metrics/RM001_out_edges_max.rq | 10 ++ queries/metrics/RM001_out_edges_median.rq | 15 +++ queries/metrics/RM001_out_edges_min.rq | 10 ++ queries/metrics/RM001_out_edges_total.rq | 7 ++ 13 files changed, 245 insertions(+) create mode 100644 docs/metrics/resource specific metrics/06_dataset.md create mode 100644 queries/metrics/RM001_assertions.rq create mode 100644 queries/metrics/RM001_connectivity.rq create mode 100644 queries/metrics/RM001_in_edges_max.rq create mode 100644 queries/metrics/RM001_in_edges_median.rq create mode 100644 queries/metrics/RM001_in_edges_min.rq create mode 100644 queries/metrics/RM001_in_edges_total.rq create mode 100644 queries/metrics/RM001_instances.rq create mode 100644 queries/metrics/RM001_linkage.rq create mode 100644 queries/metrics/RM001_out_edges_max.rq create mode 100644 queries/metrics/RM001_out_edges_median.rq create mode 100644 queries/metrics/RM001_out_edges_min.rq create mode 100644 queries/metrics/RM001_out_edges_total.rq diff --git a/docs/metrics/resource specific metrics/06_dataset.md b/docs/metrics/resource specific metrics/06_dataset.md new file mode 100644 index 0000000..cab4f21 --- /dev/null +++ b/docs/metrics/resource specific metrics/06_dataset.md @@ -0,0 +1,91 @@ +# Dataset Resource Metrics + +This document analyzes specific metrics for resources of type `Dataset` in the knowledge graph. + +## Basic Metrics + +### Number of Datasets + +see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) + +```sparql +{{ include_if_exists("queries/metrics/RM001_instances.rq") }} +``` + +### Connectivity to other resources + +```sparql +{{ include_if_exists("queries/metrics/RM001_connectivity.rq") }} +``` + +### Number of Assertions + +see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) + +```sparql +{{ include_if_exists("queries/metrics/RM001_assertions.rq") }} +``` + +### Average linkage + +see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) + +```sparql +{{ include_if_exists("queries/metrics/RM001_linkage.rq") }} +``` + +### Outgoing Edges Statistics + +see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoing_edges/) + +#### Total outgoing edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_total.rq") }} +``` + +#### Minimum outgoing edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_min.rq") }} +``` + +#### Maximum outgoing edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_max.rq") }} +``` + +#### Median outgoing edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_median.rq") }} +``` + +### Incoming Edges Statistics + +see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incoming_edges/) + +#### Total incoming edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_total.rq") }} +``` + +#### Minimum incoming edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_min.rq") }} +``` + +#### Maximum incoming edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_max.rq") }} +``` + +#### Median incoming edges + +```sparql +{{ include_if_exists("queries/metrics/RM001_out_edges_median.rq") }} +``` diff --git a/queries/metrics/RM001_assertions.rq b/queries/metrics/RM001_assertions.rq new file mode 100644 index 0000000..051632a --- /dev/null +++ b/queries/metrics/RM001_assertions.rq @@ -0,0 +1,8 @@ +# Total number of assertions (for resource type: Dataset) + +PREFIX dcat: <http://www.w3.org/ns/dcat#> + +SELECT (COUNT(*) AS ?numAssertions) +WHERE { + ?subject ?predicate dcat:Dataset . +} diff --git a/queries/metrics/RM001_connectivity.rq b/queries/metrics/RM001_connectivity.rq new file mode 100644 index 0000000..1998eba --- /dev/null +++ b/queries/metrics/RM001_connectivity.rq @@ -0,0 +1,19 @@ +# Analysis of total connected resources for datasets +# +# Explanation: +# Counts the number of resources that are connected to datasets +# This gives us a metric for dataset connectivity in the graph + +PREFIX dcat: <http://www.w3.org/ns/dcat#> +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + +SELECT (COUNT(DISTINCT ?connected) as ?numConnectedResources) +WHERE { + ?dataset a dcat:Dataset ; + ?property ?connected . + + # Ensure connected resource is not a literal + FILTER(isIRI(?connected)) + # Exclude self-references to datasets + FILTER(?connected != ?dataset) +} diff --git a/queries/metrics/RM001_in_edges_max.rq b/queries/metrics/RM001_in_edges_max.rq new file mode 100644 index 0000000..1d9a825 --- /dev/null +++ b/queries/metrics/RM001_in_edges_max.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximum out-edge for datasets only. + +SELECT ?target (COUNT(?incoming) as ?inEdges) +WHERE { +?target a <http://www.w3.org/ns/dcat#Dataset> . +?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY DESC(?inEdges) +LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_median.rq b/queries/metrics/RM001_in_edges_median.rq new file mode 100644 index 0000000..d2ea70f --- /dev/null +++ b/queries/metrics/RM001_in_edges_median.rq @@ -0,0 +1,15 @@ +# This SPARQL query calculates the median in-edge for datasets only. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?inEdges as ?inMedian +WHERE { + SELECT ?target (COUNT(?incoming) as ?inEdges) + WHERE { + ?target a <http://www.w3.org/ns/dcat#Dataset> . + ?incoming ?p ?target . + } + GROUP BY ?target + ORDER BY ASC(?inEdges) +} +OFFSET {median_position} +LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_min.rq b/queries/metrics/RM001_in_edges_min.rq new file mode 100644 index 0000000..f39e6cb --- /dev/null +++ b/queries/metrics/RM001_in_edges_min.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the minimum out-edge for datasets only. + +SELECT ?target (COUNT(?incoming) as ?inEdges) +WHERE { +?target a <http://www.w3.org/ns/dcat#Dataset> . +?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY ASC(?inEdges) +LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_total.rq b/queries/metrics/RM001_in_edges_total.rq new file mode 100644 index 0000000..27b3569 --- /dev/null +++ b/queries/metrics/RM001_in_edges_total.rq @@ -0,0 +1,7 @@ +# Returns a single number representing the cleaned count of unique incoming nodes in the graph. + +SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) +WHERE { + ?incoming ?p ?target . + ?target a <http://www.w3.org/ns/dcat#Dataset> . +} \ No newline at end of file diff --git a/queries/metrics/RM001_instances.rq b/queries/metrics/RM001_instances.rq new file mode 100644 index 0000000..73a7b0c --- /dev/null +++ b/queries/metrics/RM001_instances.rq @@ -0,0 +1,13 @@ +# Count all instances of type Dataset +# +# Explanation: +# ?dataset a dcat:Dataset finds all resources that are of type Dataset +# COUNT(DISTINCT ?dataset) counts unique dataset instances +# We use DISTINCT to avoid counting duplicates if a dataset has multiple types + +PREFIX dcat: <http://www.w3.org/ns/dcat#> + +SELECT (COUNT(DISTINCT ?dataset) AS ?datasetCount) +WHERE { + ?dataset a dcat:Dataset . +} diff --git a/queries/metrics/RM001_linkage.rq b/queries/metrics/RM001_linkage.rq new file mode 100644 index 0000000..f379777 --- /dev/null +++ b/queries/metrics/RM001_linkage.rq @@ -0,0 +1,30 @@ +# The average linkage degree for datasets only +# (i.e.: how many assertions per dataset does the graph contain) + +SELECT (AVG(?totalDegree) as ?avgLinkageDegree) +WHERE { + { + SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) + WHERE { + # Only consider entities that are datasets + ?entity a <http://www.w3.org/ns/dcat#Dataset> . + + { + SELECT ?entity (COUNT(*) as ?outDegree) + WHERE { + ?entity a <http://www.w3.org/ns/dcat#Dataset> . + ?entity ?p ?o . + } + GROUP BY ?entity + } + { + SELECT ?entity (COUNT(*) as ?inDegree) + WHERE { + ?entity a <http://www.w3.org/ns/dcat#Dataset> . + ?s ?p ?entity . # Hier zählen wir die Verbindungen, die auf das Dataset zeigen + } + GROUP BY ?entity + } + } + } +} diff --git a/queries/metrics/RM001_out_edges_max.rq b/queries/metrics/RM001_out_edges_max.rq new file mode 100644 index 0000000..4503611 --- /dev/null +++ b/queries/metrics/RM001_out_edges_max.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximum out-edge for datasets only. + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { +?source a <http://www.w3.org/ns/dcat#Dataset> . +?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY DESC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_median.rq b/queries/metrics/RM001_out_edges_median.rq new file mode 100644 index 0000000..dd705cb --- /dev/null +++ b/queries/metrics/RM001_out_edges_median.rq @@ -0,0 +1,15 @@ +# This SPARQL query calculates the median out-edge for datasets only. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?outEdges as ?outMedian +WHERE { + SELECT ?source (COUNT(?outgoing) as ?outEdges) + WHERE { + ?source a <http://www.w3.org/ns/dcat#Dataset> . + ?source ?p ?outgoing . + } + GROUP BY ?source + ORDER BY ASC(?outEdges) +} +OFFSET {median_position} +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_min.rq b/queries/metrics/RM001_out_edges_min.rq new file mode 100644 index 0000000..55bcfcf --- /dev/null +++ b/queries/metrics/RM001_out_edges_min.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximuim out-edge for datasets only. + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { +?source a <http://www.w3.org/ns/dcat#Dataset> . +?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY ASC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_total.rq b/queries/metrics/RM001_out_edges_total.rq new file mode 100644 index 0000000..8669f72 --- /dev/null +++ b/queries/metrics/RM001_out_edges_total.rq @@ -0,0 +1,7 @@ +# Returns a single number representing the cleaned count of unique outging nodes in the graph. + +SELECT COUNT(DISTINCT ?source) as ?uniqueEdges +WHERE { + ?source a <http://www.w3.org/ns/dcat#Dataset> . + ?source ?p ?outgoing . +} \ No newline at end of file -- GitLab From e9fd2fca832eb55a8c2139c33f0c24f7425d6726 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 10:59:48 +0100 Subject: [PATCH 09/59] [metrics] Add metrics for all resource type by using template --- docs/macros/main.py | 14 +++ .../resource specific metrics/06_dataset.md | 93 +------------------ .../07_publication.md | 4 + .../08_learning_resource.md | 4 + .../09_repository.md | 4 + .../10_article_lhb.md | 4 + .../resource specific metrics/11_standards.md | 4 + .../12_organization.md | 4 + .../resource specific metrics/13_software.md | 4 + .../resource specific metrics/14_service.md | 4 + .../15_dataservice.md | 4 + .../16_aggregator.md | 4 + .../resource specific metrics/17_person.md | 4 + .../resource specific metrics/18_registry.md | 4 + docs/templates/basic_metrics.md | 93 +++++++++++++++++++ queries/metrics/RM001_instances_template.rq | 11 +++ queries/metrics/RM002_assertions_template.rq | 6 ++ queries/metrics/RM003_linkage_template.rq | 30 ++++++ .../RM004_1_out_edges_total_template.rq | 7 ++ .../metrics/RM004_2_out_edges_min_template.rq | 10 ++ .../RM004_3_out_edges_median_template.rq | 15 +++ .../metrics/RM004_4_out_edges_max_template.rq | 10 ++ .../RM005_1_in_edges_total_template.rq | 7 ++ .../metrics/RM005_2_in_edges_min_template.rq | 10 ++ .../RM005_3_in_edges_median_template.rq | 15 +++ .../metrics/RM005_4_in_edges_max_template.rq | 10 ++ .../metrics/RM006_connectivity_template.rq | 16 ++++ 27 files changed, 305 insertions(+), 90 deletions(-) create mode 100644 docs/metrics/resource specific metrics/07_publication.md create mode 100644 docs/metrics/resource specific metrics/08_learning_resource.md create mode 100644 docs/metrics/resource specific metrics/09_repository.md create mode 100644 docs/metrics/resource specific metrics/10_article_lhb.md create mode 100644 docs/metrics/resource specific metrics/11_standards.md create mode 100644 docs/metrics/resource specific metrics/12_organization.md create mode 100644 docs/metrics/resource specific metrics/13_software.md create mode 100644 docs/metrics/resource specific metrics/14_service.md create mode 100644 docs/metrics/resource specific metrics/15_dataservice.md create mode 100644 docs/metrics/resource specific metrics/16_aggregator.md create mode 100644 docs/metrics/resource specific metrics/17_person.md create mode 100644 docs/metrics/resource specific metrics/18_registry.md create mode 100644 docs/templates/basic_metrics.md create mode 100644 queries/metrics/RM001_instances_template.rq create mode 100644 queries/metrics/RM002_assertions_template.rq create mode 100644 queries/metrics/RM003_linkage_template.rq create mode 100644 queries/metrics/RM004_1_out_edges_total_template.rq create mode 100644 queries/metrics/RM004_2_out_edges_min_template.rq create mode 100644 queries/metrics/RM004_3_out_edges_median_template.rq create mode 100644 queries/metrics/RM004_4_out_edges_max_template.rq create mode 100644 queries/metrics/RM005_1_in_edges_total_template.rq create mode 100644 queries/metrics/RM005_2_in_edges_min_template.rq create mode 100644 queries/metrics/RM005_3_in_edges_median_template.rq create mode 100644 queries/metrics/RM005_4_in_edges_max_template.rq create mode 100644 queries/metrics/RM006_connectivity_template.rq diff --git a/docs/macros/main.py b/docs/macros/main.py index 4287ee7..9b2e345 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -13,3 +13,17 @@ def define_env(env): return f"*Keine Ergebnisse verfügbar. Die Datei `{filename}` wurde nicht gefunden.*" except IndexError: return f"*Fehler: Die angegebene Zeile {start_line} existiert nicht in `{filename}`.*" + + @env.macro + def include_template(template_path, resource_type, **kwargs): + """ + Includes a template file and replaces {resource_type} with the given value + + Args: + template_path (str): Path to the template file + resource_type (str): The resource type URI to inject + """ + content = include_if_exists(template_path, **kwargs) + if content: + return content.replace("{resource_type}", resource_type) + return "" diff --git a/docs/metrics/resource specific metrics/06_dataset.md b/docs/metrics/resource specific metrics/06_dataset.md index cab4f21..fbbb7c0 100644 --- a/docs/metrics/resource specific metrics/06_dataset.md +++ b/docs/metrics/resource specific metrics/06_dataset.md @@ -1,91 +1,4 @@ -# Dataset Resource Metrics +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Dataset -This document analyzes specific metrics for resources of type `Dataset` in the knowledge graph. - -## Basic Metrics - -### Number of Datasets - -see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) - -```sparql -{{ include_if_exists("queries/metrics/RM001_instances.rq") }} -``` - -### Connectivity to other resources - -```sparql -{{ include_if_exists("queries/metrics/RM001_connectivity.rq") }} -``` - -### Number of Assertions - -see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) - -```sparql -{{ include_if_exists("queries/metrics/RM001_assertions.rq") }} -``` - -### Average linkage - -see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) - -```sparql -{{ include_if_exists("queries/metrics/RM001_linkage.rq") }} -``` - -### Outgoing Edges Statistics - -see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoing_edges/) - -#### Total outgoing edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_total.rq") }} -``` - -#### Minimum outgoing edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_min.rq") }} -``` - -#### Maximum outgoing edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_max.rq") }} -``` - -#### Median outgoing edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_median.rq") }} -``` - -### Incoming Edges Statistics - -see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incoming_edges/) - -#### Total incoming edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_total.rq") }} -``` - -#### Minimum incoming edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_min.rq") }} -``` - -#### Maximum incoming edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_max.rq") }} -``` - -#### Median incoming edges - -```sparql -{{ include_if_exists("queries/metrics/RM001_out_edges_median.rq") }} -``` +{{ basic_metrics("<http://www.w3.org/ns/dcat#Dataset>") }} diff --git a/docs/metrics/resource specific metrics/07_publication.md b/docs/metrics/resource specific metrics/07_publication.md new file mode 100644 index 0000000..46a7d3d --- /dev/null +++ b/docs/metrics/resource specific metrics/07_publication.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Publication + +{{ basic_metrics("<http://nfdi4earth.de/ontology/Publication>") }} diff --git a/docs/metrics/resource specific metrics/08_learning_resource.md b/docs/metrics/resource specific metrics/08_learning_resource.md new file mode 100644 index 0000000..e9d05b5 --- /dev/null +++ b/docs/metrics/resource specific metrics/08_learning_resource.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Learning Resource + +{{ basic_metrics("<http://schema.org/LearningResource>") }} diff --git a/docs/metrics/resource specific metrics/09_repository.md b/docs/metrics/resource specific metrics/09_repository.md new file mode 100644 index 0000000..a1eb845 --- /dev/null +++ b/docs/metrics/resource specific metrics/09_repository.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Repository + +{{ basic_metrics("<http://nfdi4earth.de/ontology/Repository>") }} diff --git a/docs/metrics/resource specific metrics/10_article_lhb.md b/docs/metrics/resource specific metrics/10_article_lhb.md new file mode 100644 index 0000000..e37dca7 --- /dev/null +++ b/docs/metrics/resource specific metrics/10_article_lhb.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Living Handbook Article + +{{ basic_metrics("<http://nfdi4earth.de/ontology/LHBArticle>") }} diff --git a/docs/metrics/resource specific metrics/11_standards.md b/docs/metrics/resource specific metrics/11_standards.md new file mode 100644 index 0000000..f09b4b5 --- /dev/null +++ b/docs/metrics/resource specific metrics/11_standards.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Standards + +{{ basic_metrics("<http://nfdi4earth.de/ontology/MetadataStandard>") }} diff --git a/docs/metrics/resource specific metrics/12_organization.md b/docs/metrics/resource specific metrics/12_organization.md new file mode 100644 index 0000000..61e6323 --- /dev/null +++ b/docs/metrics/resource specific metrics/12_organization.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Organization + +{{ basic_metrics("<http://xmlns.com/foaf/0.1/Organization>") }} diff --git a/docs/metrics/resource specific metrics/13_software.md b/docs/metrics/resource specific metrics/13_software.md new file mode 100644 index 0000000..bb23391 --- /dev/null +++ b/docs/metrics/resource specific metrics/13_software.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Tools & Software + +{{ basic_metrics("<http://schema.org/SoftwareSourceCode>") }} diff --git a/docs/metrics/resource specific metrics/14_service.md b/docs/metrics/resource specific metrics/14_service.md new file mode 100644 index 0000000..48efc3e --- /dev/null +++ b/docs/metrics/resource specific metrics/14_service.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Service + +{{ basic_metrics("<http://www.w3.org/ns/sparql-service-description#Service>") }} diff --git a/docs/metrics/resource specific metrics/15_dataservice.md b/docs/metrics/resource specific metrics/15_dataservice.md new file mode 100644 index 0000000..82c9a69 --- /dev/null +++ b/docs/metrics/resource specific metrics/15_dataservice.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Data Service + +{{ basic_metrics("<http://www.w3.org/ns/dcat#DataService>") }} diff --git a/docs/metrics/resource specific metrics/16_aggregator.md b/docs/metrics/resource specific metrics/16_aggregator.md new file mode 100644 index 0000000..114bcfa --- /dev/null +++ b/docs/metrics/resource specific metrics/16_aggregator.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Aggregator + +{{ basic_metrics("<http://nfdi4earth.de/ontology/Aggregator>") }} diff --git a/docs/metrics/resource specific metrics/17_person.md b/docs/metrics/resource specific metrics/17_person.md new file mode 100644 index 0000000..d806fab --- /dev/null +++ b/docs/metrics/resource specific metrics/17_person.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Person + +{{ basic_metrics("<http://schema.org/Person>") }} diff --git a/docs/metrics/resource specific metrics/18_registry.md b/docs/metrics/resource specific metrics/18_registry.md new file mode 100644 index 0000000..bfb1803 --- /dev/null +++ b/docs/metrics/resource specific metrics/18_registry.md @@ -0,0 +1,4 @@ +{% from "templates/basic_metrics.md" import basic_metrics with context %} +# Registry + +{{ basic_metrics("<http://nfdi4earth.de/ontology/Registry>") }} diff --git a/docs/templates/basic_metrics.md b/docs/templates/basic_metrics.md new file mode 100644 index 0000000..03bef10 --- /dev/null +++ b/docs/templates/basic_metrics.md @@ -0,0 +1,93 @@ +{% macro basic_metrics(resource_type) %} + +Resource type: {{resource_type}} + +## Basic Metrics + +### Number of Datasets + +see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) + +```sparql +{{ include_template("queries/metrics/RM001_instances_template.rq", resource_type) }} +``` + +### Connectivity to other resources + +```sparql +{{ include_template("queries/metrics/RM006_connectivity_template.rq", resource_type) }} +``` + +### Number of Assertions + +see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) + +```sparql +{{ include_template("queries/metrics/RM002_assertions_template.rq", resource_type) }} +``` + +### Average linkage + +see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) + +```sparql +{{ include_template("queries/metrics/RM003_linkage_template.rq", resource_type) }} +``` + +### Outgoing Edges Statistics + +see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoing_edges/) + +#### Total outgoing edges + +```sparql +{{ include_template("queries/metrics/RM004_1_out_edges_total_template.rq", resource_type) }} +``` + +#### Minimum outgoing edges + +```sparql +{{ include_template("queries/metrics/RM004_2_out_edges_min_template.rq", resource_type) }} +``` + +#### Median outgoing edges + +```sparql +{{ include_template("queries/metrics/RM004_3_out_edges_median_template.rq", resource_type) }} +``` + +#### Maximum outgoing edges + +```sparql +{{ include_template("queries/metrics/RM004_4_out_edges_max_template.rq", resource_type) }} +``` + +### Incoming Edges Statistics + +see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incoming_edges/) + +#### Total incoming edges + +```sparql +{{ include_template("queries/metrics/RM005_1_in_edges_total_template.rq", resource_type) }} +``` + +#### Minimum incoming edges + +```sparql +{{ include_template("queries/metrics/RM005_2_in_edges_min_template.rq", resource_type) }} +``` + +#### Median incoming edges + +```sparql +{{ include_template("queries/metrics/RM005_3_in_edges_median_template.rq", resource_type) }} +``` + +#### Maximum incoming edges + +```sparql +{{ include_template("queries/metrics/RM005_4_in_edges_max_template.rq", resource_type) }} +``` + +{% endmacro %} diff --git a/queries/metrics/RM001_instances_template.rq b/queries/metrics/RM001_instances_template.rq new file mode 100644 index 0000000..f294233 --- /dev/null +++ b/queries/metrics/RM001_instances_template.rq @@ -0,0 +1,11 @@ +# Count all instances of type Dataset +# +# Explanation: +# ?dataset a dcat:Dataset finds all resources that are of type Dataset +# COUNT(DISTINCT ?dataset) counts unique dataset instances +# We use DISTINCT to avoid counting duplicates if a dataset has multiple types + +SELECT (COUNT(DISTINCT ?dataset) AS ?datasetCount) +WHERE { + ?dataset a {resource_type} . +} diff --git a/queries/metrics/RM002_assertions_template.rq b/queries/metrics/RM002_assertions_template.rq new file mode 100644 index 0000000..404266a --- /dev/null +++ b/queries/metrics/RM002_assertions_template.rq @@ -0,0 +1,6 @@ +# Total number of assertions (for resource type: Dataset) + +SELECT (COUNT(*) AS ?numAssertions) +WHERE { + ?subject ?predicate {resource_type} . +} diff --git a/queries/metrics/RM003_linkage_template.rq b/queries/metrics/RM003_linkage_template.rq new file mode 100644 index 0000000..6cf796d --- /dev/null +++ b/queries/metrics/RM003_linkage_template.rq @@ -0,0 +1,30 @@ +# The average linkage degree for datasets only +# (i.e.: how many assertions per dataset does the graph contain) + +SELECT (AVG(?totalDegree) as ?avgLinkageDegree) +WHERE { + { + SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) + WHERE { + # Only consider entities that are datasets + ?entity a {resource_type} . + + { + SELECT ?entity (COUNT(*) as ?outDegree) + WHERE { + ?entity a {resource_type} . + ?entity ?p ?o . + } + GROUP BY ?entity + } + { + SELECT ?entity (COUNT(*) as ?inDegree) + WHERE { + ?entity a {resource_type} . + ?s ?p ?entity . # Hier zählen wir die Verbindungen, die auf das Dataset zeigen + } + GROUP BY ?entity + } + } + } +} diff --git a/queries/metrics/RM004_1_out_edges_total_template.rq b/queries/metrics/RM004_1_out_edges_total_template.rq new file mode 100644 index 0000000..5788a6d --- /dev/null +++ b/queries/metrics/RM004_1_out_edges_total_template.rq @@ -0,0 +1,7 @@ +# Returns a single number representing the cleaned count of unique outging nodes in the graph. + +SELECT COUNT(DISTINCT ?source) as ?uniqueEdges +WHERE { + ?source a {resource_type} . + ?source ?p ?outgoing . +} \ No newline at end of file diff --git a/queries/metrics/RM004_2_out_edges_min_template.rq b/queries/metrics/RM004_2_out_edges_min_template.rq new file mode 100644 index 0000000..817ba5e --- /dev/null +++ b/queries/metrics/RM004_2_out_edges_min_template.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximuim out-edge for datasets only. + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { +?source a {resource_type} . +?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY ASC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM004_3_out_edges_median_template.rq b/queries/metrics/RM004_3_out_edges_median_template.rq new file mode 100644 index 0000000..1bbc201 --- /dev/null +++ b/queries/metrics/RM004_3_out_edges_median_template.rq @@ -0,0 +1,15 @@ +# This SPARQL query calculates the median out-edge for datasets only. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?outEdges as ?outMedian +WHERE { + SELECT ?source (COUNT(?outgoing) as ?outEdges) + WHERE { + ?source a {resource_type} . + ?source ?p ?outgoing . + } + GROUP BY ?source + ORDER BY ASC(?outEdges) +} +OFFSET {median_position} +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM004_4_out_edges_max_template.rq b/queries/metrics/RM004_4_out_edges_max_template.rq new file mode 100644 index 0000000..02bbb4f --- /dev/null +++ b/queries/metrics/RM004_4_out_edges_max_template.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximum out-edge for datasets only. + +SELECT COUNT(?outgoing) as ?outEdges +WHERE { +?source a {resource_type} . +?source ?p ?outgoing . +} +GROUP BY ?source +ORDER BY DESC(?outEdges) +LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM005_1_in_edges_total_template.rq b/queries/metrics/RM005_1_in_edges_total_template.rq new file mode 100644 index 0000000..9d3662a --- /dev/null +++ b/queries/metrics/RM005_1_in_edges_total_template.rq @@ -0,0 +1,7 @@ +# Returns a single number representing the cleaned count of unique incoming nodes in the graph. + +SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) +WHERE { + ?incoming ?p ?target . + ?target a {resource_type} . +} \ No newline at end of file diff --git a/queries/metrics/RM005_2_in_edges_min_template.rq b/queries/metrics/RM005_2_in_edges_min_template.rq new file mode 100644 index 0000000..e44cb69 --- /dev/null +++ b/queries/metrics/RM005_2_in_edges_min_template.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the minimum out-edge for datasets only. + +SELECT ?target (COUNT(?incoming) as ?inEdges) +WHERE { +?target a {resource_type} . +?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY ASC(?inEdges) +LIMIT 1 diff --git a/queries/metrics/RM005_3_in_edges_median_template.rq b/queries/metrics/RM005_3_in_edges_median_template.rq new file mode 100644 index 0000000..f0d7ce9 --- /dev/null +++ b/queries/metrics/RM005_3_in_edges_median_template.rq @@ -0,0 +1,15 @@ +# This SPARQL query calculates the median in-edge for datasets only. +# The placeholder {median_position} must be replaced with the actual position of the median. + +SELECT ?inEdges as ?inMedian +WHERE { + SELECT ?target (COUNT(?incoming) as ?inEdges) + WHERE { + ?target a {resource_type} . + ?incoming ?p ?target . + } + GROUP BY ?target + ORDER BY ASC(?inEdges) +} +OFFSET {median_position} +LIMIT 1 diff --git a/queries/metrics/RM005_4_in_edges_max_template.rq b/queries/metrics/RM005_4_in_edges_max_template.rq new file mode 100644 index 0000000..831b3cb --- /dev/null +++ b/queries/metrics/RM005_4_in_edges_max_template.rq @@ -0,0 +1,10 @@ +# This SPARQL query calculates the maximum out-edge for datasets only. + +SELECT ?target (COUNT(?incoming) as ?inEdges) +WHERE { +?target a {resource_type} . +?incoming ?p ?target . +} +GROUP BY ?target +ORDER BY DESC(?inEdges) +LIMIT 1 diff --git a/queries/metrics/RM006_connectivity_template.rq b/queries/metrics/RM006_connectivity_template.rq new file mode 100644 index 0000000..110887c --- /dev/null +++ b/queries/metrics/RM006_connectivity_template.rq @@ -0,0 +1,16 @@ +# Analysis of total connected resources for datasets +# +# Explanation: +# Counts the number of resources that are connected to datasets +# This gives us a metric for dataset connectivity in the graph + +SELECT (COUNT(DISTINCT ?connected) as ?numConnectedResources) +WHERE { + ?dataset a {resource_type} ; + ?property ?connected . + + # Ensure connected resource is not a literal + FILTER(isIRI(?connected)) + # Exclude self-references to datasets + FILTER(?connected != ?dataset) +} -- GitLab From 22bf16c39a1bec7d83703a4080312dfafbb594ef Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 11:01:46 +0100 Subject: [PATCH 10/59] [metrics] Remove dataset-queries --- queries/metrics/RM001_assertions.rq | 8 ------ queries/metrics/RM001_connectivity.rq | 19 -------------- queries/metrics/RM001_in_edges_max.rq | 10 -------- queries/metrics/RM001_in_edges_median.rq | 15 ------------ queries/metrics/RM001_in_edges_min.rq | 10 -------- queries/metrics/RM001_in_edges_total.rq | 7 ------ queries/metrics/RM001_instances.rq | 13 ---------- queries/metrics/RM001_linkage.rq | 30 ----------------------- queries/metrics/RM001_out_edges_max.rq | 10 -------- queries/metrics/RM001_out_edges_median.rq | 15 ------------ queries/metrics/RM001_out_edges_min.rq | 10 -------- queries/metrics/RM001_out_edges_total.rq | 7 ------ 12 files changed, 154 deletions(-) delete mode 100644 queries/metrics/RM001_assertions.rq delete mode 100644 queries/metrics/RM001_connectivity.rq delete mode 100644 queries/metrics/RM001_in_edges_max.rq delete mode 100644 queries/metrics/RM001_in_edges_median.rq delete mode 100644 queries/metrics/RM001_in_edges_min.rq delete mode 100644 queries/metrics/RM001_in_edges_total.rq delete mode 100644 queries/metrics/RM001_instances.rq delete mode 100644 queries/metrics/RM001_linkage.rq delete mode 100644 queries/metrics/RM001_out_edges_max.rq delete mode 100644 queries/metrics/RM001_out_edges_median.rq delete mode 100644 queries/metrics/RM001_out_edges_min.rq delete mode 100644 queries/metrics/RM001_out_edges_total.rq diff --git a/queries/metrics/RM001_assertions.rq b/queries/metrics/RM001_assertions.rq deleted file mode 100644 index 051632a..0000000 --- a/queries/metrics/RM001_assertions.rq +++ /dev/null @@ -1,8 +0,0 @@ -# Total number of assertions (for resource type: Dataset) - -PREFIX dcat: <http://www.w3.org/ns/dcat#> - -SELECT (COUNT(*) AS ?numAssertions) -WHERE { - ?subject ?predicate dcat:Dataset . -} diff --git a/queries/metrics/RM001_connectivity.rq b/queries/metrics/RM001_connectivity.rq deleted file mode 100644 index 1998eba..0000000 --- a/queries/metrics/RM001_connectivity.rq +++ /dev/null @@ -1,19 +0,0 @@ -# Analysis of total connected resources for datasets -# -# Explanation: -# Counts the number of resources that are connected to datasets -# This gives us a metric for dataset connectivity in the graph - -PREFIX dcat: <http://www.w3.org/ns/dcat#> -PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> - -SELECT (COUNT(DISTINCT ?connected) as ?numConnectedResources) -WHERE { - ?dataset a dcat:Dataset ; - ?property ?connected . - - # Ensure connected resource is not a literal - FILTER(isIRI(?connected)) - # Exclude self-references to datasets - FILTER(?connected != ?dataset) -} diff --git a/queries/metrics/RM001_in_edges_max.rq b/queries/metrics/RM001_in_edges_max.rq deleted file mode 100644 index 1d9a825..0000000 --- a/queries/metrics/RM001_in_edges_max.rq +++ /dev/null @@ -1,10 +0,0 @@ -# This SPARQL query calculates the maximum out-edge for datasets only. - -SELECT ?target (COUNT(?incoming) as ?inEdges) -WHERE { -?target a <http://www.w3.org/ns/dcat#Dataset> . -?incoming ?p ?target . -} -GROUP BY ?target -ORDER BY DESC(?inEdges) -LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_median.rq b/queries/metrics/RM001_in_edges_median.rq deleted file mode 100644 index d2ea70f..0000000 --- a/queries/metrics/RM001_in_edges_median.rq +++ /dev/null @@ -1,15 +0,0 @@ -# This SPARQL query calculates the median in-edge for datasets only. -# The placeholder {median_position} must be replaced with the actual position of the median. - -SELECT ?inEdges as ?inMedian -WHERE { - SELECT ?target (COUNT(?incoming) as ?inEdges) - WHERE { - ?target a <http://www.w3.org/ns/dcat#Dataset> . - ?incoming ?p ?target . - } - GROUP BY ?target - ORDER BY ASC(?inEdges) -} -OFFSET {median_position} -LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_min.rq b/queries/metrics/RM001_in_edges_min.rq deleted file mode 100644 index f39e6cb..0000000 --- a/queries/metrics/RM001_in_edges_min.rq +++ /dev/null @@ -1,10 +0,0 @@ -# This SPARQL query calculates the minimum out-edge for datasets only. - -SELECT ?target (COUNT(?incoming) as ?inEdges) -WHERE { -?target a <http://www.w3.org/ns/dcat#Dataset> . -?incoming ?p ?target . -} -GROUP BY ?target -ORDER BY ASC(?inEdges) -LIMIT 1 diff --git a/queries/metrics/RM001_in_edges_total.rq b/queries/metrics/RM001_in_edges_total.rq deleted file mode 100644 index 27b3569..0000000 --- a/queries/metrics/RM001_in_edges_total.rq +++ /dev/null @@ -1,7 +0,0 @@ -# Returns a single number representing the cleaned count of unique incoming nodes in the graph. - -SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) -WHERE { - ?incoming ?p ?target . - ?target a <http://www.w3.org/ns/dcat#Dataset> . -} \ No newline at end of file diff --git a/queries/metrics/RM001_instances.rq b/queries/metrics/RM001_instances.rq deleted file mode 100644 index 73a7b0c..0000000 --- a/queries/metrics/RM001_instances.rq +++ /dev/null @@ -1,13 +0,0 @@ -# Count all instances of type Dataset -# -# Explanation: -# ?dataset a dcat:Dataset finds all resources that are of type Dataset -# COUNT(DISTINCT ?dataset) counts unique dataset instances -# We use DISTINCT to avoid counting duplicates if a dataset has multiple types - -PREFIX dcat: <http://www.w3.org/ns/dcat#> - -SELECT (COUNT(DISTINCT ?dataset) AS ?datasetCount) -WHERE { - ?dataset a dcat:Dataset . -} diff --git a/queries/metrics/RM001_linkage.rq b/queries/metrics/RM001_linkage.rq deleted file mode 100644 index f379777..0000000 --- a/queries/metrics/RM001_linkage.rq +++ /dev/null @@ -1,30 +0,0 @@ -# The average linkage degree for datasets only -# (i.e.: how many assertions per dataset does the graph contain) - -SELECT (AVG(?totalDegree) as ?avgLinkageDegree) -WHERE { - { - SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) - WHERE { - # Only consider entities that are datasets - ?entity a <http://www.w3.org/ns/dcat#Dataset> . - - { - SELECT ?entity (COUNT(*) as ?outDegree) - WHERE { - ?entity a <http://www.w3.org/ns/dcat#Dataset> . - ?entity ?p ?o . - } - GROUP BY ?entity - } - { - SELECT ?entity (COUNT(*) as ?inDegree) - WHERE { - ?entity a <http://www.w3.org/ns/dcat#Dataset> . - ?s ?p ?entity . # Hier zählen wir die Verbindungen, die auf das Dataset zeigen - } - GROUP BY ?entity - } - } - } -} diff --git a/queries/metrics/RM001_out_edges_max.rq b/queries/metrics/RM001_out_edges_max.rq deleted file mode 100644 index 4503611..0000000 --- a/queries/metrics/RM001_out_edges_max.rq +++ /dev/null @@ -1,10 +0,0 @@ -# This SPARQL query calculates the maximum out-edge for datasets only. - -SELECT COUNT(?outgoing) as ?outEdges -WHERE { -?source a <http://www.w3.org/ns/dcat#Dataset> . -?source ?p ?outgoing . -} -GROUP BY ?source -ORDER BY DESC(?outEdges) -LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_median.rq b/queries/metrics/RM001_out_edges_median.rq deleted file mode 100644 index dd705cb..0000000 --- a/queries/metrics/RM001_out_edges_median.rq +++ /dev/null @@ -1,15 +0,0 @@ -# This SPARQL query calculates the median out-edge for datasets only. -# The placeholder {median_position} must be replaced with the actual position of the median. - -SELECT ?outEdges as ?outMedian -WHERE { - SELECT ?source (COUNT(?outgoing) as ?outEdges) - WHERE { - ?source a <http://www.w3.org/ns/dcat#Dataset> . - ?source ?p ?outgoing . - } - GROUP BY ?source - ORDER BY ASC(?outEdges) -} -OFFSET {median_position} -LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_min.rq b/queries/metrics/RM001_out_edges_min.rq deleted file mode 100644 index 55bcfcf..0000000 --- a/queries/metrics/RM001_out_edges_min.rq +++ /dev/null @@ -1,10 +0,0 @@ -# This SPARQL query calculates the maximuim out-edge for datasets only. - -SELECT COUNT(?outgoing) as ?outEdges -WHERE { -?source a <http://www.w3.org/ns/dcat#Dataset> . -?source ?p ?outgoing . -} -GROUP BY ?source -ORDER BY ASC(?outEdges) -LIMIT 1 \ No newline at end of file diff --git a/queries/metrics/RM001_out_edges_total.rq b/queries/metrics/RM001_out_edges_total.rq deleted file mode 100644 index 8669f72..0000000 --- a/queries/metrics/RM001_out_edges_total.rq +++ /dev/null @@ -1,7 +0,0 @@ -# Returns a single number representing the cleaned count of unique outging nodes in the graph. - -SELECT COUNT(DISTINCT ?source) as ?uniqueEdges -WHERE { - ?source a <http://www.w3.org/ns/dcat#Dataset> . - ?source ?p ?outgoing . -} \ No newline at end of file -- GitLab From 2f3dec4a1713ad565f6db810d22f7112b16189db Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 13:41:55 +0100 Subject: [PATCH 11/59] [metrics][kg_analysis] Automatic metrics on resources --- scripts/kg_analysis/metrics_runner.py | 188 +++++++++++++++++++++++++- scripts/kg_analysis/query_runner.py | 1 - 2 files changed, 185 insertions(+), 4 deletions(-) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 4fd7144..6f59afe 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -6,11 +6,13 @@ import logging from time import time from abc import ABC, abstractmethod +from copy import deepcopy from datetime import datetime from pathlib import Path from typing import Optional +from . import rprint from .query_runner import QueryRunner log = logging.getLogger(__name__) @@ -55,10 +57,12 @@ class MetricsRunnerBase(ABC): except Exception as e: log.error(f"Failed to run metric: {e}") self.fail = True - self._execution_time += time() - start_time + self._execution_time = time() - start_time if result and result["results"]["bindings"]: + if not result["results"]["bindings"][0].values(): + return 0 return list(result["results"]["bindings"][0].values())[0]["value"] - return None + return "-" @abstractmethod def run(self) -> dict: @@ -85,6 +89,7 @@ class MetricsRunner_001(MetricsRunnerBase): """Runner for Metric 001: Instance Count""" _query_file = "GM001.rq" + _output_file = "GM001.txt" def run(self): result = self.query_metric(self.query_path) @@ -172,6 +177,182 @@ class MetricsRunner_Edges(MetricsRunnerBase): self.save(result_text) +class MetricsRunner_Resources(MetricsRunnerBase): + query_templates = { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": True, + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": False, + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": True, + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": True, + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": True, + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "execute": False, + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": True, + }, + }, + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": True, + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": True, + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "execute": False, + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": True, + }, + }, + }, + # "edges_out": { + # "name": "Edges - outgoing", + # "file": "RM004_out_edges_template.rq", + # "execute": True, # }, + # "edges_in": { + # "name": "Edges - incoming", + # "file": "RM005_in_edges_template.rq", + # "execute": True, # }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": True, + }, + } + + resource_types = { + "dataset": { + "file": "06_dataset.md", + "uri": "<http://www.w3.org/ns/dcat#Dataset>", + }, + "publication": { + "file": "07_publication.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + }, + "learning_resource": { + "file": "08_learning_resource.md", + "uri": "<http://schema.org/LearningResource>", + }, + "repository": { + "file": "09_repository.md", + "uri": "<http://nfdi4earth.de/ontology/Repository>", + }, + "article_lhb": { + "file": "10_article_lhb.md", + "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", + }, + "standards": { + "file": "11_standards.md", + "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", + }, + # "organization": { + # "file": "12_organization.md", + # "uri": "<http://xmlns.com/foaf/0.1/Organization>", + # }, + "software": { + "file": "13_software.md", + "uri": "<http://schema.org/SoftwareSourceCode>", + }, + "service": { + "file": "14_service.md", + "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", + }, + "data_service": { + "file": "15_data_service.md", + "uri": "<http://www.w3.org/ns/dcat#DataService>", + }, + "aggregator": { + "file": "16_aggregator.md", + "uri": "<http://nfdi4earth.de/ontology/Aggregator>", + }, + "person": { + "file": "17_person.md", + "uri": "<http://schema.org/Person>", + }, + "registry": { + "file": "18_registry.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + }, + } + + def _get_result(self, query, resource_type): + log.info(f"Metric: {query['name']} ({query['file']})") + if query["execute"] is False: + return + result = self.query_metric( + self.get_query_path(query["file"]), + replace_dict={ + "{resource_type}": resource_type, + }, + ) + query["result"] = result + query["execution_time"] = round(self._execution_time, 2) + + def run(self): + for resource_type, data in self.resource_types.items(): + log.info(f"Analyzing: {data["uri"]}") + queries = deepcopy(self.query_templates) + for metric, query in queries.items(): + # if metric not in ["instances", "edges_out"]: + # continue + # print(query) + if "files" in query: + print(query["files"]) + for sub_query in query["files"].values(): + self._get_result(sub_query, data["uri"]) + else: + self._get_result(query, data["uri"]) + # result = self.query_metric( + # self.get_query_path(query["file"]), + # replace_dict={ + # "{resource_type}": data["uri"], + # }, + # ) + # query["result"] = result + # query["execution_time"] = round(self._execution_time, 2) + self.resource_types[resource_type]["queries"] = queries + rprint(self.resource_types) + + class MetricsRunner(ABC): def run(self): @@ -179,4 +360,5 @@ class MetricsRunner(ABC): # MetricsRunner_002().run() # MetricsRunner_003().run() # MetricsRunner_Edges("outgoing").run() - MetricsRunner_Edges("incoming").run() + # MetricsRunner_Edges("incoming").run() + MetricsRunner_Resources().run() diff --git a/scripts/kg_analysis/query_runner.py b/scripts/kg_analysis/query_runner.py index 9978138..c6567b7 100644 --- a/scripts/kg_analysis/query_runner.py +++ b/scripts/kg_analysis/query_runner.py @@ -35,7 +35,6 @@ class QueryRunner: if replace_dict: for r_key, r_value in replace_dict.items(): query = query.replace(r_key, str(r_value)) - # Set the query and execute self.sparql.setQuery(query) result = self.sparql.query().convert() -- GitLab From 7e7d198bd1e9956b372b63791517af4a6f4c401d Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 13:49:03 +0100 Subject: [PATCH 12/59] [metrics] Remove all occurances of term 'dataset' in resource templates --- docs/templates/basic_metrics.md | 2 +- queries/metrics/RM001_instances_template.rq | 12 ++++++------ queries/metrics/RM002_assertions_template.rq | 2 +- queries/metrics/RM003_linkage_template.rq | 8 ++++---- queries/metrics/RM004_2_out_edges_min_template.rq | 2 +- queries/metrics/RM004_3_out_edges_median_template.rq | 2 +- queries/metrics/RM004_4_out_edges_max_template.rq | 2 +- queries/metrics/RM005_1_in_edges_total_template.rq | 2 +- queries/metrics/RM005_2_in_edges_min_template.rq | 2 +- queries/metrics/RM005_3_in_edges_median_template.rq | 2 +- queries/metrics/RM005_4_in_edges_max_template.rq | 2 +- queries/metrics/RM006_connectivity_template.rq | 12 ++++++------ 12 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/templates/basic_metrics.md b/docs/templates/basic_metrics.md index 03bef10..8184739 100644 --- a/docs/templates/basic_metrics.md +++ b/docs/templates/basic_metrics.md @@ -4,7 +4,7 @@ Resource type: {{resource_type}} ## Basic Metrics -### Number of Datasets +### Number of Entitis see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) diff --git a/queries/metrics/RM001_instances_template.rq b/queries/metrics/RM001_instances_template.rq index f294233..89c4b84 100644 --- a/queries/metrics/RM001_instances_template.rq +++ b/queries/metrics/RM001_instances_template.rq @@ -1,11 +1,11 @@ -# Count all instances of type Dataset +# Count all instances of the given entity # # Explanation: -# ?dataset a dcat:Dataset finds all resources that are of type Dataset -# COUNT(DISTINCT ?dataset) counts unique dataset instances -# We use DISTINCT to avoid counting duplicates if a dataset has multiple types +# ?resource a {resource_type} finds all resources that are of type {resource_type} +# COUNT(DISTINCT ?resource) counts unique resource instances +# We use DISTINCT to avoid counting duplicates if a resource has multiple types -SELECT (COUNT(DISTINCT ?dataset) AS ?datasetCount) +SELECT (COUNT(DISTINCT ?resource) AS ?resourceCount) WHERE { - ?dataset a {resource_type} . + ?resource a {resource_type} . } diff --git a/queries/metrics/RM002_assertions_template.rq b/queries/metrics/RM002_assertions_template.rq index 404266a..33c105c 100644 --- a/queries/metrics/RM002_assertions_template.rq +++ b/queries/metrics/RM002_assertions_template.rq @@ -1,4 +1,4 @@ -# Total number of assertions (for resource type: Dataset) +# Total number of assertions (for resource type: {resource_type}) SELECT (COUNT(*) AS ?numAssertions) WHERE { diff --git a/queries/metrics/RM003_linkage_template.rq b/queries/metrics/RM003_linkage_template.rq index 6cf796d..08064de 100644 --- a/queries/metrics/RM003_linkage_template.rq +++ b/queries/metrics/RM003_linkage_template.rq @@ -1,12 +1,12 @@ -# The average linkage degree for datasets only -# (i.e.: how many assertions per dataset does the graph contain) +# The average linkage degree for specific resource type only +# (i.e.: how many assertions per {resource_type} does the graph contain) SELECT (AVG(?totalDegree) as ?avgLinkageDegree) WHERE { { SELECT ?entity ((?outDegree + ?inDegree) as ?totalDegree) WHERE { - # Only consider entities that are datasets + # Only consider entities that are of given resource ?entity a {resource_type} . { @@ -21,7 +21,7 @@ WHERE { SELECT ?entity (COUNT(*) as ?inDegree) WHERE { ?entity a {resource_type} . - ?s ?p ?entity . # Hier zählen wir die Verbindungen, die auf das Dataset zeigen + ?s ?p ?entity . # Here we count the connections pointing to the resource } GROUP BY ?entity } diff --git a/queries/metrics/RM004_2_out_edges_min_template.rq b/queries/metrics/RM004_2_out_edges_min_template.rq index 817ba5e..3fb1bff 100644 --- a/queries/metrics/RM004_2_out_edges_min_template.rq +++ b/queries/metrics/RM004_2_out_edges_min_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the maximuim out-edge for datasets only. +# This SPARQL query calculates the maximuim out-edge for {resource_type} only. SELECT COUNT(?outgoing) as ?outEdges WHERE { diff --git a/queries/metrics/RM004_3_out_edges_median_template.rq b/queries/metrics/RM004_3_out_edges_median_template.rq index 1bbc201..7f0c784 100644 --- a/queries/metrics/RM004_3_out_edges_median_template.rq +++ b/queries/metrics/RM004_3_out_edges_median_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the median out-edge for datasets only. +# This SPARQL query calculates the median out-edge for {resource_type} only. # The placeholder {median_position} must be replaced with the actual position of the median. SELECT ?outEdges as ?outMedian diff --git a/queries/metrics/RM004_4_out_edges_max_template.rq b/queries/metrics/RM004_4_out_edges_max_template.rq index 02bbb4f..3bf4739 100644 --- a/queries/metrics/RM004_4_out_edges_max_template.rq +++ b/queries/metrics/RM004_4_out_edges_max_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the maximum out-edge for datasets only. +# This SPARQL query calculates the maximum out-edge for {resource_type} only. SELECT COUNT(?outgoing) as ?outEdges WHERE { diff --git a/queries/metrics/RM005_1_in_edges_total_template.rq b/queries/metrics/RM005_1_in_edges_total_template.rq index 9d3662a..285ce77 100644 --- a/queries/metrics/RM005_1_in_edges_total_template.rq +++ b/queries/metrics/RM005_1_in_edges_total_template.rq @@ -1,4 +1,4 @@ -# Returns a single number representing the cleaned count of unique incoming nodes in the graph. +# Returns a single number representing the cleaned count of unique incoming nodes for {resource_type} only. SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) WHERE { diff --git a/queries/metrics/RM005_2_in_edges_min_template.rq b/queries/metrics/RM005_2_in_edges_min_template.rq index e44cb69..c03d47a 100644 --- a/queries/metrics/RM005_2_in_edges_min_template.rq +++ b/queries/metrics/RM005_2_in_edges_min_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the minimum out-edge for datasets only. +# This SPARQL query calculates the minimum out-edge for {resource_type} only. SELECT ?target (COUNT(?incoming) as ?inEdges) WHERE { diff --git a/queries/metrics/RM005_3_in_edges_median_template.rq b/queries/metrics/RM005_3_in_edges_median_template.rq index f0d7ce9..fa2b9c9 100644 --- a/queries/metrics/RM005_3_in_edges_median_template.rq +++ b/queries/metrics/RM005_3_in_edges_median_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the median in-edge for datasets only. +# This SPARQL query calculates the median in-edge for {resource_type} only. # The placeholder {median_position} must be replaced with the actual position of the median. SELECT ?inEdges as ?inMedian diff --git a/queries/metrics/RM005_4_in_edges_max_template.rq b/queries/metrics/RM005_4_in_edges_max_template.rq index 831b3cb..69dc4d6 100644 --- a/queries/metrics/RM005_4_in_edges_max_template.rq +++ b/queries/metrics/RM005_4_in_edges_max_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the maximum out-edge for datasets only. +# This SPARQL query calculates the maximum out-edge for {resource_type} only. SELECT ?target (COUNT(?incoming) as ?inEdges) WHERE { diff --git a/queries/metrics/RM006_connectivity_template.rq b/queries/metrics/RM006_connectivity_template.rq index 110887c..113022d 100644 --- a/queries/metrics/RM006_connectivity_template.rq +++ b/queries/metrics/RM006_connectivity_template.rq @@ -1,16 +1,16 @@ -# Analysis of total connected resources for datasets +# Analysis of total connected resources for {resource_type} only. # # Explanation: -# Counts the number of resources that are connected to datasets -# This gives us a metric for dataset connectivity in the graph +# Counts the number of resources that are connected to {resource_type} +# This gives us a metric for {resource_type} connectivity in the graph SELECT (COUNT(DISTINCT ?connected) as ?numConnectedResources) WHERE { - ?dataset a {resource_type} ; + ?resource a {resource_type} ; ?property ?connected . # Ensure connected resource is not a literal FILTER(isIRI(?connected)) - # Exclude self-references to datasets - FILTER(?connected != ?dataset) + # Exclude self-references to entities + FILTER(?connected != ?resource) } -- GitLab From 6eca29d8f80777d115cedcbf0bfd24aac0273f96 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 13:52:24 +0100 Subject: [PATCH 13/59] [metrics] Rename macro 'basic_metrics' to 'resource_metrics' --- .../basic_metrics.md => macros/resource_metrics.md} | 2 +- docs/metrics/resource metrics/06_dataset.md | 4 ++++ docs/metrics/resource metrics/07_publication.md | 7 +++++++ docs/metrics/resource metrics/08_learning_resource.md | 4 ++++ docs/metrics/resource metrics/09_repository.md | 4 ++++ docs/metrics/resource metrics/10_article_lhb.md | 4 ++++ docs/metrics/resource metrics/11_standards.md | 4 ++++ docs/metrics/resource metrics/12_organization.md | 4 ++++ docs/metrics/resource metrics/13_software.md | 4 ++++ docs/metrics/resource metrics/14_service.md | 4 ++++ docs/metrics/resource metrics/15_dataservice.md | 4 ++++ docs/metrics/resource metrics/16_aggregator.md | 4 ++++ docs/metrics/resource metrics/17_person.md | 4 ++++ docs/metrics/resource metrics/18_registry.md | 4 ++++ docs/metrics/resource specific metrics/06_dataset.md | 4 ---- docs/metrics/resource specific metrics/07_publication.md | 4 ---- .../resource specific metrics/08_learning_resource.md | 4 ---- docs/metrics/resource specific metrics/09_repository.md | 4 ---- docs/metrics/resource specific metrics/10_article_lhb.md | 4 ---- docs/metrics/resource specific metrics/11_standards.md | 4 ---- docs/metrics/resource specific metrics/12_organization.md | 4 ---- docs/metrics/resource specific metrics/13_software.md | 4 ---- docs/metrics/resource specific metrics/14_service.md | 4 ---- docs/metrics/resource specific metrics/15_dataservice.md | 4 ---- docs/metrics/resource specific metrics/16_aggregator.md | 4 ---- docs/metrics/resource specific metrics/17_person.md | 4 ---- docs/metrics/resource specific metrics/18_registry.md | 4 ---- 27 files changed, 56 insertions(+), 53 deletions(-) rename docs/{templates/basic_metrics.md => macros/resource_metrics.md} (97%) create mode 100644 docs/metrics/resource metrics/06_dataset.md create mode 100644 docs/metrics/resource metrics/07_publication.md create mode 100644 docs/metrics/resource metrics/08_learning_resource.md create mode 100644 docs/metrics/resource metrics/09_repository.md create mode 100644 docs/metrics/resource metrics/10_article_lhb.md create mode 100644 docs/metrics/resource metrics/11_standards.md create mode 100644 docs/metrics/resource metrics/12_organization.md create mode 100644 docs/metrics/resource metrics/13_software.md create mode 100644 docs/metrics/resource metrics/14_service.md create mode 100644 docs/metrics/resource metrics/15_dataservice.md create mode 100644 docs/metrics/resource metrics/16_aggregator.md create mode 100644 docs/metrics/resource metrics/17_person.md create mode 100644 docs/metrics/resource metrics/18_registry.md delete mode 100644 docs/metrics/resource specific metrics/06_dataset.md delete mode 100644 docs/metrics/resource specific metrics/07_publication.md delete mode 100644 docs/metrics/resource specific metrics/08_learning_resource.md delete mode 100644 docs/metrics/resource specific metrics/09_repository.md delete mode 100644 docs/metrics/resource specific metrics/10_article_lhb.md delete mode 100644 docs/metrics/resource specific metrics/11_standards.md delete mode 100644 docs/metrics/resource specific metrics/12_organization.md delete mode 100644 docs/metrics/resource specific metrics/13_software.md delete mode 100644 docs/metrics/resource specific metrics/14_service.md delete mode 100644 docs/metrics/resource specific metrics/15_dataservice.md delete mode 100644 docs/metrics/resource specific metrics/16_aggregator.md delete mode 100644 docs/metrics/resource specific metrics/17_person.md delete mode 100644 docs/metrics/resource specific metrics/18_registry.md diff --git a/docs/templates/basic_metrics.md b/docs/macros/resource_metrics.md similarity index 97% rename from docs/templates/basic_metrics.md rename to docs/macros/resource_metrics.md index 8184739..77097ed 100644 --- a/docs/templates/basic_metrics.md +++ b/docs/macros/resource_metrics.md @@ -1,4 +1,4 @@ -{% macro basic_metrics(resource_type) %} +{% macro resource_metrics(resource_type) %} Resource type: {{resource_type}} diff --git a/docs/metrics/resource metrics/06_dataset.md b/docs/metrics/resource metrics/06_dataset.md new file mode 100644 index 0000000..1420357 --- /dev/null +++ b/docs/metrics/resource metrics/06_dataset.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Dataset + +{{ resource_metrics("<http://www.w3.org/ns/dcat#Dataset>") }} diff --git a/docs/metrics/resource metrics/07_publication.md b/docs/metrics/resource metrics/07_publication.md new file mode 100644 index 0000000..db3cf51 --- /dev/null +++ b/docs/metrics/resource metrics/07_publication.md @@ -0,0 +1,7 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Publication + +{{ resource_metrics("<SELECT (COUNT(DISTINCT ?resource) AS ?resourceCount) +WHERE { + ?resource a <http://nfdi4earth.de/ontology/Registry> . +}>") }} diff --git a/docs/metrics/resource metrics/08_learning_resource.md b/docs/metrics/resource metrics/08_learning_resource.md new file mode 100644 index 0000000..cdc7eff --- /dev/null +++ b/docs/metrics/resource metrics/08_learning_resource.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Learning Resource + +{{ resource_metrics("<http://schema.org/LearningResource>") }} diff --git a/docs/metrics/resource metrics/09_repository.md b/docs/metrics/resource metrics/09_repository.md new file mode 100644 index 0000000..fb9a763 --- /dev/null +++ b/docs/metrics/resource metrics/09_repository.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Repository + +{{ resource_metrics("<http://nfdi4earth.de/ontology/Repository>") }} diff --git a/docs/metrics/resource metrics/10_article_lhb.md b/docs/metrics/resource metrics/10_article_lhb.md new file mode 100644 index 0000000..103c445 --- /dev/null +++ b/docs/metrics/resource metrics/10_article_lhb.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Living Handbook Article + +{{ resource_metrics("<http://nfdi4earth.de/ontology/LHBArticle>") }} diff --git a/docs/metrics/resource metrics/11_standards.md b/docs/metrics/resource metrics/11_standards.md new file mode 100644 index 0000000..4b2a8ef --- /dev/null +++ b/docs/metrics/resource metrics/11_standards.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Standards + +{{ resource_metrics("<http://nfdi4earth.de/ontology/MetadataStandard>") }} diff --git a/docs/metrics/resource metrics/12_organization.md b/docs/metrics/resource metrics/12_organization.md new file mode 100644 index 0000000..7c2b9d5 --- /dev/null +++ b/docs/metrics/resource metrics/12_organization.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Organization + +{{ resource_metrics("<http://xmlns.com/foaf/0.1/Organization>") }} diff --git a/docs/metrics/resource metrics/13_software.md b/docs/metrics/resource metrics/13_software.md new file mode 100644 index 0000000..10e03f1 --- /dev/null +++ b/docs/metrics/resource metrics/13_software.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Tools & Software + +{{ resource_metrics("<http://schema.org/SoftwareSourceCode>") }} diff --git a/docs/metrics/resource metrics/14_service.md b/docs/metrics/resource metrics/14_service.md new file mode 100644 index 0000000..73c8999 --- /dev/null +++ b/docs/metrics/resource metrics/14_service.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Service + +{{ resource_metrics("<http://www.w3.org/ns/sparql-service-description#Service>") }} diff --git a/docs/metrics/resource metrics/15_dataservice.md b/docs/metrics/resource metrics/15_dataservice.md new file mode 100644 index 0000000..06cd911 --- /dev/null +++ b/docs/metrics/resource metrics/15_dataservice.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Data Service + +{{ resource_metrics("<http://www.w3.org/ns/dcat#DataService>") }} diff --git a/docs/metrics/resource metrics/16_aggregator.md b/docs/metrics/resource metrics/16_aggregator.md new file mode 100644 index 0000000..c0e794b --- /dev/null +++ b/docs/metrics/resource metrics/16_aggregator.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Aggregator + +{{ resource_metrics("<http://nfdi4earth.de/ontology/Aggregator>") }} diff --git a/docs/metrics/resource metrics/17_person.md b/docs/metrics/resource metrics/17_person.md new file mode 100644 index 0000000..f220c51 --- /dev/null +++ b/docs/metrics/resource metrics/17_person.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Person + +{{ resource_metrics("<http://schema.org/Person>") }} diff --git a/docs/metrics/resource metrics/18_registry.md b/docs/metrics/resource metrics/18_registry.md new file mode 100644 index 0000000..8b81796 --- /dev/null +++ b/docs/metrics/resource metrics/18_registry.md @@ -0,0 +1,4 @@ +{% from "macros/resource_metrics.md" import resource_metrics with context %} +# Registry + +{{ resource_metrics("<http://nfdi4earth.de/ontology/Registry>") }} diff --git a/docs/metrics/resource specific metrics/06_dataset.md b/docs/metrics/resource specific metrics/06_dataset.md deleted file mode 100644 index fbbb7c0..0000000 --- a/docs/metrics/resource specific metrics/06_dataset.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Dataset - -{{ basic_metrics("<http://www.w3.org/ns/dcat#Dataset>") }} diff --git a/docs/metrics/resource specific metrics/07_publication.md b/docs/metrics/resource specific metrics/07_publication.md deleted file mode 100644 index 46a7d3d..0000000 --- a/docs/metrics/resource specific metrics/07_publication.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Publication - -{{ basic_metrics("<http://nfdi4earth.de/ontology/Publication>") }} diff --git a/docs/metrics/resource specific metrics/08_learning_resource.md b/docs/metrics/resource specific metrics/08_learning_resource.md deleted file mode 100644 index e9d05b5..0000000 --- a/docs/metrics/resource specific metrics/08_learning_resource.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Learning Resource - -{{ basic_metrics("<http://schema.org/LearningResource>") }} diff --git a/docs/metrics/resource specific metrics/09_repository.md b/docs/metrics/resource specific metrics/09_repository.md deleted file mode 100644 index a1eb845..0000000 --- a/docs/metrics/resource specific metrics/09_repository.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Repository - -{{ basic_metrics("<http://nfdi4earth.de/ontology/Repository>") }} diff --git a/docs/metrics/resource specific metrics/10_article_lhb.md b/docs/metrics/resource specific metrics/10_article_lhb.md deleted file mode 100644 index e37dca7..0000000 --- a/docs/metrics/resource specific metrics/10_article_lhb.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Living Handbook Article - -{{ basic_metrics("<http://nfdi4earth.de/ontology/LHBArticle>") }} diff --git a/docs/metrics/resource specific metrics/11_standards.md b/docs/metrics/resource specific metrics/11_standards.md deleted file mode 100644 index f09b4b5..0000000 --- a/docs/metrics/resource specific metrics/11_standards.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Standards - -{{ basic_metrics("<http://nfdi4earth.de/ontology/MetadataStandard>") }} diff --git a/docs/metrics/resource specific metrics/12_organization.md b/docs/metrics/resource specific metrics/12_organization.md deleted file mode 100644 index 61e6323..0000000 --- a/docs/metrics/resource specific metrics/12_organization.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Organization - -{{ basic_metrics("<http://xmlns.com/foaf/0.1/Organization>") }} diff --git a/docs/metrics/resource specific metrics/13_software.md b/docs/metrics/resource specific metrics/13_software.md deleted file mode 100644 index bb23391..0000000 --- a/docs/metrics/resource specific metrics/13_software.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Tools & Software - -{{ basic_metrics("<http://schema.org/SoftwareSourceCode>") }} diff --git a/docs/metrics/resource specific metrics/14_service.md b/docs/metrics/resource specific metrics/14_service.md deleted file mode 100644 index 48efc3e..0000000 --- a/docs/metrics/resource specific metrics/14_service.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Service - -{{ basic_metrics("<http://www.w3.org/ns/sparql-service-description#Service>") }} diff --git a/docs/metrics/resource specific metrics/15_dataservice.md b/docs/metrics/resource specific metrics/15_dataservice.md deleted file mode 100644 index 82c9a69..0000000 --- a/docs/metrics/resource specific metrics/15_dataservice.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Data Service - -{{ basic_metrics("<http://www.w3.org/ns/dcat#DataService>") }} diff --git a/docs/metrics/resource specific metrics/16_aggregator.md b/docs/metrics/resource specific metrics/16_aggregator.md deleted file mode 100644 index 114bcfa..0000000 --- a/docs/metrics/resource specific metrics/16_aggregator.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Aggregator - -{{ basic_metrics("<http://nfdi4earth.de/ontology/Aggregator>") }} diff --git a/docs/metrics/resource specific metrics/17_person.md b/docs/metrics/resource specific metrics/17_person.md deleted file mode 100644 index d806fab..0000000 --- a/docs/metrics/resource specific metrics/17_person.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Person - -{{ basic_metrics("<http://schema.org/Person>") }} diff --git a/docs/metrics/resource specific metrics/18_registry.md b/docs/metrics/resource specific metrics/18_registry.md deleted file mode 100644 index bfb1803..0000000 --- a/docs/metrics/resource specific metrics/18_registry.md +++ /dev/null @@ -1,4 +0,0 @@ -{% from "templates/basic_metrics.md" import basic_metrics with context %} -# Registry - -{{ basic_metrics("<http://nfdi4earth.de/ontology/Registry>") }} -- GitLab From 0db211cefbd32084ab2a79ec7ba509ff9f281642 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 14:05:52 +0100 Subject: [PATCH 14/59] [metrics][kg_analysis] Cleanup --- scripts/kg_analysis/metrics_runner.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 6f59afe..206192d 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -244,14 +244,6 @@ class MetricsRunner_Resources(MetricsRunnerBase): }, }, }, - # "edges_out": { - # "name": "Edges - outgoing", - # "file": "RM004_out_edges_template.rq", - # "execute": True, # }, - # "edges_in": { - # "name": "Edges - incoming", - # "file": "RM005_in_edges_template.rq", - # "execute": True, # }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", @@ -331,24 +323,12 @@ class MetricsRunner_Resources(MetricsRunnerBase): for resource_type, data in self.resource_types.items(): log.info(f"Analyzing: {data["uri"]}") queries = deepcopy(self.query_templates) - for metric, query in queries.items(): - # if metric not in ["instances", "edges_out"]: - # continue - # print(query) + for query in queries.values(): if "files" in query: - print(query["files"]) for sub_query in query["files"].values(): self._get_result(sub_query, data["uri"]) else: self._get_result(query, data["uri"]) - # result = self.query_metric( - # self.get_query_path(query["file"]), - # replace_dict={ - # "{resource_type}": data["uri"], - # }, - # ) - # query["result"] = result - # query["execution_time"] = round(self._execution_time, 2) self.resource_types[resource_type]["queries"] = queries rprint(self.resource_types) -- GitLab From 910d4af932ccf48a7a9e0cbe348ad16e914e8461 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 14:57:30 +0100 Subject: [PATCH 15/59] [metrics] fix: 07_publication.md --- docs/metrics/resource metrics/07_publication.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/metrics/resource metrics/07_publication.md b/docs/metrics/resource metrics/07_publication.md index db3cf51..9e6ee28 100644 --- a/docs/metrics/resource metrics/07_publication.md +++ b/docs/metrics/resource metrics/07_publication.md @@ -1,7 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Publication -{{ resource_metrics("<SELECT (COUNT(DISTINCT ?resource) AS ?resourceCount) -WHERE { - ?resource a <http://nfdi4earth.de/ontology/Registry> . -}>") }} +{{ resource_metrics("<http://nfdi4earth.de/ontology/Publication>") }} -- GitLab From 39b8276871608b6f84aadac348dc97bedb35c0ef Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 16:24:50 +0100 Subject: [PATCH 16/59] [metrics][kg_analysis] Export resource metrics to *.json and render them via macro --- docs/macros/main.py | 61 + docs/macros/resource_metrics.md | 31 +- docs/metrics/resource metrics/06_dataset.md | 2 +- .../resource metrics/07_publication.md | 2 +- .../resource metrics/08_learning_resource.md | 2 +- .../metrics/resource metrics/09_repository.md | 2 +- .../resource metrics/10_article_lhb.md | 2 +- docs/metrics/resource metrics/11_standards.md | 2 +- .../resource metrics/12_organization.md | 2 +- docs/metrics/resource metrics/13_software.md | 2 +- docs/metrics/resource metrics/14_service.md | 2 +- .../resource metrics/15_dataservice.md | 2 +- .../metrics/resource metrics/16_aggregator.md | 2 +- docs/metrics/resource metrics/17_person.md | 2 +- docs/metrics/resource metrics/18_registry.md | 2 +- reports/metrics/resources.json | 1275 +++++++++++++++++ scripts/kg_analysis/metrics_runner.py | 170 ++- 17 files changed, 1518 insertions(+), 45 deletions(-) create mode 100644 reports/metrics/resources.json diff --git a/docs/macros/main.py b/docs/macros/main.py index 9b2e345..88446fe 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -1,3 +1,7 @@ +import json +from pathlib import Path + + def define_env(env): @env.macro def include_if_exists(filename, start_line=None, single_line=None): @@ -27,3 +31,60 @@ def define_env(env): if content: return content.replace("{resource_type}", resource_type) return "" + + def render_resource(resource_data, output, resource_type=None): + """Helper function to render a single resource""" + for query_data in resource_data["queries"].values(): + if "result" in query_data: + if resource_type: + output.append( + f"| {resource_type} | {query_data['name']} | {query_data['result']} |" + ) + else: + output.append(f"| {query_data['name']} | {query_data['result']} |") + else: + if resource_type: + output.append(f"| {resource_type} | **{query_data['name']}** | |") + else: + output.append(f"| **{query_data['name']}** | |") + for sub_query_data in query_data["files"].values(): + if resource_type: + output.append( + f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['result']} |" + ) + else: + output.append( + f"| {sub_query_data['name']} | {sub_query_data['result']} |" + ) + + @env.macro + def resource_metrics_table(resource_type=None): + """Renders metrics as markdown table for one or all resource types""" + try: + metrics_file = Path("reports/metrics/resources.json") + with open(metrics_file, "r", encoding="utf-8") as f: + resources = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" + + output = [] + timestamp = resources.get("timestamp", "unknown") + output.append(f"*Last updated: {timestamp}*\n") + + if resource_type: + # Render single resource + if resource_type not in resources: + return f"*No metrics found for {resource_type}*" + + output.append("| Metric | Result |") + output.append("|--------|--------|") + render_resource(resources[resource_type], output) + else: + # Render all resources + output.append("| Resource Type | Metric | Result |") + output.append("|--------------|--------|--------|") + for res_type, resource_data in resources.items(): + if res_type != "timestamp": + render_resource(resource_data, output, res_type) + + return "\n".join(output) diff --git a/docs/macros/resource_metrics.md b/docs/macros/resource_metrics.md index 77097ed..4aa3b47 100644 --- a/docs/macros/resource_metrics.md +++ b/docs/macros/resource_metrics.md @@ -1,6 +1,9 @@ -{% macro resource_metrics(resource_type) %} +{% macro resource_metrics(resource_type, resource_type_uri) %} -Resource type: {{resource_type}} +Resource type: {{resource_type_uri}} + +## Results +{{ resource_metrics_table(resource_type) }} ## Basic Metrics @@ -9,13 +12,13 @@ Resource type: {{resource_type}} see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) ```sparql -{{ include_template("queries/metrics/RM001_instances_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM001_instances_template.rq", resource_type_uri) }} ``` ### Connectivity to other resources ```sparql -{{ include_template("queries/metrics/RM006_connectivity_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM006_connectivity_template.rq", resource_type_uri) }} ``` ### Number of Assertions @@ -23,7 +26,7 @@ see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) ```sparql -{{ include_template("queries/metrics/RM002_assertions_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM002_assertions_template.rq", resource_type_uri) }} ``` ### Average linkage @@ -31,7 +34,7 @@ see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/ see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) ```sparql -{{ include_template("queries/metrics/RM003_linkage_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM003_linkage_template.rq", resource_type_uri) }} ``` ### Outgoing Edges Statistics @@ -41,25 +44,25 @@ see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoin #### Total outgoing edges ```sparql -{{ include_template("queries/metrics/RM004_1_out_edges_total_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM004_1_out_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum outgoing edges ```sparql -{{ include_template("queries/metrics/RM004_2_out_edges_min_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM004_2_out_edges_min_template.rq", resource_type_uri) }} ``` #### Median outgoing edges ```sparql -{{ include_template("queries/metrics/RM004_3_out_edges_median_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM004_3_out_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum outgoing edges ```sparql -{{ include_template("queries/metrics/RM004_4_out_edges_max_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM004_4_out_edges_max_template.rq", resource_type_uri) }} ``` ### Incoming Edges Statistics @@ -69,25 +72,25 @@ see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incomin #### Total incoming edges ```sparql -{{ include_template("queries/metrics/RM005_1_in_edges_total_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM005_1_in_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum incoming edges ```sparql -{{ include_template("queries/metrics/RM005_2_in_edges_min_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM005_2_in_edges_min_template.rq", resource_type_uri) }} ``` #### Median incoming edges ```sparql -{{ include_template("queries/metrics/RM005_3_in_edges_median_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM005_3_in_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum incoming edges ```sparql -{{ include_template("queries/metrics/RM005_4_in_edges_max_template.rq", resource_type) }} +{{ include_template("queries/metrics/RM005_4_in_edges_max_template.rq", resource_type_uri) }} ``` {% endmacro %} diff --git a/docs/metrics/resource metrics/06_dataset.md b/docs/metrics/resource metrics/06_dataset.md index 1420357..5d52a5f 100644 --- a/docs/metrics/resource metrics/06_dataset.md +++ b/docs/metrics/resource metrics/06_dataset.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Dataset -{{ resource_metrics("<http://www.w3.org/ns/dcat#Dataset>") }} +{{ resource_metrics("dataset", "<http://www.w3.org/ns/dcat#Dataset>") }} diff --git a/docs/metrics/resource metrics/07_publication.md b/docs/metrics/resource metrics/07_publication.md index 9e6ee28..d873284 100644 --- a/docs/metrics/resource metrics/07_publication.md +++ b/docs/metrics/resource metrics/07_publication.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Publication -{{ resource_metrics("<http://nfdi4earth.de/ontology/Publication>") }} +{{ resource_metrics("publication", "<http://nfdi4earth.de/ontology/Publication>") }} diff --git a/docs/metrics/resource metrics/08_learning_resource.md b/docs/metrics/resource metrics/08_learning_resource.md index cdc7eff..a4246ce 100644 --- a/docs/metrics/resource metrics/08_learning_resource.md +++ b/docs/metrics/resource metrics/08_learning_resource.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Learning Resource -{{ resource_metrics("<http://schema.org/LearningResource>") }} +{{ resource_metrics("learning_resource", "<http://schema.org/LearningResource>") }} diff --git a/docs/metrics/resource metrics/09_repository.md b/docs/metrics/resource metrics/09_repository.md index fb9a763..6593bba 100644 --- a/docs/metrics/resource metrics/09_repository.md +++ b/docs/metrics/resource metrics/09_repository.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Repository -{{ resource_metrics("<http://nfdi4earth.de/ontology/Repository>") }} +{{ resource_metrics("repository", "<http://nfdi4earth.de/ontology/Repository>") }} diff --git a/docs/metrics/resource metrics/10_article_lhb.md b/docs/metrics/resource metrics/10_article_lhb.md index 103c445..8ebe52a 100644 --- a/docs/metrics/resource metrics/10_article_lhb.md +++ b/docs/metrics/resource metrics/10_article_lhb.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Living Handbook Article -{{ resource_metrics("<http://nfdi4earth.de/ontology/LHBArticle>") }} +{{ resource_metrics("article_lhb", "<http://nfdi4earth.de/ontology/LHBArticle>") }} diff --git a/docs/metrics/resource metrics/11_standards.md b/docs/metrics/resource metrics/11_standards.md index 4b2a8ef..c895977 100644 --- a/docs/metrics/resource metrics/11_standards.md +++ b/docs/metrics/resource metrics/11_standards.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Standards -{{ resource_metrics("<http://nfdi4earth.de/ontology/MetadataStandard>") }} +{{ resource_metrics("standards", "<http://nfdi4earth.de/ontology/MetadataStandard>") }} diff --git a/docs/metrics/resource metrics/12_organization.md b/docs/metrics/resource metrics/12_organization.md index 7c2b9d5..42c1d8e 100644 --- a/docs/metrics/resource metrics/12_organization.md +++ b/docs/metrics/resource metrics/12_organization.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Organization -{{ resource_metrics("<http://xmlns.com/foaf/0.1/Organization>") }} +{{ resource_metrics("organization", "<http://xmlns.com/foaf/0.1/Organization>") }} diff --git a/docs/metrics/resource metrics/13_software.md b/docs/metrics/resource metrics/13_software.md index 10e03f1..6210dea 100644 --- a/docs/metrics/resource metrics/13_software.md +++ b/docs/metrics/resource metrics/13_software.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Tools & Software -{{ resource_metrics("<http://schema.org/SoftwareSourceCode>") }} +{{ resource_metrics("software", "<http://schema.org/SoftwareSourceCode>") }} diff --git a/docs/metrics/resource metrics/14_service.md b/docs/metrics/resource metrics/14_service.md index 73c8999..b58e404 100644 --- a/docs/metrics/resource metrics/14_service.md +++ b/docs/metrics/resource metrics/14_service.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Service -{{ resource_metrics("<http://www.w3.org/ns/sparql-service-description#Service>") }} +{{ resource_metrics("service", "<http://www.w3.org/ns/sparql-service-description#Service>") }} diff --git a/docs/metrics/resource metrics/15_dataservice.md b/docs/metrics/resource metrics/15_dataservice.md index 06cd911..a102d74 100644 --- a/docs/metrics/resource metrics/15_dataservice.md +++ b/docs/metrics/resource metrics/15_dataservice.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Data Service -{{ resource_metrics("<http://www.w3.org/ns/dcat#DataService>") }} +{{ resource_metrics("dataservice", "<http://www.w3.org/ns/dcat#DataService>") }} diff --git a/docs/metrics/resource metrics/16_aggregator.md b/docs/metrics/resource metrics/16_aggregator.md index c0e794b..e40651d 100644 --- a/docs/metrics/resource metrics/16_aggregator.md +++ b/docs/metrics/resource metrics/16_aggregator.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Aggregator -{{ resource_metrics("<http://nfdi4earth.de/ontology/Aggregator>") }} +{{ resource_metrics("aggregator", "<http://nfdi4earth.de/ontology/Aggregator>") }} diff --git a/docs/metrics/resource metrics/17_person.md b/docs/metrics/resource metrics/17_person.md index f220c51..09b3e64 100644 --- a/docs/metrics/resource metrics/17_person.md +++ b/docs/metrics/resource metrics/17_person.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Person -{{ resource_metrics("<http://schema.org/Person>") }} +{{ resource_metrics("person", "<http://schema.org/Person>") }} diff --git a/docs/metrics/resource metrics/18_registry.md b/docs/metrics/resource metrics/18_registry.md index 8b81796..499b9a2 100644 --- a/docs/metrics/resource metrics/18_registry.md +++ b/docs/metrics/resource metrics/18_registry.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Registry -{{ resource_metrics("<http://nfdi4earth.de/ontology/Registry>") }} +{{ resource_metrics("registry", "<http://nfdi4earth.de/ontology/Registry>") }} diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json new file mode 100644 index 0000000..89dfba1 --- /dev/null +++ b/reports/metrics/resources.json @@ -0,0 +1,1275 @@ +{ + "dataset": { + "file": "06_dataset.md", + "uri": "<http://www.w3.org/ns/dcat#Dataset>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "724903", + "execution_time": 0.52 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "724904", + "execution_time": 0.02 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "68", + "execution_time": 0.07 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "724903", + "execution_time": 4.03 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "6", + "execution_time": 0.83 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "23", + "execution_time": 1.46 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "12519", + "execution_time": 0.78 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "4", + "execution_time": 0.12 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.05 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "1", + "execution_time": 0.05 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "107", + "execution_time": 1.1 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "427594", + "execution_time": 4.33 + } + } + }, + "publication": { + "file": "07_publication.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "6", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": 0, + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "7", + "execution_time": 0.02 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "9", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "11", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "0", + "execution_time": 0.02 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.01 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "-", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.01 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "15", + "execution_time": 0.01 + } + } + }, + "learning_resource": { + "file": "08_learning_resource.md", + "uri": "<http://schema.org/LearningResource>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "512", + "execution_time": 0.02 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "513", + "execution_time": 0.02 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "25", + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "512", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "10", + "execution_time": 0.02 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "15", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "27", + "execution_time": 0.02 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "3", + "execution_time": 0.02 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "3", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "3", + "execution_time": 0.02 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "530", + "execution_time": 0.02 + } + } + }, + "repository": { + "file": "09_repository.md", + "uri": "<http://nfdi4earth.de/ontology/Repository>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "159", + "execution_time": 0.02 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "162", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "90212", + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "159", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "7", + "execution_time": 0.02 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "44", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "92", + "execution_time": 0.02 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.06 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1417", + "execution_time": 0.03 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "6718", + "execution_time": 0.04 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "432941", + "execution_time": 0.03 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "825", + "execution_time": 0.02 + } + } + }, + "article_lhb": { + "file": "10_article_lhb.md", + "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "114", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "117", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "33.245098039215686", + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "114", + "execution_time": 1.04 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "11", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "30", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "62", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "102", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "2", + "execution_time": 1.06 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "24", + "execution_time": 0.01 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "592", + "execution_time": 0.01 + } + } + }, + "standards": { + "file": "11_standards.md", + "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "89", + "execution_time": 1.07 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "101", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "13.714285714285714", + "execution_time": 1.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "89", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.02 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "9", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "27", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "77", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "56", + "execution_time": 0.02 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "100", + "execution_time": 0.02 + } + } + }, + "software": { + "file": "13_software.md", + "uri": "<http://schema.org/SoftwareSourceCode>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "147", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "148", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "11.5", + "execution_time": 0.04 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "147", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "7", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "20", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "122", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "8", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "2", + "execution_time": 0.02 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "1103", + "execution_time": 0.02 + } + } + }, + "service": { + "file": "14_service.md", + "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "1", + "execution_time": 1.06 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": 0, + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "14", + "execution_time": 0.02 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "14", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "14", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "0", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.02 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "-", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "-", + "execution_time": 1.05 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "13", + "execution_time": 0.01 + } + } + }, + "data_service": { + "file": "15_data_service.md", + "uri": "<http://www.w3.org/ns/dcat#DataService>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "363632", + "execution_time": 0.16 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "363633", + "execution_time": 0.02 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": 0, + "execution_time": 0.04 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "363632", + "execution_time": 1.71 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "11", + "execution_time": 0.4 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "28", + "execution_time": 0.69 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "12519", + "execution_time": 0.39 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "0", + "execution_time": 0.07 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.03 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "-", + "execution_time": 0.03 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.03 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "58909", + "execution_time": 2.87 + } + } + }, + "aggregator": { + "file": "16_aggregator.md", + "uri": "<http://nfdi4earth.de/ontology/Aggregator>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "38", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "39", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "187", + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "38", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "8", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "36", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "57", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "151", + "execution_time": 1.04 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "151", + "execution_time": 1.01 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "151", + "execution_time": 0.01 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "239", + "execution_time": 0.01 + } + } + }, + "person": { + "file": "17_person.md", + "uri": "<http://schema.org/Person>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "2180", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "2181", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": "4.628269848554383", + "execution_time": 0.02 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "2180", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "2", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "4", + "execution_time": 1.05 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "7", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "2179", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "1", + "execution_time": 0.02 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "2", + "execution_time": 0.01 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "2", + "execution_time": 0.01 + } + } + }, + "registry": { + "file": "18_registry.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + "queries": { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "6", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": true, + "result": 0, + "execution_time": 0.01 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": true, + "result": "7", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": true, + "result": "9", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": true, + "result": "11", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": true, + "result": "0", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.01 + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": true, + "result": "-", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": true, + "result": "-", + "execution_time": 0.01 + } + } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": true, + "result": "15", + "execution_time": 0.01 + } + } + }, + "timestamp": "2025-03-14T16:21" +} \ No newline at end of file diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 206192d..82190c6 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -3,13 +3,13 @@ # ralf.klammer@tu-dresden.de import logging -from time import time +import json from abc import ABC, abstractmethod from copy import deepcopy from datetime import datetime from pathlib import Path - +from time import time from typing import Optional from . import rprint @@ -43,10 +43,22 @@ class MetricsRunnerBase(ABC): def query_path(self): return self.get_query_path(self.query_file) + def get_output_path(self, suffix: str = ".txt"): + filename = self._output_file or self.query_file.with_suffix(suffix) + return self.base_output_path.joinpath(filename) + @property def output_path(self): - filename = self._output_file or self.query_file.with_suffix(".txt") - return self.base_output_path.joinpath(filename) + log.warning("Deprecated: Use output_path_txt or output_path_json") + return self.get_output_path() + + @property + def output_path_txt(self): + return self.get_output_path(suffix=".txt") + + @property + def output_path_json(self): + return self.get_output_path(suffix=".json") def query_metric(self, query_path: Path, **kwargs) -> Optional[int]: """Run a metric query and the result""" @@ -69,20 +81,38 @@ class MetricsRunnerBase(ABC): """Run this specific metric.""" pass - def save(self, result) -> None: + def save_report(self, result) -> None: """Run a metric query and optionally save the results.""" if self.fail: log.error(f"Failed to run metric: {self.__class__.__name__}") - with open(self.output_path, "w") as f: + with open(self.output_path_txt, "w") as f: f.write(f"{datetime.now().isoformat(timespec="minutes")}\n") f.write(str(result)) if self._execution_time: f.write( f"\n- Execution time: {self._execution_time:.2f} seconds" ) - log.info(f"Results saved to {self.output_path}") + log.info(f"Results saved to {self.output_path_txt}") + + def save(self, result) -> None: + log.warning("Deprecated: Use save_report") + return self.save_report(result) + + def save_to_json(self, dictionary, filename: Optional[str] = None): + """ + Saves a dictionary to a JSON file + + Args: + dictionary (dict): The dictionary to save + filename (str): Name of the output file + (default: self.output_path_json) + """ + with open( + filename or self.output_path_json, "w", encoding="utf-8" + ) as f: + json.dump(dictionary, f, indent=4, ensure_ascii=False) class MetricsRunner_001(MetricsRunnerBase): @@ -187,7 +217,7 @@ class MetricsRunner_Resources(MetricsRunnerBase): "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", - "execute": False, + "execute": True, }, "linkage": { "name": "Average Linkage", @@ -210,7 +240,10 @@ class MetricsRunner_Resources(MetricsRunnerBase): "median": { "name": "Median of outgoing edges", "file": "RM004_3_out_edges_median_template.rq", - "execute": False, + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": True, }, "max": { "name": "Maximum of outgoing edges", @@ -235,7 +268,10 @@ class MetricsRunner_Resources(MetricsRunnerBase): "median": { "name": "Median of incoming edges", "file": "RM005_3_in_edges_median_template.rq", - "execute": False, + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": True, }, "max": { "name": "Maximum of incoming edges", @@ -306,15 +342,18 @@ class MetricsRunner_Resources(MetricsRunnerBase): }, } - def _get_result(self, query, resource_type): + def _get_result(self, query, resource_type, replace_dict=None): log.info(f"Metric: {query['name']} ({query['file']})") if query["execute"] is False: return + _replace_dict = { + "{resource_type}": resource_type, + } + if replace_dict: + _replace_dict.update(replace_dict) result = self.query_metric( self.get_query_path(query["file"]), - replace_dict={ - "{resource_type}": resource_type, - }, + replace_dict=_replace_dict, ) query["result"] = result query["execution_time"] = round(self._execution_time, 2) @@ -325,12 +364,104 @@ class MetricsRunner_Resources(MetricsRunnerBase): queries = deepcopy(self.query_templates) for query in queries.values(): if "files" in query: - for sub_query in query["files"].values(): - self._get_result(sub_query, data["uri"]) + for metric, sub_query in query["files"].items(): + replace_dict = {} + if "replace_dict" in sub_query: + for ( + dict_element, + sub_sub_query, + ) in sub_query["replace_dict"].items(): + replace_dict[dict_element] = self.query_metric( + self.get_query_path(sub_sub_query), + replace_dict={ + "{resource_type}": data["uri"], + }, + ) + if ( + replace_dict[dict_element] + and metric == "median" + ): + replace_dict[dict_element] = ( + int(replace_dict[dict_element]) / 2 + ) + self._get_result( + sub_query, data["uri"], replace_dict=replace_dict + ) else: self._get_result(query, data["uri"]) self.resource_types[resource_type]["queries"] = queries - rprint(self.resource_types) + self.resource_types["timestamp"] = datetime.now().isoformat( + timespec="minutes" + ) + self.save_to_json( + self.resource_types, "reports/metrics/resources.json" + ) + + # def read_resources(filename="reports/metrics/resources.json"): + # """ + # Reads the resource_types from the JSON file + + # Args: + # filename (str): Path to the JSON file + # Returns: + # dict: Dictionary with the resource types + # """ + # filename = "reports/metrics/resources.json" + # try: + # with open(filename, "r", encoding="utf-8") as f: + # return json.load(f) + # except FileNotFoundError: + # print(f"Error: The file {filename} was not found.") + # return None + # except json.JSONDecodeError: + # print(f"Error: The file {filename} does not contain valid JSON.") + # return None + + # def export_report_to_table(self): + # resources = self.read_resources() + # table_header = "| Resource Type | Metric | Result |\n|--------------|--------|--------|\n" + + # for resource_type, resource_data in resources.items(): + # if resource_type == "timestamp": + # continue + + # table_content = "" + # for query_data in resource_data["queries"].values(): + # if "result" in query_data: + # table_content += f"| {resource_type} | {query_data['name']} | {query_data['result']} |\n" + # else: + # table_content += ( + # f"| {resource_type} | **{query_data['name']}** | |\n" + # ) + # for sub_query_data in query_data["files"].values(): + # table_content += f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['result']} |\n" + + # self._query_file = resource_data["file"] + # report = f"## Metrics for {resource_type}\n\n{table_header}{table_content}" + # self.save_report(report) + + # def export_reports(self): + # resources = self.read_resources() + # for resource_type, resource_data in resources.items(): + # if resource_type == "timestamp": + # continue + # # print(resource_type) + # rprint(resource_data) + # report = "" + # for query_data in resource_data["queries"].values(): + # if "result" in query_data: + # report += ( + # f"\n- {query_data['name']}: {query_data['result']}" + # ) + # else: + # report += f"\n- {query_data['name']}:" + # for sub_query_data in query_data["files"].values(): + # report += f"\n - {sub_query_data['name']}: {sub_query_data['result']}" + # self._query_file = resource_data["file"] + # self.save_report(report) + # # print(resource_data["file"]) + # # print(self.output_path) + # # self.save_report(report) class MetricsRunner(ABC): @@ -341,4 +472,7 @@ class MetricsRunner(ABC): # MetricsRunner_003().run() # MetricsRunner_Edges("outgoing").run() # MetricsRunner_Edges("incoming").run() - MetricsRunner_Resources().run() + mr = MetricsRunner_Resources() + mr.run() + # mr.export_report_to_table() + # rprint(mr.resource_types) -- GitLab From a8e16cb98abb122a811774f4a654f410a0c7e8bd Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 16:26:18 +0100 Subject: [PATCH 17/59] [metrics]fix: queries of min/max incoming edges --- queries/metrics/RM005_1_in_edges_total_template.rq | 2 +- queries/metrics/RM005_2_in_edges_min_template.rq | 4 ++-- queries/metrics/RM005_3_in_edges_median_template.rq | 2 +- queries/metrics/RM005_4_in_edges_max_template.rq | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/queries/metrics/RM005_1_in_edges_total_template.rq b/queries/metrics/RM005_1_in_edges_total_template.rq index 285ce77..3a04008 100644 --- a/queries/metrics/RM005_1_in_edges_total_template.rq +++ b/queries/metrics/RM005_1_in_edges_total_template.rq @@ -1,4 +1,4 @@ -# Returns a single number representing the cleaned count of unique incoming nodes for {resource_type} only. +# Returns a single number representing the cleaned count of unique incoming edges for {resource_type} only. SELECT (COUNT(DISTINCT ?target) as ?uniqueEdges) WHERE { diff --git a/queries/metrics/RM005_2_in_edges_min_template.rq b/queries/metrics/RM005_2_in_edges_min_template.rq index c03d47a..afef730 100644 --- a/queries/metrics/RM005_2_in_edges_min_template.rq +++ b/queries/metrics/RM005_2_in_edges_min_template.rq @@ -1,6 +1,6 @@ -# This SPARQL query calculates the minimum out-edge for {resource_type} only. +# This SPARQL query calculates the minimum incoming-edge for {resource_type} only. -SELECT ?target (COUNT(?incoming) as ?inEdges) +SELECT (COUNT(?incoming) as ?inEdges) WHERE { ?target a {resource_type} . ?incoming ?p ?target . diff --git a/queries/metrics/RM005_3_in_edges_median_template.rq b/queries/metrics/RM005_3_in_edges_median_template.rq index fa2b9c9..3d0a28e 100644 --- a/queries/metrics/RM005_3_in_edges_median_template.rq +++ b/queries/metrics/RM005_3_in_edges_median_template.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the median in-edge for {resource_type} only. +# This SPARQL query calculates the median incoming-edge for {resource_type} only. # The placeholder {median_position} must be replaced with the actual position of the median. SELECT ?inEdges as ?inMedian diff --git a/queries/metrics/RM005_4_in_edges_max_template.rq b/queries/metrics/RM005_4_in_edges_max_template.rq index 69dc4d6..0b10af8 100644 --- a/queries/metrics/RM005_4_in_edges_max_template.rq +++ b/queries/metrics/RM005_4_in_edges_max_template.rq @@ -1,6 +1,6 @@ -# This SPARQL query calculates the maximum out-edge for {resource_type} only. +# This SPARQL query calculates the maximum incoming-edge for {resource_type} only. -SELECT ?target (COUNT(?incoming) as ?inEdges) +SELECT (COUNT(?incoming) as ?inEdges) WHERE { ?target a {resource_type} . ?incoming ?p ?target . -- GitLab From 81fec78c7112847122d38cd7e3afa6e524f69bb0 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 14 Mar 2025 16:27:34 +0100 Subject: [PATCH 18/59] [metrics] MetricsRunner_Resources/run add comments and cleanup --- scripts/kg_analysis/metrics_runner.py | 167 ++++++++++---------------- 1 file changed, 65 insertions(+), 102 deletions(-) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 82190c6..65b0d35 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -358,110 +358,73 @@ class MetricsRunner_Resources(MetricsRunnerBase): query["result"] = result query["execution_time"] = round(self._execution_time, 2) - def run(self): - for resource_type, data in self.resource_types.items(): - log.info(f"Analyzing: {data["uri"]}") - queries = deepcopy(self.query_templates) - for query in queries.values(): - if "files" in query: - for metric, sub_query in query["files"].items(): - replace_dict = {} - if "replace_dict" in sub_query: - for ( - dict_element, - sub_sub_query, - ) in sub_query["replace_dict"].items(): - replace_dict[dict_element] = self.query_metric( - self.get_query_path(sub_sub_query), - replace_dict={ - "{resource_type}": data["uri"], - }, + +def run(self): + """ + Execute metrics analysis for all resource types and save results to JSON. + This method: + 1. Iterates through all resource types + 2. Executes queries for each metric + 3. Handles complex metrics with sub-queries + 4. Saves results to a JSON file + """ + # Iterate through each resource type (dataset, publication, etc.) + for resource_type, data in self.resource_types.items(): + log.info(f"Analyzing: {data['uri']}") + # Create a deep copy of query templates to avoid modifying the original + queries = deepcopy(self.query_templates) + + # Process each query for the current resource type + for query in queries.values(): + # Check if this is a composite query with sub-queries + if "files" in query: + # Process each sub-query (e.g., total, min, median, max) + for metric, sub_query in query["files"].items(): + # Initialize dictionary for replacement values + replace_dict = {} + + # Handle queries that need pre-calculated values + if "replace_dict" in sub_query: + # Process each replacement needed for this query + for dict_element, sub_sub_query in sub_query[ + "replace_dict" + ].items(): + # Execute the sub-query to get the replacement value + replace_dict[dict_element] = self.query_metric( + self.get_query_path(sub_sub_query), + replace_dict={ + "{resource_type}": data["uri"], + }, + ) + + # Special handling for median calculations + # If this is a median metric, divide the result by 2 + if ( + replace_dict[dict_element] + and metric == "median" + ): + replace_dict[dict_element] = ( + int(replace_dict[dict_element]) / 2 ) - if ( - replace_dict[dict_element] - and metric == "median" - ): - replace_dict[dict_element] = ( - int(replace_dict[dict_element]) / 2 - ) - self._get_result( - sub_query, data["uri"], replace_dict=replace_dict - ) - else: - self._get_result(query, data["uri"]) - self.resource_types[resource_type]["queries"] = queries - self.resource_types["timestamp"] = datetime.now().isoformat( - timespec="minutes" - ) - self.save_to_json( - self.resource_types, "reports/metrics/resources.json" - ) - # def read_resources(filename="reports/metrics/resources.json"): - # """ - # Reads the resource_types from the JSON file - - # Args: - # filename (str): Path to the JSON file - # Returns: - # dict: Dictionary with the resource types - # """ - # filename = "reports/metrics/resources.json" - # try: - # with open(filename, "r", encoding="utf-8") as f: - # return json.load(f) - # except FileNotFoundError: - # print(f"Error: The file {filename} was not found.") - # return None - # except json.JSONDecodeError: - # print(f"Error: The file {filename} does not contain valid JSON.") - # return None - - # def export_report_to_table(self): - # resources = self.read_resources() - # table_header = "| Resource Type | Metric | Result |\n|--------------|--------|--------|\n" - - # for resource_type, resource_data in resources.items(): - # if resource_type == "timestamp": - # continue - - # table_content = "" - # for query_data in resource_data["queries"].values(): - # if "result" in query_data: - # table_content += f"| {resource_type} | {query_data['name']} | {query_data['result']} |\n" - # else: - # table_content += ( - # f"| {resource_type} | **{query_data['name']}** | |\n" - # ) - # for sub_query_data in query_data["files"].values(): - # table_content += f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['result']} |\n" - - # self._query_file = resource_data["file"] - # report = f"## Metrics for {resource_type}\n\n{table_header}{table_content}" - # self.save_report(report) - - # def export_reports(self): - # resources = self.read_resources() - # for resource_type, resource_data in resources.items(): - # if resource_type == "timestamp": - # continue - # # print(resource_type) - # rprint(resource_data) - # report = "" - # for query_data in resource_data["queries"].values(): - # if "result" in query_data: - # report += ( - # f"\n- {query_data['name']}: {query_data['result']}" - # ) - # else: - # report += f"\n- {query_data['name']}:" - # for sub_query_data in query_data["files"].values(): - # report += f"\n - {sub_query_data['name']}: {sub_query_data['result']}" - # self._query_file = resource_data["file"] - # self.save_report(report) - # # print(resource_data["file"]) - # # print(self.output_path) - # # self.save_report(report) + # Execute the sub-query with all necessary replacements + self._get_result( + sub_query, data["uri"], replace_dict=replace_dict + ) + else: + # Execute simple queries directly + self._get_result(query, data["uri"]) + + # Store the query results for this resource type + self.resource_types[resource_type]["queries"] = queries + + # Add timestamp to track when the metrics were last updated + self.resource_types["timestamp"] = datetime.now().isoformat( + timespec="minutes" + ) + + # Save all results to JSON file + self.save_to_json(self.resource_types, "reports/metrics/resources.json") class MetricsRunner(ABC): -- GitLab From 0293eb0eb9725dd37abfd5a1904c698fc1fcefe6 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 13:20:31 +0100 Subject: [PATCH 19/59] [metrics] Uni-/Simplify metric runners (only 1 run-function) --- reports/metrics/resources.json | 2162 +++++++++++-------------- scripts/kg_analysis/metrics_runner.py | 351 ++-- 2 files changed, 1122 insertions(+), 1391 deletions(-) diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json index 89dfba1..ca20225 100644 --- a/reports/metrics/resources.json +++ b/reports/metrics/resources.json @@ -1,1275 +1,1011 @@ { "dataset": { - "file": "06_dataset.md", - "uri": "<http://www.w3.org/ns/dcat#Dataset>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "724903", - "execution_time": 0.52 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "724904", - "execution_time": 0.02 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "68", - "execution_time": 0.07 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "724903", - "execution_time": 4.03 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "6", - "execution_time": 0.83 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "23", - "execution_time": 1.46 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "12519", - "execution_time": 0.78 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "724903", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "724904", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "4", - "execution_time": 0.12 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.05 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "1", - "execution_time": 0.05 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "107", - "execution_time": 1.1 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "427594", - "execution_time": 4.33 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "06_dataset.md" }, "publication": { - "file": "07_publication.md", - "uri": "<http://nfdi4earth.de/ontology/Registry>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "6", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": 0, - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "7", - "execution_time": 0.02 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "9", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "11", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "6", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "0", - "execution_time": 0.02 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.01 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "-", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.01 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "15", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "07_publication.md" }, "learning_resource": { - "file": "08_learning_resource.md", - "uri": "<http://schema.org/LearningResource>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "512", - "execution_time": 0.02 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "513", - "execution_time": 0.02 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "25", - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "512", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "10", - "execution_time": 0.02 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "15", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "27", - "execution_time": 0.02 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "512", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "513", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "3", - "execution_time": 0.02 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "3", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "3", - "execution_time": 0.02 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "530", - "execution_time": 0.02 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "08_learning_resource.md" }, "repository": { - "file": "09_repository.md", - "uri": "<http://nfdi4earth.de/ontology/Repository>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "159", - "execution_time": 0.02 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "162", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "90212", - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "159", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "7", - "execution_time": 0.02 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "44", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "92", - "execution_time": 0.02 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "159", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "162", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.06 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1417", - "execution_time": 0.03 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "6718", - "execution_time": 0.04 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "432941", - "execution_time": 0.03 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "825", - "execution_time": 0.02 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "09_repository.md" }, "article_lhb": { - "file": "10_article_lhb.md", - "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "114", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "117", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "33.245098039215686", - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "114", - "execution_time": 1.04 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "11", - "execution_time": 0.01 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "30", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "62", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "114", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "117", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "102", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "2", - "execution_time": 1.06 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "24", - "execution_time": 0.01 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "592", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "10_article_lhb.md" }, "standards": { - "file": "11_standards.md", - "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "89", - "execution_time": 1.07 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "101", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "13.714285714285714", - "execution_time": 1.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "89", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.02 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "9", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "27", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "89", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "101", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "77", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.01 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "56", - "execution_time": 0.02 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "100", - "execution_time": 0.02 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "11_standards.md" }, "software": { - "file": "13_software.md", - "uri": "<http://schema.org/SoftwareSourceCode>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "147", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "148", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "11.5", - "execution_time": 0.04 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "147", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "7", - "execution_time": 0.01 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "20", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "122", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "147", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "148", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "8", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.01 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "2", - "execution_time": 0.02 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "1103", - "execution_time": 0.02 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "13_software.md" }, "service": { - "file": "14_service.md", - "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "1", - "execution_time": 1.06 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": 0, - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "14", - "execution_time": 0.02 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "14", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "14", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "0", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.02 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "-", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "-", - "execution_time": 1.05 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "13", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "14_service.md" }, "data_service": { - "file": "15_data_service.md", - "uri": "<http://www.w3.org/ns/dcat#DataService>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "363632", - "execution_time": 0.16 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "363633", - "execution_time": 0.02 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": 0, - "execution_time": 0.04 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "363632", - "execution_time": 1.71 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "11", - "execution_time": 0.4 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "28", - "execution_time": 0.69 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "12519", - "execution_time": 0.39 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "363632", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "363633", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "0", - "execution_time": 0.07 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.03 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "-", - "execution_time": 0.03 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.03 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "58909", - "execution_time": 2.87 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "15_data_service.md" }, "aggregator": { - "file": "16_aggregator.md", - "uri": "<http://nfdi4earth.de/ontology/Aggregator>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "38", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "39", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "187", - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "38", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "8", - "execution_time": 0.01 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "36", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "57", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "38", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "39", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "151", - "execution_time": 1.04 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "151", - "execution_time": 1.01 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "151", - "execution_time": 0.01 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "239", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "16_aggregator.md" }, "person": { - "file": "17_person.md", - "uri": "<http://schema.org/Person>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "2180", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "2181", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": "4.628269848554383", - "execution_time": 0.02 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "2180", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "2", - "execution_time": 0.01 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "4", - "execution_time": 1.05 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "7", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "2180", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "2181", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "2179", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "1", - "execution_time": 0.01 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "1", - "execution_time": 0.02 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "2", - "execution_time": 0.01 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "2", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "17_person.md" }, "registry": { - "file": "18_registry.md", - "uri": "<http://nfdi4earth.de/ontology/Registry>", - "queries": { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.01 - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": true, - "result": "6", - "execution_time": 0.01 - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": true, - "result": 0, - "execution_time": 0.01 - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": true, - "result": "5", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": true, - "result": "7", - "execution_time": 0.01 - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": true, - "result": "9", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": true, - "result": "11", - "execution_time": 0.01 - } + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": true, + "result": "5", + "execution_time": 0.01 + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": true, + "result": "6", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": false + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": false } - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": true, - "result": "0", - "execution_time": 0.01 - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.01 - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": true, - "result": "-", - "execution_time": 0.01 - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": true, - "result": "-", - "execution_time": 0.01 - } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": false + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": false + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": false + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": false } - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": true, - "result": "15", - "execution_time": 0.01 } - } + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": false + }, + "timestamp": "2025-03-17T10:10", + "file": "18_registry.md" }, - "timestamp": "2025-03-14T16:21" + "timestamp": "2025-03-17T10:10" } \ No newline at end of file diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 65b0d35..71f0ad4 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -5,7 +5,7 @@ import logging import json -from abc import ABC, abstractmethod +from abc import ABC from copy import deepcopy from datetime import datetime from pathlib import Path @@ -76,11 +76,6 @@ class MetricsRunnerBase(ABC): return list(result["results"]["bindings"][0].values())[0]["value"] return "-" - @abstractmethod - def run(self) -> dict: - """Run this specific metric.""" - pass - def save_report(self, result) -> None: """Run a metric query and optionally save the results.""" @@ -114,98 +109,172 @@ class MetricsRunnerBase(ABC): ) as f: json.dump(dictionary, f, indent=4, ensure_ascii=False) + def _get_result(self, query, resource_type, replace_dict=None): + if not query["execute"]: + return + log.info(f"Metric: {query['name']} ({query['file']})") + _replace_dict = { + "{resource_type}": resource_type, + } + if replace_dict: + _replace_dict.update(replace_dict) + result = self.query_metric( + self.get_query_path(query["file"]), + replace_dict=_replace_dict, + ) + query["result"] = result + query["execution_time"] = round(self._execution_time, 2) -class MetricsRunner_001(MetricsRunnerBase): - """Runner for Metric 001: Instance Count""" + def run(self, resource_type_uri=None): + """ + Execute metrics analysis and save results to JSON. - _query_file = "GM001.rq" - _output_file = "GM001.txt" + Args: + resource_type_uri (str, optional): URI of specific resource type. + If None, runs general metrics. + Returns: + dict: Results of the metric analysis + """ + # Create deep copy of query templates + queries = deepcopy(self.query_templates) - def run(self): - result = self.query_metric(self.query_path) - result_text = f"- Instance count: {result}" - self.save(result_text) + # Process each query + for query_key, query in queries.items(): + if query_key == "timestamp": + continue + if "files" in query: + # Handle composite queries (edges) + for metric, sub_query in query["files"].items(): + replace_dict = {} -class MetricsRunner_002(MetricsRunnerBase): - """Runner for Metric 002: Assertions Count""" + # Handle queries that need pre-calculated values + if "replace_dict" in sub_query and sub_query["execute"]: + for dict_element, dependency_query in sub_query[ + "replace_dict" + ].items(): + # Get total count for calculations (e.g. median) + total = self.query_metric( + self.get_query_path(dependency_query), + replace_dict=( + {"{resource_type}": resource_type_uri} + if resource_type_uri + else None + ), + ) + try: + total = int(total) + except ValueError: + log.error( + f"Failed to convert {total} to integer" + ) + continue - _query_file = "GM002_1.rq" - _output_file = "GM002.txt" + if total and metric == "median": + # Special handling for median calculations + replace_dict[dict_element] = int(total) / 2 + else: + replace_dict[dict_element] = total - def run(self): - result = self.query_metric(self.query_path) - result_text = f"- Assertions count: {result}" - self.save(result_text) + # Execute the sub-query + self._get_result( + sub_query, + resource_type_uri if resource_type_uri else None, + replace_dict=replace_dict, + ) + else: + # Execute simple queries + self._get_result( + query, resource_type_uri if resource_type_uri else None + ) + # Add timestamp + queries["timestamp"] = datetime.now().isoformat(timespec="minutes") -class MetricsRunner_003(MetricsRunnerBase): - """Runner for Metric 003: Average Linkage Degree""" + return queries - _query_file = "GM003_1.rq" - _output_file = "GM003.txt" + +class MetricsRunner_General(MetricsRunnerBase): + """Runner for General Metrics (GM001-GM005)""" def run(self): - result = self.query_metric(self.query_path) - result_text = f"- Assertions count: {result}" - self.save(result_text) + output_path = "reports/metrics/general.json" + queries = super().run() + self.save_to_json(queries, output_path) -class MetricsRunner_Edges(MetricsRunnerBase): - """Runner for Metric 004 & 005: Outgoing & Incoming Edges""" + return queries - _files = { - "outgoing": { - "outfile": "GM004.txt", - "total_query": "GM004_2.rq", - "median_query": "GM004_4.rq", - "min_query": "GM004_5.rq", - "max_query": "GM004_6.rq", + query_templates = { + "instances": { + "name": "Instance Count", + "file": "GM001.rq", + "execute": True, + }, + "assertions": { + "name": "Assertions Count", + "file": "GM002_1.rq", + "execute": False, }, - "incoming": { - "outfile": "GM005.txt", - "total_query": "GM005_2.rq", - "median_query": "GM005_4.rq", - "min_query": "GM005_5.rq", - "max_query": "GM005_6.rq", + "linkage": { + "name": "Average Linkage Degree", + "file": "GM003_2.rq", + "execute": False, + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "GM004_2.rq", + "execute": False, + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "GM004_5.rq", + "execute": False, + }, + "median": { + "name": "Median of outgoing edges", + "file": "GM004_4.rq", + "replace_dict": {"{median_position}": "GM004_2.rq"}, + "execute": False, + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "GM004_6.rq", + "execute": False, + }, + }, + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "GM005_2.rq", + "execute": False, + }, + "min": { + "name": "Minimum of incoming edges", + "file": "GM005_5.rq", + "execute": False, + }, + "median": { + "name": "Median of incoming edges", + "file": "GM005_4.rq", + "replace_dict": {"{median_position}": "GM005_2.rq"}, + "execute": False, + }, + "max": { + "name": "Maximum of incoming edges", + "file": "GM005_6.rq", + "execute": False, + }, + }, }, } - def __init__(self, _type: str, *args, **kwargs): - super().__init__(*args, **kwargs) - self.type = _type - self._output_file = self._files[_type]["outfile"] - - def run(self): - edges_total = self.query_metric( - self.get_query_path(self._files[self.type]["total_query"]) - ) - minimum = self.query_metric( - self.get_query_path(self._files[self.type]["min_query"]) - ) - maximum = self.query_metric( - self.get_query_path(self._files[self.type]["max_query"]) - ) - - if not edges_total: - self.fail = True - return - - edges_total = int(edges_total) - if edges_total % 2: - log.warning("Odd number of edges, median is not unique") - median_position = int(edges_total / 2) - median = self.query_metric( - self.get_query_path(self._files[self.type]["median_query"]), - replace_dict={"{median_position}": median_position}, - ) - - result_text = f"- Total count: {edges_total}" - result_text += f"\n- Minimum: {minimum}" - result_text += f"\n- Median: {median}" - result_text += f"\n- Maximum: {maximum}" - self.save(result_text) - class MetricsRunner_Resources(MetricsRunnerBase): query_templates = { @@ -222,7 +291,7 @@ class MetricsRunner_Resources(MetricsRunnerBase): "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": True, + "execute": False, }, "edges_out": { "name": "Edges - outgoing", @@ -230,12 +299,12 @@ class MetricsRunner_Resources(MetricsRunnerBase): "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": True, + "execute": False, }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": True, + "execute": False, }, "median": { "name": "Median of outgoing edges", @@ -243,12 +312,12 @@ class MetricsRunner_Resources(MetricsRunnerBase): "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": True, + "execute": False, }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": True, + "execute": False, }, }, }, @@ -258,12 +327,12 @@ class MetricsRunner_Resources(MetricsRunnerBase): "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": True, + "execute": False, }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": True, + "execute": False, }, "median": { "name": "Median of incoming edges", @@ -271,19 +340,19 @@ class MetricsRunner_Resources(MetricsRunnerBase): "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": True, + "execute": False, }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": True, + "execute": False, }, }, }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": True, + "execute": False, }, } @@ -342,100 +411,26 @@ class MetricsRunner_Resources(MetricsRunnerBase): }, } - def _get_result(self, query, resource_type, replace_dict=None): - log.info(f"Metric: {query['name']} ({query['file']})") - if query["execute"] is False: - return - _replace_dict = { - "{resource_type}": resource_type, - } - if replace_dict: - _replace_dict.update(replace_dict) - result = self.query_metric( - self.get_query_path(query["file"]), - replace_dict=_replace_dict, - ) - query["result"] = result - query["execution_time"] = round(self._execution_time, 2) - - -def run(self): - """ - Execute metrics analysis for all resource types and save results to JSON. - This method: - 1. Iterates through all resource types - 2. Executes queries for each metric - 3. Handles complex metrics with sub-queries - 4. Saves results to a JSON file - """ - # Iterate through each resource type (dataset, publication, etc.) - for resource_type, data in self.resource_types.items(): - log.info(f"Analyzing: {data['uri']}") - # Create a deep copy of query templates to avoid modifying the original - queries = deepcopy(self.query_templates) - - # Process each query for the current resource type - for query in queries.values(): - # Check if this is a composite query with sub-queries - if "files" in query: - # Process each sub-query (e.g., total, min, median, max) - for metric, sub_query in query["files"].items(): - # Initialize dictionary for replacement values - replace_dict = {} - - # Handle queries that need pre-calculated values - if "replace_dict" in sub_query: - # Process each replacement needed for this query - for dict_element, sub_sub_query in sub_query[ - "replace_dict" - ].items(): - # Execute the sub-query to get the replacement value - replace_dict[dict_element] = self.query_metric( - self.get_query_path(sub_sub_query), - replace_dict={ - "{resource_type}": data["uri"], - }, - ) - - # Special handling for median calculations - # If this is a median metric, divide the result by 2 - if ( - replace_dict[dict_element] - and metric == "median" - ): - replace_dict[dict_element] = ( - int(replace_dict[dict_element]) / 2 - ) - - # Execute the sub-query with all necessary replacements - self._get_result( - sub_query, data["uri"], replace_dict=replace_dict - ) - else: - # Execute simple queries directly - self._get_result(query, data["uri"]) - - # Store the query results for this resource type - self.resource_types[resource_type]["queries"] = queries - - # Add timestamp to track when the metrics were last updated - self.resource_types["timestamp"] = datetime.now().isoformat( - timespec="minutes" - ) + def run(self): + """Run metrics for all resource types""" + results = {} + for resource_type, data in self.resource_types.items(): + results[resource_type] = super().run(data["uri"]) + results[resource_type]["file"] = data["file"] - # Save all results to JSON file - self.save_to_json(self.resource_types, "reports/metrics/resources.json") + results["timestamp"] = datetime.now().isoformat(timespec="minutes") + self.save_to_json(results, "reports/metrics/resources.json") + return results class MetricsRunner(ABC): def run(self): - # MetricsRunner_001().run() - # MetricsRunner_002().run() - # MetricsRunner_003().run() - # MetricsRunner_Edges("outgoing").run() - # MetricsRunner_Edges("incoming").run() + """Run all metrics analysis""" + # Run general metrics + mg = MetricsRunner_General() + mg.run() + + # Run resource-specific metrics mr = MetricsRunner_Resources() mr.run() - # mr.export_report_to_table() - # rprint(mr.resource_types) -- GitLab From 2849f92501d2940bd00faa9f6580340e0f07adc3 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 14:34:42 +0100 Subject: [PATCH 20/59] [metrics] Move configuration to `interfaces/*` --- reports/metrics/resources.json | 512 ++++++++++++++------ scripts/kg_analysis/interfaces/__init__.py | 3 + scripts/kg_analysis/interfaces/general.py | 76 +++ scripts/kg_analysis/interfaces/resources.py | 140 ++++++ scripts/kg_analysis/metrics_runner.py | 312 +++--------- 5 files changed, 666 insertions(+), 377 deletions(-) create mode 100644 scripts/kg_analysis/interfaces/__init__.py create mode 100644 scripts/kg_analysis/interfaces/general.py create mode 100644 scripts/kg_analysis/interfaces/resources.py diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json index ca20225..f3a8d17 100644 --- a/reports/metrics/resources.json +++ b/reports/metrics/resources.json @@ -17,7 +17,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "68", + "execution_time": 0.07 }, "edges_out": { "name": "Edges - outgoing", @@ -25,12 +27,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "724903", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "6", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -38,12 +44,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "23", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "12519", + "execution_time": 0.01 } } }, @@ -53,12 +63,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "4", + "execution_time": 0.12 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.04 }, "median": { "name": "Median of incoming edges", @@ -66,21 +80,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "1", + "execution_time": 1.11 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "107", + "execution_time": 0.05 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "427594", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "06_dataset.md" }, "publication": { @@ -96,12 +116,14 @@ "file": "RM002_assertions_template.rq", "execute": true, "result": "6", - "execution_time": 0.01 + "execution_time": 1.05 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": 0, + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -109,12 +131,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "5", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "7", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -122,12 +148,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "9", + "execution_time": 1.03 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "11", + "execution_time": 0.02 } } }, @@ -137,12 +167,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "0", + "execution_time": 0.02 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -150,21 +184,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "15", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "07_publication.md" }, "learning_resource": { @@ -185,7 +225,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "25", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -193,12 +235,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "512", + "execution_time": 1.02 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "10", + "execution_time": 0.02 }, "median": { "name": "Median of outgoing edges", @@ -206,12 +252,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "15", + "execution_time": 0.02 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "27", + "execution_time": 0.01 } } }, @@ -221,12 +271,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "3", + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -234,21 +288,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "3", + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "3", + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "530", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "08_learning_resource.md" }, "repository": { @@ -269,7 +329,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "90212", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -277,12 +339,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "159", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "7", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -290,12 +356,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "44", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "92", + "execution_time": 0.01 } } }, @@ -305,12 +375,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "5", + "execution_time": 0.06 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1417", + "execution_time": 0.03 }, "median": { "name": "Median of incoming edges", @@ -318,21 +392,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "6718", + "execution_time": 0.02 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "432941", + "execution_time": 1.09 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "825", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "09_repository.md" }, "article_lhb": { @@ -353,7 +433,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "33.245098039215686", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -361,12 +443,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "114", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "11", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -374,12 +460,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "30", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "62", + "execution_time": 0.01 } } }, @@ -389,12 +479,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "102", + "execution_time": 0.02 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "median": { "name": "Median of incoming edges", @@ -402,21 +496,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "2", + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "24", + "execution_time": 1.08 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "592", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "10_article_lhb.md" }, "standards": { @@ -437,7 +537,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "13.714285714285714", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -445,12 +547,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "89", + "execution_time": 1.03 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "5", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -458,12 +564,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "9", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "27", + "execution_time": 0.01 } } }, @@ -473,12 +583,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "77", + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "median": { "name": "Median of incoming edges", @@ -486,21 +600,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "56", + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "100", + "execution_time": 1.02 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "11_standards.md" }, "software": { @@ -521,7 +641,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "11.5", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -529,12 +651,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "147", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "7", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -542,12 +668,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "20", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "122", + "execution_time": 0.01 } } }, @@ -557,12 +687,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "8", + "execution_time": 0.02 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -570,21 +704,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "2", + "execution_time": 0.02 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "1103", + "execution_time": 0.03 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "13_software.md" }, "service": { @@ -593,19 +733,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.23 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 1.04 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": 0, + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -613,12 +755,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "14", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -626,12 +772,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "14", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "14", + "execution_time": 0.01 } } }, @@ -641,12 +791,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "0", + "execution_time": 0.02 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -654,21 +808,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "13", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "14_service.md" }, "data_service": { @@ -689,7 +849,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": 0, + "execution_time": 0.05 }, "edges_out": { "name": "Edges - outgoing", @@ -697,12 +859,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "363632", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "11", + "execution_time": 0.02 }, "median": { "name": "Median of outgoing edges", @@ -710,12 +876,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "28", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "12519", + "execution_time": 0.01 } } }, @@ -725,12 +895,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "0", + "execution_time": 0.07 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.03 }, "median": { "name": "Median of incoming edges", @@ -738,21 +912,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": null, + "execution_time": 0.03 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.03 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "58909", + "execution_time": 1.75 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "15_data_service.md" }, "aggregator": { @@ -773,7 +953,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "187", + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -781,12 +963,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "38", + "execution_time": 0.02 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "8", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -794,12 +980,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "36", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "57", + "execution_time": 0.01 } } }, @@ -809,12 +999,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.03 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "151", + "execution_time": 0.02 }, "median": { "name": "Median of incoming edges", @@ -822,21 +1016,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "151", + "execution_time": 0.02 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "151", + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "239", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "16_aggregator.md" }, "person": { @@ -857,7 +1057,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": "4.628269848554383", + "execution_time": 1.05 }, "edges_out": { "name": "Edges - outgoing", @@ -865,12 +1067,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "2180", + "execution_time": 0.02 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "2", + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -878,12 +1084,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "4", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "7", + "execution_time": 0.01 } } }, @@ -893,12 +1103,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "2179", + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -906,21 +1120,27 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "1", + "execution_time": 0.02 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": "2", + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "2", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "17_person.md" }, "registry": { @@ -941,7 +1161,9 @@ "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", - "execute": false + "execute": true, + "result": 0, + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -949,12 +1171,16 @@ "total": { "name": "Total number of outgoing edges", "file": "RM004_1_out_edges_total_template.rq", - "execute": false + "execute": true, + "result": "5", + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", - "execute": false + "execute": true, + "result": "7", + "execution_time": 0.02 }, "median": { "name": "Median of outgoing edges", @@ -962,12 +1188,16 @@ "replace_dict": { "{median_position}": "RM004_1_out_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": "9", + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", - "execute": false + "execute": true, + "result": "11", + "execution_time": 0.01 } } }, @@ -977,12 +1207,16 @@ "total": { "name": "Total number of incoming edges", "file": "RM005_1_in_edges_total_template.rq", - "execute": false + "execute": true, + "result": "0", + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -990,22 +1224,28 @@ "replace_dict": { "{median_position}": "RM005_1_in_edges_total_template.rq" }, - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", - "execute": false + "execute": true, + "result": null, + "execution_time": 0.01 } } }, "connectivity": { "name": "Connectivity", "file": "RM006_connectivity_template.rq", - "execute": false + "execute": true, + "result": "15", + "execution_time": 0.01 }, - "timestamp": "2025-03-17T10:10", + "timestamp": "2025-03-17T13:46", "file": "18_registry.md" }, - "timestamp": "2025-03-17T10:10" + "timestamp": "2025-03-17T13:46" } \ No newline at end of file diff --git a/scripts/kg_analysis/interfaces/__init__.py b/scripts/kg_analysis/interfaces/__init__.py new file mode 100644 index 0000000..79f526c --- /dev/null +++ b/scripts/kg_analysis/interfaces/__init__.py @@ -0,0 +1,3 @@ +from .general import query_templates as general_query_templates +from .resources import query_templates as resource_query_templates +from .resources import resource_types diff --git a/scripts/kg_analysis/interfaces/general.py b/scripts/kg_analysis/interfaces/general.py new file mode 100644 index 0000000..ded2f56 --- /dev/null +++ b/scripts/kg_analysis/interfaces/general.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +log = logging.getLogger(__name__) + +query_templates = { + "instances": { + "name": "Instance Count", + "file": "GM001.rq", + "execute": True, + }, + "assertions": { + "name": "Assertions Count", + "file": "GM002_1.rq", + "execute": True, + }, + "linkage": { + "name": "Average Linkage Degree", + "file": "GM003_2.rq", + "execute": False, + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "GM004_2.rq", + "execute": True, + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "GM004_5.rq", + "execute": True, + }, + "median": { + "name": "Median of outgoing edges", + "file": "GM004_4.rq", + "replace_dict": {"{median_position}": "GM004_2.rq"}, + "execute": True, + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "GM004_6.rq", + "execute": True, + }, + }, + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "GM005_2.rq", + "execute": False, + }, + "min": { + "name": "Minimum of incoming edges", + "file": "GM005_5.rq", + "execute": False, + }, + "median": { + "name": "Median of incoming edges", + "file": "GM005_4.rq", + "replace_dict": {"{median_position}": "GM005_2.rq"}, + "execute": False, + }, + "max": { + "name": "Maximum of incoming edges", + "file": "GM005_6.rq", + "execute": False, + }, + }, + }, +} diff --git a/scripts/kg_analysis/interfaces/resources.py b/scripts/kg_analysis/interfaces/resources.py new file mode 100644 index 0000000..203685c --- /dev/null +++ b/scripts/kg_analysis/interfaces/resources.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +log = logging.getLogger(__name__) + +query_templates = { + "instances": { + "name": "Number of Resources", + "file": "RM001_instances_template.rq", + "execute": True, + }, + "assertions": { + "name": "Number of Assertions", + "file": "RM002_assertions_template.rq", + "execute": True, + }, + "linkage": { + "name": "Average Linkage", + "file": "RM003_linkage_template.rq", + "execute": True, + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "RM004_1_out_edges_total_template.rq", + "execute": True, + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "RM004_2_out_edges_min_template.rq", + "execute": True, + }, + "median": { + "name": "Median of outgoing edges", + "file": "RM004_3_out_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM004_1_out_edges_total_template.rq" + }, + "execute": True, + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "RM004_4_out_edges_max_template.rq", + "execute": True, + }, + }, + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "RM005_1_in_edges_total_template.rq", + "execute": True, + }, + "min": { + "name": "Minimum of incoming edges", + "file": "RM005_2_in_edges_min_template.rq", + "execute": True, + }, + "median": { + "name": "Median of incoming edges", + "file": "RM005_3_in_edges_median_template.rq", + "replace_dict": { + "{median_position}": "RM005_1_in_edges_total_template.rq" + }, + "execute": True, + }, + "max": { + "name": "Maximum of incoming edges", + "file": "RM005_4_in_edges_max_template.rq", + "execute": True, + }, + }, + }, + "connectivity": { + "name": "Connectivity", + "file": "RM006_connectivity_template.rq", + "execute": True, + }, +} + +resource_types = { + "dataset": { + "file": "06_dataset.md", + "uri": "<http://www.w3.org/ns/dcat#Dataset>", + }, + "publication": { + "file": "07_publication.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + }, + "learning_resource": { + "file": "08_learning_resource.md", + "uri": "<http://schema.org/LearningResource>", + }, + "repository": { + "file": "09_repository.md", + "uri": "<http://nfdi4earth.de/ontology/Repository>", + }, + "article_lhb": { + "file": "10_article_lhb.md", + "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", + }, + "standards": { + "file": "11_standards.md", + "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", + }, + # "organization": { + # "file": "12_organization.md", + # "uri": "<http://xmlns.com/foaf/0.1/Organization>", + # }, + "software": { + "file": "13_software.md", + "uri": "<http://schema.org/SoftwareSourceCode>", + }, + "service": { + "file": "14_service.md", + "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", + }, + "data_service": { + "file": "15_data_service.md", + "uri": "<http://www.w3.org/ns/dcat#DataService>", + }, + "aggregator": { + "file": "16_aggregator.md", + "uri": "<http://nfdi4earth.de/ontology/Aggregator>", + }, + "person": { + "file": "17_person.md", + "uri": "<http://schema.org/Person>", + }, + "registry": { + "file": "18_registry.md", + "uri": "<http://nfdi4earth.de/ontology/Registry>", + }, +} diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 71f0ad4..48b4141 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -12,14 +12,23 @@ from pathlib import Path from time import time from typing import Optional -from . import rprint from .query_runner import QueryRunner +from .interfaces import ( + general_query_templates, + resource_query_templates, + resource_types, +) log = logging.getLogger(__name__) class MetricsRunnerBase(ABC): - """Base class for metric runners.""" + """ + Base class for metric runners that provides common functionality for: + - Query execution and result handling + - File path management + - JSON and TXT report generation + """ _query_file: Optional[str] = None _output_file: Optional[str] = None @@ -74,7 +83,7 @@ class MetricsRunnerBase(ABC): if not result["results"]["bindings"][0].values(): return 0 return list(result["results"]["bindings"][0].values())[0]["value"] - return "-" + return None def save_report(self, result) -> None: """Run a metric query and optionally save the results.""" @@ -91,18 +100,13 @@ class MetricsRunnerBase(ABC): ) log.info(f"Results saved to {self.output_path_txt}") - def save(self, result) -> None: - log.warning("Deprecated: Use save_report") - return self.save_report(result) - def save_to_json(self, dictionary, filename: Optional[str] = None): """ - Saves a dictionary to a JSON file + Saves results dictionary to a JSON file with proper formatting Args: - dictionary (dict): The dictionary to save - filename (str): Name of the output file - (default: self.output_path_json) + dictionary (dict): Results to save + filename (str, optional): Target filename, uses default if None """ with open( filename or self.output_path_json, "w", encoding="utf-8" @@ -110,14 +114,24 @@ class MetricsRunnerBase(ABC): json.dump(dictionary, f, indent=4, ensure_ascii=False) def _get_result(self, query, resource_type, replace_dict=None): + """ + Executes a single query and stores its result + + Args: + query (dict): Query configuration with name, file and execute flag + resource_type (str): URI of the resource type to query + replace_dict (dict, optional): Additional replacements for query templates + """ if not query["execute"]: return log.info(f"Metric: {query['name']} ({query['file']})") - _replace_dict = { - "{resource_type}": resource_type, - } + + # Prepare replacement dictionary + _replace_dict = {"{resource_type}": resource_type} if replace_dict: _replace_dict.update(replace_dict) + + # Execute query and store results result = self.query_metric( self.get_query_path(query["file"]), replace_dict=_replace_dict, @@ -127,33 +141,34 @@ class MetricsRunnerBase(ABC): def run(self, resource_type_uri=None): """ - Execute metrics analysis and save results to JSON. + Main execution method for metric analysis. + Handles both general and resource-specific metrics. Args: resource_type_uri (str, optional): URI of specific resource type. - If None, runs general metrics. + If None, runs general metrics. Returns: - dict: Results of the metric analysis + dict: Collected metrics results """ - # Create deep copy of query templates + # Create deep copy to avoid modifying templates queries = deepcopy(self.query_templates) - # Process each query + # Process each query definition for query_key, query in queries.items(): if query_key == "timestamp": continue if "files" in query: - # Handle composite queries (edges) + # Handle composite metrics (like edge statistics) for metric, sub_query in query["files"].items(): replace_dict = {} - # Handle queries that need pre-calculated values + # Pre-calculate values needed for this metric if "replace_dict" in sub_query and sub_query["execute"]: for dict_element, dependency_query in sub_query[ "replace_dict" ].items(): - # Get total count for calculations (e.g. median) + # Execute dependency query (e.g. total count for median) total = self.query_metric( self.get_query_path(dependency_query), replace_dict=( @@ -162,6 +177,8 @@ class MetricsRunnerBase(ABC): else None ), ) + + # Convert and validate result try: total = int(total) except ValueError: @@ -170,251 +187,63 @@ class MetricsRunnerBase(ABC): ) continue + # Special handling for median calculations if total and metric == "median": - # Special handling for median calculations replace_dict[dict_element] = int(total) / 2 else: replace_dict[dict_element] = total - # Execute the sub-query + # Execute the actual metric query self._get_result( sub_query, resource_type_uri if resource_type_uri else None, replace_dict=replace_dict, ) else: - # Execute simple queries + # Handle simple metrics (single query) self._get_result( query, resource_type_uri if resource_type_uri else None ) - # Add timestamp + # Add execution timestamp queries["timestamp"] = datetime.now().isoformat(timespec="minutes") return queries class MetricsRunner_General(MetricsRunnerBase): - """Runner for General Metrics (GM001-GM005)""" + """Handles general metrics that apply to the entire knowledge graph""" + + query_templates = general_query_templates def run(self): + """ + Executes and saves general metrics + Returns: + dict: General metrics results + """ output_path = "reports/metrics/general.json" queries = super().run() - self.save_to_json(queries, output_path) - return queries - query_templates = { - "instances": { - "name": "Instance Count", - "file": "GM001.rq", - "execute": True, - }, - "assertions": { - "name": "Assertions Count", - "file": "GM002_1.rq", - "execute": False, - }, - "linkage": { - "name": "Average Linkage Degree", - "file": "GM003_2.rq", - "execute": False, - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "GM004_2.rq", - "execute": False, - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "GM004_5.rq", - "execute": False, - }, - "median": { - "name": "Median of outgoing edges", - "file": "GM004_4.rq", - "replace_dict": {"{median_position}": "GM004_2.rq"}, - "execute": False, - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "GM004_6.rq", - "execute": False, - }, - }, - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "GM005_2.rq", - "execute": False, - }, - "min": { - "name": "Minimum of incoming edges", - "file": "GM005_5.rq", - "execute": False, - }, - "median": { - "name": "Median of incoming edges", - "file": "GM005_4.rq", - "replace_dict": {"{median_position}": "GM005_2.rq"}, - "execute": False, - }, - "max": { - "name": "Maximum of incoming edges", - "file": "GM005_6.rq", - "execute": False, - }, - }, - }, - } - class MetricsRunner_Resources(MetricsRunnerBase): - query_templates = { - "instances": { - "name": "Number of Resources", - "file": "RM001_instances_template.rq", - "execute": True, - }, - "assertions": { - "name": "Number of Assertions", - "file": "RM002_assertions_template.rq", - "execute": True, - }, - "linkage": { - "name": "Average Linkage", - "file": "RM003_linkage_template.rq", - "execute": False, - }, - "edges_out": { - "name": "Edges - outgoing", - "files": { - "total": { - "name": "Total number of outgoing edges", - "file": "RM004_1_out_edges_total_template.rq", - "execute": False, - }, - "min": { - "name": "Minimum of outgoing edges", - "file": "RM004_2_out_edges_min_template.rq", - "execute": False, - }, - "median": { - "name": "Median of outgoing edges", - "file": "RM004_3_out_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM004_1_out_edges_total_template.rq" - }, - "execute": False, - }, - "max": { - "name": "Maximum of outgoing edges", - "file": "RM004_4_out_edges_max_template.rq", - "execute": False, - }, - }, - }, - "edges_in": { - "name": "Edges - incoming", - "files": { - "total": { - "name": "Total number of incoming edges", - "file": "RM005_1_in_edges_total_template.rq", - "execute": False, - }, - "min": { - "name": "Minimum of incoming edges", - "file": "RM005_2_in_edges_min_template.rq", - "execute": False, - }, - "median": { - "name": "Median of incoming edges", - "file": "RM005_3_in_edges_median_template.rq", - "replace_dict": { - "{median_position}": "RM005_1_in_edges_total_template.rq" - }, - "execute": False, - }, - "max": { - "name": "Maximum of incoming edges", - "file": "RM005_4_in_edges_max_template.rq", - "execute": False, - }, - }, - }, - "connectivity": { - "name": "Connectivity", - "file": "RM006_connectivity_template.rq", - "execute": False, - }, - } - - resource_types = { - "dataset": { - "file": "06_dataset.md", - "uri": "<http://www.w3.org/ns/dcat#Dataset>", - }, - "publication": { - "file": "07_publication.md", - "uri": "<http://nfdi4earth.de/ontology/Registry>", - }, - "learning_resource": { - "file": "08_learning_resource.md", - "uri": "<http://schema.org/LearningResource>", - }, - "repository": { - "file": "09_repository.md", - "uri": "<http://nfdi4earth.de/ontology/Repository>", - }, - "article_lhb": { - "file": "10_article_lhb.md", - "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", - }, - "standards": { - "file": "11_standards.md", - "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", - }, - # "organization": { - # "file": "12_organization.md", - # "uri": "<http://xmlns.com/foaf/0.1/Organization>", - # }, - "software": { - "file": "13_software.md", - "uri": "<http://schema.org/SoftwareSourceCode>", - }, - "service": { - "file": "14_service.md", - "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", - }, - "data_service": { - "file": "15_data_service.md", - "uri": "<http://www.w3.org/ns/dcat#DataService>", - }, - "aggregator": { - "file": "16_aggregator.md", - "uri": "<http://nfdi4earth.de/ontology/Aggregator>", - }, - "person": { - "file": "17_person.md", - "uri": "<http://schema.org/Person>", - }, - "registry": { - "file": "18_registry.md", - "uri": "<http://nfdi4earth.de/ontology/Registry>", - }, - } + """Handles resource-specific metrics for different resource types""" + + resource_types = resource_types + query_templates = resource_query_templates def run(self): - """Run metrics for all resource types""" + """ + Executes metrics for each resource type and saves combined results + Returns: + dict: Resource-specific metrics results + """ results = {} + # Execute metrics for each resource type for resource_type, data in self.resource_types.items(): + log.info(f"Resource type: ###{resource_type.upper()}###") results[resource_type] = super().run(data["uri"]) results[resource_type]["file"] = data["file"] @@ -424,13 +253,14 @@ class MetricsRunner_Resources(MetricsRunnerBase): class MetricsRunner(ABC): + """Main entry point for executing all metrics""" def run(self): - """Run all metrics analysis""" - # Run general metrics - mg = MetricsRunner_General() - mg.run() - - # Run resource-specific metrics - mr = MetricsRunner_Resources() - mr.run() + """ + Executes both general and resource-specific metrics + """ + # Run general metrics for entire knowledge graph + MetricsRunner_General().run() + + # Run metrics for specific resource types + MetricsRunner_Resources().run() -- GitLab From 349e568a6f3b2d9c1dd64741adedc1bf06a79624 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 14:37:07 +0100 Subject: [PATCH 21/59] [metrics] Link to query-files in resource_metrics-macro --- docs/macros/main.py | 43 +++++---- docs/macros/resource_metrics.md | 25 ++++- reports/metrics/resources.json | 156 ++++++++++++++++---------------- 3 files changed, 125 insertions(+), 99 deletions(-) diff --git a/docs/macros/main.py b/docs/macros/main.py index 88446fe..8f6c843 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -34,28 +34,30 @@ def define_env(env): def render_resource(resource_data, output, resource_type=None): """Helper function to render a single resource""" - for query_data in resource_data["queries"].values(): - if "result" in query_data: + for query_data in resource_data.values(): + if "file" in query_data: if resource_type: output.append( - f"| {resource_type} | {query_data['name']} | {query_data['result']} |" + f"| {resource_type} | {query_data['name']} | {query_data['file']} | {query_data['result']} |" ) else: - output.append(f"| {query_data['name']} | {query_data['result']} |") - else: + output.append( + f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" + ) + elif "files" in query_data: if resource_type: - output.append(f"| {resource_type} | **{query_data['name']}** | |") + output.append(f"| {resource_type} | **{query_data['name']}** | | |") else: - output.append(f"| **{query_data['name']}** | |") - for sub_query_data in query_data["files"].values(): - if resource_type: - output.append( - f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['result']} |" - ) - else: - output.append( - f"| {sub_query_data['name']} | {sub_query_data['result']} |" - ) + output.append(f"| **{query_data['name']}** | | |") + for sub_query_data in query_data["files"].values(): + if resource_type: + output.append( + f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['file']} | {sub_query_data['result']} |" + ) + else: + output.append( + f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" + ) @env.macro def resource_metrics_table(resource_type=None): @@ -76,13 +78,14 @@ def define_env(env): if resource_type not in resources: return f"*No metrics found for {resource_type}*" - output.append("| Metric | Result |") - output.append("|--------|--------|") + output.append("| Metric | File | Result |") + output.append("|--------|------|--------|") + print(resources[resource_type]) render_resource(resources[resource_type], output) else: # Render all resources - output.append("| Resource Type | Metric | Result |") - output.append("|--------------|--------|--------|") + output.append("| Resource Type | Metric | File | Result |") + output.append("|--------------|--------|------|--------|") for res_type, resource_data in resources.items(): if res_type != "timestamp": render_resource(resource_data, output, res_type) diff --git a/docs/macros/resource_metrics.md b/docs/macros/resource_metrics.md index 4aa3b47..ce5130b 100644 --- a/docs/macros/resource_metrics.md +++ b/docs/macros/resource_metrics.md @@ -11,12 +11,15 @@ Resource type: {{resource_type_uri}} see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) +<i id="RM001_instances_template.rq">file: RM001_instances_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM001_instances_template.rq", resource_type_uri) }} ``` - ### Connectivity to other resources +<i id="RM006_connectivity_template.rq">file: RM006_connectivity_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM006_connectivity_template.rq", resource_type_uri) }} ``` @@ -25,6 +28,8 @@ see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) +<i id="RM002_assertions_template.rq">file: RM002_assertions_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM002_assertions_template.rq", resource_type_uri) }} ``` @@ -33,6 +38,8 @@ see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/ see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) +<i id="RM003_linkage_template.rq">file: RM003_linkage_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM003_linkage_template.rq", resource_type_uri) }} ``` @@ -43,24 +50,32 @@ see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoin #### Total outgoing edges +<i id="RM004_1_out_edges_total_template.rq">file: RM004_1_out_edges_total_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM004_1_out_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum outgoing edges +<i id="RM004_2_out_edges_min_template.rq">file: RM004_2_out_edges_min_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM004_2_out_edges_min_template.rq", resource_type_uri) }} ``` #### Median outgoing edges +<i id="RM004_3_out_edges_median_template.rq">file: RM004_3_out_edges_median_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM004_3_out_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum outgoing edges +<i id="RM004_4_out_edges_max_template.rq">file: RM004_4_out_edges_max_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM004_4_out_edges_max_template.rq", resource_type_uri) }} ``` @@ -71,24 +86,32 @@ see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incomin #### Total incoming edges +<i id="RM005_1_in_edges_total_template.rq">file: RM005_1_in_edges_total_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM005_1_in_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum incoming edges +<i id="RM005_2_in_edges_min_template.rq">file: RM005_2_in_edges_min_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM005_2_in_edges_min_template.rq", resource_type_uri) }} ``` #### Median incoming edges +<i id="RM005_3_in_edges_median_template.rq">file: RM005_3_in_edges_median_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM005_3_in_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum incoming edges +<i id="RM005_4_in_edges_max_template.rq">file: RM005_4_in_edges_max_template.rq</i> + ```sparql {{ include_template("queries/metrics/RM005_4_in_edges_max_template.rq", resource_type_uri) }} ``` diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json index f3a8d17..0953a73 100644 --- a/reports/metrics/resources.json +++ b/reports/metrics/resources.json @@ -5,21 +5,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "724903", - "execution_time": 0.01 + "execution_time": 1.31 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "724904", - "execution_time": 0.01 + "execution_time": 0.02 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "68", - "execution_time": 0.07 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -36,7 +36,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "6", - "execution_time": 0.01 + "execution_time": 1.04 }, "median": { "name": "Median of outgoing edges", @@ -65,14 +65,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "4", - "execution_time": 0.12 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.04 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -82,14 +82,14 @@ }, "execute": true, "result": "1", - "execution_time": 1.11 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "107", - "execution_time": 0.05 + "execution_time": 0.01 } } }, @@ -98,9 +98,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "427594", - "execution_time": 0.01 + "execution_time": 4.33 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "06_dataset.md" }, "publication": { @@ -109,21 +109,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.02 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "6", - "execution_time": 1.05 + "execution_time": 0.01 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -150,14 +150,14 @@ }, "execute": true, "result": "9", - "execution_time": 1.03 + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "11", - "execution_time": 0.02 + "execution_time": 0.01 } } }, @@ -169,7 +169,7 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", @@ -204,7 +204,7 @@ "result": "15", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "07_publication.md" }, "learning_resource": { @@ -213,7 +213,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "512", - "execution_time": 0.01 + "execution_time": 0.02 }, "assertions": { "name": "Number of Assertions", @@ -227,7 +227,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "25", - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -237,14 +237,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "512", - "execution_time": 1.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "10", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -254,7 +254,7 @@ }, "execute": true, "result": "15", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of outgoing edges", @@ -273,7 +273,7 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", @@ -308,7 +308,7 @@ "result": "530", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "08_learning_resource.md" }, "repository": { @@ -331,7 +331,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "90212", - "execution_time": 0.02 + "execution_time": 1.04 }, "edges_out": { "name": "Edges - outgoing", @@ -377,14 +377,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "5", - "execution_time": 0.06 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1417", - "execution_time": 0.03 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -401,7 +401,7 @@ "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "432941", - "execution_time": 1.09 + "execution_time": 0.01 } } }, @@ -412,7 +412,7 @@ "result": "825", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "09_repository.md" }, "article_lhb": { @@ -435,7 +435,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "33.245098039215686", - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -469,7 +469,7 @@ "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "62", - "execution_time": 0.01 + "execution_time": 1.05 } } }, @@ -481,14 +481,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "102", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -498,14 +498,14 @@ }, "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.02 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "24", - "execution_time": 1.08 + "execution_time": 0.01 } } }, @@ -516,7 +516,7 @@ "result": "592", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "10_article_lhb.md" }, "standards": { @@ -539,7 +539,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "13.714285714285714", - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -549,7 +549,7 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "89", - "execution_time": 1.03 + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", @@ -592,7 +592,7 @@ "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -602,7 +602,7 @@ }, "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", @@ -618,9 +618,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "100", - "execution_time": 1.02 + "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "11_standards.md" }, "software": { @@ -643,7 +643,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "11.5", - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -689,7 +689,7 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "8", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", @@ -713,7 +713,7 @@ "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "2", - "execution_time": 0.02 + "execution_time": 0.01 } } }, @@ -722,9 +722,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "1103", - "execution_time": 0.03 + "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "13_software.md" }, "service": { @@ -733,21 +733,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "1", - "execution_time": 0.23 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "1", - "execution_time": 1.04 + "execution_time": 0.01 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -757,14 +757,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "14", - "execution_time": 0.01 + "execution_time": 0.02 }, "median": { "name": "Median of outgoing edges", @@ -793,7 +793,7 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", @@ -828,7 +828,7 @@ "result": "13", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "14_service.md" }, "data_service": { @@ -837,7 +837,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "363632", - "execution_time": 0.01 + "execution_time": 0.13 }, "assertions": { "name": "Number of Assertions", @@ -851,7 +851,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.05 + "execution_time": 0.02 }, "edges_out": { "name": "Edges - outgoing", @@ -897,14 +897,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.07 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": null, - "execution_time": 0.03 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -914,14 +914,14 @@ }, "execute": true, "result": null, - "execution_time": 0.03 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": null, - "execution_time": 0.03 + "execution_time": 0.01 } } }, @@ -930,9 +930,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "58909", - "execution_time": 1.75 + "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "15_data_service.md" }, "aggregator": { @@ -955,7 +955,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "187", - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -965,7 +965,7 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "38", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", @@ -1001,14 +1001,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.03 + "execution_time": 0.01 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "151", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of incoming edges", @@ -1018,7 +1018,7 @@ }, "execute": true, "result": "151", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", @@ -1036,7 +1036,7 @@ "result": "239", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "16_aggregator.md" }, "person": { @@ -1045,7 +1045,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "2180", - "execution_time": 0.01 + "execution_time": 0.02 }, "assertions": { "name": "Number of Assertions", @@ -1059,7 +1059,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "4.628269848554383", - "execution_time": 1.05 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -1069,7 +1069,7 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "2180", - "execution_time": 0.02 + "execution_time": 0.01 }, "min": { "name": "Minimum of outgoing edges", @@ -1122,7 +1122,7 @@ }, "execute": true, "result": "1", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", @@ -1140,7 +1140,7 @@ "result": "2", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "17_person.md" }, "registry": { @@ -1149,7 +1149,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 1.04 }, "assertions": { "name": "Number of Assertions", @@ -1163,7 +1163,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -1180,7 +1180,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "7", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -1244,8 +1244,8 @@ "result": "15", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:46", + "timestamp": "2025-03-17T13:55", "file": "18_registry.md" }, - "timestamp": "2025-03-17T13:46" + "timestamp": "2025-03-17T13:55" } \ No newline at end of file -- GitLab From b1afd20c3dae83971b8def5485bcbf1710fec388 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:07:39 +0100 Subject: [PATCH 22/59] [metrics] 1 Table per general metric + total tables in overview --- docs/macros/main.py | 124 +++++++++++++++++- docs/metrics/general metrics/01_instances.md | 10 +- docs/metrics/general metrics/02_assertions.md | 4 +- .../general metrics/03_linkage_degree.md | 4 +- .../general metrics/04_outgoing_edges.md | 4 +- .../general metrics/05_incoming_edges.md | 10 +- docs/metrics/index.md | 39 +++++- reports/metrics/general.json | 96 ++++++++++++++ reports/metrics/resources.json | 74 +++++------ scripts/kg_analysis/interfaces/general.py | 10 +- 10 files changed, 308 insertions(+), 67 deletions(-) create mode 100644 reports/metrics/general.json diff --git a/docs/macros/main.py b/docs/macros/main.py index 8f6c843..ad7ea4c 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -78,16 +78,134 @@ def define_env(env): if resource_type not in resources: return f"*No metrics found for {resource_type}*" - output.append("| Metric | File | Result |") + output.append("| Metric | Query (file) | Result |") output.append("|--------|------|--------|") - print(resources[resource_type]) render_resource(resources[resource_type], output) else: # Render all resources - output.append("| Resource Type | Metric | File | Result |") + output.append("| Resource Type | Metric | Query (file) | Result |") output.append("|--------------|--------|------|--------|") for res_type, resource_data in resources.items(): if res_type != "timestamp": render_resource(resource_data, output, res_type) return "\n".join(output) + + @env.macro + def resources_metrics_table(): + """Rendert eine einfache Übersichtstabelle der Resource-Metriken""" + try: + metrics_file = Path("reports/metrics/resources.json") + with open(metrics_file, "r", encoding="utf-8") as f: + resources = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Fehler beim Laden der Metriken: {e}*" + + output = [] + + resource_names = [ + key for key, resource in resources.items() if isinstance(resource, dict) + ] + ordered_resource_names = sorted(resource_names) + + metric_names = [] + rows = [] + for resource_name in ordered_resource_names: + resource = resources[resource_name] + row = f"| {resource_name.upper()} |" + for metric in resource.values(): + if isinstance(metric, str): + continue + elif "files" in metric: + for sub_metric in metric["files"].values(): + if sub_metric["name"] not in metric_names: + metric_names.append(sub_metric["name"]) + row += f" {sub_metric.get('result', '-')} |" + elif metric["name"] not in metric_names: + row += f" {metric.get('result', '-')} |" + metric_names.append(metric["name"]) + rows.append(row) + + output.append(f"| Resource type | {' | '.join(metric_names)} |") + output.append(f"| --- |{' | '.join(['---' for m in metric_names])} |") + for row in rows: + output.append(row) + + output.append(f"\n*Last updated: {resources.get("timestamp", "-")}*") + return "\n".join(output) + + @env.macro + def render_metric_table(metric_key): + """ + Renders a specific metric as markdown table. + + Args: + metric_key (str): Key of the metric to render (e.g. 'instances', 'edges_out') + """ + try: + metrics_file = Path("reports/metrics/general.json") + with open(metrics_file, "r", encoding="utf-8") as f: + metrics = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" + + if metric_key not in metrics: + return f"*No data found for metric: {metric_key}*" + + output = [] + timestamp = metrics.get("timestamp", "unknown") + output.append(f"*Last updated: {timestamp}*\n") + + output.append("| Metric | Query (file) | Result |") + output.append("|--------|------|--------|") + + metric_data = metrics[metric_key] + if "files" in metric_data: + # Handle composite metrics (like edge statistics) + output.append(f"| **{metric_data['name']}** | | |") + for sub_query in metric_data["files"].values(): + output.append( + f"| {sub_query['name']} | [{sub_query['file']}](#{sub_query['file']}) | {sub_query.get('result', '-')} |" + ) + else: + # Handle simple metrics + output.append( + f"| {metric_data['name']} | [{metric_data['file']}](#{metric_data['file']}) | {metric_data.get('result', '-')} |" + ) + + return "\n".join(output) + + @env.macro + def general_metrics_table(): + """Renders a complete overview table of all general metrics""" + try: + metrics_file = Path("reports/metrics/general.json") + with open(metrics_file, "r", encoding="utf-8") as f: + metrics = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" + + output = [] + + output.append("| Metric | File | Result |") + output.append("|--------|------|--------|") + + for metric_key, metric_data in metrics.items(): + if metric_key == "timestamp": + continue + + if "files" in metric_data: + # Handle composite metrics + output.append(f"| **{metric_data['name']}** | | |") + for sub_query in metric_data["files"].values(): + output.append( + f"| {sub_query['name']} | {sub_query['file']} | {sub_query.get('result')} |" + ) + else: + # Handle simple metrics + output.append( + f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result')} |" + ) + + output.append(f"\n*Last updated: {metrics.get("timestamp", "-")}*") + return "\n".join(output) diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md index a869138..150c6be 100644 --- a/docs/metrics/general metrics/01_instances.md +++ b/docs/metrics/general metrics/01_instances.md @@ -1,4 +1,4 @@ -# 01 - Instances in a graph +# Instances in a graph This metric counts the total number of instances (nodes) in an RDF graph. An instance is counted as any node that appears as either a subject or object in a triple. The instance count provides a fundamental measure of the graph's size and gives a first indication of its complexity. @@ -9,6 +9,8 @@ The metric helps to: - Compare different graph versions or datasets - Establish a baseline for other metrics +{{ render_metric_table('instances') }} + ## Queries ### Count Number of instances in a graph @@ -16,9 +18,3 @@ The metric helps to: ```sparql {{ include_if_exists("queries/metrics/GM001.rq") }} ``` - -## Results -{{ include_if_exists("reports/metrics/GM001.txt", start_line=1) }} - -Last execution: -{{ include_if_exists("reports/metrics/GM001.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md index c2d3927..e671c34 100644 --- a/docs/metrics/general metrics/02_assertions.md +++ b/docs/metrics/general metrics/02_assertions.md @@ -1,4 +1,4 @@ -# 02 - Assertions in a graph +# Assertions in a graph This metric counts the total number of assertions (triples/edges) in an RDF graph. An assertion is any statement in the form of subject-predicate-object that exists in the graph. The assertion count provides a fundamental measure of the graph's connectivity and density. @@ -9,6 +9,8 @@ The metric helps to: - Track the growth of relationships over time - Compare connectivity between different graph versions +{{ render_metric_table('assertions') }} + ## Queries ### Count Total Number of Assertions diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md index ebfd0ee..86bdbc9 100644 --- a/docs/metrics/general metrics/03_linkage_degree.md +++ b/docs/metrics/general metrics/03_linkage_degree.md @@ -1,4 +1,4 @@ -# 03 - Linkage Degree Analysis +# Linkage Degree Analysis This metric analyzes the connectivity patterns in the graph by measuring the linkage degree of entities. The linkage degree represents how well entities are connected to other entities through relationships. It helps understand the graph's structural characteristics and identifies patterns of connectivity. @@ -9,6 +9,8 @@ The metric provides insights into: - Identification of highly connected or isolated entities - Overall graph connectivity patterns +{{ render_metric_table('linkage') }} + ## Queries ### Average Linkage Degree diff --git a/docs/metrics/general metrics/04_outgoing_edges.md b/docs/metrics/general metrics/04_outgoing_edges.md index 5a4afb4..f27874e 100644 --- a/docs/metrics/general metrics/04_outgoing_edges.md +++ b/docs/metrics/general metrics/04_outgoing_edges.md @@ -1,7 +1,9 @@ -# 04 - Outgoing Edges +# Outgoing Edges This metric determines the median number of outgoing edges across all nodes in the graph. The calculation requires multiple steps. +{{ render_metric_table('edges_out') }} + ## Queries ### Step 1: Count Outgoing Edges Per Node diff --git a/docs/metrics/general metrics/05_incoming_edges.md b/docs/metrics/general metrics/05_incoming_edges.md index 79fc98b..1f9c99e 100644 --- a/docs/metrics/general metrics/05_incoming_edges.md +++ b/docs/metrics/general metrics/05_incoming_edges.md @@ -1,7 +1,9 @@ -# 05 - Incoming Edges +# Incoming Edges This metric determines the median number of incoming edges across all nodes in the graph. The calculation requires multiple steps. +{{ render_metric_table('edges_in') }} + ## Queries ### Step 1: Count Incoming Edges Per Node @@ -41,9 +43,3 @@ Using the total node count (n), median position is: position = (n+1)/2 ```sparql {{ include_if_exists("queries/metrics/GM005_6.rq") }} ``` - -## Results -{{ include_if_exists("reports/metrics/GM005.txt", start_line=1) }} - -Last execution: -{{ include_if_exists("reports/metrics/GM005.txt", single_line=0) }} diff --git a/docs/metrics/index.md b/docs/metrics/index.md index b97d544..f3e7249 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -1,10 +1,39 @@ # Overview -The KnowledgeGraph metrics provide quantitative insights into the structure and content of our knowledge graph. These measurements help us to: +The NFDI4Earth Knowledge Graph metrics provide quantitative insights into our semantic data structure. We distinguish between two main metric categories: -- Understand the graph's size and complexity +## General Metrics + +These metrics analyze the entire knowledge graph structure and provide insights into: + +- Overall size and complexity +- Connection patterns +- Graph density and distribution +- Edge statistics (incoming/outgoing) + +{{ general_metrics_table() }} + +## Resource-specific Metrics + +These metrics focus on specific resource types within the knowledge graph: + +{{ resources_metrics_table() }} + +## About the Metrics + +All metrics: + +- Are implemented as SPARQL queries (stored as `.rq` files) +- Can be executed against the [NFDI4Earth KnowledgeGraph endpoint](https://sparql.knowledgehub.nfdi4earth.de) +- Include execution timestamps + +## Purpose + +These measurements help us to: + +- Monitor the knowledge graph's growth and development - Evaluate the coverage of earth science domains - Identify areas for potential improvement -- Monitor the graph's growth and development - -The queries are stored in separate `.rq` files and can be executed against the NFDI4Earth [KnowledgeGraph endpoint](https://sparql.knowledgehub.nfdi4earth.de). +- Understand interconnections between different resource types +- Guide data quality improvements +- Track the integration of new resources diff --git a/reports/metrics/general.json b/reports/metrics/general.json new file mode 100644 index 0000000..511a23d --- /dev/null +++ b/reports/metrics/general.json @@ -0,0 +1,96 @@ +{ + "instances": { + "name": "Instance Count", + "file": "GM001.rq", + "execute": true, + "result": "1252089", + "execution_time": 1.06 + }, + "assertions": { + "name": "Assertions Count", + "file": "GM002_1.rq", + "execute": true, + "result": "40786353", + "execution_time": 0.01 + }, + "linkage": { + "name": "Average Linkage Degree", + "file": "GM003_2.rq", + "execute": true, + "result": "3.24508957824795", + "execution_time": 0.01 + }, + "edges_out": { + "name": "Edges - outgoing", + "files": { + "total": { + "name": "Total number of outgoing edges", + "file": "GM004_2.rq", + "execute": true, + "result": "6705836", + "execution_time": 0.01 + }, + "min": { + "name": "Minimum of outgoing edges", + "file": "GM004_5.rq", + "execute": true, + "result": "1", + "execution_time": 0.01 + }, + "median": { + "name": "Median of outgoing edges", + "file": "GM004_4.rq", + "replace_dict": { + "{median_position}": "GM004_2.rq" + }, + "execute": true, + "result": "2", + "execution_time": 0.01 + }, + "max": { + "name": "Maximum of outgoing edges", + "file": "GM004_6.rq", + "execute": true, + "result": "282499", + "execution_time": 0.01 + } + } + }, + "edges_in": { + "name": "Edges - incoming", + "files": { + "total": { + "name": "Total number of incoming edges", + "file": "GM005_2.rq", + "execute": true, + "result": "15111836", + "execution_time": 24.99 + }, + "min": { + "name": "Minimum of incoming edges", + "file": "GM005_5.rq", + "execute": true, + "result": "1", + "execution_time": 21.37 + }, + "median": { + "name": "Median of incoming edges", + "file": "GM005_4.rq", + "replace_dict": { + "{median_position}": "GM005_2.rq" + }, + "execute": true, + "result": "1", + "execution_time": 35.6 + }, + "max": { + "name": "Maximum of incoming edges", + "file": "GM005_6.rq", + "execute": true, + "result": "1121975", + "execution_time": 22.57 + } + } + }, + "timestamp": "2025-03-17T15:15" +} \ No newline at end of file diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json index 0953a73..9e34292 100644 --- a/reports/metrics/resources.json +++ b/reports/metrics/resources.json @@ -5,14 +5,14 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "724903", - "execution_time": 1.31 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "724904", - "execution_time": 0.02 + "execution_time": 0.01 }, "linkage": { "name": "Average Linkage", @@ -36,7 +36,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "6", - "execution_time": 1.04 + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -98,9 +98,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "427594", - "execution_time": 4.33 + "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "06_dataset.md" }, "publication": { @@ -109,7 +109,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 0.02 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", @@ -204,7 +204,7 @@ "result": "15", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "07_publication.md" }, "learning_resource": { @@ -213,7 +213,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "512", - "execution_time": 0.02 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", @@ -280,7 +280,7 @@ "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "3", - "execution_time": 0.01 + "execution_time": 1.02 }, "median": { "name": "Median of incoming edges", @@ -308,7 +308,7 @@ "result": "530", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "08_learning_resource.md" }, "repository": { @@ -331,7 +331,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "90212", - "execution_time": 1.04 + "execution_time": 1.03 }, "edges_out": { "name": "Edges - outgoing", @@ -394,7 +394,7 @@ }, "execute": true, "result": "6718", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", @@ -412,7 +412,7 @@ "result": "825", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "09_repository.md" }, "article_lhb": { @@ -469,7 +469,7 @@ "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "62", - "execution_time": 1.05 + "execution_time": 0.01 } } }, @@ -498,7 +498,7 @@ }, "execute": true, "result": "2", - "execution_time": 0.02 + "execution_time": 0.01 }, "max": { "name": "Maximum of incoming edges", @@ -516,7 +516,7 @@ "result": "592", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "10_article_lhb.md" }, "standards": { @@ -539,7 +539,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "13.714285714285714", - "execution_time": 0.01 + "execution_time": 1.02 }, "edges_out": { "name": "Edges - outgoing", @@ -620,7 +620,7 @@ "result": "100", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "11_standards.md" }, "software": { @@ -724,7 +724,7 @@ "result": "1103", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "13_software.md" }, "service": { @@ -764,7 +764,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "14", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -826,9 +826,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "13", - "execution_time": 0.01 + "execution_time": 1.04 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "14_service.md" }, "data_service": { @@ -837,7 +837,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "363632", - "execution_time": 0.13 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", @@ -851,7 +851,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.02 + "execution_time": 0.01 }, "edges_out": { "name": "Edges - outgoing", @@ -868,7 +868,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "11", - "execution_time": 0.02 + "execution_time": 0.01 }, "median": { "name": "Median of outgoing edges", @@ -930,9 +930,9 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "58909", - "execution_time": 0.01 + "execution_time": 0.02 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "15_data_service.md" }, "aggregator": { @@ -955,7 +955,7 @@ "file": "RM003_linkage_template.rq", "execute": true, "result": "187", - "execution_time": 0.01 + "execution_time": 1.05 }, "edges_out": { "name": "Edges - outgoing", @@ -972,7 +972,7 @@ "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "8", - "execution_time": 0.01 + "execution_time": 0.02 }, "median": { "name": "Median of outgoing edges", @@ -982,7 +982,7 @@ }, "execute": true, "result": "36", - "execution_time": 0.01 + "execution_time": 0.02 }, "max": { "name": "Maximum of outgoing edges", @@ -1008,7 +1008,7 @@ "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "151", - "execution_time": 0.01 + "execution_time": 0.02 }, "median": { "name": "Median of incoming edges", @@ -1036,7 +1036,7 @@ "result": "239", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "16_aggregator.md" }, "person": { @@ -1045,7 +1045,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "2180", - "execution_time": 0.02 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", @@ -1140,7 +1140,7 @@ "result": "2", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "17_person.md" }, "registry": { @@ -1149,7 +1149,7 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 1.04 + "execution_time": 0.01 }, "assertions": { "name": "Number of Assertions", @@ -1244,8 +1244,8 @@ "result": "15", "execution_time": 0.01 }, - "timestamp": "2025-03-17T13:55", + "timestamp": "2025-03-17T15:15", "file": "18_registry.md" }, - "timestamp": "2025-03-17T13:55" + "timestamp": "2025-03-17T15:15" } \ No newline at end of file diff --git a/scripts/kg_analysis/interfaces/general.py b/scripts/kg_analysis/interfaces/general.py index ded2f56..0db7ca3 100644 --- a/scripts/kg_analysis/interfaces/general.py +++ b/scripts/kg_analysis/interfaces/general.py @@ -19,7 +19,7 @@ query_templates = { "linkage": { "name": "Average Linkage Degree", "file": "GM003_2.rq", - "execute": False, + "execute": True, }, "edges_out": { "name": "Edges - outgoing", @@ -53,23 +53,23 @@ query_templates = { "total": { "name": "Total number of incoming edges", "file": "GM005_2.rq", - "execute": False, + "execute": True, }, "min": { "name": "Minimum of incoming edges", "file": "GM005_5.rq", - "execute": False, + "execute": True, }, "median": { "name": "Median of incoming edges", "file": "GM005_4.rq", "replace_dict": {"{median_position}": "GM005_2.rq"}, - "execute": False, + "execute": True, }, "max": { "name": "Maximum of incoming edges", "file": "GM005_6.rq", - "execute": False, + "execute": True, }, }, }, -- GitLab From 1f6981108b186faede1908e8fffa19f92de986ac Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:34:17 +0100 Subject: [PATCH 23/59] [metrics] exclude 'macros' from navigation --- mkdocs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 7184e25..f6e240f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,6 +18,9 @@ plugins: - macros: module_name: docs/macros/main +exclude_docs: + macros + markdown_extensions: - admonition - pymdownx.details -- GitLab From b6d1743d920e67bd545e392b36de35447daaa631 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:34:44 +0100 Subject: [PATCH 24/59] [metrics] unify/simplify macros on table rendering --- docs/macros/main.py | 246 ++++++++++-------- .../resource metrics/15_dataservice.md | 2 +- 2 files changed, 142 insertions(+), 106 deletions(-) diff --git a/docs/macros/main.py b/docs/macros/main.py index ad7ea4c..7d7a58f 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -5,6 +5,17 @@ from pathlib import Path def define_env(env): @env.macro def include_if_exists(filename, start_line=None, single_line=None): + """ + Includes the content of a file if it exists. + + Args: + filename (str): The path to the file. + start_line (int, optional): The line number to start including from. + single_line (int, optional): The specific line number to include. + + Returns: + str: The content of the file or an error message if the file does not exist. + """ try: with open(filename, "r") as f: lines = f.readlines() @@ -14,54 +25,48 @@ def define_env(env): return lines[single_line] return "".join(lines) except FileNotFoundError: - return f"*Keine Ergebnisse verfügbar. Die Datei `{filename}` wurde nicht gefunden.*" + return f"*No results available. The file `{filename}` was not found.*" except IndexError: - return f"*Fehler: Die angegebene Zeile {start_line} existiert nicht in `{filename}`.*" + return f"*Error: The specified line {start_line} does not exist in `{filename}`.*" @env.macro def include_template(template_path, resource_type, **kwargs): """ - Includes a template file and replaces {resource_type} with the given value + Includes a template file and replaces {resource_type} with the given value. Args: - template_path (str): Path to the template file - resource_type (str): The resource type URI to inject + template_path (str): Path to the template file. + resource_type (str): The resource type URI to inject. + + Returns: + str: The content of the template with the resource type injected. """ content = include_if_exists(template_path, **kwargs) if content: return content.replace("{resource_type}", resource_type) return "" - def render_resource(resource_data, output, resource_type=None): - """Helper function to render a single resource""" - for query_data in resource_data.values(): - if "file" in query_data: - if resource_type: - output.append( - f"| {resource_type} | {query_data['name']} | {query_data['file']} | {query_data['result']} |" - ) - else: - output.append( - f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" - ) - elif "files" in query_data: - if resource_type: - output.append(f"| {resource_type} | **{query_data['name']}** | | |") - else: - output.append(f"| **{query_data['name']}** | | |") - for sub_query_data in query_data["files"].values(): - if resource_type: - output.append( - f"| {resource_type} | {sub_query_data['name']} | {sub_query_data['file']} | {sub_query_data['result']} |" - ) - else: - output.append( - f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" - ) + def render_resource(resource_data, output): + """ + Helper function to render a single resource. + + Args: + resource_data (dict): The data of the resource. + output (list): The list to append the rendered resource to. + """ + pass @env.macro def resource_metrics_table(resource_type=None): - """Renders metrics as markdown table for one or all resource types""" + """ + Renders metrics as a markdown table for one or all resource types. + + Args: + resource_type (str, optional): The specific resource type to render metrics for. + + Returns: + str: The rendered markdown table or an error message. + """ try: metrics_file = Path("reports/metrics/resources.json") with open(metrics_file, "r", encoding="utf-8") as f: @@ -70,36 +75,43 @@ def define_env(env): return f"*Error loading metrics: {e}*" output = [] - timestamp = resources.get("timestamp", "unknown") - output.append(f"*Last updated: {timestamp}*\n") - if resource_type: - # Render single resource - if resource_type not in resources: - return f"*No metrics found for {resource_type}*" + if resource_type not in resources: + return f"*No metrics found for {resource_type}*" - output.append("| Metric | Query (file) | Result |") - output.append("|--------|------|--------|") - render_resource(resources[resource_type], output) - else: - # Render all resources - output.append("| Resource Type | Metric | Query (file) | Result |") - output.append("|--------------|--------|------|--------|") - for res_type, resource_data in resources.items(): - if res_type != "timestamp": - render_resource(resource_data, output, res_type) + output.append("| Metric | Query (file) | Result |") + output.append("|--------|------|--------|") + + for query_data in resources[resource_type].values(): + if "file" in query_data: + output.append( + f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" + ) + elif "files" in query_data: + output.append(f"| **{query_data['name']}** | | |") + for sub_query_data in query_data["files"].values(): + output.append( + f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" + ) + + output.append(f"\n*Last updated: {resources.get('timestamp', '-')}*") return "\n".join(output) @env.macro def resources_metrics_table(): - """Rendert eine einfache Übersichtstabelle der Resource-Metriken""" + """ + Renders a simple overview table of resource metrics. + + Returns: + str: The rendered markdown table or an error message. + """ try: metrics_file = Path("reports/metrics/resources.json") with open(metrics_file, "r", encoding="utf-8") as f: resources = json.load(f) except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Fehler beim Laden der Metriken: {e}*" + return f"*Error loading metrics: {e}*" output = [] @@ -131,81 +143,105 @@ def define_env(env): for row in rows: output.append(row) - output.append(f"\n*Last updated: {resources.get("timestamp", "-")}*") + output.append(f"\n*Last updated: {resources.get('timestamp', '-')}*") return "\n".join(output) - @env.macro - def render_metric_table(metric_key): + def render_metrics_table(metrics_data, table_type="general"): """ - Renders a specific metric as markdown table. + Central function to render metric tables. Args: - metric_key (str): Key of the metric to render (e.g. 'instances', 'edges_out') - """ - try: - metrics_file = Path("reports/metrics/general.json") - with open(metrics_file, "r", encoding="utf-8") as f: - metrics = json.load(f) - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" - - if metric_key not in metrics: - return f"*No data found for metric: {metric_key}*" + metrics_data (dict): The JSON data with the metrics. + table_type (str): Type of the table ('general', 'resource', 'overview'). + Returns: + str: The rendered markdown table. + """ output = [] - timestamp = metrics.get("timestamp", "unknown") + timestamp = metrics_data.get("timestamp", "unknown") output.append(f"*Last updated: {timestamp}*\n") - output.append("| Metric | Query (file) | Result |") - output.append("|--------|------|--------|") + if table_type == "overview": + resource_types = sorted( + [rt for rt in metrics_data.keys() if rt != "timestamp"] + ) + metric_names = [] + rows = [] + + first_rt = metrics_data[resource_types[0]] + for query in first_rt.get("queries", {}).values(): + if "name" in query: + metric_names.append(query["name"]) + + output.append("| Resource Type | " + " | ".join(metric_names) + " |") + output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") + + for rt in resource_types: + row = [rt] + queries = metrics_data[rt].get("queries", {}) + for metric in metric_names: + result = "-" + for q in queries.values(): + if q.get("name") == metric: + result = str(q.get("result", "-")) + break + row.append(result) + output.append("| " + " | ".join(row) + " |") - metric_data = metrics[metric_key] - if "files" in metric_data: - # Handle composite metrics (like edge statistics) - output.append(f"| **{metric_data['name']}** | | |") - for sub_query in metric_data["files"].values(): - output.append( - f"| {sub_query['name']} | [{sub_query['file']}](#{sub_query['file']}) | {sub_query.get('result', '-')} |" - ) else: - # Handle simple metrics - output.append( - f"| {metric_data['name']} | [{metric_data['file']}](#{metric_data['file']}) | {metric_data.get('result', '-')} |" - ) + output.append("| Metric | Query | Result |") + output.append("|--------|-------|--------|") + + for metric_key, metric_data in metrics_data.items(): + if metric_key == "timestamp": + continue + + if isinstance(metric_data, dict): + if "files" in metric_data: + output.append(f"| **{metric_data['name']}** | | |") + for sub_query in metric_data["files"].values(): + output.append( + f"| {sub_query['name']} | [{sub_query['file']}](#{sub_query['file']}) | {sub_query.get('result', '-')} |" + ) + elif "name" in metric_data: + output.append( + f"| {metric_data['name']} | [{metric_data['file']}](#{metric_data['file']}) | {metric_data.get('result', '-')} |" + ) return "\n".join(output) @env.macro def general_metrics_table(): - """Renders a complete overview table of all general metrics""" + """ + Renders the overview table of general metrics. + + Returns: + str: The rendered markdown table or an error message. + """ try: - metrics_file = Path("reports/metrics/general.json") - with open(metrics_file, "r", encoding="utf-8") as f: - metrics = json.load(f) + with open(Path("reports/metrics/general.json"), "r") as f: + return render_metrics_table(json.load(f), "general") except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" - output = [] - - output.append("| Metric | File | Result |") - output.append("|--------|------|--------|") + @env.macro + def render_metric_table(metric_key): + """ + Renders a single metric. - for metric_key, metric_data in metrics.items(): - if metric_key == "timestamp": - continue + Args: + metric_key (str): The key of the metric to render. - if "files" in metric_data: - # Handle composite metrics - output.append(f"| **{metric_data['name']}** | | |") - for sub_query in metric_data["files"].values(): - output.append( - f"| {sub_query['name']} | {sub_query['file']} | {sub_query.get('result')} |" - ) - else: - # Handle simple metrics - output.append( - f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result')} |" + Returns: + str: The rendered markdown table or an error message. + """ + try: + with open(Path("reports/metrics/general.json"), "r") as f: + metrics = json.load(f) + if metric_key not in metrics: + return f"*No data for metric: {metric_key}*" + return render_metrics_table( + {metric_key: metrics[metric_key]}, "general" ) - - output.append(f"\n*Last updated: {metrics.get("timestamp", "-")}*") - return "\n".join(output) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" diff --git a/docs/metrics/resource metrics/15_dataservice.md b/docs/metrics/resource metrics/15_dataservice.md index a102d74..c43d8c1 100644 --- a/docs/metrics/resource metrics/15_dataservice.md +++ b/docs/metrics/resource metrics/15_dataservice.md @@ -1,4 +1,4 @@ {% from "macros/resource_metrics.md" import resource_metrics with context %} # Data Service -{{ resource_metrics("dataservice", "<http://www.w3.org/ns/dcat#DataService>") }} +{{ resource_metrics("data_service", "<http://www.w3.org/ns/dcat#DataService>") }} -- GitLab From 29b778adc46e6a640a88d0456970fdb18a479af8 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:35:21 +0100 Subject: [PATCH 25/59] [metrics] use 8Mio for avarage linkage --- queries/metrics/GM003_2.rq | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/queries/metrics/GM003_2.rq b/queries/metrics/GM003_2.rq index de8c8e1..2111a12 100644 --- a/queries/metrics/GM003_2.rq +++ b/queries/metrics/GM003_2.rq @@ -2,8 +2,11 @@ # (i.e.: how many assertions per entity does the graph contain) # # Explanation: -# This is an alternative version (esp. for testing purposes) -# as it enables a limitation of analysed entities +# This is an alternative version as it enables a limitation of +# analysed entities. +# We have found that starting with a limit of 8,000,000 entities, +# the results begin to provide meaningful outcome, which does not change +# significantly with a higher limit. SELECT (AVG(?totalDegree) as ?avgLinkageDegree) WHERE { @@ -15,8 +18,7 @@ WHERE { WHERE { { ?entity ?p ?o } UNION { ?s ?p ?entity } } - LIMIT 1000 # Process 1000 entities at a time - OFFSET 0 # Start with first batch, increment for next batches + LIMIT 8000000 # Process limited entities at a time } { SELECT ?entity (COUNT(*) as ?outDegree) -- GitLab From cf90b917f251e8e552e8a0a1b7c38da4fe436e9d Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:35:50 +0100 Subject: [PATCH 26/59] [metrics] forget main.pyc --- docs/macros/__pycache__/main.cpython-312.pyc | Bin 1290 -> 12511 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/macros/__pycache__/main.cpython-312.pyc b/docs/macros/__pycache__/main.cpython-312.pyc index ae0eaebbb09fdee392a5b5d374f2647080079500..538bfb47a654264f2a795d752d539b09ebbf3cb6 100644 GIT binary patch literal 12511 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 delta 608 zcmcbg*u@oenwOW0fq{Xcblb)B4NMFSk3k$5W`r_6gOp5XNMUGUh+?Q@)MQGA$$%6v zGcYiK@MkRs28QVj6X%JEq%fs0wlJ;+$ucl7)G}5wXfjVOVvJ*CntYK_QkX%CVKvl* zWTq4b5S;v%QCb+xTFnH}#>f!NkOERKHd$7jg9ENGkV%%2VRAl`Bs+5r!)%5WmdX80 zlJ%@LjI$Y1*uWy}b6Jy_A{l}i${8w{BN@sWH97olF&5ooDAi<OV9@eT&CE+xa4kwt zP0GtGE>2Y_OD#$}yeB;=u}DwBB{NmQC9xzmQz4;BB0-_Nv?wK2AulsIqeLM+HLWx+ zB{fe^t4dDGEj1%2wa5yrJ~1y{J~cfxDK#%uAu2U9Csm<JR3Wt@v$!NPwWtKFd-FaP zHbw!E&x#K*FfcSQd|=_`OrCs&wZSrp@rJ0~j*1;w7d#R!2q#|RNxZ`^`H6v%Gnw&* zsPP7`4K^3-LoS5IUI>Z15SMsCIO!5k(g$Wn&g99xY=QNfOn!cvjJH^G6O)VbHJNTP zr{<Lv2{JG+6oV{NP$&{$U|_h#m6Dp4nU@+5k`!TJU;qVvu@u-LH&{8^BO4<>G4Ze_ zGk#`fVB`M?VS-}8Pm{3-q<$qskthQL1K6-%95%W6DWy57c17Y0(onOw85kHoFf%eT NerDohOlAbD1^^iLmxTZT -- GitLab From 505e8ea29247c1a207e741a8e6b71ed7362ea831 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Mon, 17 Mar 2025 16:36:23 +0100 Subject: [PATCH 27/59] [rmetrics] remove former reports --- reports/metrics/004.txt | 4 ---- reports/metrics/005.txt | 4 ---- reports/metrics/GM001.txt | 3 --- reports/metrics/GM002.txt | 3 --- reports/metrics/GM003.txt | 3 --- reports/metrics/GM004.txt | 4 ---- reports/metrics/GM005.txt | 4 ---- 7 files changed, 25 deletions(-) delete mode 100644 reports/metrics/004.txt delete mode 100644 reports/metrics/005.txt delete mode 100644 reports/metrics/GM001.txt delete mode 100644 reports/metrics/GM002.txt delete mode 100644 reports/metrics/GM003.txt delete mode 100644 reports/metrics/GM004.txt delete mode 100644 reports/metrics/GM005.txt diff --git a/reports/metrics/004.txt b/reports/metrics/004.txt deleted file mode 100644 index 6b57c75..0000000 --- a/reports/metrics/004.txt +++ /dev/null @@ -1,4 +0,0 @@ -2024-03-12 15:20 -- Total Edges: 6705836 -- Median Position: 3352918 -- Median Outgoing Edges: 2 \ No newline at end of file diff --git a/reports/metrics/005.txt b/reports/metrics/005.txt deleted file mode 100644 index d337df0..0000000 --- a/reports/metrics/005.txt +++ /dev/null @@ -1,4 +0,0 @@ -2024-03-12 15:20 -- Total Edges: 15111836 -- Median Position: 7555918 -- Median Outgoing Edges: 1 \ No newline at end of file diff --git a/reports/metrics/GM001.txt b/reports/metrics/GM001.txt deleted file mode 100644 index 4778d12..0000000 --- a/reports/metrics/GM001.txt +++ /dev/null @@ -1,3 +0,0 @@ -2025-03-13T13:57 -- Instance count: 1252089 -- Execution time: 0.05 seconds \ No newline at end of file diff --git a/reports/metrics/GM002.txt b/reports/metrics/GM002.txt deleted file mode 100644 index f83346c..0000000 --- a/reports/metrics/GM002.txt +++ /dev/null @@ -1,3 +0,0 @@ -2025-03-13T13:57 -- Assertions count: 40786353 -- Execution time: 0.04 seconds \ No newline at end of file diff --git a/reports/metrics/GM003.txt b/reports/metrics/GM003.txt deleted file mode 100644 index 4680c25..0000000 --- a/reports/metrics/GM003.txt +++ /dev/null @@ -1,3 +0,0 @@ -2025-03-13T13:57 -- Assertions count: 3.894342748611638 -- Execution time: 0.01 seconds \ No newline at end of file diff --git a/reports/metrics/GM004.txt b/reports/metrics/GM004.txt deleted file mode 100644 index 7dc2b50..0000000 --- a/reports/metrics/GM004.txt +++ /dev/null @@ -1,4 +0,0 @@ -2025-03-13T13:57 -- Total count: 6705836 -- Median: None -- Execution time: 12.72 seconds \ No newline at end of file diff --git a/reports/metrics/GM005.txt b/reports/metrics/GM005.txt deleted file mode 100644 index 2c28f94..0000000 --- a/reports/metrics/GM005.txt +++ /dev/null @@ -1,4 +0,0 @@ -2025-03-13T13:57 -- Total count: 15111836 -- Median: 1 -- Execution time: 0.02 seconds \ No newline at end of file -- GitLab From b202e24e99a6a30353e3403f1cd5beae790cc120 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Tue, 18 Mar 2025 11:05:37 +0100 Subject: [PATCH 28/59] [metrics]fix: Show all metrics in 'resources_metrics_table' --- docs/macros/main.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/macros/main.py b/docs/macros/main.py index 7d7a58f..cf59d4e 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -122,24 +122,33 @@ def define_env(env): metric_names = [] rows = [] + # Create a row for each resource type for resource_name in ordered_resource_names: resource = resources[resource_name] - row = f"| {resource_name.upper()} |" + row = [resource_name.upper()] for metric in resource.values(): + # Skip the timestamp (or other non-dict values) if isinstance(metric, str): continue + # Check if the metric has sub-metrics elif "files" in metric: for sub_metric in metric["files"].values(): + # Append the name of the sub-metric if sub_metric["name"] not in metric_names: metric_names.append(sub_metric["name"]) - row += f" {sub_metric.get('result', '-')} |" - elif metric["name"] not in metric_names: - row += f" {metric.get('result', '-')} |" - metric_names.append(metric["name"]) - rows.append(row) - + # Append the result of the sub-metric + row.append(sub_metric.get("result", "-") or "-") + else: + # Append the result of the metric + row.append(metric.get("result", "-") or "-") + if metric["name"] not in metric_names: + metric_names.append(metric["name"]) + rows.append("|".join(row)) + + # Create the header row output.append(f"| Resource type | {' | '.join(metric_names)} |") output.append(f"| --- |{' | '.join(['---' for m in metric_names])} |") + # Append the rows for row in rows: output.append(row) -- GitLab From 83922d1af764023b9a78c9c19d3dc821a1c31a2f Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Tue, 18 Mar 2025 13:16:54 +0100 Subject: [PATCH 29/59] [metrics] ignore '__pycache__' --- .gitignore | 1 + docs/macros/__pycache__/main.cpython-312.pyc | Bin 12511 -> 5974 bytes 2 files changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 854d509..41dd612 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *venv* +docs/macros/__pycache__/* \ No newline at end of file diff --git a/docs/macros/__pycache__/main.cpython-312.pyc b/docs/macros/__pycache__/main.cpython-312.pyc index 538bfb47a654264f2a795d752d539b09ebbf3cb6..f5171ef96bc57fca46195c8c6b61980147820c96 100644 GIT binary patch delta 3045 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 literal 12511 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 -- GitLab From 71614bac3885095c10fd1960b711a6eecc80b724 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Tue, 18 Mar 2025 13:17:50 +0100 Subject: [PATCH 30/59] [metrics] Add table_renderer/MetricsTableRenderer --- docs/macros/main.py | 208 ++---------------- docs/macros/resource_metrics.md | 2 +- docs/macros/table_renderer.py | 148 +++++++++++++ docs/metrics/general metrics/01_instances.md | 2 +- docs/metrics/general metrics/02_assertions.md | 2 +- .../general metrics/03_linkage_degree.md | 2 +- .../general metrics/04_outgoing_edges.md | 2 +- .../general metrics/05_incoming_edges.md | 2 +- docs/metrics/index.md | 4 +- 9 files changed, 180 insertions(+), 192 deletions(-) create mode 100644 docs/macros/table_renderer.py diff --git a/docs/macros/main.py b/docs/macros/main.py index cf59d4e..64a249d 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -1,6 +1,12 @@ import json +import os +import sys from pathlib import Path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from table_renderer import MetricsTableRenderer + def define_env(env): @env.macro @@ -46,211 +52,45 @@ def define_env(env): return content.replace("{resource_type}", resource_type) return "" - def render_resource(resource_data, output): - """ - Helper function to render a single resource. - - Args: - resource_data (dict): The data of the resource. - output (list): The list to append the rendered resource to. - """ - pass - @env.macro - def resource_metrics_table(resource_type=None): - """ - Renders metrics as a markdown table for one or all resource types. - - Args: - resource_type (str, optional): The specific resource type to render metrics for. - - Returns: - str: The rendered markdown table or an error message. - """ + def metrics_table_single_resource(resource_type=None): try: - metrics_file = Path("reports/metrics/resources.json") - with open(metrics_file, "r", encoding="utf-8") as f: - resources = json.load(f) + with open( + Path("reports/metrics/resources.json"), "r", encoding="utf-8" + ) as f: + renderer = MetricsTableRenderer(json.load(f)) + return renderer.render("resource", resource_type) except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" - output = [] - - if resource_type not in resources: - return f"*No metrics found for {resource_type}*" - - output.append("| Metric | Query (file) | Result |") - output.append("|--------|------|--------|") - - for query_data in resources[resource_type].values(): - if "file" in query_data: - output.append( - f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" - ) - elif "files" in query_data: - output.append(f"| **{query_data['name']}** | | |") - for sub_query_data in query_data["files"].values(): - output.append( - f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" - ) - - output.append(f"\n*Last updated: {resources.get('timestamp', '-')}*") - - return "\n".join(output) - @env.macro - def resources_metrics_table(): - """ - Renders a simple overview table of resource metrics. - - Returns: - str: The rendered markdown table or an error message. - """ + def metrics_table_overview_resource(): try: - metrics_file = Path("reports/metrics/resources.json") - with open(metrics_file, "r", encoding="utf-8") as f: - resources = json.load(f) + with open( + Path("reports/metrics/resources.json"), "r", encoding="utf-8" + ) as f: + renderer = MetricsTableRenderer(json.load(f)) + return renderer.render("resource_overview") except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" - output = [] - - resource_names = [ - key for key, resource in resources.items() if isinstance(resource, dict) - ] - ordered_resource_names = sorted(resource_names) - - metric_names = [] - rows = [] - # Create a row for each resource type - for resource_name in ordered_resource_names: - resource = resources[resource_name] - row = [resource_name.upper()] - for metric in resource.values(): - # Skip the timestamp (or other non-dict values) - if isinstance(metric, str): - continue - # Check if the metric has sub-metrics - elif "files" in metric: - for sub_metric in metric["files"].values(): - # Append the name of the sub-metric - if sub_metric["name"] not in metric_names: - metric_names.append(sub_metric["name"]) - # Append the result of the sub-metric - row.append(sub_metric.get("result", "-") or "-") - else: - # Append the result of the metric - row.append(metric.get("result", "-") or "-") - if metric["name"] not in metric_names: - metric_names.append(metric["name"]) - rows.append("|".join(row)) - - # Create the header row - output.append(f"| Resource type | {' | '.join(metric_names)} |") - output.append(f"| --- |{' | '.join(['---' for m in metric_names])} |") - # Append the rows - for row in rows: - output.append(row) - - output.append(f"\n*Last updated: {resources.get('timestamp', '-')}*") - return "\n".join(output) - - def render_metrics_table(metrics_data, table_type="general"): - """ - Central function to render metric tables. - - Args: - metrics_data (dict): The JSON data with the metrics. - table_type (str): Type of the table ('general', 'resource', 'overview'). - - Returns: - str: The rendered markdown table. - """ - output = [] - timestamp = metrics_data.get("timestamp", "unknown") - output.append(f"*Last updated: {timestamp}*\n") - - if table_type == "overview": - resource_types = sorted( - [rt for rt in metrics_data.keys() if rt != "timestamp"] - ) - metric_names = [] - rows = [] - - first_rt = metrics_data[resource_types[0]] - for query in first_rt.get("queries", {}).values(): - if "name" in query: - metric_names.append(query["name"]) - - output.append("| Resource Type | " + " | ".join(metric_names) + " |") - output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") - - for rt in resource_types: - row = [rt] - queries = metrics_data[rt].get("queries", {}) - for metric in metric_names: - result = "-" - for q in queries.values(): - if q.get("name") == metric: - result = str(q.get("result", "-")) - break - row.append(result) - output.append("| " + " | ".join(row) + " |") - - else: - output.append("| Metric | Query | Result |") - output.append("|--------|-------|--------|") - - for metric_key, metric_data in metrics_data.items(): - if metric_key == "timestamp": - continue - - if isinstance(metric_data, dict): - if "files" in metric_data: - output.append(f"| **{metric_data['name']}** | | |") - for sub_query in metric_data["files"].values(): - output.append( - f"| {sub_query['name']} | [{sub_query['file']}](#{sub_query['file']}) | {sub_query.get('result', '-')} |" - ) - elif "name" in metric_data: - output.append( - f"| {metric_data['name']} | [{metric_data['file']}](#{metric_data['file']}) | {metric_data.get('result', '-')} |" - ) - - return "\n".join(output) - @env.macro - def general_metrics_table(): - """ - Renders the overview table of general metrics. - - Returns: - str: The rendered markdown table or an error message. - """ + def metrics_table_overview_general(): try: with open(Path("reports/metrics/general.json"), "r") as f: - return render_metrics_table(json.load(f), "general") + renderer = MetricsTableRenderer(json.load(f)) + return renderer.render("general") except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" @env.macro - def render_metric_table(metric_key): - """ - Renders a single metric. - - Args: - metric_key (str): The key of the metric to render. - - Returns: - str: The rendered markdown table or an error message. - """ + def metrics_table_single_general(metric_key): try: with open(Path("reports/metrics/general.json"), "r") as f: metrics = json.load(f) if metric_key not in metrics: return f"*No data for metric: {metric_key}*" - return render_metrics_table( - {metric_key: metrics[metric_key]}, "general" - ) + renderer = MetricsTableRenderer({metric_key: metrics[metric_key]}) + return renderer.render("general") except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" diff --git a/docs/macros/resource_metrics.md b/docs/macros/resource_metrics.md index ce5130b..42bfcd3 100644 --- a/docs/macros/resource_metrics.md +++ b/docs/macros/resource_metrics.md @@ -3,7 +3,7 @@ Resource type: {{resource_type_uri}} ## Results -{{ resource_metrics_table(resource_type) }} +{{ metrics_table_single_resource(resource_type) }} ## Basic Metrics diff --git a/docs/macros/table_renderer.py b/docs/macros/table_renderer.py new file mode 100644 index 0000000..da8ea24 --- /dev/null +++ b/docs/macros/table_renderer.py @@ -0,0 +1,148 @@ +class MetricsTableRenderer: + """Class for rendering metric tables in various formats.""" + + def __init__(self, data): + """ + Initializes the renderer with metric data. + + Args: + data (dict): The JSON data containing the metrics + """ + self.data = data + self.timestamp = data.get("timestamp", "unknown") + self.output = [] + + def render(self, table_type="general", resource_type=None): + """ + Renders the table in the desired format. + + Args: + table_type (str): Type of table ('general', 'resource', 'general_overview', 'resource_overview') + resource_type (str, optional): Specific resource type for resource tables + + Returns: + str: The rendered markdown table + """ + self.output = [] + + # Dictionary mapping table types to their corresponding rendering methods + render_methods = { + "resource_overview": self._render_resource_overview, + "resource": lambda: self._render_resource(resource_type), + "general_overview": self._render_general_overview, + "general": self._render_general, + } + + # Get the appropriate rendering method based on the table type + render_method = render_methods.get(table_type, self._render_general) + render_method() + + # Append the timestamp to the output + self.output.append(f"\n*Last updated: {self.timestamp}*") + return "\n".join(self.output) + + def _render_resource_overview(self): + """Renders the resource overview table.""" + # Get the names of all resources + resource_names = [ + key for key, resource in self.data.items() if isinstance(resource, dict) + ] + ordered_resource_names = sorted(resource_names) + + metric_names = [] + rows = [] + + # Iterate over each resource to collect metric names and results + for resource_name in ordered_resource_names: + resource = self.data[resource_name] + row = [resource_name.upper()] + for metric in resource.values(): + if isinstance(metric, str): + continue + elif "files" in metric: + for sub_metric in metric["files"].values(): + if sub_metric["name"] not in metric_names: + metric_names.append(sub_metric["name"]) + row.append(sub_metric.get("result", "-") or "-") + else: + row.append(metric.get("result", "-") or "-") + if metric["name"] not in metric_names: + metric_names.append(metric["name"]) + rows.append("| " + " | ".join(row) + " |") + + # Construct the table header and rows + self.output.append(f"| Resource type | {' | '.join(metric_names)} |") + self.output.append(f"| --- |{' | '.join(['---' for _ in metric_names])} |") + self.output.extend(rows) + + def _render_resource(self, resource_type): + """Renders the resource-specific table.""" + if resource_type not in self.data: + return f"*No metrics found for {resource_type}*" + + self.output.append("| Metric | Query (file) | Result |") + self.output.append("|--------|------|--------|") + + # Iterate over each query data for the specified resource type + for query_data in self.data[resource_type].values(): + if "file" in query_data: + self.output.append( + f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" + ) + elif "files" in query_data: + self.output.append(f"| **{query_data['name']}** | | |") + for sub_query_data in query_data["files"].values(): + self.output.append( + f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" + ) + + def _render_general_overview(self): + """Renders the general overview table.""" + # Get the sorted list of resource types + resource_types = sorted([rt for rt in self.data.keys() if rt != "timestamp"]) + metric_names = [] + + # Collect metric names from the first resource type + first_rt = self.data[resource_types[0]] + for query in first_rt.get("queries", {}).values(): + if "name" in query: + metric_names.append(query["name"]) + + # Construct the table header + self.output.append("| Resource Type | " + " | ".join(metric_names) + " |") + self.output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") + + # Iterate over each resource type to collect results + for rt in resource_types: + row = [rt] + queries = self.data[rt].get("queries", {}) + for metric in metric_names: + result = "-" + for q in queries.values(): + if q.get("name") == metric: + result = str(q.get("result", "-")) + break + row.append(result) + self.output.append("| " + " | ".join(row) + " |") + + def _render_general(self): + """Renders the general table.""" + self.output.append("| Metric | Query | Result |") + self.output.append("|--------|-------|--------|") + + # Iterate over each metric data to construct the table rows + for metric_key, metric_data in self.data.items(): + if metric_key == "timestamp": + continue + + if isinstance(metric_data, dict): + if "files" in metric_data: + self.output.append(f"| **{metric_data['name']}** | | |") + for sub_query in metric_data["files"].values(): + self.output.append( + f"| {sub_query['name']} | {sub_query['file']} | {sub_query.get('result', '-')} |" + ) + elif "name" in metric_data: + self.output.append( + f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result', '-')} |" + ) diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md index 150c6be..e37a978 100644 --- a/docs/metrics/general metrics/01_instances.md +++ b/docs/metrics/general metrics/01_instances.md @@ -9,7 +9,7 @@ The metric helps to: - Compare different graph versions or datasets - Establish a baseline for other metrics -{{ render_metric_table('instances') }} +{{ metrics_table_single_general('instances') }} ## Queries diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md index e671c34..11d911d 100644 --- a/docs/metrics/general metrics/02_assertions.md +++ b/docs/metrics/general metrics/02_assertions.md @@ -9,7 +9,7 @@ The metric helps to: - Track the growth of relationships over time - Compare connectivity between different graph versions -{{ render_metric_table('assertions') }} +{{ metrics_table_single_general('assertions') }} ## Queries diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md index 86bdbc9..e640cf4 100644 --- a/docs/metrics/general metrics/03_linkage_degree.md +++ b/docs/metrics/general metrics/03_linkage_degree.md @@ -9,7 +9,7 @@ The metric provides insights into: - Identification of highly connected or isolated entities - Overall graph connectivity patterns -{{ render_metric_table('linkage') }} +{{ metrics_table_single_general('linkage') }} ## Queries diff --git a/docs/metrics/general metrics/04_outgoing_edges.md b/docs/metrics/general metrics/04_outgoing_edges.md index f27874e..2ecec14 100644 --- a/docs/metrics/general metrics/04_outgoing_edges.md +++ b/docs/metrics/general metrics/04_outgoing_edges.md @@ -2,7 +2,7 @@ This metric determines the median number of outgoing edges across all nodes in the graph. The calculation requires multiple steps. -{{ render_metric_table('edges_out') }} +{{ metrics_table_single_general('edges_out') }} ## Queries diff --git a/docs/metrics/general metrics/05_incoming_edges.md b/docs/metrics/general metrics/05_incoming_edges.md index 1f9c99e..899f5ea 100644 --- a/docs/metrics/general metrics/05_incoming_edges.md +++ b/docs/metrics/general metrics/05_incoming_edges.md @@ -2,7 +2,7 @@ This metric determines the median number of incoming edges across all nodes in the graph. The calculation requires multiple steps. -{{ render_metric_table('edges_in') }} +{{ metrics_table_single_general('edges_in') }} ## Queries diff --git a/docs/metrics/index.md b/docs/metrics/index.md index f3e7249..e946a39 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -11,13 +11,13 @@ These metrics analyze the entire knowledge graph structure and provide insights - Graph density and distribution - Edge statistics (incoming/outgoing) -{{ general_metrics_table() }} +{{ metrics_table_overview_general() }} ## Resource-specific Metrics These metrics focus on specific resource types within the knowledge graph: -{{ resources_metrics_table() }} +{{ metrics_table_overview_resource() }} ## About the Metrics -- GitLab From 71ad7bc8f004e47c7d72712afb32d5bcc9af4b39 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Tue, 18 Mar 2025 13:30:51 +0100 Subject: [PATCH 31/59] [metrics] Remove numbers from filenames --- docs/metrics/fromality_metrics/classes.md | 0 .../{02_assertions.md => assertions.md} | 0 ...05_incoming_edges.md => edges_incoming.md} | 0 ...04_outgoing_edges.md => edges_outgoing.md} | 0 .../{01_instances.md => instances.md} | 0 ...03_linkage_degree.md => linkage_degree.md} | 0 .../{16_aggregator.md => aggregator.md} | 0 .../{10_article_lhb.md => article_lhb.md} | 0 .../{15_dataservice.md => data_service.md} | 0 .../{06_dataset.md => dataset.md} | 0 ...rning_resource.md => learning_resource.md} | 0 .../{12_organization.md => organization.md} | 0 .../{17_person.md => person.md} | 0 .../{07_publication.md => publication.md} | 0 .../{18_registry.md => registry.md} | 0 .../{09_repository.md => repository.md} | 0 .../{14_service.md => service.md} | 0 .../{13_software.md => software.md} | 0 .../{11_standards.md => standards.md} | 0 scripts/kg_analysis/interfaces/resources.py | 26 +++++++++---------- 20 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 docs/metrics/fromality_metrics/classes.md rename docs/metrics/general metrics/{02_assertions.md => assertions.md} (100%) rename docs/metrics/general metrics/{05_incoming_edges.md => edges_incoming.md} (100%) rename docs/metrics/general metrics/{04_outgoing_edges.md => edges_outgoing.md} (100%) rename docs/metrics/general metrics/{01_instances.md => instances.md} (100%) rename docs/metrics/general metrics/{03_linkage_degree.md => linkage_degree.md} (100%) rename docs/metrics/resource metrics/{16_aggregator.md => aggregator.md} (100%) rename docs/metrics/resource metrics/{10_article_lhb.md => article_lhb.md} (100%) rename docs/metrics/resource metrics/{15_dataservice.md => data_service.md} (100%) rename docs/metrics/resource metrics/{06_dataset.md => dataset.md} (100%) rename docs/metrics/resource metrics/{08_learning_resource.md => learning_resource.md} (100%) rename docs/metrics/resource metrics/{12_organization.md => organization.md} (100%) rename docs/metrics/resource metrics/{17_person.md => person.md} (100%) rename docs/metrics/resource metrics/{07_publication.md => publication.md} (100%) rename docs/metrics/resource metrics/{18_registry.md => registry.md} (100%) rename docs/metrics/resource metrics/{09_repository.md => repository.md} (100%) rename docs/metrics/resource metrics/{14_service.md => service.md} (100%) rename docs/metrics/resource metrics/{13_software.md => software.md} (100%) rename docs/metrics/resource metrics/{11_standards.md => standards.md} (100%) diff --git a/docs/metrics/fromality_metrics/classes.md b/docs/metrics/fromality_metrics/classes.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/assertions.md similarity index 100% rename from docs/metrics/general metrics/02_assertions.md rename to docs/metrics/general metrics/assertions.md diff --git a/docs/metrics/general metrics/05_incoming_edges.md b/docs/metrics/general metrics/edges_incoming.md similarity index 100% rename from docs/metrics/general metrics/05_incoming_edges.md rename to docs/metrics/general metrics/edges_incoming.md diff --git a/docs/metrics/general metrics/04_outgoing_edges.md b/docs/metrics/general metrics/edges_outgoing.md similarity index 100% rename from docs/metrics/general metrics/04_outgoing_edges.md rename to docs/metrics/general metrics/edges_outgoing.md diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/instances.md similarity index 100% rename from docs/metrics/general metrics/01_instances.md rename to docs/metrics/general metrics/instances.md diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/linkage_degree.md similarity index 100% rename from docs/metrics/general metrics/03_linkage_degree.md rename to docs/metrics/general metrics/linkage_degree.md diff --git a/docs/metrics/resource metrics/16_aggregator.md b/docs/metrics/resource metrics/aggregator.md similarity index 100% rename from docs/metrics/resource metrics/16_aggregator.md rename to docs/metrics/resource metrics/aggregator.md diff --git a/docs/metrics/resource metrics/10_article_lhb.md b/docs/metrics/resource metrics/article_lhb.md similarity index 100% rename from docs/metrics/resource metrics/10_article_lhb.md rename to docs/metrics/resource metrics/article_lhb.md diff --git a/docs/metrics/resource metrics/15_dataservice.md b/docs/metrics/resource metrics/data_service.md similarity index 100% rename from docs/metrics/resource metrics/15_dataservice.md rename to docs/metrics/resource metrics/data_service.md diff --git a/docs/metrics/resource metrics/06_dataset.md b/docs/metrics/resource metrics/dataset.md similarity index 100% rename from docs/metrics/resource metrics/06_dataset.md rename to docs/metrics/resource metrics/dataset.md diff --git a/docs/metrics/resource metrics/08_learning_resource.md b/docs/metrics/resource metrics/learning_resource.md similarity index 100% rename from docs/metrics/resource metrics/08_learning_resource.md rename to docs/metrics/resource metrics/learning_resource.md diff --git a/docs/metrics/resource metrics/12_organization.md b/docs/metrics/resource metrics/organization.md similarity index 100% rename from docs/metrics/resource metrics/12_organization.md rename to docs/metrics/resource metrics/organization.md diff --git a/docs/metrics/resource metrics/17_person.md b/docs/metrics/resource metrics/person.md similarity index 100% rename from docs/metrics/resource metrics/17_person.md rename to docs/metrics/resource metrics/person.md diff --git a/docs/metrics/resource metrics/07_publication.md b/docs/metrics/resource metrics/publication.md similarity index 100% rename from docs/metrics/resource metrics/07_publication.md rename to docs/metrics/resource metrics/publication.md diff --git a/docs/metrics/resource metrics/18_registry.md b/docs/metrics/resource metrics/registry.md similarity index 100% rename from docs/metrics/resource metrics/18_registry.md rename to docs/metrics/resource metrics/registry.md diff --git a/docs/metrics/resource metrics/09_repository.md b/docs/metrics/resource metrics/repository.md similarity index 100% rename from docs/metrics/resource metrics/09_repository.md rename to docs/metrics/resource metrics/repository.md diff --git a/docs/metrics/resource metrics/14_service.md b/docs/metrics/resource metrics/service.md similarity index 100% rename from docs/metrics/resource metrics/14_service.md rename to docs/metrics/resource metrics/service.md diff --git a/docs/metrics/resource metrics/13_software.md b/docs/metrics/resource metrics/software.md similarity index 100% rename from docs/metrics/resource metrics/13_software.md rename to docs/metrics/resource metrics/software.md diff --git a/docs/metrics/resource metrics/11_standards.md b/docs/metrics/resource metrics/standards.md similarity index 100% rename from docs/metrics/resource metrics/11_standards.md rename to docs/metrics/resource metrics/standards.md diff --git a/scripts/kg_analysis/interfaces/resources.py b/scripts/kg_analysis/interfaces/resources.py index 203685c..302ad9c 100644 --- a/scripts/kg_analysis/interfaces/resources.py +++ b/scripts/kg_analysis/interfaces/resources.py @@ -86,55 +86,55 @@ query_templates = { resource_types = { "dataset": { - "file": "06_dataset.md", + "file": "dataset.md", "uri": "<http://www.w3.org/ns/dcat#Dataset>", }, "publication": { - "file": "07_publication.md", + "file": "publication.md", "uri": "<http://nfdi4earth.de/ontology/Registry>", }, "learning_resource": { - "file": "08_learning_resource.md", + "file": "learning_resource.md", "uri": "<http://schema.org/LearningResource>", }, "repository": { - "file": "09_repository.md", + "file": "repository.md", "uri": "<http://nfdi4earth.de/ontology/Repository>", }, "article_lhb": { - "file": "10_article_lhb.md", + "file": "article_lhb.md", "uri": "<http://nfdi4earth.de/ontology/LHBArticle>", }, "standards": { - "file": "11_standards.md", + "file": "standards.md", "uri": "<http://nfdi4earth.de/ontology/MetadataStandard>", }, # "organization": { - # "file": "12_organization.md", + # "file": "organization.md", # "uri": "<http://xmlns.com/foaf/0.1/Organization>", # }, "software": { - "file": "13_software.md", + "file": "software.md", "uri": "<http://schema.org/SoftwareSourceCode>", }, "service": { - "file": "14_service.md", + "file": "service.md", "uri": "<http://www.w3.org/ns/sparql-service-description#Service>", }, "data_service": { - "file": "15_data_service.md", + "file": "data_service.md", "uri": "<http://www.w3.org/ns/dcat#DataService>", }, "aggregator": { - "file": "16_aggregator.md", + "file": "aggregator.md", "uri": "<http://nfdi4earth.de/ontology/Aggregator>", }, "person": { - "file": "17_person.md", + "file": "person.md", "uri": "<http://schema.org/Person>", }, "registry": { - "file": "18_registry.md", + "file": "registry.md", "uri": "<http://nfdi4earth.de/ontology/Registry>", }, } -- GitLab From 0ea113ca78695195478cc8f25ae42344e520d94a Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Tue, 18 Mar 2025 16:47:34 +0100 Subject: [PATCH 32/59] [metrics] Adding 'complexity metrics' --- docs/metrics/fromality_metrics/classes.md | 0 .../{instances.md => 01_instances.md} | 0 .../{assertions.md => 02_assertions.md} | 0 ...linkage_degree.md => 03_linkage_degree.md} | 0 ...edges_incoming.md => 04_edges_incoming.md} | 0 ...edges_outgoing.md => 05_edges_outgoing.md} | 0 docs/metrics/index.md | 2 + .../schema_complexity_metrics/01_overview.md | 60 +++++++++++++++++++ .../schema_complexity_metrics/02_classes.md | 31 ++++++++++ .../03_properties.md | 51 ++++++++++++++++ .../schema_complexity_metrics/04_depth.md | 35 +++++++++++ .../schema_complexity_metrics/05_width.md | 39 ++++++++++++ .../06_restrictions.md | 40 +++++++++++++ .../schema_complexity_metrics/07_axioms.md | 39 ++++++++++++ queries/metrics/RF_001.rq | 23 +++++++ queries/metrics/RF_002_1.rq | 25 ++++++++ queries/metrics/RF_002_2.rq | 14 +++++ queries/metrics/RF_002_3.rq | 14 +++++ queries/metrics/RF_002_4.rq | 29 +++++++++ queries/metrics/RF_002_5.rq | 29 +++++++++ queries/metrics/RF_002_6.rq | 45 ++++++++++++++ queries/metrics/RF_003.rq | 33 ++++++++++ queries/metrics/RF_004.rq | 43 +++++++++++++ queries/metrics/RF_005.rq | 38 ++++++++++++ queries/metrics/RF_006.rq | 37 ++++++++++++ queries/metrics/RF_007.rq | 21 +++++++ 26 files changed, 648 insertions(+) delete mode 100644 docs/metrics/fromality_metrics/classes.md rename docs/metrics/general metrics/{instances.md => 01_instances.md} (100%) rename docs/metrics/general metrics/{assertions.md => 02_assertions.md} (100%) rename docs/metrics/general metrics/{linkage_degree.md => 03_linkage_degree.md} (100%) rename docs/metrics/general metrics/{edges_incoming.md => 04_edges_incoming.md} (100%) rename docs/metrics/general metrics/{edges_outgoing.md => 05_edges_outgoing.md} (100%) create mode 100644 docs/metrics/schema_complexity_metrics/01_overview.md create mode 100644 docs/metrics/schema_complexity_metrics/02_classes.md create mode 100644 docs/metrics/schema_complexity_metrics/03_properties.md create mode 100644 docs/metrics/schema_complexity_metrics/04_depth.md create mode 100644 docs/metrics/schema_complexity_metrics/05_width.md create mode 100644 docs/metrics/schema_complexity_metrics/06_restrictions.md create mode 100644 docs/metrics/schema_complexity_metrics/07_axioms.md create mode 100644 queries/metrics/RF_001.rq create mode 100644 queries/metrics/RF_002_1.rq create mode 100644 queries/metrics/RF_002_2.rq create mode 100644 queries/metrics/RF_002_3.rq create mode 100644 queries/metrics/RF_002_4.rq create mode 100644 queries/metrics/RF_002_5.rq create mode 100644 queries/metrics/RF_002_6.rq create mode 100644 queries/metrics/RF_003.rq create mode 100644 queries/metrics/RF_004.rq create mode 100644 queries/metrics/RF_005.rq create mode 100644 queries/metrics/RF_006.rq create mode 100644 queries/metrics/RF_007.rq diff --git a/docs/metrics/fromality_metrics/classes.md b/docs/metrics/fromality_metrics/classes.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/metrics/general metrics/instances.md b/docs/metrics/general metrics/01_instances.md similarity index 100% rename from docs/metrics/general metrics/instances.md rename to docs/metrics/general metrics/01_instances.md diff --git a/docs/metrics/general metrics/assertions.md b/docs/metrics/general metrics/02_assertions.md similarity index 100% rename from docs/metrics/general metrics/assertions.md rename to docs/metrics/general metrics/02_assertions.md diff --git a/docs/metrics/general metrics/linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md similarity index 100% rename from docs/metrics/general metrics/linkage_degree.md rename to docs/metrics/general metrics/03_linkage_degree.md diff --git a/docs/metrics/general metrics/edges_incoming.md b/docs/metrics/general metrics/04_edges_incoming.md similarity index 100% rename from docs/metrics/general metrics/edges_incoming.md rename to docs/metrics/general metrics/04_edges_incoming.md diff --git a/docs/metrics/general metrics/edges_outgoing.md b/docs/metrics/general metrics/05_edges_outgoing.md similarity index 100% rename from docs/metrics/general metrics/edges_outgoing.md rename to docs/metrics/general metrics/05_edges_outgoing.md diff --git a/docs/metrics/index.md b/docs/metrics/index.md index e946a39..801ddcc 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -19,6 +19,8 @@ These metrics focus on specific resource types within the knowledge graph: {{ metrics_table_overview_resource() }} +{{ include_if_exists("docs/metrics/schema_complexity_metrics/01_overview.md") }} + ## About the Metrics All metrics: diff --git a/docs/metrics/schema_complexity_metrics/01_overview.md b/docs/metrics/schema_complexity_metrics/01_overview.md new file mode 100644 index 0000000..9bb98a6 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/01_overview.md @@ -0,0 +1,60 @@ +# Overview - Schema Complexity Metrics + +To calculate the complexity of RDF schemas, we combine the presented formality metrics focusing on both structural and semantic aspects. + +## 1. Basic Structural Complexity + +These metrics measure the size and diversity of the RDF schema: + +- **[Number of classes](classes.md)** (_C_) → More classes indicate a more complex schema. +- **[Number of properties](properties.md)** (_P_) → More properties mean more relationships between entities. +- **[Average class hierarchy depth](depth.md)** (_D_avg_) → The mean number of hierarchy levels in the schema. +- **[Average class hierarchy width](width.md)** (_W_avg_) → The mean number of sibling classes at each hierarchy level. + +A structural complexity score can be calculated as: + + C_structural = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + +where _w_i_ are weights that determine the relative importance of each factor. + +## 2. Semantic Complexity + +If OWL is used, the complexity increases due to advanced semantics: + +- **[Number of restrictions](restrictions.md)** (_R_) → More restrictions indicate more constraints and rules. +- **[Number of logical axioms](axioms.md)** (_A_) → More axioms mean more logical statements and inferences. + +A semantic complexity score can be calculated as: + + C_semantic = w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +## 3. Combined Formula for Schema Complexity + +To create an overall complexity score, we can combine both structural and semantic aspects: + + C_schema = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. + +## References + +> !!!Not the final references!!! + +1. Structural Metrics for Ontologies + - Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" + - Describes metrics like number of classes, hierarchy depth, and relations + - Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 + +2. OWL and Schema Complexity Measurements + - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" + - Develops the OntoQA model combining structural and semantic metrics + - Source: https://doi.org/10.1109/ICDEW.2005.43 + +3. SPARQL Analysis and RDF Complexity + - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" + - Describes hierarchical depth as key metric for RDF schema complexity + - Source: https://doi.org/10.1007/978-3-540-92673-3_10 \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/02_classes.md b/docs/metrics/schema_complexity_metrics/02_classes.md new file mode 100644 index 0000000..2b2da15 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/02_classes.md @@ -0,0 +1,31 @@ +# Number of Schema Classes + +This metric counts the total number of classes defined in our schema. + +## SPARQL Query + +```sparql +{{ include_if_exists("queries/metrics/RF_001.rq") }} +``` + +## Description + +This query: + +- Counts distinct classes defined using rdfs:Class, owl:Class, or sh:NodeShape +- Excludes built-in classes from RDF, RDFS +- Returns a single number representing the total count of user-defined classes in the schema + +## Interpretation + +A higher number indicates: + +- More complex domain modeling +- Broader coverage of concepts +- More detailed classification system + +A lower number might suggest: + +- Simpler schema structure +- Focus on core concepts +- Potential for extended modeling diff --git a/docs/metrics/schema_complexity_metrics/03_properties.md b/docs/metrics/schema_complexity_metrics/03_properties.md new file mode 100644 index 0000000..6547ab8 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/03_properties.md @@ -0,0 +1,51 @@ +# Number of Schema Properties + +This metric analyzes the properties defined in our schema through multiple aspects. + +## Total Properties Count + +```sparql +{{ include_if_exists("queries/metrics/RF_002_1.rq") }} +``` + +## Object Properties Count + +```sparql +{{ include_if_exists("queries/metrics/RF_002_2.rq") }} +``` + +## Datatype Properties Count + +```sparql +{{ include_if_exists("queries/metrics/RF_002_3.rq") }} +``` + +## Properties with Domain + +```sparql +{{ include_if_exists("queries/metrics/RF_002_4.rq") }} +``` + +## Properties with Range + +```sparql +{{ include_if_exists("queries/metrics/RF_002_5.rq") }} +``` + +## Combined Metrics Query + +```sparql +{{ include_if_exists("queries/metrics/RF_002_6.rq") }} +``` + +## Interpretation + +Each metric provides specific insights: + +1. **Total** Properties: Overall schema complexity +2. **Object** Properties: Resource interlinking capability +3. **Datatype** Properties: Attribute richness +4. **Domain** Coverage: Property context definition +5. **Range** Coverage: Value constraints and type safety + +These separate metrics allow for more detailed analysis and easier maintenance. Additionally, the **combined query** provides a comprehensive overview in a single execution. diff --git a/docs/metrics/schema_complexity_metrics/04_depth.md b/docs/metrics/schema_complexity_metrics/04_depth.md new file mode 100644 index 0000000..2dc4a40 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/04_depth.md @@ -0,0 +1,35 @@ +# Depth of Schema (Average) + +This metric calculates the average depth of the class hierarchy in the schema. + +## SPARQL Query + +```sparql +{{ include_if_exists("queries/metrics/RF_003.rq") }} +``` + +## Description + +This query: +- Identifies all user-defined classes +- Calculates the path length to all superclasses +- Determines the average of these path lengths +- Excludes standard RDF/OWL classes + +## Interpretation + +A higher average value indicates: +- Deeper class hierarchies +- More detailed concept modeling +- Stronger specialization + +A lower value suggests: +- Flatter hierarchies +- Broader rather than deeper structuring +- Potentially easier maintenance + +## Notes + +- Depth is calculated by counting superclass relationships +- owl:Thing is not counted +- Multiple inheritance is considered \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/05_width.md b/docs/metrics/schema_complexity_metrics/05_width.md new file mode 100644 index 0000000..3f0c38e --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/05_width.md @@ -0,0 +1,39 @@ +# Width of Schema (Average) + +This metric calculates the average number of subclasses per class (branching factor) in the schema. + +## SPARQL Query + +```sparql +{{ include_if_exists("queries/metrics/RF_004.rq") }} +``` + +## Description + +This query: + +- Identifies all classes in the schema +- Counts the number of direct subclasses for each class +- Calculates the average number of subclasses (branching factor) +- Excludes built-in RDF/OWL classes +- Considers only direct subclass relationships (no transitive closure) + +## Interpretation + +A higher average width indicates: + +- Broader classification at each level +- More horizontal spread in the taxonomy +- Potentially flatter hierarchies + +A lower average width suggests: + +- More vertical organization +- More specialized hierarchies +- Potentially deeper class trees + +## Notes + +- Only counts direct subclass relationships +- Includes classes with no subclasses (count = 0) +- Multiple inheritance is handled correctly through DISTINCT counting \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/06_restrictions.md b/docs/metrics/schema_complexity_metrics/06_restrictions.md new file mode 100644 index 0000000..4f27219 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/06_restrictions.md @@ -0,0 +1,40 @@ +# Number of Restrictions + +This metric counts the various types of OWL restrictions defined in the schema. + +## SPARQL Query + +```sparql +{{ include_if_exists("queries/metrics/RF_006.rq") }} +``` + +## Description + +This query counts: + +- Total number of OWL restrictions +- Specific restriction types: + - someValuesFrom restrictions + - allValuesFrom restrictions + - hasValue restrictions + - Cardinality restrictions (min, max, exact) + +## Interpretation + +Higher numbers indicate: + +- More constrained schema +- More precise data modeling +- Higher validation requirements + +Breakdown by type shows: + +- Value constraints (someValues/allValues) +- Fixed value requirements (hasValue) +- Quantity rules (cardinality) + +## Notes + +- Excludes built-in OWL restrictions +- One restriction can have multiple types +- Cardinality includes min/max/exact \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/07_axioms.md b/docs/metrics/schema_complexity_metrics/07_axioms.md new file mode 100644 index 0000000..16434c5 --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/07_axioms.md @@ -0,0 +1,39 @@ +# Number of Logical Axioms + +This metric counts the various types of OWL logical axioms defined in the schema. + +## SPARQL Query + +```sparql +{{ include_if_exists("queries/metrics/RF_007.rq") }} +``` + +## Description + +This query counts OWL logical axioms including: + +- Equivalent classes (owl:equivalentClass) +- Disjoint classes (owl:disjointWith) +- Complement classes (owl:complementOf) +- Intersection classes (owl:intersectionOf) +- Union classes (owl:unionOf) + +## Interpretation + +Higher numbers indicate: + +- More complex logical relationships +- Richer semantic modeling +- Greater inferencing potential + +Type distribution shows: + +- Class equivalence relationships +- Class disjointness constraints +- Complex class definitions + +## Notes + +- Excludes built-in OWL axioms +- Counts distinct class usages +- Combined total gives overall axiom complexity \ No newline at end of file diff --git a/queries/metrics/RF_001.rq b/queries/metrics/RF_001.rq new file mode 100644 index 0000000..9ccb05d --- /dev/null +++ b/queries/metrics/RF_001.rq @@ -0,0 +1,23 @@ +# This query counts the number of distinct RDFS and OWL classes, +# excluding built-in classes from RDF/RDFS/OWL vocabularies + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?class) AS ?numberOfClasses) +WHERE { + { + # Count RDFS classes + ?class a rdfs:Class . + } + UNION + { + # Count OWL classes + ?class a owl:Class . + } + + # Filter out built-in classes from RDF/RDFS/OWL vocabularies + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} diff --git a/queries/metrics/RF_002_1.rq b/queries/metrics/RF_002_1.rq new file mode 100644 index 0000000..423b129 --- /dev/null +++ b/queries/metrics/RF_002_1.rq @@ -0,0 +1,25 @@ +# This SPARQL query counts the total number of distinct properties, +# excluding built-in properties + +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?property) AS ?totalProperties) +WHERE { + { + ?property a rdf:Property . + } + UNION + { + ?property a owl:ObjectProperty . + } + UNION + { + ?property a owl:DatatypeProperty . + } + + # Filter for built-in properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} \ No newline at end of file diff --git a/queries/metrics/RF_002_2.rq b/queries/metrics/RF_002_2.rq new file mode 100644 index 0000000..4338eb7 --- /dev/null +++ b/queries/metrics/RF_002_2.rq @@ -0,0 +1,14 @@ +# This SPARQL query counts the number of distinct object properties, +# excluding built-in properties + +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?property) AS ?objectProperties) +WHERE { + ?property a owl:ObjectProperty . + + # Filter für eingebaute Properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} \ No newline at end of file diff --git a/queries/metrics/RF_002_3.rq b/queries/metrics/RF_002_3.rq new file mode 100644 index 0000000..1fa9bcd --- /dev/null +++ b/queries/metrics/RF_002_3.rq @@ -0,0 +1,14 @@ +# This SPARQL query counts the number of distinct datatype properties, +# excluding built-in properties + +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?property) AS ?datatypeProperties) +WHERE { + ?property a owl:DatatypeProperty . + + # Filter out built-in properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} \ No newline at end of file diff --git a/queries/metrics/RF_002_4.rq b/queries/metrics/RF_002_4.rq new file mode 100644 index 0000000..82ebe44 --- /dev/null +++ b/queries/metrics/RF_002_4.rq @@ -0,0 +1,29 @@ +# This SPARQL query counts the number of distinct properties with a +# specified domain, excluding built-in properties + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?property) AS ?propertiesWithDomain) +WHERE { + { + ?property a rdf:Property ; + rdfs:domain ?domain . + } + UNION + { + ?property a owl:ObjectProperty ; + rdfs:domain ?domain . + } + UNION + { + ?property a owl:DatatypeProperty ; + rdfs:domain ?domain . + } + + # Filter out built-in properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} diff --git a/queries/metrics/RF_002_5.rq b/queries/metrics/RF_002_5.rq new file mode 100644 index 0000000..4fea73c --- /dev/null +++ b/queries/metrics/RF_002_5.rq @@ -0,0 +1,29 @@ +# This query counts the number of distinct properties with a specified range, +# excluding built-in properties + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (COUNT(DISTINCT ?property) AS ?propertiesWithRange) +WHERE { + { + ?property a rdf:Property ; + rdfs:range ?range . + } + UNION + { + ?property a owl:ObjectProperty ; + rdfs:range ?range . + } + UNION + { + ?property a owl:DatatypeProperty ; + rdfs:range ?range . + } + + # Filter out built-in properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) +} diff --git a/queries/metrics/RF_002_6.rq b/queries/metrics/RF_002_6.rq new file mode 100644 index 0000000..a4bfec1 --- /dev/null +++ b/queries/metrics/RF_002_6.rq @@ -0,0 +1,45 @@ +# This SPARQL query counts various types of properties in an RDF dataset, +# excluding built-in properties. + +PREFIX owl: <http://www.w3.org/2002/07/owl#> +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> + +SELECT + (COUNT(DISTINCT ?property) AS ?totalProperties) + (COUNT(DISTINCT ?objectProperty) AS ?objectProperties) + (COUNT(DISTINCT ?datatypeProperty) AS ?datatypeProperties) + (COUNT(DISTINCT ?withDomain) AS ?propertiesWithDomain) + (COUNT(DISTINCT ?withRange) AS ?propertiesWithRange) +WHERE { + { + # Count RDF/RDFS properties + ?property a rdf:Property . + } + UNION + { + # Count OWL Object Properties + ?property a owl:ObjectProperty . + BIND(?property AS ?objectProperty) + } + UNION + { + # Count OWL Datatype Properties + ?property a owl:DatatypeProperty . + BIND(?property AS ?datatypeProperty) + } + + # Filter out built-in properties + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/1999/02/22-rdf-syntax-ns")) + + # Check for domain and range definitions + OPTIONAL { + ?property rdfs:domain ?domain . + BIND(?property AS ?withDomain) + } + OPTIONAL { + ?property rdfs:range ?range . + BIND(?property AS ?withRange) + } +} \ No newline at end of file diff --git a/queries/metrics/RF_003.rq b/queries/metrics/RF_003.rq new file mode 100644 index 0000000..601397c --- /dev/null +++ b/queries/metrics/RF_003.rq @@ -0,0 +1,33 @@ +# This SPARQL query calculates the average hierarchy depth of all classes +# in an RDF dataset, excluding built-in RDF and OWL classes. + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (AVG(?depth) AS ?averageHierarchyDepth) +WHERE { + { + SELECT ?class (COUNT(?superClass) AS ?depth) + WHERE { + # Find all classes + { + ?class a rdfs:Class . + } UNION { + ?class a owl:Class . + } + + # Ensure class exists + ?class rdfs:subClassOf ?directSuper . + + # Calculate path to all superclasses + ?directSuper rdfs:subClassOf ?superClass . + + # Filter out built-in classes + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?superClass), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?superClass), "http://www.w3.org/2000/01/rdf-schema")) + } + GROUP BY ?class + } +} \ No newline at end of file diff --git a/queries/metrics/RF_004.rq b/queries/metrics/RF_004.rq new file mode 100644 index 0000000..51ad8ed --- /dev/null +++ b/queries/metrics/RF_004.rq @@ -0,0 +1,43 @@ +# This SPARQL query calculates the average branching factor of classes in +# an RDF dataset. +# The branching factor is defined as the average number of direct +# subclasses per class. +# The query first identifies all classes (both rdfs:Class and owl:Class) +# and counts their direct subclasses, excluding built-in properties + +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT (AVG(?subClassCount) AS ?averageBranchingFactor) +WHERE { + { + SELECT ?class (COUNT(DISTINCT ?subClass) AS ?subClassCount) + WHERE { + # Find all classes that have subclasses + { + ?class a rdfs:Class . + } UNION { + ?class a owl:Class . + } + + # Count direct subclasses + OPTIONAL { + ?subClass rdfs:subClassOf ?class . + + # Ensure subClass is actually a class + { + ?subClass a rdfs:Class . + } UNION { + ?subClass a owl:Class . + } + } + + # Filter out built-in classes + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/2000/01/rdf-schema")) + FILTER(!STRSTARTS(STR(?subClass), "http://www.w3.org/2002/07/owl")) + FILTER(!STRSTARTS(STR(?subClass), "http://www.w3.org/2000/01/rdf-schema")) + } + GROUP BY ?class + } +} \ No newline at end of file diff --git a/queries/metrics/RF_005.rq b/queries/metrics/RF_005.rq new file mode 100644 index 0000000..003e342 --- /dev/null +++ b/queries/metrics/RF_005.rq @@ -0,0 +1,38 @@ +PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT + (COUNT(DISTINCT ?class) AS ?totalClasses) + (COUNT(DISTINCT ?property) AS ?totalProperties) + (COUNT(DISTINCT ?restriction) AS ?totalRestrictions) + (?totalClasses + ?totalProperties + ?totalRestrictions AS ?complexityScore) +WHERE { + { + # Count Classes + { + ?class a rdfs:Class . + } UNION { + ?class a owl:Class . + } + FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/")) + } + UNION + { + # Count Properties + { + ?property a rdf:Property . + } UNION { + ?property a owl:ObjectProperty . + } UNION { + ?property a owl:DatatypeProperty . + } + FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/")) + } + UNION + { + # Count Restrictions + ?restriction a owl:Restriction . + FILTER(!STRSTARTS(STR(?restriction), "http://www.w3.org/")) + } +} \ No newline at end of file diff --git a/queries/metrics/RF_006.rq b/queries/metrics/RF_006.rq new file mode 100644 index 0000000..6a02413 --- /dev/null +++ b/queries/metrics/RF_006.rq @@ -0,0 +1,37 @@ +PREFIX owl: <http://www.w3.org/2002/07/owl#> +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> + +SELECT + (COUNT(DISTINCT ?restriction) as ?totalRestrictions) + (COUNT(DISTINCT ?someValues) as ?someValuesFrom) + (COUNT(DISTINCT ?allValues) as ?allValuesFrom) + (COUNT(DISTINCT ?hasValue) as ?hasValueRestrictions) + (COUNT(DISTINCT ?cardinality) as ?cardinalityRestrictions) +WHERE { + { + ?restriction a owl:Restriction . + FILTER(!STRSTARTS(STR(?restriction), "http://www.w3.org/")) + } + OPTIONAL { + ?someValues a owl:Restriction ; + owl:someValuesFrom ?target1 . + } + OPTIONAL { + ?allValues a owl:Restriction ; + owl:allValuesFrom ?target2 . + } + OPTIONAL { + ?hasValue a owl:Restriction ; + owl:hasValue ?target3 . + } + OPTIONAL { + ?cardinality a owl:Restriction . + { + ?cardinality owl:cardinality ?card1 . + } UNION { + ?cardinality owl:minCardinality ?card2 . + } UNION { + ?cardinality owl:maxCardinality ?card3 . + } + } +} \ No newline at end of file diff --git a/queries/metrics/RF_007.rq b/queries/metrics/RF_007.rq new file mode 100644 index 0000000..2ba5613 --- /dev/null +++ b/queries/metrics/RF_007.rq @@ -0,0 +1,21 @@ +# This query counts the total number of distinct logical axioms in the dataset, +# excluding built-in properties. + +PREFIX owl: <http://www.w3.org/2002/07/owl#> + +SELECT + (COUNT(DISTINCT ?axiom) as ?totalLogicalAxioms) +WHERE { + VALUES ?property { + owl:equivalentClass + owl:disjointWith + owl:complementOf + owl:intersectionOf + owl:unionOf + } + + ?axiom ?property ?target . + + # Filter for built-in properties + FILTER(!STRSTARTS(STR(?axiom), "http://www.w3.org/")) +} \ No newline at end of file -- GitLab From a7c388e014fdcf6772bdfa8f3d0ab754826240ed Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 08:49:18 +0100 Subject: [PATCH 33/59] [metrics] Resort/-name complexity metric docs --- .../schema_complexity_metrics/{01_overview.md => 00_overview.md} | 0 .../schema_complexity_metrics/{02_classes.md => 01_classes.md} | 0 .../{03_properties.md => 02_properties.md} | 0 .../schema_complexity_metrics/{04_depth.md => 03_depth.md} | 0 .../schema_complexity_metrics/{05_width.md => 04_width.md} | 0 .../{06_restrictions.md => 05_restrictions.md} | 0 .../schema_complexity_metrics/{07_axioms.md => 06_axioms.md} | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename docs/metrics/schema_complexity_metrics/{01_overview.md => 00_overview.md} (100%) rename docs/metrics/schema_complexity_metrics/{02_classes.md => 01_classes.md} (100%) rename docs/metrics/schema_complexity_metrics/{03_properties.md => 02_properties.md} (100%) rename docs/metrics/schema_complexity_metrics/{04_depth.md => 03_depth.md} (100%) rename docs/metrics/schema_complexity_metrics/{05_width.md => 04_width.md} (100%) rename docs/metrics/schema_complexity_metrics/{06_restrictions.md => 05_restrictions.md} (100%) rename docs/metrics/schema_complexity_metrics/{07_axioms.md => 06_axioms.md} (100%) diff --git a/docs/metrics/schema_complexity_metrics/01_overview.md b/docs/metrics/schema_complexity_metrics/00_overview.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/01_overview.md rename to docs/metrics/schema_complexity_metrics/00_overview.md diff --git a/docs/metrics/schema_complexity_metrics/02_classes.md b/docs/metrics/schema_complexity_metrics/01_classes.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/02_classes.md rename to docs/metrics/schema_complexity_metrics/01_classes.md diff --git a/docs/metrics/schema_complexity_metrics/03_properties.md b/docs/metrics/schema_complexity_metrics/02_properties.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/03_properties.md rename to docs/metrics/schema_complexity_metrics/02_properties.md diff --git a/docs/metrics/schema_complexity_metrics/04_depth.md b/docs/metrics/schema_complexity_metrics/03_depth.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/04_depth.md rename to docs/metrics/schema_complexity_metrics/03_depth.md diff --git a/docs/metrics/schema_complexity_metrics/05_width.md b/docs/metrics/schema_complexity_metrics/04_width.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/05_width.md rename to docs/metrics/schema_complexity_metrics/04_width.md diff --git a/docs/metrics/schema_complexity_metrics/06_restrictions.md b/docs/metrics/schema_complexity_metrics/05_restrictions.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/06_restrictions.md rename to docs/metrics/schema_complexity_metrics/05_restrictions.md diff --git a/docs/metrics/schema_complexity_metrics/07_axioms.md b/docs/metrics/schema_complexity_metrics/06_axioms.md similarity index 100% rename from docs/metrics/schema_complexity_metrics/07_axioms.md rename to docs/metrics/schema_complexity_metrics/06_axioms.md -- GitLab From 3135539a6781c492d33f31630d148ea9d90492d6 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 09:20:36 +0100 Subject: [PATCH 34/59] [metrics] More renaming + refinement of metrics/index.md --- docs/macros/resource_metrics.md | 10 +-- .../01_instances.md | 0 .../02_assertions.md | 0 .../03_linkage_degree.md | 0 .../04_edges_incoming.md | 0 .../05_edges_outgoing.md | 0 docs/metrics/index.md | 84 +++++++++++++++++-- .../aggregator.md | 0 .../article_lhb.md | 0 .../data_service.md | 0 .../dataset.md | 0 .../learning_resource.md | 0 .../organization.md | 0 .../person.md | 0 .../publication.md | 0 .../registry.md | 0 .../repository.md | 0 .../service.md | 0 .../software.md | 0 .../standards.md | 0 .../schema_complexity_metrics/00_overview.md | 60 ------------- 21 files changed, 82 insertions(+), 72 deletions(-) rename docs/metrics/{general metrics => general_metrics}/01_instances.md (100%) rename docs/metrics/{general metrics => general_metrics}/02_assertions.md (100%) rename docs/metrics/{general metrics => general_metrics}/03_linkage_degree.md (100%) rename docs/metrics/{general metrics => general_metrics}/04_edges_incoming.md (100%) rename docs/metrics/{general metrics => general_metrics}/05_edges_outgoing.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/aggregator.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/article_lhb.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/data_service.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/dataset.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/learning_resource.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/organization.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/person.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/publication.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/registry.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/repository.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/service.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/software.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/standards.md (100%) delete mode 100644 docs/metrics/schema_complexity_metrics/00_overview.md diff --git a/docs/macros/resource_metrics.md b/docs/macros/resource_metrics.md index 42bfcd3..de24e0c 100644 --- a/docs/macros/resource_metrics.md +++ b/docs/macros/resource_metrics.md @@ -9,7 +9,7 @@ Resource type: {{resource_type_uri}} ### Number of Entitis -see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) +see also: [general_metricsinstances](/metrics/general%20metrics/01_instances/) <i id="RM001_instances_template.rq">file: RM001_instances_template.rq</i> @@ -26,7 +26,7 @@ see also: [general metrics/instances](/metrics/general%20metrics/01_instances/) ### Number of Assertions -see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/) +see also: [general_metricsassortions](/metrics/general%20metrics/02_assertions/) <i id="RM002_assertions_template.rq">file: RM002_assertions_template.rq</i> @@ -36,7 +36,7 @@ see also: [general metrics/assortions](/metrics/general%20metrics/02_assertions/ ### Average linkage -see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree/) +see also: [general_metricslinkage](/metrics/general%20metrics/03_linkage_degree/) <i id="RM003_linkage_template.rq">file: RM003_linkage_template.rq</i> @@ -46,7 +46,7 @@ see also: [general metrics/linkage](/metrics/general%20metrics/03_linkage_degree ### Outgoing Edges Statistics -see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoing_edges/) +see also: [general_metricsoutgoing edges](/metrics/general%20metrics/04_outgoing_edges/) #### Total outgoing edges @@ -82,7 +82,7 @@ see also: [general metrics/outgoing edges](/metrics/general%20metrics/04_outgoin ### Incoming Edges Statistics -see also: [general metrics/incoming edges](/metrics/general%20metrics/05_incoming_edges/) +see also: [general_metricsincoming edges](/metrics/general%20metrics/05_incoming_edges/) #### Total incoming edges diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general_metrics/01_instances.md similarity index 100% rename from docs/metrics/general metrics/01_instances.md rename to docs/metrics/general_metrics/01_instances.md diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general_metrics/02_assertions.md similarity index 100% rename from docs/metrics/general metrics/02_assertions.md rename to docs/metrics/general_metrics/02_assertions.md diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general_metrics/03_linkage_degree.md similarity index 100% rename from docs/metrics/general metrics/03_linkage_degree.md rename to docs/metrics/general_metrics/03_linkage_degree.md diff --git a/docs/metrics/general metrics/04_edges_incoming.md b/docs/metrics/general_metrics/04_edges_incoming.md similarity index 100% rename from docs/metrics/general metrics/04_edges_incoming.md rename to docs/metrics/general_metrics/04_edges_incoming.md diff --git a/docs/metrics/general metrics/05_edges_outgoing.md b/docs/metrics/general_metrics/05_edges_outgoing.md similarity index 100% rename from docs/metrics/general metrics/05_edges_outgoing.md rename to docs/metrics/general_metrics/05_edges_outgoing.md diff --git a/docs/metrics/index.md b/docs/metrics/index.md index 801ddcc..ec6f879 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -6,20 +6,90 @@ The NFDI4Earth Knowledge Graph metrics provide quantitative insights into our se These metrics analyze the entire knowledge graph structure and provide insights into: -- Overall size and complexity -- Connection patterns -- Graph density and distribution -- Edge statistics (incoming/outgoing) +- [Overall size and complexity](general_metrics/01_instances) +- [Graph density and distribution](general_metrics/02_assertions) +- [Linkage](general_metrics/03_linkage_degree) +- Edge statistics ([incoming](general_metrics/04_edges_incoming)/[outgoing](general_metrics/05_edges_outgoing)) {{ metrics_table_overview_general() }} -## Resource-specific Metrics +## Resource specific Metrics -These metrics focus on specific resource types within the knowledge graph: +These metrics focus on specific resource types within the knowledge graph, analyzing key entities in the earth science domain: + +- Research outputs ([datasets](resource_metrics/dataset), [publications](resource_metrics/publication), [articles](resource_metrics/article_lhb)) +- Infrastructure components ([repositories](resource_metrics/repository), [services](resource_metrics/service), [software](resource_metrics/software)) +- [Learning materials and standards](resource_metrics/learning_resource) +- [Organizations](resource_metrics/organization) and [people](resource_metrics/person) +- Digital resources ([data services](resource_metrics/data_service), [registries](resource_metrics/registry), [aggregators](resource_metrics/aggregator)) + +Each resource type is analyzed individually to understand its representation, completeness, and interconnections within the knowledge graph: {{ metrics_table_overview_resource() }} -{{ include_if_exists("docs/metrics/schema_complexity_metrics/01_overview.md") }} +## Schema Complexity Metrics + +To calculate the complexity of RDF schemas, we combine the presented formality metrics focusing on both structural and semantic aspects. + +### 1. Basic Structural Complexity + +These metrics measure the size and diversity of the RDF schema: + +- **[Number of classes](./schema_complexity_metrics/01_classes)** (_C_) → More classes indicate a more complex schema. +- **[Number of properties](./schema_complexity_metrics/02_properties)** (_P_) → More properties mean more relationships between entities. +- **[Average class hierarchy depth](./schema_complexity_metrics/03_depth)** (_D_avg_) → The mean number of hierarchy levels in the schema. +- **[Average class hierarchy width](./schema_complexity_metrics/04_width)** (_W_avg_) → The mean number of sibling classes at each hierarchy level. + +A structural complexity score can be calculated as: + + C_structural = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + +where _w_i_ are weights that determine the relative importance of each factor. + +### 2. Semantic Complexity + +If OWL is used, the complexity increases due to advanced semantics: + +- **[Number of restrictions](./schema_complexity_metrics/05_restrictions)** (_R_) → More restrictions indicate more constraints and rules. +- **[Number of logical axioms](./schema_complexity_metrics/06_axioms)** (_A_) → More axioms mean more logical statements and inferences. + +A semantic complexity score can be calculated as: + + C_semantic = w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +### 3. Combined Formula for Schema Complexity + +To create an overall complexity score, we can combine both structural and semantic aspects: + + C_schema = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. + +### References + +> !!!Not the final references!!! + +1. Structural Metrics for Ontologies: + +- Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" +- Describes metrics like number of classes, hierarchy depth, and relations +- Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 + +2. OWL and Schema Complexity Measurements: + + - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" + - Develops the OntoQA model combining structural and semantic metrics + - Source: https://doi.org/10.1109/ICDEW.2005.43 + +3. SPARQL Analysis and RDF Complexity: + + - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" + - Describes hierarchical depth as key metric for RDF schema complexity + - Source: https://doi.org/10.1007/978-3-540-92673-3_10 ## About the Metrics diff --git a/docs/metrics/resource metrics/aggregator.md b/docs/metrics/resource_metrics/aggregator.md similarity index 100% rename from docs/metrics/resource metrics/aggregator.md rename to docs/metrics/resource_metrics/aggregator.md diff --git a/docs/metrics/resource metrics/article_lhb.md b/docs/metrics/resource_metrics/article_lhb.md similarity index 100% rename from docs/metrics/resource metrics/article_lhb.md rename to docs/metrics/resource_metrics/article_lhb.md diff --git a/docs/metrics/resource metrics/data_service.md b/docs/metrics/resource_metrics/data_service.md similarity index 100% rename from docs/metrics/resource metrics/data_service.md rename to docs/metrics/resource_metrics/data_service.md diff --git a/docs/metrics/resource metrics/dataset.md b/docs/metrics/resource_metrics/dataset.md similarity index 100% rename from docs/metrics/resource metrics/dataset.md rename to docs/metrics/resource_metrics/dataset.md diff --git a/docs/metrics/resource metrics/learning_resource.md b/docs/metrics/resource_metrics/learning_resource.md similarity index 100% rename from docs/metrics/resource metrics/learning_resource.md rename to docs/metrics/resource_metrics/learning_resource.md diff --git a/docs/metrics/resource metrics/organization.md b/docs/metrics/resource_metrics/organization.md similarity index 100% rename from docs/metrics/resource metrics/organization.md rename to docs/metrics/resource_metrics/organization.md diff --git a/docs/metrics/resource metrics/person.md b/docs/metrics/resource_metrics/person.md similarity index 100% rename from docs/metrics/resource metrics/person.md rename to docs/metrics/resource_metrics/person.md diff --git a/docs/metrics/resource metrics/publication.md b/docs/metrics/resource_metrics/publication.md similarity index 100% rename from docs/metrics/resource metrics/publication.md rename to docs/metrics/resource_metrics/publication.md diff --git a/docs/metrics/resource metrics/registry.md b/docs/metrics/resource_metrics/registry.md similarity index 100% rename from docs/metrics/resource metrics/registry.md rename to docs/metrics/resource_metrics/registry.md diff --git a/docs/metrics/resource metrics/repository.md b/docs/metrics/resource_metrics/repository.md similarity index 100% rename from docs/metrics/resource metrics/repository.md rename to docs/metrics/resource_metrics/repository.md diff --git a/docs/metrics/resource metrics/service.md b/docs/metrics/resource_metrics/service.md similarity index 100% rename from docs/metrics/resource metrics/service.md rename to docs/metrics/resource_metrics/service.md diff --git a/docs/metrics/resource metrics/software.md b/docs/metrics/resource_metrics/software.md similarity index 100% rename from docs/metrics/resource metrics/software.md rename to docs/metrics/resource_metrics/software.md diff --git a/docs/metrics/resource metrics/standards.md b/docs/metrics/resource_metrics/standards.md similarity index 100% rename from docs/metrics/resource metrics/standards.md rename to docs/metrics/resource_metrics/standards.md diff --git a/docs/metrics/schema_complexity_metrics/00_overview.md b/docs/metrics/schema_complexity_metrics/00_overview.md deleted file mode 100644 index 9bb98a6..0000000 --- a/docs/metrics/schema_complexity_metrics/00_overview.md +++ /dev/null @@ -1,60 +0,0 @@ -# Overview - Schema Complexity Metrics - -To calculate the complexity of RDF schemas, we combine the presented formality metrics focusing on both structural and semantic aspects. - -## 1. Basic Structural Complexity - -These metrics measure the size and diversity of the RDF schema: - -- **[Number of classes](classes.md)** (_C_) → More classes indicate a more complex schema. -- **[Number of properties](properties.md)** (_P_) → More properties mean more relationships between entities. -- **[Average class hierarchy depth](depth.md)** (_D_avg_) → The mean number of hierarchy levels in the schema. -- **[Average class hierarchy width](width.md)** (_W_avg_) → The mean number of sibling classes at each hierarchy level. - -A structural complexity score can be calculated as: - - C_structural = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg - -where _w_i_ are weights that determine the relative importance of each factor. - -## 2. Semantic Complexity - -If OWL is used, the complexity increases due to advanced semantics: - -- **[Number of restrictions](restrictions.md)** (_R_) → More restrictions indicate more constraints and rules. -- **[Number of logical axioms](axioms.md)** (_A_) → More axioms mean more logical statements and inferences. - -A semantic complexity score can be calculated as: - - C_semantic = w5 * R + w6 * A - -where _w_i_ are weights that determine the relative importance of each factor. - -## 3. Combined Formula for Schema Complexity - -To create an overall complexity score, we can combine both structural and semantic aspects: - - C_schema = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + w5 * R + w6 * A - -where _w_i_ are weights that determine the relative importance of each factor. - -This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. - -## References - -> !!!Not the final references!!! - -1. Structural Metrics for Ontologies - - Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" - - Describes metrics like number of classes, hierarchy depth, and relations - - Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 - -2. OWL and Schema Complexity Measurements - - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" - - Develops the OntoQA model combining structural and semantic metrics - - Source: https://doi.org/10.1109/ICDEW.2005.43 - -3. SPARQL Analysis and RDF Complexity - - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" - - Describes hierarchical depth as key metric for RDF schema complexity - - Source: https://doi.org/10.1007/978-3-540-92673-3_10 \ No newline at end of file -- GitLab From 397313a3fe3a5de5c2017142972b1c639545e4ad Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 14:12:50 +0100 Subject: [PATCH 35/59] [metrics] Add automatic stuff for complexity metrics --- docs/macros/main.py | 20 +++++ docs/macros/table_renderer.py | 75 ++++++++++++++++++- docs/metrics/index.md | 8 ++ scripts/kg_analysis/interfaces/__init__.py | 1 + scripts/kg_analysis/interfaces/complexity.py | 77 ++++++++++++++++++++ scripts/kg_analysis/metrics_runner.py | 70 +++++++++++++++++- 6 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 scripts/kg_analysis/interfaces/complexity.py diff --git a/docs/macros/main.py b/docs/macros/main.py index 64a249d..4789dae 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -94,3 +94,23 @@ def define_env(env): return renderer.render("general") except (FileNotFoundError, json.JSONDecodeError) as e: return f"*Error loading metrics: {e}*" + + @env.macro + def metrics_table_overview_complexity(): + try: + with open(Path("reports/metrics/complexity.json"), "r") as f: + renderer = MetricsTableRenderer(json.load(f)) + return renderer.render("complexity") + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" + + @env.macro + def metrics_table_single_complexity(metric_key): + try: + with open( + Path("reports/metrics/complexity.json"), "r", encoding="utf-8" + ) as f: + renderer = MetricsTableRenderer(json.load(f)) + return renderer.render("complexity", metric_key=metric_key) + except (FileNotFoundError, json.JSONDecodeError) as e: + return f"*Error loading metrics: {e}*" diff --git a/docs/macros/table_renderer.py b/docs/macros/table_renderer.py index da8ea24..a330a20 100644 --- a/docs/macros/table_renderer.py +++ b/docs/macros/table_renderer.py @@ -12,7 +12,7 @@ class MetricsTableRenderer: self.timestamp = data.get("timestamp", "unknown") self.output = [] - def render(self, table_type="general", resource_type=None): + def render(self, table_type="general", resource_type=None, metric_key=None): """ Renders the table in the desired format. @@ -31,6 +31,7 @@ class MetricsTableRenderer: "resource": lambda: self._render_resource(resource_type), "general_overview": self._render_general_overview, "general": self._render_general, + "complexity": lambda: self._render_complexity(metric_key), } # Get the appropriate rendering method based on the table type @@ -38,7 +39,7 @@ class MetricsTableRenderer: render_method() # Append the timestamp to the output - self.output.append(f"\n*Last updated: {self.timestamp}*") + self.output.append(f"\n*Last updated: {self.timestamp}*\n") return "\n".join(self.output) def _render_resource_overview(self): @@ -134,7 +135,6 @@ class MetricsTableRenderer: for metric_key, metric_data in self.data.items(): if metric_key == "timestamp": continue - if isinstance(metric_data, dict): if "files" in metric_data: self.output.append(f"| **{metric_data['name']}** | | |") @@ -146,3 +146,72 @@ class MetricsTableRenderer: self.output.append( f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result', '-')} |" ) + + def _render_complexity(self, cmplxty_type=None): + """Renders a complexity table. + + Args: + cmplxty_type (str, optional): Specific complexity type. + If None, an overview of all types is created. + """ + # Base header for all tables + header = ["Metric", "Query", "Value", "Weight", "Weighted Value"] + + # Add Category column for overview + if not cmplxty_type: + header += ["Category"] + + # Create table header + self.output.append("| " + " | ".join(header) + " |") + self.output.append("| " + " | ".join(["---" for _ in header]) + " |") + + # Select categories based on type + categories = {} + if cmplxty_type: + # Search directly or in nested structures + if cmplxty_type in self.data: + categories[cmplxty_type] = self.data[cmplxty_type] + else: + for value in self.data.values(): + if ( + isinstance(value, dict) + and "files" in value + and cmplxty_type in value["files"] + ): + categories[cmplxty_type] = { + "name": cmplxty_type, + "files": { + cmplxty_type: value["files"][cmplxty_type], + }, + } + break + else: + # Include all categories except the timestamp + categories = {k: v for k, v in self.data.items() if k != "timestamp"} + + # Iterate through the categories + for data in categories.values(): + if "files" not in data: + continue + + # Category header with appropriate number of empty columns + empty_cols = len(header) - 1 + if not cmplxty_type: + self.output.append( + f"| **{data['name']}** | " + " | " * empty_cols + "|" + ) + + for metric_data in data["files"].values(): + row = [ + str(metric_data["name"]), + str(metric_data.get("file", "-")), + str(metric_data.get("result", "-")), + str(metric_data.get("weight", "-")), + str(metric_data.get("weighted_value", "-")), + ] + + # Add Category column for overview + if not cmplxty_type: + row.insert(0, "") + + self.output.append("| " + " | ".join(row) + " |") diff --git a/docs/metrics/index.md b/docs/metrics/index.md index ec6f879..064667b 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -11,6 +11,8 @@ These metrics analyze the entire knowledge graph structure and provide insights - [Linkage](general_metrics/03_linkage_degree) - Edge statistics ([incoming](general_metrics/04_edges_incoming)/[outgoing](general_metrics/05_edges_outgoing)) +### Results + {{ metrics_table_overview_general() }} ## Resource specific Metrics @@ -25,6 +27,8 @@ These metrics focus on specific resource types within the knowledge graph, analy Each resource type is analyzed individually to understand its representation, completeness, and interconnections within the knowledge graph: +### Results + {{ metrics_table_overview_resource() }} ## Schema Complexity Metrics @@ -69,6 +73,10 @@ where _w_i_ are weights that determine the relative importance of each factor. This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. +### Results + +{{ metrics_table_overview_complexity() }} + ### References > !!!Not the final references!!! diff --git a/scripts/kg_analysis/interfaces/__init__.py b/scripts/kg_analysis/interfaces/__init__.py index 79f526c..c223999 100644 --- a/scripts/kg_analysis/interfaces/__init__.py +++ b/scripts/kg_analysis/interfaces/__init__.py @@ -1,3 +1,4 @@ +from .complexity import query_templates as complexity_query_templates from .general import query_templates as general_query_templates from .resources import query_templates as resource_query_templates from .resources import resource_types diff --git a/scripts/kg_analysis/interfaces/complexity.py b/scripts/kg_analysis/interfaces/complexity.py new file mode 100644 index 0000000..2d98193 --- /dev/null +++ b/scripts/kg_analysis/interfaces/complexity.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + +log = logging.getLogger(__name__) + +query_templates = { + "structural_complexity": { + "name": "Structural Complexity", + "files": { + "classes": { + "name": "Number of Classes", + "file": "RF_001.rq", + "execute": True, + "weight": 0.2, + }, + "properties": { + "name": "Number of properties", + "file": "RF_002_1.rq", + "execute": True, + "weight": 0.2, + }, + "depth": { + "name": "Average class hierarchy depth", + "file": "RF_003.rq", + "execute": True, + "weight": 1, + }, + "width": { + "name": "Average class hierarchy width", + "file": "RF_004.rq", + "execute": True, + "weight": 1, + }, + }, + }, + "semantic_complexity": { + "name": "Semantic Complexity", + "files": { + "restrictions": { + "name": "Number of restrictions", + "file": "RF_005.rq", + "execute": True, + "weight": 0.2, + }, + "axioms": { + "name": "Number of logical axioms", + "file": "RF_006.rq", + "execute": True, + "weight": 1, + }, + }, + }, + "schematic_complexity": { + "name": "Schematic Complexity", + "files": { + "overall_complexity": { + "execute": False, + "name": "Overall Complexity", + "result": 0, + }, + "structural_complexity": { + "name": "Structural Complexity", + "execute": False, + "result": 0, + "weight": 1, + }, + "semantic_complexity": { + "name": "Semantic Complexity", + "execute": False, + "result": 0, + "weight": 1, + }, + }, + }, +} diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 48b4141..8f12e9d 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -14,6 +14,7 @@ from typing import Optional from .query_runner import QueryRunner from .interfaces import ( + complexity_query_templates, general_query_templates, resource_query_templates, resource_types, @@ -252,6 +253,68 @@ class MetricsRunner_Resources(MetricsRunnerBase): return results +class MetricsRunner_Complexity(MetricsRunnerBase): + + query_templates = complexity_query_templates + + def _calculate_category_complexity(self, category_data): + """Calculates the weighted complexity for a category""" + total = 0.0 + for metric, data in category_data["files"].items(): + if "result" in data and data["result"] is not None: + print(data) + try: + value = float(data["result"]) + weight = float(data.get("weight", 1.0)) + weighted_value = value * weight + data["weighted_value"] = weighted_value + total += weighted_value + except (ValueError, TypeError) as e: + log.error(f"Error calculating {metric}: {e}") + return total + + def run(self): + """ + Executes complexity metrics and calculates the overall complexity. + This method runs the complexity metrics, calculates the weighted + complexity for each category, and updates the overall complexity. + Finally, it saves the results to a JSON file. + + Returns: + dict: Complexity metrics results + """ + # Execute the base run method to get initial query results + queries = super().run() + + # Extract schematic complexity data + schematic_complexity = queries["schematic_complexity"] + + # Calculate complexity for each category and update overall complexity + for category in [ + category + for category in ["structural_complexity", "semantic_complexity"] + ]: + # Calculate the weighted complexity for the current category + result = self._calculate_category_complexity(queries[category]) + # Update the result for the current category + schematic_complexity["files"][category]["result"] = result + schematic_complexity["files"][category]["weighted_value"] = ( + result * schematic_complexity["files"][category]["weight"] + ) + # Update the overall complexity with the weighted result + schematic_complexity["files"]["overall_complexity"][ + "result" + ] += schematic_complexity["files"][category]["weighted_value"] + + # Update the schematic complexity in the queries dictionary + queries["schematic_complexity"] = schematic_complexity + + # Save the results to a JSON file + self.save_to_json(queries, "reports/metrics/complexity.json") + + return queries + + class MetricsRunner(ABC): """Main entry point for executing all metrics""" @@ -260,7 +323,10 @@ class MetricsRunner(ABC): Executes both general and resource-specific metrics """ # Run general metrics for entire knowledge graph - MetricsRunner_General().run() + # MetricsRunner_General().run() # Run metrics for specific resource types - MetricsRunner_Resources().run() + # MetricsRunner_Resources().run() + + # Run metrics on schematic complexity + MetricsRunner_Complexity().run() -- GitLab From 8501ae8d0dd93cbfd533aadd3d1fb29dbcf9df96 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 14:14:10 +0100 Subject: [PATCH 36/59] [metrics] Adain renaming file structure --- docs/metrics/general metrics/00_overview.md | 10 ++ .../01_instances.md | 0 .../02_assertions.md | 0 .../03_linkage_degree.md | 0 .../04_edges_incoming.md | 0 .../05_edges_outgoing.md | 0 .../aggregator.md | 0 .../article_lhb.md | 0 .../data_service.md | 0 .../dataset.md | 0 .../learning_resource.md | 0 .../organization.md | 0 .../person.md | 0 .../publication.md | 0 .../registry.md | 0 .../repository.md | 0 .../service.md | 0 .../software.md | 0 .../standards.md | 0 .../schema_complexity_metrics/00_overview.md | 63 +++++++++++++ .../schema_complexity_metrics/01_classes.md | 4 + .../02_properties.md | 4 + .../schema_complexity_metrics/03_depth.md | 5 + .../schema_complexity_metrics/04_width.md | 4 + .../05_restrictions.md | 4 + .../schema_complexity_metrics/06_axioms.md | 4 + queries/metrics/RF_004.rq | 2 +- queries/metrics/RF_005.rq | 54 +++++------ queries/metrics/RF_006.rq | 46 +++------- queries/metrics/RF_007.rq | 21 ----- reports/metrics/complexity.json | 91 +++++++++++++++++++ 31 files changed, 233 insertions(+), 79 deletions(-) create mode 100644 docs/metrics/general metrics/00_overview.md rename docs/metrics/{general_metrics => general metrics}/01_instances.md (100%) rename docs/metrics/{general_metrics => general metrics}/02_assertions.md (100%) rename docs/metrics/{general_metrics => general metrics}/03_linkage_degree.md (100%) rename docs/metrics/{general_metrics => general metrics}/04_edges_incoming.md (100%) rename docs/metrics/{general_metrics => general metrics}/05_edges_outgoing.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/aggregator.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/article_lhb.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/data_service.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/dataset.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/learning_resource.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/organization.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/person.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/publication.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/registry.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/repository.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/service.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/software.md (100%) rename docs/metrics/{resource_metrics => resource metrics}/standards.md (100%) create mode 100644 docs/metrics/schema_complexity_metrics/00_overview.md delete mode 100644 queries/metrics/RF_007.rq create mode 100644 reports/metrics/complexity.json diff --git a/docs/metrics/general metrics/00_overview.md b/docs/metrics/general metrics/00_overview.md new file mode 100644 index 0000000..cc412a3 --- /dev/null +++ b/docs/metrics/general metrics/00_overview.md @@ -0,0 +1,10 @@ +# Overview + +These metrics analyze the entire knowledge graph structure and provide insights into: + +- Overall size and complexity +- Connection patterns +- Graph density and distribution +- Edge statistics (incoming/outgoing) + +{{ metrics_table_overview_general() }} \ No newline at end of file diff --git a/docs/metrics/general_metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md similarity index 100% rename from docs/metrics/general_metrics/01_instances.md rename to docs/metrics/general metrics/01_instances.md diff --git a/docs/metrics/general_metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md similarity index 100% rename from docs/metrics/general_metrics/02_assertions.md rename to docs/metrics/general metrics/02_assertions.md diff --git a/docs/metrics/general_metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md similarity index 100% rename from docs/metrics/general_metrics/03_linkage_degree.md rename to docs/metrics/general metrics/03_linkage_degree.md diff --git a/docs/metrics/general_metrics/04_edges_incoming.md b/docs/metrics/general metrics/04_edges_incoming.md similarity index 100% rename from docs/metrics/general_metrics/04_edges_incoming.md rename to docs/metrics/general metrics/04_edges_incoming.md diff --git a/docs/metrics/general_metrics/05_edges_outgoing.md b/docs/metrics/general metrics/05_edges_outgoing.md similarity index 100% rename from docs/metrics/general_metrics/05_edges_outgoing.md rename to docs/metrics/general metrics/05_edges_outgoing.md diff --git a/docs/metrics/resource_metrics/aggregator.md b/docs/metrics/resource metrics/aggregator.md similarity index 100% rename from docs/metrics/resource_metrics/aggregator.md rename to docs/metrics/resource metrics/aggregator.md diff --git a/docs/metrics/resource_metrics/article_lhb.md b/docs/metrics/resource metrics/article_lhb.md similarity index 100% rename from docs/metrics/resource_metrics/article_lhb.md rename to docs/metrics/resource metrics/article_lhb.md diff --git a/docs/metrics/resource_metrics/data_service.md b/docs/metrics/resource metrics/data_service.md similarity index 100% rename from docs/metrics/resource_metrics/data_service.md rename to docs/metrics/resource metrics/data_service.md diff --git a/docs/metrics/resource_metrics/dataset.md b/docs/metrics/resource metrics/dataset.md similarity index 100% rename from docs/metrics/resource_metrics/dataset.md rename to docs/metrics/resource metrics/dataset.md diff --git a/docs/metrics/resource_metrics/learning_resource.md b/docs/metrics/resource metrics/learning_resource.md similarity index 100% rename from docs/metrics/resource_metrics/learning_resource.md rename to docs/metrics/resource metrics/learning_resource.md diff --git a/docs/metrics/resource_metrics/organization.md b/docs/metrics/resource metrics/organization.md similarity index 100% rename from docs/metrics/resource_metrics/organization.md rename to docs/metrics/resource metrics/organization.md diff --git a/docs/metrics/resource_metrics/person.md b/docs/metrics/resource metrics/person.md similarity index 100% rename from docs/metrics/resource_metrics/person.md rename to docs/metrics/resource metrics/person.md diff --git a/docs/metrics/resource_metrics/publication.md b/docs/metrics/resource metrics/publication.md similarity index 100% rename from docs/metrics/resource_metrics/publication.md rename to docs/metrics/resource metrics/publication.md diff --git a/docs/metrics/resource_metrics/registry.md b/docs/metrics/resource metrics/registry.md similarity index 100% rename from docs/metrics/resource_metrics/registry.md rename to docs/metrics/resource metrics/registry.md diff --git a/docs/metrics/resource_metrics/repository.md b/docs/metrics/resource metrics/repository.md similarity index 100% rename from docs/metrics/resource_metrics/repository.md rename to docs/metrics/resource metrics/repository.md diff --git a/docs/metrics/resource_metrics/service.md b/docs/metrics/resource metrics/service.md similarity index 100% rename from docs/metrics/resource_metrics/service.md rename to docs/metrics/resource metrics/service.md diff --git a/docs/metrics/resource_metrics/software.md b/docs/metrics/resource metrics/software.md similarity index 100% rename from docs/metrics/resource_metrics/software.md rename to docs/metrics/resource metrics/software.md diff --git a/docs/metrics/resource_metrics/standards.md b/docs/metrics/resource metrics/standards.md similarity index 100% rename from docs/metrics/resource_metrics/standards.md rename to docs/metrics/resource metrics/standards.md diff --git a/docs/metrics/schema_complexity_metrics/00_overview.md b/docs/metrics/schema_complexity_metrics/00_overview.md new file mode 100644 index 0000000..27b45fd --- /dev/null +++ b/docs/metrics/schema_complexity_metrics/00_overview.md @@ -0,0 +1,63 @@ +# Overview + +To calculate the complexity of RDF schemas, we combine the presented formality metrics focusing on both structural and semantic aspects. + +### 1. Basic Structural Complexity + +These metrics measure the size and diversity of the RDF schema: + +- **[Number of classes](./schema_complexity_metrics/01_classes)** (_C_) → More classes indicate a more complex schema. +- **[Number of properties](./schema_complexity_metrics/02_properties)** (_P_) → More properties mean more relationships between entities. +- **[Average class hierarchy depth](./schema_complexity_metrics/03_depth)** (_D_avg_) → The mean number of hierarchy levels in the schema. +- **[Average class hierarchy width](./schema_complexity_metrics/04_width)** (_W_avg_) → The mean number of sibling classes at each hierarchy level. + +A structural complexity score can be calculated as: + + C_structural = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + +where _w_i_ are weights that determine the relative importance of each factor. + +### 2. Semantic Complexity + +If OWL is used, the complexity increases due to advanced semantics: + +- **[Number of restrictions](./schema_complexity_metrics/05_restrictions)** (_R_) → More restrictions indicate more constraints and rules. +- **[Number of logical axioms](./schema_complexity_metrics/06_axioms)** (_A_) → More axioms mean more logical statements and inferences. + +A semantic complexity score can be calculated as: + + C_semantic = w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +### 3. Combined Formula for Schema Complexity + +To create an overall complexity score, we can combine both structural and semantic aspects: + + C_schema = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + w5 * R + w6 * A + +where _w_i_ are weights that determine the relative importance of each factor. + +This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. + +### References + +> !!!Not the final references!!! + +1. Structural Metrics for Ontologies: + +- Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" +- Describes metrics like number of classes, hierarchy depth, and relations +- Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 + +2. OWL and Schema Complexity Measurements: + + - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" + - Develops the OntoQA model combining structural and semantic metrics + - Source: https://doi.org/10.1109/ICDEW.2005.43 + +3. SPARQL Analysis and RDF Complexity: + + - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" + - Describes hierarchical depth as key metric for RDF schema complexity + - Source: https://doi.org/10.1007/978-3-540-92673-3_10 \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/01_classes.md b/docs/metrics/schema_complexity_metrics/01_classes.md index 2b2da15..953bbaf 100644 --- a/docs/metrics/schema_complexity_metrics/01_classes.md +++ b/docs/metrics/schema_complexity_metrics/01_classes.md @@ -2,6 +2,10 @@ This metric counts the total number of classes defined in our schema. +## Results + +{{ metrics_table_single_complexity('classes') }} + ## SPARQL Query ```sparql diff --git a/docs/metrics/schema_complexity_metrics/02_properties.md b/docs/metrics/schema_complexity_metrics/02_properties.md index 6547ab8..a703c88 100644 --- a/docs/metrics/schema_complexity_metrics/02_properties.md +++ b/docs/metrics/schema_complexity_metrics/02_properties.md @@ -2,6 +2,10 @@ This metric analyzes the properties defined in our schema through multiple aspects. +## Results + +{{ metrics_table_single_complexity('properties') }} + ## Total Properties Count ```sparql diff --git a/docs/metrics/schema_complexity_metrics/03_depth.md b/docs/metrics/schema_complexity_metrics/03_depth.md index 2dc4a40..671be88 100644 --- a/docs/metrics/schema_complexity_metrics/03_depth.md +++ b/docs/metrics/schema_complexity_metrics/03_depth.md @@ -2,6 +2,11 @@ This metric calculates the average depth of the class hierarchy in the schema. +## Results + +{{ metrics_table_single_complexity('depth') }} + + ## SPARQL Query ```sparql diff --git a/docs/metrics/schema_complexity_metrics/04_width.md b/docs/metrics/schema_complexity_metrics/04_width.md index 3f0c38e..5acfb26 100644 --- a/docs/metrics/schema_complexity_metrics/04_width.md +++ b/docs/metrics/schema_complexity_metrics/04_width.md @@ -2,6 +2,10 @@ This metric calculates the average number of subclasses per class (branching factor) in the schema. +## Results + +{{ metrics_table_single_complexity('width') }} + ## SPARQL Query ```sparql diff --git a/docs/metrics/schema_complexity_metrics/05_restrictions.md b/docs/metrics/schema_complexity_metrics/05_restrictions.md index 4f27219..2c68bc9 100644 --- a/docs/metrics/schema_complexity_metrics/05_restrictions.md +++ b/docs/metrics/schema_complexity_metrics/05_restrictions.md @@ -2,6 +2,10 @@ This metric counts the various types of OWL restrictions defined in the schema. +## Results + +{{ metrics_table_single_complexity('restrictions') }} + ## SPARQL Query ```sparql diff --git a/docs/metrics/schema_complexity_metrics/06_axioms.md b/docs/metrics/schema_complexity_metrics/06_axioms.md index 16434c5..53734f6 100644 --- a/docs/metrics/schema_complexity_metrics/06_axioms.md +++ b/docs/metrics/schema_complexity_metrics/06_axioms.md @@ -2,6 +2,10 @@ This metric counts the various types of OWL logical axioms defined in the schema. +## Results + +{{ metrics_table_single_complexity('axioms') }} + ## SPARQL Query ```sparql diff --git a/queries/metrics/RF_004.rq b/queries/metrics/RF_004.rq index 51ad8ed..aa0e49d 100644 --- a/queries/metrics/RF_004.rq +++ b/queries/metrics/RF_004.rq @@ -1,4 +1,4 @@ -# This SPARQL query calculates the average branching factor of classes in +# This SPARQL query calculates the average branching factor (widht) of classes in # an RDF dataset. # The branching factor is defined as the average number of direct # subclasses per class. diff --git a/queries/metrics/RF_005.rq b/queries/metrics/RF_005.rq index 003e342..b256108 100644 --- a/queries/metrics/RF_005.rq +++ b/queries/metrics/RF_005.rq @@ -1,38 +1,40 @@ -PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> -PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> +# This SPARQL query retrieves various counts of OWL restrictions +# from a dataset, excluding built-in properties + PREFIX owl: <http://www.w3.org/2002/07/owl#> +PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT - (COUNT(DISTINCT ?class) AS ?totalClasses) - (COUNT(DISTINCT ?property) AS ?totalProperties) - (COUNT(DISTINCT ?restriction) AS ?totalRestrictions) - (?totalClasses + ?totalProperties + ?totalRestrictions AS ?complexityScore) + (COUNT(DISTINCT ?restriction) as ?totalRestrictions) + (COUNT(DISTINCT ?someValues) as ?someValuesFrom) + (COUNT(DISTINCT ?allValues) as ?allValuesFrom) + (COUNT(DISTINCT ?hasValue) as ?hasValueRestrictions) + (COUNT(DISTINCT ?cardinality) as ?cardinalityRestrictions) WHERE { { - # Count Classes - { - ?class a rdfs:Class . - } UNION { - ?class a owl:Class . - } - FILTER(!STRSTARTS(STR(?class), "http://www.w3.org/")) + ?restriction a owl:Restriction . + FILTER(!STRSTARTS(STR(?restriction), "http://www.w3.org/")) } - UNION - { - # Count Properties + OPTIONAL { + ?someValues a owl:Restriction ; + owl:someValuesFrom ?target1 . + } + OPTIONAL { + ?allValues a owl:Restriction ; + owl:allValuesFrom ?target2 . + } + OPTIONAL { + ?hasValue a owl:Restriction ; + owl:hasValue ?target3 . + } + OPTIONAL { + ?cardinality a owl:Restriction . { - ?property a rdf:Property . + ?cardinality owl:cardinality ?card1 . } UNION { - ?property a owl:ObjectProperty . + ?cardinality owl:minCardinality ?card2 . } UNION { - ?property a owl:DatatypeProperty . + ?cardinality owl:maxCardinality ?card3 . } - FILTER(!STRSTARTS(STR(?property), "http://www.w3.org/")) - } - UNION - { - # Count Restrictions - ?restriction a owl:Restriction . - FILTER(!STRSTARTS(STR(?restriction), "http://www.w3.org/")) } } \ No newline at end of file diff --git a/queries/metrics/RF_006.rq b/queries/metrics/RF_006.rq index 6a02413..2ba5613 100644 --- a/queries/metrics/RF_006.rq +++ b/queries/metrics/RF_006.rq @@ -1,37 +1,21 @@ +# This query counts the total number of distinct logical axioms in the dataset, +# excluding built-in properties. + PREFIX owl: <http://www.w3.org/2002/07/owl#> -PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT - (COUNT(DISTINCT ?restriction) as ?totalRestrictions) - (COUNT(DISTINCT ?someValues) as ?someValuesFrom) - (COUNT(DISTINCT ?allValues) as ?allValuesFrom) - (COUNT(DISTINCT ?hasValue) as ?hasValueRestrictions) - (COUNT(DISTINCT ?cardinality) as ?cardinalityRestrictions) + (COUNT(DISTINCT ?axiom) as ?totalLogicalAxioms) WHERE { - { - ?restriction a owl:Restriction . - FILTER(!STRSTARTS(STR(?restriction), "http://www.w3.org/")) - } - OPTIONAL { - ?someValues a owl:Restriction ; - owl:someValuesFrom ?target1 . - } - OPTIONAL { - ?allValues a owl:Restriction ; - owl:allValuesFrom ?target2 . - } - OPTIONAL { - ?hasValue a owl:Restriction ; - owl:hasValue ?target3 . - } - OPTIONAL { - ?cardinality a owl:Restriction . - { - ?cardinality owl:cardinality ?card1 . - } UNION { - ?cardinality owl:minCardinality ?card2 . - } UNION { - ?cardinality owl:maxCardinality ?card3 . + VALUES ?property { + owl:equivalentClass + owl:disjointWith + owl:complementOf + owl:intersectionOf + owl:unionOf } - } + + ?axiom ?property ?target . + + # Filter for built-in properties + FILTER(!STRSTARTS(STR(?axiom), "http://www.w3.org/")) } \ No newline at end of file diff --git a/queries/metrics/RF_007.rq b/queries/metrics/RF_007.rq deleted file mode 100644 index 2ba5613..0000000 --- a/queries/metrics/RF_007.rq +++ /dev/null @@ -1,21 +0,0 @@ -# This query counts the total number of distinct logical axioms in the dataset, -# excluding built-in properties. - -PREFIX owl: <http://www.w3.org/2002/07/owl#> - -SELECT - (COUNT(DISTINCT ?axiom) as ?totalLogicalAxioms) -WHERE { - VALUES ?property { - owl:equivalentClass - owl:disjointWith - owl:complementOf - owl:intersectionOf - owl:unionOf - } - - ?axiom ?property ?target . - - # Filter for built-in properties - FILTER(!STRSTARTS(STR(?axiom), "http://www.w3.org/")) -} \ No newline at end of file diff --git a/reports/metrics/complexity.json b/reports/metrics/complexity.json new file mode 100644 index 0000000..f7b6070 --- /dev/null +++ b/reports/metrics/complexity.json @@ -0,0 +1,91 @@ +{ + "structural_complexity": { + "name": "Structural Complexity", + "files": { + "classes": { + "name": "Number of Classes", + "file": "RF_001.rq", + "execute": true, + "weight": 0.2, + "result": "410", + "execution_time": 0.05, + "weighted_value": 82.0 + }, + "properties": { + "name": "Number of properties", + "file": "RF_002_1.rq", + "execute": true, + "weight": 0.2, + "result": "110", + "execution_time": 0.02, + "weighted_value": 22.0 + }, + "depth": { + "name": "Average class hierarchy depth", + "file": "RF_003.rq", + "execute": true, + "weight": 1, + "result": "3.34965034965035", + "execution_time": 0.02, + "weighted_value": 3.34965034965035 + }, + "width": { + "name": "Average class hierarchy width", + "file": "RF_004.rq", + "execute": true, + "weight": 1, + "result": "4.75", + "execution_time": 0.03, + "weighted_value": 4.75 + } + } + }, + "semantic_complexity": { + "name": "Semantic Complexity", + "files": { + "restrictions": { + "name": "Number of restrictions", + "file": "RF_005.rq", + "execute": true, + "weight": 0.2, + "result": "326", + "execution_time": 3.18, + "weighted_value": 65.2 + }, + "axioms": { + "name": "Number of logical axioms", + "file": "RF_006.rq", + "execute": true, + "weight": 1, + "result": "44", + "execution_time": 0.02, + "weighted_value": 44.0 + } + } + }, + "schematic_complexity": { + "name": "Schematic Complexity", + "files": { + "overall_complexity": { + "execute": false, + "name": "Overall Complexity", + "result": 221.29965034965034 + }, + "structural_complexity": { + "name": "Structural Complexity", + "execute": false, + "result": 112.09965034965035, + "weight": 1, + "weighted_value": 112.09965034965035 + }, + "semantic_complexity": { + "name": "Semantic Complexity", + "execute": false, + "result": 109.2, + "weight": 1, + "weighted_value": 109.2 + } + } + }, + "timestamp": "2025-03-19T10:40" +} \ No newline at end of file -- GitLab From b3ea949125b1da38a528f7855970d4684548aaa9 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 14:17:19 +0100 Subject: [PATCH 37/59] [metrics] forget *.pyc file --- docs/macros/__pycache__/main.cpython-312.pyc | Bin 5974 -> 7396 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/macros/__pycache__/main.cpython-312.pyc b/docs/macros/__pycache__/main.cpython-312.pyc index f5171ef96bc57fca46195c8c6b61980147820c96..c9a5d0a84bc4d87018d5c15c3c3186d023e43ba2 100644 GIT binary patch delta 757 zcmcbn_r#L#G%qg~0|Ntt@r+yPWr7>|^cXqx7#SGaIi@pAViM=)Okr$cTn$pdz`)SO zF^Nfxse@y36QepK*W^`<Rg;*+dD&YSU<zv4CNYT%av%w?S2AdFZ`NRX&(HXIvxHDN z^X8NMdW?qhMX3e(MJ2`hxv3>ZnaRca$@#ejIjI$yC6#(v#rb(f><kPHx400Zla&OO z7!@b4=M(2a2v(U+zQCt#h!99(U|=XNXJBAxVEDko%4vK@P;^Sl1!<ehVzxVqFAF+< zU|`}kJy1B=fL~I4LCs}xlkcodyr$nlqMty-?=NhV6Zth|icA?87?K&G-Ud-@3=9my z3=9mPB_==MSD&25%Rf1VE1YQ!+vMF`>Fn$^%(EF%I3{ay%ZGFN6<ISdFo1nfBoAUK zfCw7~1_n*WB6AR18APan2vv|M3q({9B%lQ%v_alrVPIfTP*6}P0$F=YezF6PC~|n5 z<5QXJ$HQ)l@O(B%S1Z)>>|oE|5fHf`YIa$`e24IX@-NIxLVO<>Sa^LHC+qUdNH3^4 zz%`-%vbgtmPLSAlu*@egIXRBs#8ZJ`rlF$<^Fc07M+Jt1BCL+G><1;8L2OwtsUYdZ z$$Zd~)rp1qpdF(VC-Wf|RwqtoO(s7-O-4UW##^kpiOEIznw*mtib~m9f;?smBJ4qg z2*|OVP%nbK=>YL3S4wJHW?pK1YF=5969WSSH%Q&#$!|m_vVUOWWHtUEHaSF0k(-0n z_%kyD8~;Zpj>!#TsWKp?D;bJpKt_YY>lcSjZhlH>PO4pz?_@@CAI?lhMq{uv0O&5c AEC2ui delta 233 zcmaE2c}<V+G%qg~0|Ntty5G%oQ@)LSdW;;;7#JAZ*`_m0ViM<PPho6fTn$pdz`)SO zHi=1$se^5D6QepK$K+LvRV<YZnw*;pnBMa<7H>`vDrb(>Wb*UVWc1Tyyv3TEm|T>v z$$pDDHLt8lnt_3#NC!k1fCv*1VF4mU7#J9e*%=rZ6u`itNSJ|v;TBg)YFcJqYCK4* zH3I_!H%Qgw$zIYEnLjddPUe$Il>rH_WGIpW83NMti^C>2KczG$)vm~Ga=(lZXDB11 HF<2S^Sb;R4 -- GitLab From 22665c5ae808c3a75b0746030e8a4cbee1e2f471 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:15:32 +0100 Subject: [PATCH 38/59] [metrics] forget *.pyc file --- docs/macros/__pycache__/main.cpython-312.pyc | Bin 7396 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/macros/__pycache__/main.cpython-312.pyc diff --git a/docs/macros/__pycache__/main.cpython-312.pyc b/docs/macros/__pycache__/main.cpython-312.pyc deleted file mode 100644 index c9a5d0a84bc4d87018d5c15c3c3186d023e43ba2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7396 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 Y1<T6<R#zCTpEBrwX3Jz`GzQxO0Gjlbq5uE@ -- GitLab From 9443349eb57d16f0942c4a8e72269fa15ddca596 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:16:16 +0100 Subject: [PATCH 39/59] [metrics] Enable setting reports-directory --- docs/macros/main.py | 60 +--- docs/macros/table_renderer.py | 174 +++++++---- reports/metrics/complexity.json | 6 +- reports/metrics/general.json | 24 +- reports/metrics/resources.json | 338 ++++++++++----------- scripts/kg_analysis/cli.py | 33 +- scripts/kg_analysis/interfaces/__init__.py | 5 + scripts/kg_analysis/metrics_runner.py | 39 ++- 8 files changed, 369 insertions(+), 310 deletions(-) diff --git a/docs/macros/main.py b/docs/macros/main.py index 4789dae..54bf058 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -7,6 +7,8 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) from table_renderer import MetricsTableRenderer +DEFAULT_REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports")) + def define_env(env): @env.macro @@ -54,63 +56,31 @@ def define_env(env): @env.macro def metrics_table_single_resource(resource_type=None): - try: - with open( - Path("reports/metrics/resources.json"), "r", encoding="utf-8" - ) as f: - renderer = MetricsTableRenderer(json.load(f)) - return renderer.render("resource", resource_type) - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + return MetricsTableRenderer( + table_type="resource", resource_type=resource_type + ).render() @env.macro def metrics_table_overview_resource(): - try: - with open( - Path("reports/metrics/resources.json"), "r", encoding="utf-8" - ) as f: - renderer = MetricsTableRenderer(json.load(f)) - return renderer.render("resource_overview") - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + return MetricsTableRenderer(table_type="resource_overview").render() @env.macro def metrics_table_overview_general(): - try: - with open(Path("reports/metrics/general.json"), "r") as f: - renderer = MetricsTableRenderer(json.load(f)) - return renderer.render("general") - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + return MetricsTableRenderer(table_type="general").render() @env.macro def metrics_table_single_general(metric_key): - try: - with open(Path("reports/metrics/general.json"), "r") as f: - metrics = json.load(f) - if metric_key not in metrics: - return f"*No data for metric: {metric_key}*" - renderer = MetricsTableRenderer({metric_key: metrics[metric_key]}) - return renderer.render("general") - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + print(metric_key) + return MetricsTableRenderer( + table_type="general", metric_key=metric_key + ).render() @env.macro def metrics_table_overview_complexity(): - try: - with open(Path("reports/metrics/complexity.json"), "r") as f: - renderer = MetricsTableRenderer(json.load(f)) - return renderer.render("complexity") - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + return MetricsTableRenderer(table_type="complexity").render() @env.macro def metrics_table_single_complexity(metric_key): - try: - with open( - Path("reports/metrics/complexity.json"), "r", encoding="utf-8" - ) as f: - renderer = MetricsTableRenderer(json.load(f)) - return renderer.render("complexity", metric_key=metric_key) - except (FileNotFoundError, json.JSONDecodeError) as e: - return f"*Error loading metrics: {e}*" + return MetricsTableRenderer( + table_type="complexity", metric_key=metric_key + ).render() diff --git a/docs/macros/table_renderer.py b/docs/macros/table_renderer.py index a330a20..b7d19bc 100644 --- a/docs/macros/table_renderer.py +++ b/docs/macros/table_renderer.py @@ -1,18 +1,82 @@ +import json +import os + +from pathlib import Path + +REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports/metrics")) + + class MetricsTableRenderer: """Class for rendering metric tables in various formats.""" - def __init__(self, data): + def __init__( + self, + data=None, + table_type="general", + resource_type=None, + metric_key=None, + ): """ Initializes the renderer with metric data. Args: data (dict): The JSON data containing the metrics """ - self.data = data - self.timestamp = data.get("timestamp", "unknown") + self._data = data + self.table_type = table_type + self.resource_type = resource_type + self.metric_key = metric_key self.output = [] - def render(self, table_type="general", resource_type=None, metric_key=None): + def open_file(self): + """Opens the file and returns the content.""" + try: + with open( + REPORTS_DIR.joinpath(self.table_option["report"]), + "r", + encoding="utf-8", + ) as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + raise f"*Error loading metrics: {e}*" + + @property + def data(self): + if not self._data: + self._data = self.open_file() + return self._data + + @property + def timestamp(self): + return self.data.get("timestamp", "unknown") + + @property + def table_option(self): + table_options = { + "resource_overview": { + "method": self._render_resource_overview, + "report": "resources.json", + }, + "resource": { + "method": lambda: self._render_resource(self.resource_type), + "report": "resources.json", + }, + # "general_overview": { + # "method": self._render_general, + # "report": "general.json", + # }, + "general": { + "method": self._render_general, + "report": "general.json", + }, + "complexity": { + "method": self._render_complexity, + "report": "complexity.json", + }, + } + return table_options[self.table_type] + + def render(self): """ Renders the table in the desired format. @@ -26,17 +90,9 @@ class MetricsTableRenderer: self.output = [] # Dictionary mapping table types to their corresponding rendering methods - render_methods = { - "resource_overview": self._render_resource_overview, - "resource": lambda: self._render_resource(resource_type), - "general_overview": self._render_general_overview, - "general": self._render_general, - "complexity": lambda: self._render_complexity(metric_key), - } # Get the appropriate rendering method based on the table type - render_method = render_methods.get(table_type, self._render_general) - render_method() + self.table_option["method"]() # Append the timestamp to the output self.output.append(f"\n*Last updated: {self.timestamp}*\n") @@ -97,34 +153,34 @@ class MetricsTableRenderer: f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" ) - def _render_general_overview(self): - """Renders the general overview table.""" - # Get the sorted list of resource types - resource_types = sorted([rt for rt in self.data.keys() if rt != "timestamp"]) - metric_names = [] + # def _render_general_overview(self): + # """Renders the general overview table.""" + # # Get the sorted list of resource types + # resource_types = sorted([rt for rt in self.data.keys() if rt != "timestamp"]) + # metric_names = [] - # Collect metric names from the first resource type - first_rt = self.data[resource_types[0]] - for query in first_rt.get("queries", {}).values(): - if "name" in query: - metric_names.append(query["name"]) - - # Construct the table header - self.output.append("| Resource Type | " + " | ".join(metric_names) + " |") - self.output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") - - # Iterate over each resource type to collect results - for rt in resource_types: - row = [rt] - queries = self.data[rt].get("queries", {}) - for metric in metric_names: - result = "-" - for q in queries.values(): - if q.get("name") == metric: - result = str(q.get("result", "-")) - break - row.append(result) - self.output.append("| " + " | ".join(row) + " |") + # # Collect metric names from the first resource type + # first_rt = self.data[resource_types[0]] + # for query in first_rt.get("queries", {}).values(): + # if "name" in query: + # metric_names.append(query["name"]) + + # # Construct the table header + # self.output.append("| Resource Type | " + " | ".join(metric_names) + " |") + # self.output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") + + # # Iterate over each resource type to collect results + # for rt in resource_types: + # row = [rt] + # queries = self.data[rt].get("queries", {}) + # for metric in metric_names: + # result = "-" + # for q in queries.values(): + # if q.get("name") == metric: + # result = str(q.get("result", "-")) + # break + # row.append(result) + # self.output.append("| " + " | ".join(row) + " |") def _render_general(self): """Renders the general table.""" @@ -132,7 +188,12 @@ class MetricsTableRenderer: self.output.append("|--------|-------|--------|") # Iterate over each metric data to construct the table rows - for metric_key, metric_data in self.data.items(): + data = ( + {self.metric_key: self.data[self.metric_key]} + if self.metric_key + else self.data + ) + for metric_key, metric_data in data.items(): if metric_key == "timestamp": continue if isinstance(metric_data, dict): @@ -147,18 +208,15 @@ class MetricsTableRenderer: f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result', '-')} |" ) - def _render_complexity(self, cmplxty_type=None): - """Renders a complexity table. - - Args: - cmplxty_type (str, optional): Specific complexity type. - If None, an overview of all types is created. - """ + def _render_complexity(self): + """Renders a complexity table.""" # Base header for all tables header = ["Metric", "Query", "Value", "Weight", "Weighted Value"] + metric_type = self.metric_key + # Add Category column for overview - if not cmplxty_type: + if not metric_type: header += ["Category"] # Create table header @@ -167,21 +225,21 @@ class MetricsTableRenderer: # Select categories based on type categories = {} - if cmplxty_type: + if metric_type: # Search directly or in nested structures - if cmplxty_type in self.data: - categories[cmplxty_type] = self.data[cmplxty_type] + if metric_type in self.data: + categories[metric_type] = self.data[metric_type] else: for value in self.data.values(): if ( isinstance(value, dict) and "files" in value - and cmplxty_type in value["files"] + and metric_type in value["files"] ): - categories[cmplxty_type] = { - "name": cmplxty_type, + categories[metric_type] = { + "name": metric_type, "files": { - cmplxty_type: value["files"][cmplxty_type], + metric_type: value["files"][metric_type], }, } break @@ -196,7 +254,7 @@ class MetricsTableRenderer: # Category header with appropriate number of empty columns empty_cols = len(header) - 1 - if not cmplxty_type: + if not metric_type: self.output.append( f"| **{data['name']}** | " + " | " * empty_cols + "|" ) @@ -211,7 +269,7 @@ class MetricsTableRenderer: ] # Add Category column for overview - if not cmplxty_type: + if not metric_type: row.insert(0, "") self.output.append("| " + " | ".join(row) + " |") diff --git a/reports/metrics/complexity.json b/reports/metrics/complexity.json index f7b6070..aee2df8 100644 --- a/reports/metrics/complexity.json +++ b/reports/metrics/complexity.json @@ -26,7 +26,7 @@ "execute": true, "weight": 1, "result": "3.34965034965035", - "execution_time": 0.02, + "execution_time": 0.03, "weighted_value": 3.34965034965035 }, "width": { @@ -49,7 +49,7 @@ "execute": true, "weight": 0.2, "result": "326", - "execution_time": 3.18, + "execution_time": 0.02, "weighted_value": 65.2 }, "axioms": { @@ -87,5 +87,5 @@ } } }, - "timestamp": "2025-03-19T10:40" + "timestamp": "2025-03-19T14:41" } \ No newline at end of file diff --git a/reports/metrics/general.json b/reports/metrics/general.json index 511a23d..1128352 100644 --- a/reports/metrics/general.json +++ b/reports/metrics/general.json @@ -4,21 +4,21 @@ "file": "GM001.rq", "execute": true, "result": "1252089", - "execution_time": 1.06 + "execution_time": 0.43 }, "assertions": { "name": "Assertions Count", "file": "GM002_1.rq", "execute": true, "result": "40786353", - "execution_time": 0.01 + "execution_time": 0.24 }, "linkage": { "name": "Average Linkage Degree", "file": "GM003_2.rq", "execute": true, "result": "3.24508957824795", - "execution_time": 0.01 + "execution_time": 23.04 }, "edges_out": { "name": "Edges - outgoing", @@ -28,14 +28,14 @@ "file": "GM004_2.rq", "execute": true, "result": "6705836", - "execution_time": 0.01 + "execution_time": 13.4 }, "min": { "name": "Minimum of outgoing edges", "file": "GM004_5.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 23.76 }, "median": { "name": "Median of outgoing edges", @@ -45,14 +45,14 @@ }, "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 31.02 }, "max": { "name": "Maximum of outgoing edges", "file": "GM004_6.rq", "execute": true, "result": "282499", - "execution_time": 0.01 + "execution_time": 24.42 } } }, @@ -64,14 +64,14 @@ "file": "GM005_2.rq", "execute": true, "result": "15111836", - "execution_time": 24.99 + "execution_time": 25.74 }, "min": { "name": "Minimum of incoming edges", "file": "GM005_5.rq", "execute": true, "result": "1", - "execution_time": 21.37 + "execution_time": 21.76 }, "median": { "name": "Median of incoming edges", @@ -81,16 +81,16 @@ }, "execute": true, "result": "1", - "execution_time": 35.6 + "execution_time": 36.21 }, "max": { "name": "Maximum of incoming edges", "file": "GM005_6.rq", "execute": true, "result": "1121975", - "execution_time": 22.57 + "execution_time": 22.61 } } }, - "timestamp": "2025-03-17T15:15" + "timestamp": "2025-03-18T10:45" } \ No newline at end of file diff --git a/reports/metrics/resources.json b/reports/metrics/resources.json index 9e34292..3015d03 100644 --- a/reports/metrics/resources.json +++ b/reports/metrics/resources.json @@ -5,21 +5,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "724903", - "execution_time": 0.01 + "execution_time": 0.42 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "724904", - "execution_time": 0.01 + "execution_time": 0.28 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "68", - "execution_time": 0.01 + "execution_time": 0.22 }, "edges_out": { "name": "Edges - outgoing", @@ -29,14 +29,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "724903", - "execution_time": 0.01 + "execution_time": 4.67 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "6", - "execution_time": 0.01 + "execution_time": 0.83 }, "median": { "name": "Median of outgoing edges", @@ -46,14 +46,14 @@ }, "execute": true, "result": "23", - "execution_time": 0.01 + "execution_time": 1.5 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "12519", - "execution_time": 0.01 + "execution_time": 1.26 } } }, @@ -65,14 +65,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "4", - "execution_time": 0.01 + "execution_time": 1.32 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of incoming edges", @@ -82,14 +82,14 @@ }, "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.22 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "107", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -98,10 +98,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "427594", - "execution_time": 0.01 + "execution_time": 4.48 }, - "timestamp": "2025-03-17T15:15", - "file": "06_dataset.md" + "timestamp": "2025-03-18T10:45", + "file": "dataset.md" }, "publication": { "instances": { @@ -109,21 +109,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.22 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "6", - "execution_time": 0.01 + "execution_time": 0.22 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.01 + "execution_time": 0.19 }, "edges_out": { "name": "Edges - outgoing", @@ -133,14 +133,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.19 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "7", - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of outgoing edges", @@ -150,14 +150,14 @@ }, "execute": true, "result": "9", - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "11", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -169,14 +169,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.01 + "execution_time": 0.22 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of incoming edges", @@ -186,14 +186,14 @@ }, "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -202,10 +202,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "15", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "07_publication.md" + "timestamp": "2025-03-18T10:45", + "file": "publication.md" }, "learning_resource": { "instances": { @@ -213,21 +213,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "512", - "execution_time": 0.01 + "execution_time": 0.21 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "513", - "execution_time": 0.01 + "execution_time": 0.19 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "25", - "execution_time": 0.01 + "execution_time": 0.2 }, "edges_out": { "name": "Edges - outgoing", @@ -237,14 +237,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "512", - "execution_time": 0.01 + "execution_time": 0.19 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "10", - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of outgoing edges", @@ -254,14 +254,14 @@ }, "execute": true, "result": "15", - "execution_time": 0.01 + "execution_time": 0.21 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "27", - "execution_time": 0.01 + "execution_time": 0.2 } } }, @@ -273,14 +273,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "3", - "execution_time": 1.02 + "execution_time": 0.2 }, "median": { "name": "Median of incoming edges", @@ -290,14 +290,14 @@ }, "execute": true, "result": "3", - "execution_time": 0.01 + "execution_time": 0.1 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "3", - "execution_time": 0.01 + "execution_time": 0.19 } } }, @@ -306,10 +306,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "530", - "execution_time": 0.01 + "execution_time": 0.19 }, - "timestamp": "2025-03-17T15:15", - "file": "08_learning_resource.md" + "timestamp": "2025-03-18T10:46", + "file": "learning_resource.md" }, "repository": { "instances": { @@ -317,21 +317,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "159", - "execution_time": 0.01 + "execution_time": 0.19 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "162", - "execution_time": 0.01 + "execution_time": 0.19 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "90212", - "execution_time": 1.03 + "execution_time": 0.22 }, "edges_out": { "name": "Edges - outgoing", @@ -341,14 +341,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "159", - "execution_time": 0.01 + "execution_time": 0.1 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "7", - "execution_time": 0.01 + "execution_time": 0.17 }, "median": { "name": "Median of outgoing edges", @@ -358,14 +358,14 @@ }, "execute": true, "result": "44", - "execution_time": 0.01 + "execution_time": 0.21 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "92", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -377,14 +377,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.22 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1417", - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of incoming edges", @@ -394,14 +394,14 @@ }, "execute": true, "result": "6718", - "execution_time": 0.01 + "execution_time": 0.22 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "432941", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -410,10 +410,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "825", - "execution_time": 0.01 + "execution_time": 0.2 }, - "timestamp": "2025-03-17T15:15", - "file": "09_repository.md" + "timestamp": "2025-03-18T10:46", + "file": "repository.md" }, "article_lhb": { "instances": { @@ -421,21 +421,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "114", - "execution_time": 0.01 + "execution_time": 0.22 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "117", - "execution_time": 0.01 + "execution_time": 0.21 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "33.245098039215686", - "execution_time": 0.01 + "execution_time": 0.21 }, "edges_out": { "name": "Edges - outgoing", @@ -445,14 +445,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "114", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "11", - "execution_time": 0.01 + "execution_time": 0.2 }, "median": { "name": "Median of outgoing edges", @@ -462,14 +462,14 @@ }, "execute": true, "result": "30", - "execution_time": 0.01 + "execution_time": 0.19 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "62", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -481,14 +481,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "102", - "execution_time": 0.01 + "execution_time": 0.14 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.15 }, "median": { "name": "Median of incoming edges", @@ -498,14 +498,14 @@ }, "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "24", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -514,10 +514,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "592", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "10_article_lhb.md" + "timestamp": "2025-03-18T10:46", + "file": "article_lhb.md" }, "standards": { "instances": { @@ -525,21 +525,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "89", - "execution_time": 0.01 + "execution_time": 0.22 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "101", - "execution_time": 0.01 + "execution_time": 0.13 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "13.714285714285714", - "execution_time": 1.02 + "execution_time": 0.15 }, "edges_out": { "name": "Edges - outgoing", @@ -549,14 +549,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "89", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.19 }, "median": { "name": "Median of outgoing edges", @@ -566,14 +566,14 @@ }, "execute": true, "result": "9", - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "27", - "execution_time": 0.01 + "execution_time": 0.1 } } }, @@ -585,14 +585,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "77", - "execution_time": 0.01 + "execution_time": 0.19 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.2 }, "median": { "name": "Median of incoming edges", @@ -602,14 +602,14 @@ }, "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.19 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "56", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -618,10 +618,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "100", - "execution_time": 0.01 + "execution_time": 1.2 }, - "timestamp": "2025-03-17T15:15", - "file": "11_standards.md" + "timestamp": "2025-03-18T10:46", + "file": "standards.md" }, "software": { "instances": { @@ -629,21 +629,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "147", - "execution_time": 0.01 + "execution_time": 0.2 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "148", - "execution_time": 0.01 + "execution_time": 0.22 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "11.5", - "execution_time": 0.01 + "execution_time": 0.23 }, "edges_out": { "name": "Edges - outgoing", @@ -653,14 +653,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "147", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "7", - "execution_time": 0.01 + "execution_time": 0.17 }, "median": { "name": "Median of outgoing edges", @@ -670,14 +670,14 @@ }, "execute": true, "result": "20", - "execution_time": 0.01 + "execution_time": 0.22 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "122", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -689,14 +689,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "8", - "execution_time": 0.01 + "execution_time": 0.22 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.1 }, "median": { "name": "Median of incoming edges", @@ -706,14 +706,14 @@ }, "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.17 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.19 } } }, @@ -722,10 +722,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "1103", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "13_software.md" + "timestamp": "2025-03-18T10:46", + "file": "software.md" }, "service": { "instances": { @@ -733,21 +733,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.24 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.19 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.01 + "execution_time": 0.22 }, "edges_out": { "name": "Edges - outgoing", @@ -757,14 +757,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.1 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "14", - "execution_time": 0.01 + "execution_time": 0.2 }, "median": { "name": "Median of outgoing edges", @@ -774,14 +774,14 @@ }, "execute": true, "result": "14", - "execution_time": 0.01 + "execution_time": 0.19 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "14", - "execution_time": 0.01 + "execution_time": 0.16 } } }, @@ -793,14 +793,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.01 + "execution_time": 0.12 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of incoming edges", @@ -810,14 +810,14 @@ }, "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.19 } } }, @@ -826,10 +826,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "13", - "execution_time": 1.04 + "execution_time": 0.19 }, - "timestamp": "2025-03-17T15:15", - "file": "14_service.md" + "timestamp": "2025-03-18T10:46", + "file": "service.md" }, "data_service": { "instances": { @@ -837,21 +837,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "363632", - "execution_time": 0.01 + "execution_time": 0.25 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "363633", - "execution_time": 0.01 + "execution_time": 0.2 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.01 + "execution_time": 1.2 }, "edges_out": { "name": "Edges - outgoing", @@ -861,14 +861,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "363632", - "execution_time": 0.01 + "execution_time": 1.94 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "11", - "execution_time": 0.01 + "execution_time": 0.6 }, "median": { "name": "Median of outgoing edges", @@ -878,14 +878,14 @@ }, "execute": true, "result": "28", - "execution_time": 0.01 + "execution_time": 0.99 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "12519", - "execution_time": 0.01 + "execution_time": 0.64 } } }, @@ -897,14 +897,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.01 + "execution_time": 0.24 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 }, "median": { "name": "Median of incoming edges", @@ -914,14 +914,14 @@ }, "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.19 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -930,10 +930,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "58909", - "execution_time": 0.02 + "execution_time": 2.42 }, - "timestamp": "2025-03-17T15:15", - "file": "15_data_service.md" + "timestamp": "2025-03-18T10:46", + "file": "data_service.md" }, "aggregator": { "instances": { @@ -941,21 +941,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "38", - "execution_time": 0.01 + "execution_time": 0.1 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "39", - "execution_time": 0.01 + "execution_time": 0.12 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "187", - "execution_time": 1.05 + "execution_time": 0.2 }, "edges_out": { "name": "Edges - outgoing", @@ -965,14 +965,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "38", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "8", - "execution_time": 0.02 + "execution_time": 0.16 }, "median": { "name": "Median of outgoing edges", @@ -982,14 +982,14 @@ }, "execute": true, "result": "36", - "execution_time": 0.02 + "execution_time": 0.22 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "57", - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -1001,14 +1001,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.22 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "151", - "execution_time": 0.02 + "execution_time": 0.1 }, "median": { "name": "Median of incoming edges", @@ -1018,14 +1018,14 @@ }, "execute": true, "result": "151", - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "151", - "execution_time": 0.01 + "execution_time": 0.21 } } }, @@ -1034,10 +1034,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "239", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "16_aggregator.md" + "timestamp": "2025-03-18T10:46", + "file": "aggregator.md" }, "person": { "instances": { @@ -1045,21 +1045,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "2180", - "execution_time": 0.01 + "execution_time": 0.22 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "2181", - "execution_time": 0.01 + "execution_time": 0.22 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": "4.628269848554383", - "execution_time": 0.01 + "execution_time": 0.2 }, "edges_out": { "name": "Edges - outgoing", @@ -1069,14 +1069,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "2180", - "execution_time": 0.01 + "execution_time": 0.21 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.14 }, "median": { "name": "Median of outgoing edges", @@ -1086,14 +1086,14 @@ }, "execute": true, "result": "4", - "execution_time": 0.01 + "execution_time": 0.23 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "7", - "execution_time": 0.01 + "execution_time": 0.21 } } }, @@ -1105,14 +1105,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "2179", - "execution_time": 0.01 + "execution_time": 0.19 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.19 }, "median": { "name": "Median of incoming edges", @@ -1122,14 +1122,14 @@ }, "execute": true, "result": "1", - "execution_time": 0.01 + "execution_time": 0.18 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.19 } } }, @@ -1138,10 +1138,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "2", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "17_person.md" + "timestamp": "2025-03-18T10:46", + "file": "person.md" }, "registry": { "instances": { @@ -1149,21 +1149,21 @@ "file": "RM001_instances_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.21 }, "assertions": { "name": "Number of Assertions", "file": "RM002_assertions_template.rq", "execute": true, "result": "6", - "execution_time": 0.01 + "execution_time": 0.2 }, "linkage": { "name": "Average Linkage", "file": "RM003_linkage_template.rq", "execute": true, "result": 0, - "execution_time": 0.01 + "execution_time": 0.22 }, "edges_out": { "name": "Edges - outgoing", @@ -1173,14 +1173,14 @@ "file": "RM004_1_out_edges_total_template.rq", "execute": true, "result": "5", - "execution_time": 0.01 + "execution_time": 0.1 }, "min": { "name": "Minimum of outgoing edges", "file": "RM004_2_out_edges_min_template.rq", "execute": true, "result": "7", - "execution_time": 0.01 + "execution_time": 0.19 }, "median": { "name": "Median of outgoing edges", @@ -1190,14 +1190,14 @@ }, "execute": true, "result": "9", - "execution_time": 0.01 + "execution_time": 0.19 }, "max": { "name": "Maximum of outgoing edges", "file": "RM004_4_out_edges_max_template.rq", "execute": true, "result": "11", - "execution_time": 0.01 + "execution_time": 0.16 } } }, @@ -1209,14 +1209,14 @@ "file": "RM005_1_in_edges_total_template.rq", "execute": true, "result": "0", - "execution_time": 0.01 + "execution_time": 0.13 }, "min": { "name": "Minimum of incoming edges", "file": "RM005_2_in_edges_min_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.2 }, "median": { "name": "Median of incoming edges", @@ -1226,14 +1226,14 @@ }, "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.2 }, "max": { "name": "Maximum of incoming edges", "file": "RM005_4_in_edges_max_template.rq", "execute": true, "result": null, - "execution_time": 0.01 + "execution_time": 0.22 } } }, @@ -1242,10 +1242,10 @@ "file": "RM006_connectivity_template.rq", "execute": true, "result": "15", - "execution_time": 0.01 + "execution_time": 0.22 }, - "timestamp": "2025-03-17T15:15", - "file": "18_registry.md" + "timestamp": "2025-03-18T10:46", + "file": "registry.md" }, - "timestamp": "2025-03-17T15:15" + "timestamp": "2025-03-18T10:46" } \ No newline at end of file diff --git a/scripts/kg_analysis/cli.py b/scripts/kg_analysis/cli.py index 989caf5..0d06351 100644 --- a/scripts/kg_analysis/cli.py +++ b/scripts/kg_analysis/cli.py @@ -12,16 +12,20 @@ from .util import cli_startup from .query_runner import QueryRunner # type: ignore from .metrics_runner import MetricsRunner # type: ignore +from .interfaces import DEFAULT_REPORTS_DIR + log = logging.getLogger(__name__) @click.group() @click.option("--debug/--no-debug", "-d", is_flag=True, default=False) +@click.option("--output", "-o", type=click.Path(), help="Path to save results") @click.pass_context -def main(ctx, debug): +def main(ctx, debug, output): cli_startup(log_level=debug and logging.DEBUG or logging.INFO) ctx.ensure_object(dict) ctx.obj["DEBUG"] = debug + ctx.obj["output"] = output @main.command() @@ -32,22 +36,29 @@ def main(ctx, debug): help="Path to SPARQL query file", required=True, ) -@click.option("--output", "-o", type=click.Path(), help="Path to save results") @click.pass_context -def query(ctx, query, output): +def query(ctx, query): """Run analysis on the KnowledgeGraph.""" runner = QueryRunner() query_path = Path(query) try: - results = runner.run_metric( - query_path, output_path=Path(output) if output else None + output_path = ( + Path(ctx.obj["output"]) + if ctx.obj["output"] + else DEFAULT_REPORTS_DIR + / "queries" + / query_path.name.replace(".rq", ".json") ) + output_path.parent.mkdir(parents=True, exist_ok=True) + + results = runner.run_metric(query_path, output_path=output_path) + click.echo( click.style("Query executed successfully: ", fg="green") + click.style(query_path.name, fg="blue") ) - if not output: + if not ctx.obj["output"]: print_json(data=results) except Exception as e: click.echo( @@ -56,8 +67,14 @@ def query(ctx, query, output): @main.command() -def metrics(): - MetricsRunner().run() +@click.pass_context +def metrics(ctx): + """Run all metrics and save reports.""" + output_path = ( + Path(ctx.obj["output"]) if ctx.obj["output"] else DEFAULT_REPORTS_DIR + ) + output_path.mkdir(parents=True, exist_ok=True) + MetricsRunner(output_dir=output_path).run() if __name__ == "__main__": diff --git a/scripts/kg_analysis/interfaces/__init__.py b/scripts/kg_analysis/interfaces/__init__.py index c223999..79b755f 100644 --- a/scripts/kg_analysis/interfaces/__init__.py +++ b/scripts/kg_analysis/interfaces/__init__.py @@ -2,3 +2,8 @@ from .complexity import query_templates as complexity_query_templates from .general import query_templates as general_query_templates from .resources import query_templates as resource_query_templates from .resources import resource_types + +from pathlib import Path +import os + +DEFAULT_REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports")) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 8f12e9d..b63da0b 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -18,6 +18,7 @@ from .interfaces import ( general_query_templates, resource_query_templates, resource_types, + DEFAULT_REPORTS_DIR, ) log = logging.getLogger(__name__) @@ -36,10 +37,14 @@ class MetricsRunnerBase(ABC): _execution_time: float = 0 fail: bool = False - def __init__(self): + def __init__(self, output_dir=None): self.runner = QueryRunner() self.base_query_path = Path("queries/metrics") - self.base_output_path = Path("reports/metrics") + self.base_output_path = ( + Path(output_dir) if output_dir else DEFAULT_REPORTS_DIR + ) + print(self.base_output_path) + self.base_output_path.mkdir(parents=True, exist_ok=True) log.info(f"Running metric: {self.__class__.__name__}") @property @@ -55,7 +60,8 @@ class MetricsRunnerBase(ABC): def get_output_path(self, suffix: str = ".txt"): filename = self._output_file or self.query_file.with_suffix(suffix) - return self.base_output_path.joinpath(filename) + p = self.base_output_path.joinpath(filename) + return p @property def output_path(self): @@ -66,9 +72,10 @@ class MetricsRunnerBase(ABC): def output_path_txt(self): return self.get_output_path(suffix=".txt") - @property - def output_path_json(self): - return self.get_output_path(suffix=".json") + # @property + # def output_path_json(self): + # breakpoint() + # return self.get_output_path(suffix=".json") def query_metric(self, query_path: Path, **kwargs) -> Optional[int]: """Run a metric query and the result""" @@ -101,7 +108,7 @@ class MetricsRunnerBase(ABC): ) log.info(f"Results saved to {self.output_path_txt}") - def save_to_json(self, dictionary, filename: Optional[str] = None): + def save_to_json(self, dictionary: dict, filename: str): """ Saves results dictionary to a JSON file with proper formatting @@ -110,7 +117,7 @@ class MetricsRunnerBase(ABC): filename (str, optional): Target filename, uses default if None """ with open( - filename or self.output_path_json, "w", encoding="utf-8" + self.base_output_path.joinpath(filename), "w", encoding="utf-8" ) as f: json.dump(dictionary, f, indent=4, ensure_ascii=False) @@ -223,7 +230,7 @@ class MetricsRunner_General(MetricsRunnerBase): Returns: dict: General metrics results """ - output_path = "reports/metrics/general.json" + output_path = "general.json" queries = super().run() self.save_to_json(queries, output_path) return queries @@ -249,7 +256,7 @@ class MetricsRunner_Resources(MetricsRunnerBase): results[resource_type]["file"] = data["file"] results["timestamp"] = datetime.now().isoformat(timespec="minutes") - self.save_to_json(results, "reports/metrics/resources.json") + self.save_to_json(results, "resources.json") return results @@ -310,23 +317,25 @@ class MetricsRunner_Complexity(MetricsRunnerBase): queries["schematic_complexity"] = schematic_complexity # Save the results to a JSON file - self.save_to_json(queries, "reports/metrics/complexity.json") - + self.save_to_json(queries, "complexity.json") return queries class MetricsRunner(ABC): """Main entry point for executing all metrics""" + def __init__(self, output_dir=None): + self.output_dir = output_dir + def run(self): """ Executes both general and resource-specific metrics """ # Run general metrics for entire knowledge graph - # MetricsRunner_General().run() + # MetricsRunner_General(output_dir=self.output_dir).run() # Run metrics for specific resource types - # MetricsRunner_Resources().run() + # MetricsRunner_Resources(output_dir=self.output_dir).run() # Run metrics on schematic complexity - MetricsRunner_Complexity().run() + MetricsRunner_Complexity(output_dir=self.output_dir).run() -- GitLab From 4d814df7aee55623f6b485f1943ec05e864f3744 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:23:38 +0100 Subject: [PATCH 40/59] [metrics] move MetricsTableRenderer to package + use only REPORTS_DIR --- docs/macros/main.py | 11 +++------- scripts/kg_analysis/__init__.py | 9 +++++--- scripts/kg_analysis/cli.py | 9 ++++---- scripts/kg_analysis/interfaces/__init__.py | 5 ----- scripts/kg_analysis/metrics_runner.py | 8 +++---- .../kg_analysis}/table_renderer.py | 22 ++++++++++++++----- 6 files changed, 32 insertions(+), 32 deletions(-) rename {docs/macros => scripts/kg_analysis}/table_renderer.py (95%) diff --git a/docs/macros/main.py b/docs/macros/main.py index 54bf058..76ce0f8 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -1,13 +1,8 @@ -import json -import os -import sys -from pathlib import Path +import logging -sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from kg_analysis.table_renderer import MetricsTableRenderer # type: ignore -from table_renderer import MetricsTableRenderer - -DEFAULT_REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports")) +log = logging.getLogger(__name__) def define_env(env): diff --git a/scripts/kg_analysis/__init__.py b/scripts/kg_analysis/__init__.py index ae88787..231e26c 100644 --- a/scripts/kg_analysis/__init__.py +++ b/scripts/kg_analysis/__init__.py @@ -1,8 +1,9 @@ import os -from rich.console import Console -from rich import inspect -from rich import print as rprint +from pathlib import Path +from rich.console import Console # type: ignore +from rich import inspect # type: ignore +from rich import print as rprint # type: ignore console = Console() @@ -10,3 +11,5 @@ SPARQL_ENDPOINT = os.getenv( "SPARQL_ENDPOINT", "https://sparql.knowledgehub.nfdi4earth.de" ) SPARQL_TIMEOUT = int(os.getenv("SPARQL_TIMEOUT", 120)) + +REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports/metrics")) diff --git a/scripts/kg_analysis/cli.py b/scripts/kg_analysis/cli.py index 0d06351..f6a1de7 100644 --- a/scripts/kg_analysis/cli.py +++ b/scripts/kg_analysis/cli.py @@ -8,11 +8,12 @@ import click from pathlib import Path from rich import print_json +from . import REPORTS_DIR + from .util import cli_startup from .query_runner import QueryRunner # type: ignore from .metrics_runner import MetricsRunner # type: ignore -from .interfaces import DEFAULT_REPORTS_DIR log = logging.getLogger(__name__) @@ -46,7 +47,7 @@ def query(ctx, query): output_path = ( Path(ctx.obj["output"]) if ctx.obj["output"] - else DEFAULT_REPORTS_DIR + else REPORTS_DIR / "queries" / query_path.name.replace(".rq", ".json") ) @@ -70,9 +71,7 @@ def query(ctx, query): @click.pass_context def metrics(ctx): """Run all metrics and save reports.""" - output_path = ( - Path(ctx.obj["output"]) if ctx.obj["output"] else DEFAULT_REPORTS_DIR - ) + output_path = Path(ctx.obj["output"]) if ctx.obj["output"] else REPORTS_DIR output_path.mkdir(parents=True, exist_ok=True) MetricsRunner(output_dir=output_path).run() diff --git a/scripts/kg_analysis/interfaces/__init__.py b/scripts/kg_analysis/interfaces/__init__.py index 79b755f..c223999 100644 --- a/scripts/kg_analysis/interfaces/__init__.py +++ b/scripts/kg_analysis/interfaces/__init__.py @@ -2,8 +2,3 @@ from .complexity import query_templates as complexity_query_templates from .general import query_templates as general_query_templates from .resources import query_templates as resource_query_templates from .resources import resource_types - -from pathlib import Path -import os - -DEFAULT_REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports")) diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index b63da0b..4558fbf 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -12,13 +12,13 @@ from pathlib import Path from time import time from typing import Optional +from . import REPORTS_DIR from .query_runner import QueryRunner -from .interfaces import ( +from . import ( complexity_query_templates, general_query_templates, resource_query_templates, resource_types, - DEFAULT_REPORTS_DIR, ) log = logging.getLogger(__name__) @@ -40,9 +40,7 @@ class MetricsRunnerBase(ABC): def __init__(self, output_dir=None): self.runner = QueryRunner() self.base_query_path = Path("queries/metrics") - self.base_output_path = ( - Path(output_dir) if output_dir else DEFAULT_REPORTS_DIR - ) + self.base_output_path = Path(output_dir) if output_dir else REPORTS_DIR print(self.base_output_path) self.base_output_path.mkdir(parents=True, exist_ok=True) log.info(f"Running metric: {self.__class__.__name__}") diff --git a/docs/macros/table_renderer.py b/scripts/kg_analysis/table_renderer.py similarity index 95% rename from docs/macros/table_renderer.py rename to scripts/kg_analysis/table_renderer.py index b7d19bc..8a676c0 100644 --- a/docs/macros/table_renderer.py +++ b/scripts/kg_analysis/table_renderer.py @@ -1,9 +1,13 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2025 TU-Dresden, ZIH +# ralf.klammer@tu-dresden.de +import logging + import json -import os -from pathlib import Path +from . import REPORTS_DIR -REPORTS_DIR = Path(os.getenv("REPORTS_DIR", "./reports/metrics")) +log = logging.getLogger(__name__) class MetricsTableRenderer: @@ -102,7 +106,9 @@ class MetricsTableRenderer: """Renders the resource overview table.""" # Get the names of all resources resource_names = [ - key for key, resource in self.data.items() if isinstance(resource, dict) + key + for key, resource in self.data.items() + if isinstance(resource, dict) ] ordered_resource_names = sorted(resource_names) @@ -129,7 +135,9 @@ class MetricsTableRenderer: # Construct the table header and rows self.output.append(f"| Resource type | {' | '.join(metric_names)} |") - self.output.append(f"| --- |{' | '.join(['---' for _ in metric_names])} |") + self.output.append( + f"| --- |{' | '.join(['---' for _ in metric_names])} |" + ) self.output.extend(rows) def _render_resource(self, resource_type): @@ -245,7 +253,9 @@ class MetricsTableRenderer: break else: # Include all categories except the timestamp - categories = {k: v for k, v in self.data.items() if k != "timestamp"} + categories = { + k: v for k, v in self.data.items() if k != "timestamp" + } # Iterate through the categories for data in categories.values(): -- GitLab From f68a2ac27f57fe001d44666a7fa86ad714166816 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:24:25 +0100 Subject: [PATCH 41/59] [metrics] Remove method 'general_overview' --- scripts/kg_analysis/table_renderer.py | 33 --------------------------- 1 file changed, 33 deletions(-) diff --git a/scripts/kg_analysis/table_renderer.py b/scripts/kg_analysis/table_renderer.py index 8a676c0..965fb2d 100644 --- a/scripts/kg_analysis/table_renderer.py +++ b/scripts/kg_analysis/table_renderer.py @@ -65,10 +65,6 @@ class MetricsTableRenderer: "method": lambda: self._render_resource(self.resource_type), "report": "resources.json", }, - # "general_overview": { - # "method": self._render_general, - # "report": "general.json", - # }, "general": { "method": self._render_general, "report": "general.json", @@ -161,35 +157,6 @@ class MetricsTableRenderer: f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" ) - # def _render_general_overview(self): - # """Renders the general overview table.""" - # # Get the sorted list of resource types - # resource_types = sorted([rt for rt in self.data.keys() if rt != "timestamp"]) - # metric_names = [] - - # # Collect metric names from the first resource type - # first_rt = self.data[resource_types[0]] - # for query in first_rt.get("queries", {}).values(): - # if "name" in query: - # metric_names.append(query["name"]) - - # # Construct the table header - # self.output.append("| Resource Type | " + " | ".join(metric_names) + " |") - # self.output.append("|" + "|".join(["---"] * (len(metric_names) + 1)) + "|") - - # # Iterate over each resource type to collect results - # for rt in resource_types: - # row = [rt] - # queries = self.data[rt].get("queries", {}) - # for metric in metric_names: - # result = "-" - # for q in queries.values(): - # if q.get("name") == metric: - # result = str(q.get("result", "-")) - # break - # row.append(result) - # self.output.append("| " + " | ".join(row) + " |") - def _render_general(self): """Renders the general table.""" self.output.append("| Metric | Query | Result |") -- GitLab From c7ce4cd431e9c55d44659cf78fc7557f980379cc Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:43:30 +0100 Subject: [PATCH 42/59] [metrics] refine comments --- docs/macros/main.py | 56 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 5 deletions(-) diff --git a/docs/macros/main.py b/docs/macros/main.py index 76ce0f8..9c92b78 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -13,11 +13,12 @@ def define_env(env): Args: filename (str): The path to the file. - start_line (int, optional): The line number to start including from. + start_line (int, optional): The line number to start from. single_line (int, optional): The specific line number to include. Returns: - str: The content of the file or an error message if the file does not exist. + str: The content of the file or an error message if the file + does not exist. """ try: with open(filename, "r") as f: @@ -28,14 +29,15 @@ def define_env(env): return lines[single_line] return "".join(lines) except FileNotFoundError: - return f"*No results available. The file `{filename}` was not found.*" + return f"No results available. The file `{filename}` wasnt found.*" except IndexError: return f"*Error: The specified line {start_line} does not exist in `{filename}`.*" @env.macro def include_template(template_path, resource_type, **kwargs): """ - Includes a template file and replaces {resource_type} with the given value. + Includes a template file and replaces {resource_type} with the given + value. Args: template_path (str): Path to the template file. @@ -51,31 +53,75 @@ def define_env(env): @env.macro def metrics_table_single_resource(resource_type=None): + """ + Renders a metrics table for a single resource type. + + Args: + resource_type (str, optional): The resource type URI. + + Returns: + str: The rendered metrics table. + """ return MetricsTableRenderer( table_type="resource", resource_type=resource_type ).render() @env.macro def metrics_table_overview_resource(): + """ + Renders an overview metrics table for all resource types. + + Returns: + str: The rendered overview metrics table. + """ return MetricsTableRenderer(table_type="resource_overview").render() @env.macro def metrics_table_overview_general(): + """ + Renders an overview metrics table for general metrics. + + Returns: + str: The rendered overview metrics table. + """ return MetricsTableRenderer(table_type="general").render() @env.macro def metrics_table_single_general(metric_key): - print(metric_key) + """ + Renders a metrics table for a single general metric. + + Args: + metric_key (str): The key of the general metric. + + Returns: + str: The rendered metrics table. + """ return MetricsTableRenderer( table_type="general", metric_key=metric_key ).render() @env.macro def metrics_table_overview_complexity(): + """ + Renders an overview metrics table for complexity metrics. + + Returns: + str: The rendered overview metrics table. + """ return MetricsTableRenderer(table_type="complexity").render() @env.macro def metrics_table_single_complexity(metric_key): + """ + Renders a metrics table for a single complexity metric. + + Args: + metric_key (str): The key of the complexity metric. + + Returns: + str: The rendered metrics table. + """ return MetricsTableRenderer( table_type="complexity", metric_key=metric_key ).render() -- GitLab From 3012f3d9be4b6f2bf3444987107744aa1cdc7a92 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:43:51 +0100 Subject: [PATCH 43/59] [metrics] refine comments & unify horizontal table rendering --- scripts/kg_analysis/table_renderer.py | 92 ++++++++++++--------------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/scripts/kg_analysis/table_renderer.py b/scripts/kg_analysis/table_renderer.py index 965fb2d..b2b3e12 100644 --- a/scripts/kg_analysis/table_renderer.py +++ b/scripts/kg_analysis/table_renderer.py @@ -80,17 +80,11 @@ class MetricsTableRenderer: """ Renders the table in the desired format. - Args: - table_type (str): Type of table ('general', 'resource', 'general_overview', 'resource_overview') - resource_type (str, optional): Specific resource type for resource tables - Returns: str: The rendered markdown table """ self.output = [] - # Dictionary mapping table types to their corresponding rendering methods - # Get the appropriate rendering method based on the table type self.table_option["method"]() @@ -98,6 +92,45 @@ class MetricsTableRenderer: self.output.append(f"\n*Last updated: {self.timestamp}*\n") return "\n".join(self.output) + def _render_horizontal_table(self, data): + self.output.append("| Metric | Query (file) | Result |") + self.output.append("|--------|------|--------|") + + # Iterate over each query data for the specified resource type + for q_data in data.values(): + if "file" in q_data: + row = [ + q_data["name"], + f"[{q_data['file']}](#{q_data['file']})", + str(q_data["result"]), + ] + self.output.append("| " + " | ".join(row) + " |") + elif "files" in q_data: + self.output.append(f"| **{q_data['name']}** | | |") + for sub_q_data in q_data["files"].values(): + sub_row = [ + sub_q_data["name"], + f"[{sub_q_data['file']}](#{sub_q_data['file']})", + str(sub_q_data["result"]), + ] + self.output.append("| " + " | ".join(sub_row) + " |") + + def _render_resource(self, resource_type): + """Renders the resource-specific table - single metric only""" + if resource_type not in self.data: + return f"*No metrics found for {resource_type}*" + + self._render_horizontal_table(self.data[resource_type]) + + def _render_general(self): + """Renders a general table - general metrics or a single metric.""" + data = ( + {self.metric_key: self.data[self.metric_key]} + if self.metric_key + else self.data + ) + self._render_horizontal_table(data) + def _render_resource_overview(self): """Renders the resource overview table.""" # Get the names of all resources @@ -136,53 +169,6 @@ class MetricsTableRenderer: ) self.output.extend(rows) - def _render_resource(self, resource_type): - """Renders the resource-specific table.""" - if resource_type not in self.data: - return f"*No metrics found for {resource_type}*" - - self.output.append("| Metric | Query (file) | Result |") - self.output.append("|--------|------|--------|") - - # Iterate over each query data for the specified resource type - for query_data in self.data[resource_type].values(): - if "file" in query_data: - self.output.append( - f"| {query_data['name']} | [{query_data['file']}](#{query_data['file']}) | {query_data['result']} |" - ) - elif "files" in query_data: - self.output.append(f"| **{query_data['name']}** | | |") - for sub_query_data in query_data["files"].values(): - self.output.append( - f"| {sub_query_data['name']} | [{sub_query_data['file']}](#{sub_query_data['file']}) | {sub_query_data['result']} |" - ) - - def _render_general(self): - """Renders the general table.""" - self.output.append("| Metric | Query | Result |") - self.output.append("|--------|-------|--------|") - - # Iterate over each metric data to construct the table rows - data = ( - {self.metric_key: self.data[self.metric_key]} - if self.metric_key - else self.data - ) - for metric_key, metric_data in data.items(): - if metric_key == "timestamp": - continue - if isinstance(metric_data, dict): - if "files" in metric_data: - self.output.append(f"| **{metric_data['name']}** | | |") - for sub_query in metric_data["files"].values(): - self.output.append( - f"| {sub_query['name']} | {sub_query['file']} | {sub_query.get('result', '-')} |" - ) - elif "name" in metric_data: - self.output.append( - f"| {metric_data['name']} | {metric_data['file']} | {metric_data.get('result', '-')} |" - ) - def _render_complexity(self): """Renders a complexity table.""" # Base header for all tables -- GitLab From 3a51c39bd1abd41302b07a3ca9a1a098408739ba Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 19 Mar 2025 16:45:49 +0100 Subject: [PATCH 44/59] [metrics] Remove unnecessary overviews --- docs/metrics/general metrics/00_overview.md | 10 --- .../schema_complexity_metrics/00_overview.md | 63 ------------------- 2 files changed, 73 deletions(-) delete mode 100644 docs/metrics/general metrics/00_overview.md delete mode 100644 docs/metrics/schema_complexity_metrics/00_overview.md diff --git a/docs/metrics/general metrics/00_overview.md b/docs/metrics/general metrics/00_overview.md deleted file mode 100644 index cc412a3..0000000 --- a/docs/metrics/general metrics/00_overview.md +++ /dev/null @@ -1,10 +0,0 @@ -# Overview - -These metrics analyze the entire knowledge graph structure and provide insights into: - -- Overall size and complexity -- Connection patterns -- Graph density and distribution -- Edge statistics (incoming/outgoing) - -{{ metrics_table_overview_general() }} \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/00_overview.md b/docs/metrics/schema_complexity_metrics/00_overview.md deleted file mode 100644 index 27b45fd..0000000 --- a/docs/metrics/schema_complexity_metrics/00_overview.md +++ /dev/null @@ -1,63 +0,0 @@ -# Overview - -To calculate the complexity of RDF schemas, we combine the presented formality metrics focusing on both structural and semantic aspects. - -### 1. Basic Structural Complexity - -These metrics measure the size and diversity of the RDF schema: - -- **[Number of classes](./schema_complexity_metrics/01_classes)** (_C_) → More classes indicate a more complex schema. -- **[Number of properties](./schema_complexity_metrics/02_properties)** (_P_) → More properties mean more relationships between entities. -- **[Average class hierarchy depth](./schema_complexity_metrics/03_depth)** (_D_avg_) → The mean number of hierarchy levels in the schema. -- **[Average class hierarchy width](./schema_complexity_metrics/04_width)** (_W_avg_) → The mean number of sibling classes at each hierarchy level. - -A structural complexity score can be calculated as: - - C_structural = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg - -where _w_i_ are weights that determine the relative importance of each factor. - -### 2. Semantic Complexity - -If OWL is used, the complexity increases due to advanced semantics: - -- **[Number of restrictions](./schema_complexity_metrics/05_restrictions)** (_R_) → More restrictions indicate more constraints and rules. -- **[Number of logical axioms](./schema_complexity_metrics/06_axioms)** (_A_) → More axioms mean more logical statements and inferences. - -A semantic complexity score can be calculated as: - - C_semantic = w5 * R + w6 * A - -where _w_i_ are weights that determine the relative importance of each factor. - -### 3. Combined Formula for Schema Complexity - -To create an overall complexity score, we can combine both structural and semantic aspects: - - C_schema = w1 * C + w2 * P + w3 * D_avg + w4 * W_avg + w5 * R + w6 * A - -where _w_i_ are weights that determine the relative importance of each factor. - -This formula provides a single numerical value representing schema complexity, which can be normalized (e.g., 0–100) for comparison across different RDF schemas. - -### References - -> !!!Not the final references!!! - -1. Structural Metrics for Ontologies: - -- Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" -- Describes metrics like number of classes, hierarchy depth, and relations -- Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 - -2. OWL and Schema Complexity Measurements: - - - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" - - Develops the OntoQA model combining structural and semantic metrics - - Source: https://doi.org/10.1109/ICDEW.2005.43 - -3. SPARQL Analysis and RDF Complexity: - - - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" - - Describes hierarchical depth as key metric for RDF schema complexity - - Source: https://doi.org/10.1007/978-3-540-92673-3_10 \ No newline at end of file -- GitLab From 53e0d7bc61977e3edb451c857cc21fbc2761cb2c Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 13:58:49 +0100 Subject: [PATCH 45/59] [metrics] Enable CI/CD-pipeline for pages --- .gitignore | 3 ++- .gitlab-ci.yml | 25 +++++++++++++++++++++++++ mkdocs.yml | 5 ----- scripts/kg_analysis/metrics_runner.py | 2 -- 4 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitignore b/.gitignore index 41dd612..594b9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *venv* -docs/macros/__pycache__/* \ No newline at end of file +docs/macros/__pycache__/* +public/* \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2b37190 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,25 @@ +image: python:3.12-alpine + +before_script: + - pip install -r requirements.txt + - pip install -e scripts + +test: + stage: test + script: + - mkdocs build --strict --verbose --site-dir test + artifacts: + paths: + - test + # rules: + # - if: $CI_COMMIT_REF_NAME != $CI_DEFAULT_BRANCH + +pages: + stage: deploy + script: + - mkdocs build --strict --verbose --site-dir public + artifacts: + paths: + - public + # rules: + # - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH diff --git a/mkdocs.yml b/mkdocs.yml index f6e240f..7b9d85b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,10 +1,5 @@ site_name: NFDI4Earth - KnowledgeGraph - Questions & Metrics -nav: - - Home: index.md - - Questions: questions.md - - Metrics: metrics/ - theme: name: material language: de diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 4558fbf..9e39983 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -41,7 +41,6 @@ class MetricsRunnerBase(ABC): self.runner = QueryRunner() self.base_query_path = Path("queries/metrics") self.base_output_path = Path(output_dir) if output_dir else REPORTS_DIR - print(self.base_output_path) self.base_output_path.mkdir(parents=True, exist_ok=True) log.info(f"Running metric: {self.__class__.__name__}") @@ -267,7 +266,6 @@ class MetricsRunner_Complexity(MetricsRunnerBase): total = 0.0 for metric, data in category_data["files"].items(): if "result" in data and data["result"] is not None: - print(data) try: value = float(data["result"]) weight = float(data.get("weight", 1.0)) -- GitLab From bb30f65dea4a4570ef0c504d18680f6053e89541 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:06:11 +0100 Subject: [PATCH 46/59] [metrics] Move queries to docs/queries --- docs/macros/main.py | 10 ++++++++-- .../queries}/examples/Educational resources.rq | 0 {queries => docs/queries}/examples/Metadata.rq | 0 .../queries}/examples/Services in the NFDI4Earth.rq | 0 {queries => docs/queries}/metrics/GM001.rq | 0 {queries => docs/queries}/metrics/GM002_1.rq | 0 {queries => docs/queries}/metrics/GM002_2.rq | 0 {queries => docs/queries}/metrics/GM002_3.rq | 0 {queries => docs/queries}/metrics/GM003_1.rq | 0 {queries => docs/queries}/metrics/GM003_2.rq | 0 {queries => docs/queries}/metrics/GM003_3.rq | 0 {queries => docs/queries}/metrics/GM003_4.rq | 0 {queries => docs/queries}/metrics/GM004_1.rq | 0 {queries => docs/queries}/metrics/GM004_2.rq | 0 {queries => docs/queries}/metrics/GM004_3.rq | 0 {queries => docs/queries}/metrics/GM004_4.rq | 0 {queries => docs/queries}/metrics/GM004_5.rq | 0 {queries => docs/queries}/metrics/GM004_6.rq | 0 {queries => docs/queries}/metrics/GM005_1.rq | 0 {queries => docs/queries}/metrics/GM005_2.rq | 0 {queries => docs/queries}/metrics/GM005_3.rq | 0 {queries => docs/queries}/metrics/GM005_4.rq | 0 {queries => docs/queries}/metrics/GM005_5.rq | 0 {queries => docs/queries}/metrics/GM005_6.rq | 0 {queries => docs/queries}/metrics/RF_001.rq | 0 {queries => docs/queries}/metrics/RF_002_1.rq | 0 {queries => docs/queries}/metrics/RF_002_2.rq | 0 {queries => docs/queries}/metrics/RF_002_3.rq | 0 {queries => docs/queries}/metrics/RF_002_4.rq | 0 {queries => docs/queries}/metrics/RF_002_5.rq | 0 {queries => docs/queries}/metrics/RF_002_6.rq | 0 {queries => docs/queries}/metrics/RF_003.rq | 0 {queries => docs/queries}/metrics/RF_004.rq | 0 {queries => docs/queries}/metrics/RF_005.rq | 0 {queries => docs/queries}/metrics/RF_006.rq | 0 .../queries}/metrics/RM001_instances_template.rq | 0 .../queries}/metrics/RM002_assertions_template.rq | 0 .../queries}/metrics/RM003_linkage_template.rq | 0 .../metrics/RM004_1_out_edges_total_template.rq | 0 .../queries}/metrics/RM004_2_out_edges_min_template.rq | 0 .../metrics/RM004_3_out_edges_median_template.rq | 0 .../queries}/metrics/RM004_4_out_edges_max_template.rq | 0 .../metrics/RM005_1_in_edges_total_template.rq | 0 .../queries}/metrics/RM005_2_in_edges_min_template.rq | 0 .../metrics/RM005_3_in_edges_median_template.rq | 0 .../queries}/metrics/RM005_4_in_edges_max_template.rq | 0 .../queries}/metrics/RM006_connectivity_template.rq | 0 {queries => docs/queries}/questions/AG001.rq | 0 {queries => docs/queries}/questions/AG001_2.rq | 0 {queries => docs/queries}/questions/AG002_1.rq | 0 {queries => docs/queries}/questions/AG002_2.rq | 0 {queries => docs/queries}/questions/AT001.rq | 0 {queries => docs/queries}/questions/AT002_1.rq | 0 {queries => docs/queries}/questions/AT002_2.rq | 0 {queries => docs/queries}/questions/DA001.rq | 0 {queries => docs/queries}/questions/DA002_1.rq | 0 {queries => docs/queries}/questions/DA002_2.rq | 0 {queries => docs/queries}/questions/DA003_1.rq | 0 {queries => docs/queries}/questions/LH001.rq | 0 {queries => docs/queries}/questions/LH002_1.rq | 0 {queries => docs/queries}/questions/LH002_2.rq | 0 {queries => docs/queries}/questions/LR001.rq | 0 {queries => docs/queries}/questions/LR002_1.rq | 0 {queries => docs/queries}/questions/LR002_2.rq | 0 {queries => docs/queries}/questions/MS001.rq | 0 {queries => docs/queries}/questions/MS002_1.rq | 0 {queries => docs/queries}/questions/MS002_2.rq | 0 {queries => docs/queries}/questions/OG001.rq | 0 {queries => docs/queries}/questions/OG002_1.rq | 0 {queries => docs/queries}/questions/OG002_2.rq | 0 {queries => docs/queries}/questions/PE001.rq | 0 {queries => docs/queries}/questions/PE002_1.rq | 0 {queries => docs/queries}/questions/PE002_2.rq | 0 {queries => docs/queries}/questions/REG001.rq | 0 {queries => docs/queries}/questions/REG002_1.rq | 0 {queries => docs/queries}/questions/REG002_2.rq | 0 {queries => docs/queries}/questions/REP001.rq | 0 {queries => docs/queries}/questions/REP002_1.rq | 0 {queries => docs/queries}/questions/REP002_2.rq | 0 {queries => docs/queries}/questions/RP001.rq | 0 {queries => docs/queries}/questions/RP002_1.rq | 0 {queries => docs/queries}/questions/RP002_2.rq | 0 {queries => docs/queries}/questions/SC001.rq | 0 {queries => docs/queries}/questions/SC002_1.rq | 0 {queries => docs/queries}/questions/SC002_2.rq | 0 {queries => docs/queries}/questions/TY001.rq | 0 {queries => docs/queries}/questions/old/DR001_1.rq | 0 {queries => docs/queries}/questions/old/OR001_1.rq | 0 {queries => docs/queries}/questions/old/OR001_2.rq | 0 {queries => docs/queries}/questions/old/OR002_1.rq | 0 {queries => docs/queries}/questions/old/OR003_1.rq | 0 {queries => docs/queries}/questions/old/OR004_1.rq | 0 {queries => docs/queries}/questions/old/OR005_1.rq | 0 {queries => docs/queries}/questions/old/OR006_1.rq | 0 scripts/kg_analysis/metrics_runner.py | 2 +- 95 files changed, 9 insertions(+), 3 deletions(-) rename {queries => docs/queries}/examples/Educational resources.rq (100%) rename {queries => docs/queries}/examples/Metadata.rq (100%) rename {queries => docs/queries}/examples/Services in the NFDI4Earth.rq (100%) rename {queries => docs/queries}/metrics/GM001.rq (100%) rename {queries => docs/queries}/metrics/GM002_1.rq (100%) rename {queries => docs/queries}/metrics/GM002_2.rq (100%) rename {queries => docs/queries}/metrics/GM002_3.rq (100%) rename {queries => docs/queries}/metrics/GM003_1.rq (100%) rename {queries => docs/queries}/metrics/GM003_2.rq (100%) rename {queries => docs/queries}/metrics/GM003_3.rq (100%) rename {queries => docs/queries}/metrics/GM003_4.rq (100%) rename {queries => docs/queries}/metrics/GM004_1.rq (100%) rename {queries => docs/queries}/metrics/GM004_2.rq (100%) rename {queries => docs/queries}/metrics/GM004_3.rq (100%) rename {queries => docs/queries}/metrics/GM004_4.rq (100%) rename {queries => docs/queries}/metrics/GM004_5.rq (100%) rename {queries => docs/queries}/metrics/GM004_6.rq (100%) rename {queries => docs/queries}/metrics/GM005_1.rq (100%) rename {queries => docs/queries}/metrics/GM005_2.rq (100%) rename {queries => docs/queries}/metrics/GM005_3.rq (100%) rename {queries => docs/queries}/metrics/GM005_4.rq (100%) rename {queries => docs/queries}/metrics/GM005_5.rq (100%) rename {queries => docs/queries}/metrics/GM005_6.rq (100%) rename {queries => docs/queries}/metrics/RF_001.rq (100%) rename {queries => docs/queries}/metrics/RF_002_1.rq (100%) rename {queries => docs/queries}/metrics/RF_002_2.rq (100%) rename {queries => docs/queries}/metrics/RF_002_3.rq (100%) rename {queries => docs/queries}/metrics/RF_002_4.rq (100%) rename {queries => docs/queries}/metrics/RF_002_5.rq (100%) rename {queries => docs/queries}/metrics/RF_002_6.rq (100%) rename {queries => docs/queries}/metrics/RF_003.rq (100%) rename {queries => docs/queries}/metrics/RF_004.rq (100%) rename {queries => docs/queries}/metrics/RF_005.rq (100%) rename {queries => docs/queries}/metrics/RF_006.rq (100%) rename {queries => docs/queries}/metrics/RM001_instances_template.rq (100%) rename {queries => docs/queries}/metrics/RM002_assertions_template.rq (100%) rename {queries => docs/queries}/metrics/RM003_linkage_template.rq (100%) rename {queries => docs/queries}/metrics/RM004_1_out_edges_total_template.rq (100%) rename {queries => docs/queries}/metrics/RM004_2_out_edges_min_template.rq (100%) rename {queries => docs/queries}/metrics/RM004_3_out_edges_median_template.rq (100%) rename {queries => docs/queries}/metrics/RM004_4_out_edges_max_template.rq (100%) rename {queries => docs/queries}/metrics/RM005_1_in_edges_total_template.rq (100%) rename {queries => docs/queries}/metrics/RM005_2_in_edges_min_template.rq (100%) rename {queries => docs/queries}/metrics/RM005_3_in_edges_median_template.rq (100%) rename {queries => docs/queries}/metrics/RM005_4_in_edges_max_template.rq (100%) rename {queries => docs/queries}/metrics/RM006_connectivity_template.rq (100%) rename {queries => docs/queries}/questions/AG001.rq (100%) rename {queries => docs/queries}/questions/AG001_2.rq (100%) rename {queries => docs/queries}/questions/AG002_1.rq (100%) rename {queries => docs/queries}/questions/AG002_2.rq (100%) rename {queries => docs/queries}/questions/AT001.rq (100%) rename {queries => docs/queries}/questions/AT002_1.rq (100%) rename {queries => docs/queries}/questions/AT002_2.rq (100%) rename {queries => docs/queries}/questions/DA001.rq (100%) rename {queries => docs/queries}/questions/DA002_1.rq (100%) rename {queries => docs/queries}/questions/DA002_2.rq (100%) rename {queries => docs/queries}/questions/DA003_1.rq (100%) rename {queries => docs/queries}/questions/LH001.rq (100%) rename {queries => docs/queries}/questions/LH002_1.rq (100%) rename {queries => docs/queries}/questions/LH002_2.rq (100%) rename {queries => docs/queries}/questions/LR001.rq (100%) rename {queries => docs/queries}/questions/LR002_1.rq (100%) rename {queries => docs/queries}/questions/LR002_2.rq (100%) rename {queries => docs/queries}/questions/MS001.rq (100%) rename {queries => docs/queries}/questions/MS002_1.rq (100%) rename {queries => docs/queries}/questions/MS002_2.rq (100%) rename {queries => docs/queries}/questions/OG001.rq (100%) rename {queries => docs/queries}/questions/OG002_1.rq (100%) rename {queries => docs/queries}/questions/OG002_2.rq (100%) rename {queries => docs/queries}/questions/PE001.rq (100%) rename {queries => docs/queries}/questions/PE002_1.rq (100%) rename {queries => docs/queries}/questions/PE002_2.rq (100%) rename {queries => docs/queries}/questions/REG001.rq (100%) rename {queries => docs/queries}/questions/REG002_1.rq (100%) rename {queries => docs/queries}/questions/REG002_2.rq (100%) rename {queries => docs/queries}/questions/REP001.rq (100%) rename {queries => docs/queries}/questions/REP002_1.rq (100%) rename {queries => docs/queries}/questions/REP002_2.rq (100%) rename {queries => docs/queries}/questions/RP001.rq (100%) rename {queries => docs/queries}/questions/RP002_1.rq (100%) rename {queries => docs/queries}/questions/RP002_2.rq (100%) rename {queries => docs/queries}/questions/SC001.rq (100%) rename {queries => docs/queries}/questions/SC002_1.rq (100%) rename {queries => docs/queries}/questions/SC002_2.rq (100%) rename {queries => docs/queries}/questions/TY001.rq (100%) rename {queries => docs/queries}/questions/old/DR001_1.rq (100%) rename {queries => docs/queries}/questions/old/OR001_1.rq (100%) rename {queries => docs/queries}/questions/old/OR001_2.rq (100%) rename {queries => docs/queries}/questions/old/OR002_1.rq (100%) rename {queries => docs/queries}/questions/old/OR003_1.rq (100%) rename {queries => docs/queries}/questions/old/OR004_1.rq (100%) rename {queries => docs/queries}/questions/old/OR005_1.rq (100%) rename {queries => docs/queries}/questions/old/OR006_1.rq (100%) diff --git a/docs/macros/main.py b/docs/macros/main.py index 9c92b78..86b5886 100644 --- a/docs/macros/main.py +++ b/docs/macros/main.py @@ -7,7 +7,12 @@ log = logging.getLogger(__name__) def define_env(env): @env.macro - def include_if_exists(filename, start_line=None, single_line=None): + def include_if_exists( + filename, + path="docs/queries", + start_line=None, + single_line=None, + ): """ Includes the content of a file if it exists. @@ -21,6 +26,7 @@ def define_env(env): does not exist. """ try: + filename = f"{path}/{filename}" with open(filename, "r") as f: lines = f.readlines() if start_line is not None: @@ -29,7 +35,7 @@ def define_env(env): return lines[single_line] return "".join(lines) except FileNotFoundError: - return f"No results available. The file `{filename}` wasnt found.*" + return f"No results available. The file `{filename}` wasn't found.*" except IndexError: return f"*Error: The specified line {start_line} does not exist in `{filename}`.*" diff --git a/queries/examples/Educational resources.rq b/docs/queries/examples/Educational resources.rq similarity index 100% rename from queries/examples/Educational resources.rq rename to docs/queries/examples/Educational resources.rq diff --git a/queries/examples/Metadata.rq b/docs/queries/examples/Metadata.rq similarity index 100% rename from queries/examples/Metadata.rq rename to docs/queries/examples/Metadata.rq diff --git a/queries/examples/Services in the NFDI4Earth.rq b/docs/queries/examples/Services in the NFDI4Earth.rq similarity index 100% rename from queries/examples/Services in the NFDI4Earth.rq rename to docs/queries/examples/Services in the NFDI4Earth.rq diff --git a/queries/metrics/GM001.rq b/docs/queries/metrics/GM001.rq similarity index 100% rename from queries/metrics/GM001.rq rename to docs/queries/metrics/GM001.rq diff --git a/queries/metrics/GM002_1.rq b/docs/queries/metrics/GM002_1.rq similarity index 100% rename from queries/metrics/GM002_1.rq rename to docs/queries/metrics/GM002_1.rq diff --git a/queries/metrics/GM002_2.rq b/docs/queries/metrics/GM002_2.rq similarity index 100% rename from queries/metrics/GM002_2.rq rename to docs/queries/metrics/GM002_2.rq diff --git a/queries/metrics/GM002_3.rq b/docs/queries/metrics/GM002_3.rq similarity index 100% rename from queries/metrics/GM002_3.rq rename to docs/queries/metrics/GM002_3.rq diff --git a/queries/metrics/GM003_1.rq b/docs/queries/metrics/GM003_1.rq similarity index 100% rename from queries/metrics/GM003_1.rq rename to docs/queries/metrics/GM003_1.rq diff --git a/queries/metrics/GM003_2.rq b/docs/queries/metrics/GM003_2.rq similarity index 100% rename from queries/metrics/GM003_2.rq rename to docs/queries/metrics/GM003_2.rq diff --git a/queries/metrics/GM003_3.rq b/docs/queries/metrics/GM003_3.rq similarity index 100% rename from queries/metrics/GM003_3.rq rename to docs/queries/metrics/GM003_3.rq diff --git a/queries/metrics/GM003_4.rq b/docs/queries/metrics/GM003_4.rq similarity index 100% rename from queries/metrics/GM003_4.rq rename to docs/queries/metrics/GM003_4.rq diff --git a/queries/metrics/GM004_1.rq b/docs/queries/metrics/GM004_1.rq similarity index 100% rename from queries/metrics/GM004_1.rq rename to docs/queries/metrics/GM004_1.rq diff --git a/queries/metrics/GM004_2.rq b/docs/queries/metrics/GM004_2.rq similarity index 100% rename from queries/metrics/GM004_2.rq rename to docs/queries/metrics/GM004_2.rq diff --git a/queries/metrics/GM004_3.rq b/docs/queries/metrics/GM004_3.rq similarity index 100% rename from queries/metrics/GM004_3.rq rename to docs/queries/metrics/GM004_3.rq diff --git a/queries/metrics/GM004_4.rq b/docs/queries/metrics/GM004_4.rq similarity index 100% rename from queries/metrics/GM004_4.rq rename to docs/queries/metrics/GM004_4.rq diff --git a/queries/metrics/GM004_5.rq b/docs/queries/metrics/GM004_5.rq similarity index 100% rename from queries/metrics/GM004_5.rq rename to docs/queries/metrics/GM004_5.rq diff --git a/queries/metrics/GM004_6.rq b/docs/queries/metrics/GM004_6.rq similarity index 100% rename from queries/metrics/GM004_6.rq rename to docs/queries/metrics/GM004_6.rq diff --git a/queries/metrics/GM005_1.rq b/docs/queries/metrics/GM005_1.rq similarity index 100% rename from queries/metrics/GM005_1.rq rename to docs/queries/metrics/GM005_1.rq diff --git a/queries/metrics/GM005_2.rq b/docs/queries/metrics/GM005_2.rq similarity index 100% rename from queries/metrics/GM005_2.rq rename to docs/queries/metrics/GM005_2.rq diff --git a/queries/metrics/GM005_3.rq b/docs/queries/metrics/GM005_3.rq similarity index 100% rename from queries/metrics/GM005_3.rq rename to docs/queries/metrics/GM005_3.rq diff --git a/queries/metrics/GM005_4.rq b/docs/queries/metrics/GM005_4.rq similarity index 100% rename from queries/metrics/GM005_4.rq rename to docs/queries/metrics/GM005_4.rq diff --git a/queries/metrics/GM005_5.rq b/docs/queries/metrics/GM005_5.rq similarity index 100% rename from queries/metrics/GM005_5.rq rename to docs/queries/metrics/GM005_5.rq diff --git a/queries/metrics/GM005_6.rq b/docs/queries/metrics/GM005_6.rq similarity index 100% rename from queries/metrics/GM005_6.rq rename to docs/queries/metrics/GM005_6.rq diff --git a/queries/metrics/RF_001.rq b/docs/queries/metrics/RF_001.rq similarity index 100% rename from queries/metrics/RF_001.rq rename to docs/queries/metrics/RF_001.rq diff --git a/queries/metrics/RF_002_1.rq b/docs/queries/metrics/RF_002_1.rq similarity index 100% rename from queries/metrics/RF_002_1.rq rename to docs/queries/metrics/RF_002_1.rq diff --git a/queries/metrics/RF_002_2.rq b/docs/queries/metrics/RF_002_2.rq similarity index 100% rename from queries/metrics/RF_002_2.rq rename to docs/queries/metrics/RF_002_2.rq diff --git a/queries/metrics/RF_002_3.rq b/docs/queries/metrics/RF_002_3.rq similarity index 100% rename from queries/metrics/RF_002_3.rq rename to docs/queries/metrics/RF_002_3.rq diff --git a/queries/metrics/RF_002_4.rq b/docs/queries/metrics/RF_002_4.rq similarity index 100% rename from queries/metrics/RF_002_4.rq rename to docs/queries/metrics/RF_002_4.rq diff --git a/queries/metrics/RF_002_5.rq b/docs/queries/metrics/RF_002_5.rq similarity index 100% rename from queries/metrics/RF_002_5.rq rename to docs/queries/metrics/RF_002_5.rq diff --git a/queries/metrics/RF_002_6.rq b/docs/queries/metrics/RF_002_6.rq similarity index 100% rename from queries/metrics/RF_002_6.rq rename to docs/queries/metrics/RF_002_6.rq diff --git a/queries/metrics/RF_003.rq b/docs/queries/metrics/RF_003.rq similarity index 100% rename from queries/metrics/RF_003.rq rename to docs/queries/metrics/RF_003.rq diff --git a/queries/metrics/RF_004.rq b/docs/queries/metrics/RF_004.rq similarity index 100% rename from queries/metrics/RF_004.rq rename to docs/queries/metrics/RF_004.rq diff --git a/queries/metrics/RF_005.rq b/docs/queries/metrics/RF_005.rq similarity index 100% rename from queries/metrics/RF_005.rq rename to docs/queries/metrics/RF_005.rq diff --git a/queries/metrics/RF_006.rq b/docs/queries/metrics/RF_006.rq similarity index 100% rename from queries/metrics/RF_006.rq rename to docs/queries/metrics/RF_006.rq diff --git a/queries/metrics/RM001_instances_template.rq b/docs/queries/metrics/RM001_instances_template.rq similarity index 100% rename from queries/metrics/RM001_instances_template.rq rename to docs/queries/metrics/RM001_instances_template.rq diff --git a/queries/metrics/RM002_assertions_template.rq b/docs/queries/metrics/RM002_assertions_template.rq similarity index 100% rename from queries/metrics/RM002_assertions_template.rq rename to docs/queries/metrics/RM002_assertions_template.rq diff --git a/queries/metrics/RM003_linkage_template.rq b/docs/queries/metrics/RM003_linkage_template.rq similarity index 100% rename from queries/metrics/RM003_linkage_template.rq rename to docs/queries/metrics/RM003_linkage_template.rq diff --git a/queries/metrics/RM004_1_out_edges_total_template.rq b/docs/queries/metrics/RM004_1_out_edges_total_template.rq similarity index 100% rename from queries/metrics/RM004_1_out_edges_total_template.rq rename to docs/queries/metrics/RM004_1_out_edges_total_template.rq diff --git a/queries/metrics/RM004_2_out_edges_min_template.rq b/docs/queries/metrics/RM004_2_out_edges_min_template.rq similarity index 100% rename from queries/metrics/RM004_2_out_edges_min_template.rq rename to docs/queries/metrics/RM004_2_out_edges_min_template.rq diff --git a/queries/metrics/RM004_3_out_edges_median_template.rq b/docs/queries/metrics/RM004_3_out_edges_median_template.rq similarity index 100% rename from queries/metrics/RM004_3_out_edges_median_template.rq rename to docs/queries/metrics/RM004_3_out_edges_median_template.rq diff --git a/queries/metrics/RM004_4_out_edges_max_template.rq b/docs/queries/metrics/RM004_4_out_edges_max_template.rq similarity index 100% rename from queries/metrics/RM004_4_out_edges_max_template.rq rename to docs/queries/metrics/RM004_4_out_edges_max_template.rq diff --git a/queries/metrics/RM005_1_in_edges_total_template.rq b/docs/queries/metrics/RM005_1_in_edges_total_template.rq similarity index 100% rename from queries/metrics/RM005_1_in_edges_total_template.rq rename to docs/queries/metrics/RM005_1_in_edges_total_template.rq diff --git a/queries/metrics/RM005_2_in_edges_min_template.rq b/docs/queries/metrics/RM005_2_in_edges_min_template.rq similarity index 100% rename from queries/metrics/RM005_2_in_edges_min_template.rq rename to docs/queries/metrics/RM005_2_in_edges_min_template.rq diff --git a/queries/metrics/RM005_3_in_edges_median_template.rq b/docs/queries/metrics/RM005_3_in_edges_median_template.rq similarity index 100% rename from queries/metrics/RM005_3_in_edges_median_template.rq rename to docs/queries/metrics/RM005_3_in_edges_median_template.rq diff --git a/queries/metrics/RM005_4_in_edges_max_template.rq b/docs/queries/metrics/RM005_4_in_edges_max_template.rq similarity index 100% rename from queries/metrics/RM005_4_in_edges_max_template.rq rename to docs/queries/metrics/RM005_4_in_edges_max_template.rq diff --git a/queries/metrics/RM006_connectivity_template.rq b/docs/queries/metrics/RM006_connectivity_template.rq similarity index 100% rename from queries/metrics/RM006_connectivity_template.rq rename to docs/queries/metrics/RM006_connectivity_template.rq diff --git a/queries/questions/AG001.rq b/docs/queries/questions/AG001.rq similarity index 100% rename from queries/questions/AG001.rq rename to docs/queries/questions/AG001.rq diff --git a/queries/questions/AG001_2.rq b/docs/queries/questions/AG001_2.rq similarity index 100% rename from queries/questions/AG001_2.rq rename to docs/queries/questions/AG001_2.rq diff --git a/queries/questions/AG002_1.rq b/docs/queries/questions/AG002_1.rq similarity index 100% rename from queries/questions/AG002_1.rq rename to docs/queries/questions/AG002_1.rq diff --git a/queries/questions/AG002_2.rq b/docs/queries/questions/AG002_2.rq similarity index 100% rename from queries/questions/AG002_2.rq rename to docs/queries/questions/AG002_2.rq diff --git a/queries/questions/AT001.rq b/docs/queries/questions/AT001.rq similarity index 100% rename from queries/questions/AT001.rq rename to docs/queries/questions/AT001.rq diff --git a/queries/questions/AT002_1.rq b/docs/queries/questions/AT002_1.rq similarity index 100% rename from queries/questions/AT002_1.rq rename to docs/queries/questions/AT002_1.rq diff --git a/queries/questions/AT002_2.rq b/docs/queries/questions/AT002_2.rq similarity index 100% rename from queries/questions/AT002_2.rq rename to docs/queries/questions/AT002_2.rq diff --git a/queries/questions/DA001.rq b/docs/queries/questions/DA001.rq similarity index 100% rename from queries/questions/DA001.rq rename to docs/queries/questions/DA001.rq diff --git a/queries/questions/DA002_1.rq b/docs/queries/questions/DA002_1.rq similarity index 100% rename from queries/questions/DA002_1.rq rename to docs/queries/questions/DA002_1.rq diff --git a/queries/questions/DA002_2.rq b/docs/queries/questions/DA002_2.rq similarity index 100% rename from queries/questions/DA002_2.rq rename to docs/queries/questions/DA002_2.rq diff --git a/queries/questions/DA003_1.rq b/docs/queries/questions/DA003_1.rq similarity index 100% rename from queries/questions/DA003_1.rq rename to docs/queries/questions/DA003_1.rq diff --git a/queries/questions/LH001.rq b/docs/queries/questions/LH001.rq similarity index 100% rename from queries/questions/LH001.rq rename to docs/queries/questions/LH001.rq diff --git a/queries/questions/LH002_1.rq b/docs/queries/questions/LH002_1.rq similarity index 100% rename from queries/questions/LH002_1.rq rename to docs/queries/questions/LH002_1.rq diff --git a/queries/questions/LH002_2.rq b/docs/queries/questions/LH002_2.rq similarity index 100% rename from queries/questions/LH002_2.rq rename to docs/queries/questions/LH002_2.rq diff --git a/queries/questions/LR001.rq b/docs/queries/questions/LR001.rq similarity index 100% rename from queries/questions/LR001.rq rename to docs/queries/questions/LR001.rq diff --git a/queries/questions/LR002_1.rq b/docs/queries/questions/LR002_1.rq similarity index 100% rename from queries/questions/LR002_1.rq rename to docs/queries/questions/LR002_1.rq diff --git a/queries/questions/LR002_2.rq b/docs/queries/questions/LR002_2.rq similarity index 100% rename from queries/questions/LR002_2.rq rename to docs/queries/questions/LR002_2.rq diff --git a/queries/questions/MS001.rq b/docs/queries/questions/MS001.rq similarity index 100% rename from queries/questions/MS001.rq rename to docs/queries/questions/MS001.rq diff --git a/queries/questions/MS002_1.rq b/docs/queries/questions/MS002_1.rq similarity index 100% rename from queries/questions/MS002_1.rq rename to docs/queries/questions/MS002_1.rq diff --git a/queries/questions/MS002_2.rq b/docs/queries/questions/MS002_2.rq similarity index 100% rename from queries/questions/MS002_2.rq rename to docs/queries/questions/MS002_2.rq diff --git a/queries/questions/OG001.rq b/docs/queries/questions/OG001.rq similarity index 100% rename from queries/questions/OG001.rq rename to docs/queries/questions/OG001.rq diff --git a/queries/questions/OG002_1.rq b/docs/queries/questions/OG002_1.rq similarity index 100% rename from queries/questions/OG002_1.rq rename to docs/queries/questions/OG002_1.rq diff --git a/queries/questions/OG002_2.rq b/docs/queries/questions/OG002_2.rq similarity index 100% rename from queries/questions/OG002_2.rq rename to docs/queries/questions/OG002_2.rq diff --git a/queries/questions/PE001.rq b/docs/queries/questions/PE001.rq similarity index 100% rename from queries/questions/PE001.rq rename to docs/queries/questions/PE001.rq diff --git a/queries/questions/PE002_1.rq b/docs/queries/questions/PE002_1.rq similarity index 100% rename from queries/questions/PE002_1.rq rename to docs/queries/questions/PE002_1.rq diff --git a/queries/questions/PE002_2.rq b/docs/queries/questions/PE002_2.rq similarity index 100% rename from queries/questions/PE002_2.rq rename to docs/queries/questions/PE002_2.rq diff --git a/queries/questions/REG001.rq b/docs/queries/questions/REG001.rq similarity index 100% rename from queries/questions/REG001.rq rename to docs/queries/questions/REG001.rq diff --git a/queries/questions/REG002_1.rq b/docs/queries/questions/REG002_1.rq similarity index 100% rename from queries/questions/REG002_1.rq rename to docs/queries/questions/REG002_1.rq diff --git a/queries/questions/REG002_2.rq b/docs/queries/questions/REG002_2.rq similarity index 100% rename from queries/questions/REG002_2.rq rename to docs/queries/questions/REG002_2.rq diff --git a/queries/questions/REP001.rq b/docs/queries/questions/REP001.rq similarity index 100% rename from queries/questions/REP001.rq rename to docs/queries/questions/REP001.rq diff --git a/queries/questions/REP002_1.rq b/docs/queries/questions/REP002_1.rq similarity index 100% rename from queries/questions/REP002_1.rq rename to docs/queries/questions/REP002_1.rq diff --git a/queries/questions/REP002_2.rq b/docs/queries/questions/REP002_2.rq similarity index 100% rename from queries/questions/REP002_2.rq rename to docs/queries/questions/REP002_2.rq diff --git a/queries/questions/RP001.rq b/docs/queries/questions/RP001.rq similarity index 100% rename from queries/questions/RP001.rq rename to docs/queries/questions/RP001.rq diff --git a/queries/questions/RP002_1.rq b/docs/queries/questions/RP002_1.rq similarity index 100% rename from queries/questions/RP002_1.rq rename to docs/queries/questions/RP002_1.rq diff --git a/queries/questions/RP002_2.rq b/docs/queries/questions/RP002_2.rq similarity index 100% rename from queries/questions/RP002_2.rq rename to docs/queries/questions/RP002_2.rq diff --git a/queries/questions/SC001.rq b/docs/queries/questions/SC001.rq similarity index 100% rename from queries/questions/SC001.rq rename to docs/queries/questions/SC001.rq diff --git a/queries/questions/SC002_1.rq b/docs/queries/questions/SC002_1.rq similarity index 100% rename from queries/questions/SC002_1.rq rename to docs/queries/questions/SC002_1.rq diff --git a/queries/questions/SC002_2.rq b/docs/queries/questions/SC002_2.rq similarity index 100% rename from queries/questions/SC002_2.rq rename to docs/queries/questions/SC002_2.rq diff --git a/queries/questions/TY001.rq b/docs/queries/questions/TY001.rq similarity index 100% rename from queries/questions/TY001.rq rename to docs/queries/questions/TY001.rq diff --git a/queries/questions/old/DR001_1.rq b/docs/queries/questions/old/DR001_1.rq similarity index 100% rename from queries/questions/old/DR001_1.rq rename to docs/queries/questions/old/DR001_1.rq diff --git a/queries/questions/old/OR001_1.rq b/docs/queries/questions/old/OR001_1.rq similarity index 100% rename from queries/questions/old/OR001_1.rq rename to docs/queries/questions/old/OR001_1.rq diff --git a/queries/questions/old/OR001_2.rq b/docs/queries/questions/old/OR001_2.rq similarity index 100% rename from queries/questions/old/OR001_2.rq rename to docs/queries/questions/old/OR001_2.rq diff --git a/queries/questions/old/OR002_1.rq b/docs/queries/questions/old/OR002_1.rq similarity index 100% rename from queries/questions/old/OR002_1.rq rename to docs/queries/questions/old/OR002_1.rq diff --git a/queries/questions/old/OR003_1.rq b/docs/queries/questions/old/OR003_1.rq similarity index 100% rename from queries/questions/old/OR003_1.rq rename to docs/queries/questions/old/OR003_1.rq diff --git a/queries/questions/old/OR004_1.rq b/docs/queries/questions/old/OR004_1.rq similarity index 100% rename from queries/questions/old/OR004_1.rq rename to docs/queries/questions/old/OR004_1.rq diff --git a/queries/questions/old/OR005_1.rq b/docs/queries/questions/old/OR005_1.rq similarity index 100% rename from queries/questions/old/OR005_1.rq rename to docs/queries/questions/old/OR005_1.rq diff --git a/queries/questions/old/OR006_1.rq b/docs/queries/questions/old/OR006_1.rq similarity index 100% rename from queries/questions/old/OR006_1.rq rename to docs/queries/questions/old/OR006_1.rq diff --git a/scripts/kg_analysis/metrics_runner.py b/scripts/kg_analysis/metrics_runner.py index 9e39983..efb2854 100644 --- a/scripts/kg_analysis/metrics_runner.py +++ b/scripts/kg_analysis/metrics_runner.py @@ -39,7 +39,7 @@ class MetricsRunnerBase(ABC): def __init__(self, output_dir=None): self.runner = QueryRunner() - self.base_query_path = Path("queries/metrics") + self.base_query_path = Path("metrics") self.base_output_path = Path(output_dir) if output_dir else REPORTS_DIR self.base_output_path.mkdir(parents=True, exist_ok=True) log.info(f"Running metric: {self.__class__.__name__}") -- GitLab From a3b07d58edfe5f22b76e263044b8b534fd6b0784 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:07:00 +0100 Subject: [PATCH 47/59] [metrics] Put questions.md in position --- docs/index.md | 2 +- docs/questions.md | 170 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 docs/questions.md diff --git a/docs/index.md b/docs/index.md index 5d30e05..31f12d7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,4 @@ -# {{ config.site_name }} +# Overview Welcome to the NFDI4Earth KnowledgeGraph Query Collection. This documentation provides insights into the NFDI4Earth KnowledgeHub Graph through SPARQL queries and their analysis. diff --git a/docs/questions.md b/docs/questions.md new file mode 100644 index 0000000..c712676 --- /dev/null +++ b/docs/questions.md @@ -0,0 +1,170 @@ +# Questions + +This is a collection of relevant questions and corresponding SPARQL-Queries, that answer those questions. + +The questions are grouped according to the different entities of interest +(datasets, organizations, ...). + +The entities appear in alphabetical order. The first query is useful to get an overview of all entities available in the Knowledge Hub. + +The questions listed below form, altogether, the **domain coverage** of the Knowledge Hub. For details, see the [NFDI4Earth Deliverable D4.3.2](https://zenodo.org/records/7950860). + +## Overview of the types of entities + +| ID | Question | Query/ies | +|---|---|---| +| TY001 | What are the types of entities available in the knowledge graph? | [TY001](queries/questions/TY001.rq)| + + + + +### Aggregator + +| ID | Question | Query/ies | +|---|---|---| +| AG001 | What are all entities of type Aggregator? | [AG001](queries/questions/AG001.rq)| +| AG001_2 | What are name and geometry of Aggregator? | [AG001_2](queries/questions/AG001_2.rq)| +| AG002_1 | What are all attributes available for the type "Aggregator"? | [AG002_1](queries/questions/AG002_1.rq)| +| AG002_2 | How many attributes are available for the type "Aggregator"? | [AG002_2](queries/questions/AG002_2.rq)| + +### Article + +| ID | Question | Query/ies | +|---|---|---| +| AT001 | What are all entities of type schema:Article? | [AT001](queries/questions/AT001.rq)| +| AT002_1 | What are all attributes available for the type "schema:Article"? | [AT002_1](queries/questions/AT002_1.rq)| +| AT002_2 | How many attributes are available for the type "schema:Article"? | [AT002_2](queries/questions/AT002_2.rq)| + +### Dataset + +| ID | Question | Query/ies | +|---|---|---| +| DA001 | What are all entities of type dcat:Dataset? | [DA001](queries/questions/DA001.rq)| +| DA002_1 | What are all attributes available for the type "dcat:Dataset"? | [DA002_1](queries/questions/DA002_1.rq)| +| DA002_2 | How many attributes are available for the type "dcat:Dataset"? | [DA002_2](queries/questions/DA002_2.rq)| +| DA003_1 | What are the datasets having the string 'world settlement footprint' in title or description? | [DA003_1](queries/questions/DA003_1.rq)| + +### LHBArticle + +| ID | Question | Query/ies | +|---|---|---| +| LH001 | What are all entities of type LHBArticle? | [LH001](queries/questions/LH001.rq)| +| LH002_1 | What are all attributes available for the type "LHBArticle"? | [LH002_1](queries/questions/LH002_1.rq)| +| LH002_2 | How many attributes are available for the type "LHBArticle"? | [LH002_2](queries/questions/LH002_2.rq)| + +### LearningResource + +| ID | Question | Query/ies | +|---|---|---| +| LR001 | What are all entities of type LearningResource? | [LR001](queries/questions/LR001.rq)| +| LR002_1 | What are all attributes available for the type "LearningResource"? | [LR002_1](queries/questions/LR002_1.rq)| +| LR002_2 | How many attributes are available for the type "LearningResource"? | [LR002_2](queries/questions/LR002_2.rq)| + +### MetadataStandard + +| ID | Question | Query/ies | +|---|---|---| +| MS001 | What are all entities of type MetadataStandard? | [MS001](queries/questions/MS001.rq)| +| MS002_1 | What are all attributes available for the type "MetadataStandard"? | [MS002_1](queries/questions/MS002_1.rq)| +| MS002_2 | How many attributes are available for the type "MetadataStandard"? | [MS002_2](queries/questions/MS002_2.rq)| + +### Organization + +| ID | Question | Query/ies | +|---|---|---| +| OG001 | What are all entities of type Organization? | [OG001](queries/questions/OG001.rq)| +| OG002_1 | What are all attributes available for the type "Organization"? | [OG002_1](queries/questions/OG002_1.rq)| +| OG002_2 | How many attributes are available for the type "Organization"? | [OG002_2](queries/questions/OG002_2.rq)| + +### Person + +| ID | Question | Query/ies | +|---|---|---| +| PE001 | What are all entities of type Person? | [PE001](queries/questions/PE001.rq)| +| PE002_1 | What are all attributes available for the type "Person"? | [PE002_1](queries/questions/PE002_1.rq)| +| PE002_2 | How many attributes are available for the type "Person"? | [PE002_2](queries/questions/PE002_2.rq)| + +### Registry + +| ID | Question | Query/ies | +|---|---|---| +| REG001 | What are all entities of type Registry? | [REG001](queries/questions/REG001.rq)| +| REG002_1 | What are all attributes available for the type "Registry"? | [REG002_1](queries/questions/REG002_1.rq)| +| REG002_2 | How many attributes are available for the type "Registry"? | [REG002_2](queries/questions/REG002_2.rq)| + +### Repository + +| ID | Question | Query/ies | +|---|---|---| +| REP001 | What are all entities of type Repository? | [REP001](queries/questions/REP001.rq)| +| REP002_1 | What are all attributes available for the type "Repository"? | [REP002_1](queries/questions/REP002_1.rq)| +| REP002_2 | How many attributes are available for the type "Repository"? | [REP002_2](queries/questions/REP002_2.rq)| + +### ResearchProject + +| ID | Question | Query/ies | +|---|---|---| +| RP001 | What are all entities of type ResearchProject? | [RP001](queries/questions/RP001.rq)| +| RP002_1 | What are all attributes available for the type "ResearchProject"? | [RP002_1](queries/questions/RP002_1.rq)| +| RP002_2 | How many attributes are available for the type "ResearchProject"? | [RP002_2](queries/questions/RP002_2.rq)| + + +### SoftwareSourceCode + +| ID | Question | Query/ies | +|---|---|---| +| SC001 | What are all entities of type SoftwareSourceCode? | [SC001](queries/questions/SC001.rq)| +| SC002_1 | What are all attributes available for the type "SoftwareSourceCode"? | [SC002_1](queries/questions/SC002_1.rq)| +| SC002_2 | How many attributes are available for the type "SoftwareSourceCode"? | [SC002_2](queries/questions/SC002_2.rq)| + + +<!--- Template for a new table (including first line) + +### EntityType + +| ID | Question | Query/ies | +|---|---|---| +| XX001 | What are all entities of type EntityType? | [XX001](queries/questions/XX001.rq)| + +--> + + +<!--- + + +### Organizations + +| ID | Question | Query/ies | +|---|---|---| +| OR001 | What is the URL of the homepage for the organization with the following name: 'Karlsruhe Institute of Technology'? | [OR001_1](queries/questions/OR001_1.rq),[OR001_2](queries/questions/OR001_2.rq) | +| OR002 | What is the URL of the homepage for the organization with the following ID: 'https://nfdi4earth-knowledgehub.geo.tu-dresden.de/api/objects/n4ekh/a38143be5e15bed94a20' | [OR003_1](queries/questions/OR003_1.rq) | +| OR003 | Which organizations have not defined any homepage? | [OR003_1](queries/questions/OR003_1.rq) | +| OR004 | Which services are published by the organization? | [OR004_1](queries/questions/OR004_1.rq) | +| OR005 | What is the geolocation of the organization called 'TU Dresden'? | [OR005_1](queries/questions/OR005_1.rq) | +| OR006 | What is the geolocation of all organizations, that are members of the NFDI4Earth consortium? | [OR006_1](queries/questions/OR006_1.rq) | + +### Repositories + +| ID | Question | Query/ies | +|----|----------|-----------| +| DR1 | At which repository can I archive my [geophysical] data of [2] GB?| [OR004_1](queries/questions/OR004_1.rq) | +| DR2 | What is the temporal coverage of a data repository?|| +| DR3 | What is the spatial coverage of a data repository?|| +| DR4 | What is the curation policy of the data repository?|| +| DR5 | Which licences are supported by the data repository?|| +| DR6 | Does the repository give identifiers for its ressources?|| +| DR7 | Which metadata harversting interface is supported by the repository?|| +| DR8 | Which type of (persistent) identifiers are used by the repository?|| +| DR9 | What is the thematic area/subject of a repository?|| +| DR10 | Limitations of data deposit at the repository?|| +| DR11 | When was the medatada for a given repository first collected/last updated?|| +| DR12 | Is the repository still available?|| +| DR13 | Which repository allows long term archiving?|| + +--> + +## Notes + +This question-based approach takes inspiration from the [GeoSPARQLBenchmark](https://github.com/OpenLinkSoftware/GeoSPARQLBenchmark). + +It is directly linked to the [Knowledge Hub landing page project](https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_landingpage) as all the questions are taken to explain the basic idea and demonstrate usage of the [Knowledge Hub](https://knowledgehub.nfdi4earth.de). -- GitLab From 07b4ff22f09d713bd5b005ac77dedf0697f8d537 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:08:19 +0100 Subject: [PATCH 48/59] [metrics]fix: links to queries --- docs/macros/resource_metrics.md | 24 ++++++++-------- docs/metrics/general metrics/01_instances.md | 4 ++- docs/metrics/general metrics/02_assertions.md | 18 ++++++------ .../general metrics/03_linkage_degree.md | 24 ++++++++-------- .../general metrics/04_edges_incoming.md | 22 ++++++++++----- .../general metrics/05_edges_outgoing.md | 28 ++++++++++--------- .../schema_complexity_metrics/01_classes.md | 2 +- .../02_properties.md | 12 ++++---- .../schema_complexity_metrics/03_depth.md | 2 +- .../schema_complexity_metrics/04_width.md | 2 +- .../05_restrictions.md | 2 +- .../schema_complexity_metrics/06_axioms.md | 2 +- scripts/kg_analysis/table_renderer.py | 23 ++++++++++++--- 13 files changed, 96 insertions(+), 69 deletions(-) diff --git a/docs/macros/resource_metrics.md b/docs/macros/resource_metrics.md index de24e0c..ba82029 100644 --- a/docs/macros/resource_metrics.md +++ b/docs/macros/resource_metrics.md @@ -14,14 +14,14 @@ see also: [general_metricsinstances](/metrics/general%20metrics/01_instances/) <i id="RM001_instances_template.rq">file: RM001_instances_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM001_instances_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM001_instances_template.rq", resource_type_uri) }} ``` ### Connectivity to other resources <i id="RM006_connectivity_template.rq">file: RM006_connectivity_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM006_connectivity_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM006_connectivity_template.rq", resource_type_uri) }} ``` ### Number of Assertions @@ -31,7 +31,7 @@ see also: [general_metricsassortions](/metrics/general%20metrics/02_assertions/) <i id="RM002_assertions_template.rq">file: RM002_assertions_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM002_assertions_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM002_assertions_template.rq", resource_type_uri) }} ``` ### Average linkage @@ -41,7 +41,7 @@ see also: [general_metricslinkage](/metrics/general%20metrics/03_linkage_degree/ <i id="RM003_linkage_template.rq">file: RM003_linkage_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM003_linkage_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM003_linkage_template.rq", resource_type_uri) }} ``` ### Outgoing Edges Statistics @@ -53,7 +53,7 @@ see also: [general_metricsoutgoing edges](/metrics/general%20metrics/04_outgoing <i id="RM004_1_out_edges_total_template.rq">file: RM004_1_out_edges_total_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM004_1_out_edges_total_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM004_1_out_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum outgoing edges @@ -61,7 +61,7 @@ see also: [general_metricsoutgoing edges](/metrics/general%20metrics/04_outgoing <i id="RM004_2_out_edges_min_template.rq">file: RM004_2_out_edges_min_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM004_2_out_edges_min_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM004_2_out_edges_min_template.rq", resource_type_uri) }} ``` #### Median outgoing edges @@ -69,7 +69,7 @@ see also: [general_metricsoutgoing edges](/metrics/general%20metrics/04_outgoing <i id="RM004_3_out_edges_median_template.rq">file: RM004_3_out_edges_median_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM004_3_out_edges_median_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM004_3_out_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum outgoing edges @@ -77,7 +77,7 @@ see also: [general_metricsoutgoing edges](/metrics/general%20metrics/04_outgoing <i id="RM004_4_out_edges_max_template.rq">file: RM004_4_out_edges_max_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM004_4_out_edges_max_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM004_4_out_edges_max_template.rq", resource_type_uri) }} ``` ### Incoming Edges Statistics @@ -89,7 +89,7 @@ see also: [general_metricsincoming edges](/metrics/general%20metrics/05_incoming <i id="RM005_1_in_edges_total_template.rq">file: RM005_1_in_edges_total_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM005_1_in_edges_total_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM005_1_in_edges_total_template.rq", resource_type_uri) }} ``` #### Minimum incoming edges @@ -97,7 +97,7 @@ see also: [general_metricsincoming edges](/metrics/general%20metrics/05_incoming <i id="RM005_2_in_edges_min_template.rq">file: RM005_2_in_edges_min_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM005_2_in_edges_min_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM005_2_in_edges_min_template.rq", resource_type_uri) }} ``` #### Median incoming edges @@ -105,7 +105,7 @@ see also: [general_metricsincoming edges](/metrics/general%20metrics/05_incoming <i id="RM005_3_in_edges_median_template.rq">file: RM005_3_in_edges_median_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM005_3_in_edges_median_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM005_3_in_edges_median_template.rq", resource_type_uri) }} ``` #### Maximum incoming edges @@ -113,7 +113,7 @@ see also: [general_metricsincoming edges](/metrics/general%20metrics/05_incoming <i id="RM005_4_in_edges_max_template.rq">file: RM005_4_in_edges_max_template.rq</i> ```sparql -{{ include_template("queries/metrics/RM005_4_in_edges_max_template.rq", resource_type_uri) }} +{{ include_template("metrics/RM005_4_in_edges_max_template.rq", resource_type_uri) }} ``` {% endmacro %} diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general metrics/01_instances.md index e37a978..57ae0b3 100644 --- a/docs/metrics/general metrics/01_instances.md +++ b/docs/metrics/general metrics/01_instances.md @@ -15,6 +15,8 @@ The metric helps to: ### Count Number of instances in a graph +<i id="GM001.rq">file: GM001.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM001.rq") }} +{{ include_if_exists("metrics/GM001.rq") }} ``` diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general metrics/02_assertions.md index 11d911d..a3d1363 100644 --- a/docs/metrics/general metrics/02_assertions.md +++ b/docs/metrics/general metrics/02_assertions.md @@ -15,24 +15,24 @@ The metric helps to: ### Count Total Number of Assertions +<i id="GM002_1.rq">file: GM002_1.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM002_1.rq") }} +{{ include_if_exists("metrics/GM002_1.rq") }} ``` ### Count Number of Entity-to-Entity Assertions +<i id="GM002_2.rq">file: GM002_2.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM002_2.rq") }} +{{ include_if_exists("metrics/GM002_2.rq") }} ``` ### Count Number of Entity-to-Literal Assertions +<i id="GM002_3.rq">file: GM002_3.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM002_3.rq") }} +{{ include_if_exists("metrics/GM002_3.rq") }} ``` - -## Results -{{ include_if_exists("reports/metrics/GM002.txt", start_line=1) }} - -Last execution: -{{ include_if_exists("reports/metrics/GM002.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general metrics/03_linkage_degree.md index e640cf4..defdff0 100644 --- a/docs/metrics/general metrics/03_linkage_degree.md +++ b/docs/metrics/general metrics/03_linkage_degree.md @@ -17,8 +17,10 @@ The metric provides insights into: This query calculates the average number of relationships (both incoming and outgoing) per entity in the graph. +<i id="GM003_1.rq">file: GM003_1.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM003_1.rq") }} +{{ include_if_exists("metrics/GM003_1.rq") }} ``` ??? warning "Batch Processing Limitation" @@ -30,30 +32,28 @@ This query calculates the average number of relationships (both incoming and out This is an optimized version of the linkage degree calculation that uses batch processing. It's particularly useful for large datasets where the standard query might timeout or consume too many resources. The query processes a limited number of entities at a time. +<i id="GM003_2.rq">file: GM003_2.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM003_2.rq") }} +{{ include_if_exists("metrics/GM003_2.rq") }} ``` ### Average Outgoing Linkage Degree (Batch Processing) This query focuses specifically on outgoing relationships, calculating the average number of outgoing edges per entity. It uses batch processing for efficient execution on larger datasets. +<i id="GM003_3.rq">file: GM003_3.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM003_3.rq") }} +{{ include_if_exists("metrics/GM003_3.rq") }} ``` ### Average Incoming Linkage Degree (Batch Processing) This query calculates the average number of incoming relationships per entity, providing insights into how frequently entities are referenced by others in the graph. It also uses batch processing for efficiency. +<i id="GM003_4.rq">file: GM003_4.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM003_4.rq") }} +{{ include_if_exists("metrics/GM003_4.rq") }} ``` - -## Results - -### Average Linkage Degree -{{ include_if_exists("reports/metrics/GM003.txt", start_line=1) }} - -Last execution: -{{ include_if_exists("reports/metrics/GM003.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/general metrics/04_edges_incoming.md b/docs/metrics/general metrics/04_edges_incoming.md index 899f5ea..d649181 100644 --- a/docs/metrics/general metrics/04_edges_incoming.md +++ b/docs/metrics/general metrics/04_edges_incoming.md @@ -9,13 +9,15 @@ This metric determines the median number of incoming edges across all nodes in t ### Step 1: Count Incoming Edges Per Node ```sparql -{{ include_if_exists("queries/metrics/GM005_1.rq") }} +{{ include_if_exists("metrics/GM005_1.rq") }} ``` ### Step 2: Get Total Number of Nodes +<i id="GM005_2.rq">file: GM005_2.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM005_2.rq") }} +{{ include_if_exists("metrics/GM005_2.rq") }} ``` ### Step 3: Calculate Median Position @@ -23,23 +25,29 @@ This metric determines the median number of incoming edges across all nodes in t Using the total node count (n), median position is: position = (n+1)/2 ```sparql -{{ include_if_exists("queries/metrics/GM005_3.rq") }} +{{ include_if_exists("metrics/GM005_3.rq") }} ``` ### Step 4: Get Median Value +<i id="GM005_4.rq">file: GM005_4.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM005_4.rq") }} +{{ include_if_exists("metrics/GM005_4.rq") }} ``` ### Step 5: Get Minimum Value +<i id="GM005_5.rq">file: GM005_5.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM005_5.rq") }} +{{ include_if_exists("metrics/GM005_5.rq") }} ``` -### Step 5: Get Maximum Value +### Step 6: Get Maximum Value + +<i id="GM005_6.rq">file: GM005_6.rq</i> ```sparql -{{ include_if_exists("queries/metrics/GM005_6.rq") }} +{{ include_if_exists("metrics/GM005_6.rq") }} ``` diff --git a/docs/metrics/general metrics/05_edges_outgoing.md b/docs/metrics/general metrics/05_edges_outgoing.md index 2ecec14..5531d38 100644 --- a/docs/metrics/general metrics/05_edges_outgoing.md +++ b/docs/metrics/general metrics/05_edges_outgoing.md @@ -9,13 +9,15 @@ This metric determines the median number of outgoing edges across all nodes in t ### Step 1: Count Outgoing Edges Per Node ```sparql -{{ include_if_exists("queries/metrics/GM004_1.rq") }} +{{ include_if_exists("metrics/GM004_1.rq") }} ``` ### Step 2: Get Total Number of Nodes +<i id="GM004_2.rq">file: GM004_2.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM004_2.rq") }} +{{ include_if_exists("metrics/GM004_2.rq") }} ``` ### Step 3: Calculate Median Position @@ -23,29 +25,29 @@ This metric determines the median number of outgoing edges across all nodes in t Using the total node count (n), median position is: position = (n+1)/2 ```sparql -{{ include_if_exists("queries/metrics/GM004_3.rq") }} +{{ include_if_exists("metrics/GM004_3.rq") }} ``` ### Step 4: Get Median Value(s) +<i id="GM004_4.rq">file: GM004_4.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM004_4.rq") }} +{{ include_if_exists("metrics/GM004_4.rq") }} ``` ### Step 5: Get Minimum Value +<i id="GM004_5.rq">file: GM004_5.rq</i> + ```sparql -{{ include_if_exists("queries/metrics/GM004_5.rq") }} +{{ include_if_exists("metrics/GM004_5.rq") }} ``` -### Step 5: Get Maximum Value +### Step 6: Get Maximum Value + +<i id="GM004_6.rq">file: GM004_6.rq</i> ```sparql -{{ include_if_exists("queries/metrics/GM004_6.rq") }} +{{ include_if_exists("metrics/GM004_6.rq") }} ``` - -## Results -{{ include_if_exists("reports/metrics/GM004.txt", start_line=1) }} - -Last execution: -{{ include_if_exists("reports/metrics/GM004.txt", single_line=0) }} \ No newline at end of file diff --git a/docs/metrics/schema_complexity_metrics/01_classes.md b/docs/metrics/schema_complexity_metrics/01_classes.md index 953bbaf..c1f3b96 100644 --- a/docs/metrics/schema_complexity_metrics/01_classes.md +++ b/docs/metrics/schema_complexity_metrics/01_classes.md @@ -9,7 +9,7 @@ This metric counts the total number of classes defined in our schema. ## SPARQL Query ```sparql -{{ include_if_exists("queries/metrics/RF_001.rq") }} +{{ include_if_exists("metrics/RF_001.rq") }} ``` ## Description diff --git a/docs/metrics/schema_complexity_metrics/02_properties.md b/docs/metrics/schema_complexity_metrics/02_properties.md index a703c88..a052801 100644 --- a/docs/metrics/schema_complexity_metrics/02_properties.md +++ b/docs/metrics/schema_complexity_metrics/02_properties.md @@ -9,37 +9,37 @@ This metric analyzes the properties defined in our schema through multiple aspec ## Total Properties Count ```sparql -{{ include_if_exists("queries/metrics/RF_002_1.rq") }} +{{ include_if_exists("metrics/RF_002_1.rq") }} ``` ## Object Properties Count ```sparql -{{ include_if_exists("queries/metrics/RF_002_2.rq") }} +{{ include_if_exists("metrics/RF_002_2.rq") }} ``` ## Datatype Properties Count ```sparql -{{ include_if_exists("queries/metrics/RF_002_3.rq") }} +{{ include_if_exists("metrics/RF_002_3.rq") }} ``` ## Properties with Domain ```sparql -{{ include_if_exists("queries/metrics/RF_002_4.rq") }} +{{ include_if_exists("metrics/RF_002_4.rq") }} ``` ## Properties with Range ```sparql -{{ include_if_exists("queries/metrics/RF_002_5.rq") }} +{{ include_if_exists("metrics/RF_002_5.rq") }} ``` ## Combined Metrics Query ```sparql -{{ include_if_exists("queries/metrics/RF_002_6.rq") }} +{{ include_if_exists("metrics/RF_002_6.rq") }} ``` ## Interpretation diff --git a/docs/metrics/schema_complexity_metrics/03_depth.md b/docs/metrics/schema_complexity_metrics/03_depth.md index 671be88..4602061 100644 --- a/docs/metrics/schema_complexity_metrics/03_depth.md +++ b/docs/metrics/schema_complexity_metrics/03_depth.md @@ -10,7 +10,7 @@ This metric calculates the average depth of the class hierarchy in the schema. ## SPARQL Query ```sparql -{{ include_if_exists("queries/metrics/RF_003.rq") }} +{{ include_if_exists("metrics/RF_003.rq") }} ``` ## Description diff --git a/docs/metrics/schema_complexity_metrics/04_width.md b/docs/metrics/schema_complexity_metrics/04_width.md index 5acfb26..3d07f42 100644 --- a/docs/metrics/schema_complexity_metrics/04_width.md +++ b/docs/metrics/schema_complexity_metrics/04_width.md @@ -9,7 +9,7 @@ This metric calculates the average number of subclasses per class (branching fac ## SPARQL Query ```sparql -{{ include_if_exists("queries/metrics/RF_004.rq") }} +{{ include_if_exists("metrics/RF_004.rq") }} ``` ## Description diff --git a/docs/metrics/schema_complexity_metrics/05_restrictions.md b/docs/metrics/schema_complexity_metrics/05_restrictions.md index 2c68bc9..7fa5d5e 100644 --- a/docs/metrics/schema_complexity_metrics/05_restrictions.md +++ b/docs/metrics/schema_complexity_metrics/05_restrictions.md @@ -9,7 +9,7 @@ This metric counts the various types of OWL restrictions defined in the schema. ## SPARQL Query ```sparql -{{ include_if_exists("queries/metrics/RF_006.rq") }} +{{ include_if_exists("metrics/RF_006.rq") }} ``` ## Description diff --git a/docs/metrics/schema_complexity_metrics/06_axioms.md b/docs/metrics/schema_complexity_metrics/06_axioms.md index 53734f6..66846df 100644 --- a/docs/metrics/schema_complexity_metrics/06_axioms.md +++ b/docs/metrics/schema_complexity_metrics/06_axioms.md @@ -9,7 +9,7 @@ This metric counts the various types of OWL logical axioms defined in the schema ## SPARQL Query ```sparql -{{ include_if_exists("queries/metrics/RF_007.rq") }} +{{ include_if_exists("metrics/RF_006.rq") }} ``` ## Description diff --git a/scripts/kg_analysis/table_renderer.py b/scripts/kg_analysis/table_renderer.py index b2b3e12..38d0d01 100644 --- a/scripts/kg_analysis/table_renderer.py +++ b/scripts/kg_analysis/table_renderer.py @@ -92,25 +92,37 @@ class MetricsTableRenderer: self.output.append(f"\n*Last updated: {self.timestamp}*\n") return "\n".join(self.output) - def _render_horizontal_table(self, data): + def _render_horizontal_table(self, data, add_links=True): + """Renders a horizontal table for the given data.""" + # Create the table header self.output.append("| Metric | Query (file) | Result |") self.output.append("|--------|------|--------|") # Iterate over each query data for the specified resource type for q_data in data.values(): if "file" in q_data: + filename = ( + f"[{q_data['file']}](#{q_data['file']})" + if add_links + else q_data["file"] + ) row = [ q_data["name"], - f"[{q_data['file']}](#{q_data['file']})", + filename, str(q_data["result"]), ] self.output.append("| " + " | ".join(row) + " |") elif "files" in q_data: self.output.append(f"| **{q_data['name']}** | | |") for sub_q_data in q_data["files"].values(): + filename = ( + f"[{sub_q_data['file']}](#{sub_q_data['file']})" + if add_links + else sub_q_data["file"] + ) sub_row = [ sub_q_data["name"], - f"[{sub_q_data['file']}](#{sub_q_data['file']})", + filename, str(sub_q_data["result"]), ] self.output.append("| " + " | ".join(sub_row) + " |") @@ -129,7 +141,10 @@ class MetricsTableRenderer: if self.metric_key else self.data ) - self._render_horizontal_table(data) + # Render the horizontal table + # Add links to the file names if a metric key is specified, + # i.e. the table is for a single metric + self._render_horizontal_table(data, add_links=bool(self.metric_key)) def _render_resource_overview(self): """Renders the resource overview table.""" -- GitLab From 8c97ba401dbe8c877b3f20ee83f4f43b5c3d18e4 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:33:50 +0100 Subject: [PATCH 49/59] [metrics] Remove whitespace from folder names --- docs/metrics/{general metrics => general_metrics}/01_instances.md | 0 .../metrics/{general metrics => general_metrics}/02_assertions.md | 0 .../{general metrics => general_metrics}/03_linkage_degree.md | 0 .../{general metrics => general_metrics}/04_edges_incoming.md | 0 .../{general metrics => general_metrics}/05_edges_outgoing.md | 0 docs/metrics/{resource metrics => resource_metrics}/aggregator.md | 0 .../metrics/{resource metrics => resource_metrics}/article_lhb.md | 0 .../{resource metrics => resource_metrics}/data_service.md | 0 docs/metrics/{resource metrics => resource_metrics}/dataset.md | 0 .../{resource metrics => resource_metrics}/learning_resource.md | 0 .../{resource metrics => resource_metrics}/organization.md | 0 docs/metrics/{resource metrics => resource_metrics}/person.md | 0 .../metrics/{resource metrics => resource_metrics}/publication.md | 0 docs/metrics/{resource metrics => resource_metrics}/registry.md | 0 docs/metrics/{resource metrics => resource_metrics}/repository.md | 0 docs/metrics/{resource metrics => resource_metrics}/service.md | 0 docs/metrics/{resource metrics => resource_metrics}/software.md | 0 docs/metrics/{resource metrics => resource_metrics}/standards.md | 0 18 files changed, 0 insertions(+), 0 deletions(-) rename docs/metrics/{general metrics => general_metrics}/01_instances.md (100%) rename docs/metrics/{general metrics => general_metrics}/02_assertions.md (100%) rename docs/metrics/{general metrics => general_metrics}/03_linkage_degree.md (100%) rename docs/metrics/{general metrics => general_metrics}/04_edges_incoming.md (100%) rename docs/metrics/{general metrics => general_metrics}/05_edges_outgoing.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/aggregator.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/article_lhb.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/data_service.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/dataset.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/learning_resource.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/organization.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/person.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/publication.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/registry.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/repository.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/service.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/software.md (100%) rename docs/metrics/{resource metrics => resource_metrics}/standards.md (100%) diff --git a/docs/metrics/general metrics/01_instances.md b/docs/metrics/general_metrics/01_instances.md similarity index 100% rename from docs/metrics/general metrics/01_instances.md rename to docs/metrics/general_metrics/01_instances.md diff --git a/docs/metrics/general metrics/02_assertions.md b/docs/metrics/general_metrics/02_assertions.md similarity index 100% rename from docs/metrics/general metrics/02_assertions.md rename to docs/metrics/general_metrics/02_assertions.md diff --git a/docs/metrics/general metrics/03_linkage_degree.md b/docs/metrics/general_metrics/03_linkage_degree.md similarity index 100% rename from docs/metrics/general metrics/03_linkage_degree.md rename to docs/metrics/general_metrics/03_linkage_degree.md diff --git a/docs/metrics/general metrics/04_edges_incoming.md b/docs/metrics/general_metrics/04_edges_incoming.md similarity index 100% rename from docs/metrics/general metrics/04_edges_incoming.md rename to docs/metrics/general_metrics/04_edges_incoming.md diff --git a/docs/metrics/general metrics/05_edges_outgoing.md b/docs/metrics/general_metrics/05_edges_outgoing.md similarity index 100% rename from docs/metrics/general metrics/05_edges_outgoing.md rename to docs/metrics/general_metrics/05_edges_outgoing.md diff --git a/docs/metrics/resource metrics/aggregator.md b/docs/metrics/resource_metrics/aggregator.md similarity index 100% rename from docs/metrics/resource metrics/aggregator.md rename to docs/metrics/resource_metrics/aggregator.md diff --git a/docs/metrics/resource metrics/article_lhb.md b/docs/metrics/resource_metrics/article_lhb.md similarity index 100% rename from docs/metrics/resource metrics/article_lhb.md rename to docs/metrics/resource_metrics/article_lhb.md diff --git a/docs/metrics/resource metrics/data_service.md b/docs/metrics/resource_metrics/data_service.md similarity index 100% rename from docs/metrics/resource metrics/data_service.md rename to docs/metrics/resource_metrics/data_service.md diff --git a/docs/metrics/resource metrics/dataset.md b/docs/metrics/resource_metrics/dataset.md similarity index 100% rename from docs/metrics/resource metrics/dataset.md rename to docs/metrics/resource_metrics/dataset.md diff --git a/docs/metrics/resource metrics/learning_resource.md b/docs/metrics/resource_metrics/learning_resource.md similarity index 100% rename from docs/metrics/resource metrics/learning_resource.md rename to docs/metrics/resource_metrics/learning_resource.md diff --git a/docs/metrics/resource metrics/organization.md b/docs/metrics/resource_metrics/organization.md similarity index 100% rename from docs/metrics/resource metrics/organization.md rename to docs/metrics/resource_metrics/organization.md diff --git a/docs/metrics/resource metrics/person.md b/docs/metrics/resource_metrics/person.md similarity index 100% rename from docs/metrics/resource metrics/person.md rename to docs/metrics/resource_metrics/person.md diff --git a/docs/metrics/resource metrics/publication.md b/docs/metrics/resource_metrics/publication.md similarity index 100% rename from docs/metrics/resource metrics/publication.md rename to docs/metrics/resource_metrics/publication.md diff --git a/docs/metrics/resource metrics/registry.md b/docs/metrics/resource_metrics/registry.md similarity index 100% rename from docs/metrics/resource metrics/registry.md rename to docs/metrics/resource_metrics/registry.md diff --git a/docs/metrics/resource metrics/repository.md b/docs/metrics/resource_metrics/repository.md similarity index 100% rename from docs/metrics/resource metrics/repository.md rename to docs/metrics/resource_metrics/repository.md diff --git a/docs/metrics/resource metrics/service.md b/docs/metrics/resource_metrics/service.md similarity index 100% rename from docs/metrics/resource metrics/service.md rename to docs/metrics/resource_metrics/service.md diff --git a/docs/metrics/resource metrics/software.md b/docs/metrics/resource_metrics/software.md similarity index 100% rename from docs/metrics/resource metrics/software.md rename to docs/metrics/resource_metrics/software.md diff --git a/docs/metrics/resource metrics/standards.md b/docs/metrics/resource_metrics/standards.md similarity index 100% rename from docs/metrics/resource metrics/standards.md rename to docs/metrics/resource_metrics/standards.md -- GitLab From b6609d259ac4c19ab884f9eba515ba5831711793 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:34:06 +0100 Subject: [PATCH 50/59] [metrics] Add repo-url to config --- mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index 7b9d85b..65887ba 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,7 @@ site_name: NFDI4Earth - KnowledgeGraph - Questions & Metrics +repo_url: https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_questions + theme: name: material language: de -- GitLab From c4af45cd15a69f23a714768a2129ac59819e422d Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:34:31 +0100 Subject: [PATCH 51/59] [metrics] Modify index.md & add examples.md --- docs/examples.md | 124 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 54 ++++++++++++++++++--- 2 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 docs/examples.md diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..7f952e7 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,124 @@ +# Examples + +This collection contains representative example queries that serve as entry points for using the NFDI4Earth KnowledgeGraph. + +## About this Collection + +The SPARQL queries presented here demonstrate fundamental query patterns and typical use cases. They are intentionally kept simple and serve as: + +- Examples for commonly needed query types +- Templates for your own, more complex queries +- Learning material for SPARQL beginners + +## Educational Resources Example + +This example demonstrates how to retrieve comprehensive information about educational resources from the NFDI4Earth Knowledge Hub. + +### Query Purpose + +The query finds all learning resources and their associated metadata, including: + +- Publishers and their names +- Subject areas with labels +- Licensing information +- Related topics and their titles + +### SPARQL Features Demonstrated + +- Use of `CONSTRUCT` for graph pattern matching +- Multiple `OPTIONAL` patterns for flexible data retrieval +- Property path navigation +- Handling of multiple vocabularies (schema.org, FOAF, DCT) + +### Query + +```sparql +{{ include_if_exists("examples/Educational resources.rq") }} +``` + +### Understanding the Results + +The query returns a graph structure where: + +- Each learning resource is connected to its direct properties +- Additional metadata about publishers, subjects, and licenses is included +- Labels and titles are retrieved for better human readability + + +## Repository Metadata Standards Example + +This example shows how to query metadata standards and API types supported by repositories in the NFDI4Earth Knowledge Hub. + +### Query Purpose + +The query retrieves information about: +- Repository names +- Supported metadata standards +- Available API types and interfaces + +### SPARQL Features Demonstrated + +- Basic `SELECT` query structure +- Multiple triple patterns +- Property traversal +- Use of domain-specific vocabularies (n4e) + +### Query + +```sparql +{{ include_if_exists("examples/Metadata.rq") }} +``` + +### Understanding the Results + +The query returns: + +- Repository names for clear identification +- Names of supported metadata standards +- Types of APIs available for each repository + +This information is particularly useful for: + +- Understanding repository capabilities +- Planning data integration +- Evaluating technical compatibility + +## NFDI4Earth Services Example + +This example demonstrates how to query services that are provided by organizations within the NFDI4Earth consortium. + +### Query Purpose + +The query identifies: +- Services (Repositories and Aggregators) +- Their names and types +- Publishing organizations within NFDI4Earth +- Including services from sub-organizations + +### SPARQL Features Demonstrated + +- Complex `SELECT` query with `GROUP_CONCAT` +- `UNION` patterns for alternative paths +- Organization hierarchy traversal +- Filter conditions with `NOT EXISTS` +- Value constraints using `VALUES` + +### Query + +```sparql +{{ include_if_exists("examples/Services in the NFDI4Earth.rq") }} +``` + +### Understanding the Results + +The query returns: + +- Service names and their types (Repository or Aggregator) +- Concatenated list of publishing organizations +- Services from both direct NFDI4Earth members and their sub-organizations + +This query is useful for: + +- Getting an overview of NFDI4Earth service landscape +- Understanding organizational relationships +- Service discovery and analysis \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 31f12d7..a6b2ad4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,20 +2,60 @@ Welcome to the NFDI4Earth KnowledgeGraph Query Collection. This documentation provides insights into the NFDI4Earth KnowledgeHub Graph through SPARQL queries and their analysis. +## About the Project + +The NFDI4Earth KnowledgeGraph represents a comprehensive network of earth science research data, connecting various domains, datasets, and research artifacts. Based on the NFDI4Earth metadata schema, it enables: + +- **Linked Research Data**: Integration of various data sources and research artifacts +- **Semantic Search**: Intelligent discovery of relevant resources +- **Community Integration**: Connecting researchers and their work + ## Purpose -The NFDI4Earth KnowledgeGraph represents a comprehensive network of earth science research data, connecting various domains, datasets, and research artifacts. Through SPARQL queries, we explore: +Through SPARQL queries, we explore: - **Data Discovery**: Finding relevant research data across earth science domains - **Domain Coverage**: Understanding the breadth and depth of represented research areas - **Graph Structure**: Analyzing the knowledge graph's characteristics and connectivity -## Exploration Areas +## Query Collection + +Our queries are organized into three main categories. + +[**1. Basic Examples**](/examples) + + - Common query patterns + - Graph exploration basics + - Getting started guides + +[**2. Domain Questions**](/questions) + + - Domain-specific research questions + - Real-world use cases + - Complex query patterns + +[**3. Graph Metrics**](/metrics) + + - Structural metrics + - Quality analysis + - Network characteristics + +## Getting Started + +Each query is documented with: +- Detailed description of the use case +- SPARQL code with explanations +- Example results and visualizations +- Interpretation guidelines +- Performance considerations + +## Contributing -We collect queries for three main purposes: +We invite the community to contribute their own queries! -1. **Basic Examples** to demonstrate common query patterns and graph exploration -2. **Domain Questions** addressing specific research data discovery needs -3. **Graph Metrics** providing insights into the knowledge graph's structure +## Resources -Each query is documented with its purpose, expected results, and practical implications for understanding and utilizing the NFDI4Earth KnowledgeHub. +- [NFDI4Earth OneStop4All](https://onestop4all.nfdi4earth.de) +- [KnowledgeHub Documentation](https://knowledgehub.nfdi4earth.de) +- [SPARQL Endpoint](https://sparql.knowledgehub.nfdi4earth.de) +- [GitHub Repository](https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_questions) -- GitLab From 1a4181c4db2eabff917d5b65a761eb0bb563b0d4 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:49:52 +0100 Subject: [PATCH 52/59] [metrics] fix: links in main index.md --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index a6b2ad4..de2ff1d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,19 +22,19 @@ Through SPARQL queries, we explore: Our queries are organized into three main categories. -[**1. Basic Examples**](/examples) +[**1. Basic Examples**](examples) - Common query patterns - Graph exploration basics - Getting started guides -[**2. Domain Questions**](/questions) +[**2. Domain Questions**](questions) - Domain-specific research questions - Real-world use cases - Complex query patterns -[**3. Graph Metrics**](/metrics) +[**3. Graph Metrics**](metrics) - Structural metrics - Quality analysis -- GitLab From 4bb1d5025cfbe0f86e1d21974fdcebe0d391d30a Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Thu, 20 Mar 2025 15:52:05 +0100 Subject: [PATCH 53/59] [metrics] fix: links in main index.md --- docs/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index de2ff1d..bedb30f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,19 +22,19 @@ Through SPARQL queries, we explore: Our queries are organized into three main categories. -[**1. Basic Examples**](examples) +[**1. Basic Examples**](./examples) - Common query patterns - Graph exploration basics - Getting started guides -[**2. Domain Questions**](questions) +[**2. Domain Questions**](./questions) - Domain-specific research questions - Real-world use cases - Complex query patterns -[**3. Graph Metrics**](metrics) +[**3. Graph Metrics**](./metrics) - Structural metrics - Quality analysis -- GitLab From 33110c2c93dee514e5aff8ace15487182999777d Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 21 Mar 2025 09:36:00 +0100 Subject: [PATCH 54/59] [metrics] Add explanation on incoming & outgoing edges --- .../general_metrics/04_edges_incoming.md | 35 ++++++++++++++++++ .../general_metrics/05_edges_outgoing.md | 37 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/docs/metrics/general_metrics/04_edges_incoming.md b/docs/metrics/general_metrics/04_edges_incoming.md index d649181..c4ad423 100644 --- a/docs/metrics/general_metrics/04_edges_incoming.md +++ b/docs/metrics/general_metrics/04_edges_incoming.md @@ -2,6 +2,41 @@ This metric determines the median number of incoming edges across all nodes in the graph. The calculation requires multiple steps. +## Understanding Incoming Edges + +In a knowledge graph, incoming edges represent relationships where other nodes point to or reference a particular node. Think of them as "arrows" pointing towards a node. For example: + +- If Dataset A `references` Resource B, then Resource B has an incoming edge +- If Organization X `publishes` Dataset Y, then Dataset Y has an incoming edge +- If Service M `supports` Standard N, then Standard N has an incoming edge + +### Significance + +The number of incoming edges can indicate: +- How frequently a resource is referenced or used +- The centrality or importance of a node in the network +- Potential bottlenecks or key connection points +- The interconnectedness of different resources + +### Example + +``` +Resource A ---hasLicense---> License X (X has 1 incoming edge) +Resource B ---hasLicense---> License X (X now has 2 incoming edges) +Resource C ---hasLicense---> License X (X now has 3 incoming edges) +``` + +```turtle +@prefix ex: <http://example.org/> . +@prefix dct: <http://purl.org/dc/terms/> . + +ex:ResourceA dct:license ex:LicenseX . +ex:ResourceB dct:license ex:LicenseX . +ex:ResourceC dct:license ex:LicenseX . +``` + +In this example, License X has 3 incoming edges, indicating it's a commonly used license in the graph. + {{ metrics_table_single_general('edges_in') }} ## Queries diff --git a/docs/metrics/general_metrics/05_edges_outgoing.md b/docs/metrics/general_metrics/05_edges_outgoing.md index 5531d38..4b96894 100644 --- a/docs/metrics/general_metrics/05_edges_outgoing.md +++ b/docs/metrics/general_metrics/05_edges_outgoing.md @@ -2,6 +2,43 @@ This metric determines the median number of outgoing edges across all nodes in the graph. The calculation requires multiple steps. +## Understanding Outgoing Edges + +In a knowledge graph, outgoing edges represent relationships where a node points to or references other nodes. Think of them as "arrows" pointing away from a node. For example: + +- If Dataset A `references` Resource B, then Dataset A has an outgoing edge +- If Organization X `publishes` Dataset Y, then Organization X has an outgoing edge +- If Service M `supports` Standard N, then Service M has an outgoing edge + +### Significance + +The number of outgoing edges can indicate: +- How many relationships a node initiates +- The completeness of resource descriptions +- Connection patterns and data modeling practices +- The level of detail in resource metadata + +### Example + +``` +Dataset A ---dct:license-----> License X (A has now 1 outgoing edge) +Dataset A ---dct:publisher---> Organization Y (A has now 2 outgoing edges) +Dataset A ---schema:about----> Topic Z (A has now 3 outgoing edges) +``` + +```turtle +@prefix ex: <http://example.org/> . +@prefix dct: <http://purl.org/dc/terms/> . +@prefix schema: <http://schema.org/> . + +# Dataset has 3 outgoing edges +ex:DatasetA dct:license ex:LicenseX ; + dct:publisher ex:OrganizationY ; + schema:about ex:TopicZ . +``` + +In this example, Dataset A has 3 outgoing edges, demonstrating a well-described resource with license, publisher, and topic information. + {{ metrics_table_single_general('edges_out') }} ## Queries -- GitLab From 9c0d112b9b4928442fce98cb8daa39549715322a Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 21 Mar 2025 09:56:55 +0100 Subject: [PATCH 55/59] [metrics] Update readme's --- README.md | 60 ++++++++++++++++++++++++++++++----------------- scripts/README.MD | 44 ---------------------------------- scripts/README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 65 deletions(-) delete mode 100644 scripts/README.MD create mode 100644 scripts/README.md diff --git a/README.md b/README.md index d750a1c..094b43d 100644 --- a/README.md +++ b/README.md @@ -5,48 +5,66 @@ A collection of SPARQL queries for analyzing the KnowledgeGraph of NFDI4Earth's ## Repository Structure ``` -queries/ -├── examples/ # Basic SPARQL examples -├── questions/ # Domain-specific queries -└── metrics/ # Graph analysis queries - -docs/ -├── examples.md # Documentation for basic examples -├── questions.md # Documentation for domain questions -└── metrics.md # Documentation for graph metrics +├── docs/ +│ ├── index.md # Main documentation page +│ ├── examples.md +│ ├── questions.md +│ ├── macros/ +│ ├── metrics/ +│ │ ├── general_metrics/ +│ │ ├── resource_metrics/ +│ │ └── schema_complexity_metrics/ +│ └── queries/ # Query documentation +│ ├── examples/ +│ ├── metrics/ +│ └── questions/ +│ +└── scripts/ # Python analysis tools + └── kg_analysis/ + └── ... ``` ## Usage -All queries are stored in `.rq` files and can be executed against the NFDI4Earth KnowledgeGraph endpoint. +The repository consists of three main components: + +1. **Documentation** (`/docs`): Comprehensive documentation of queries, examples and metrics +2. **Query Collection** (`/docs/queries`): SPARQL queries organized by purpose +3. **Analysis Tool** (`/scripts`): Python package for executing queries and calculating metrics ### Local Development -0. Setup virtual environment +1. Setup virtual environment: ```bash python3 -m venv venv -. venv/bin/activate +source venv/bin/activate ``` -1. Install dependencies: +2. Install dependencies: ```bash pip install -r requirements.txt +pip install -e scripts/ ``` -2. Start local documentation server: +3. Start documentation server: ```bash mkdocs serve ``` -3. Build documentation: -```bash -mkdocs build -``` - ## Contributing -We welcome contributions! Please check our contribution guidelines for adding new queries. +We welcome contributions: + +- New SPARQL queries +- Documentation improvements +- Tool enhancements + +### Contributors + +Ralf Klammer, Auriol Degbelo, Jonas Grieb ## Contact -For questions about the NFDI4Earth KnowledgeHub Graph, contact [helpdesk@nfdi4earth.de](mailto:helpdesk@nfdi4earth.de?subject=[NFDI4Earth][KnowlegeGraph]). +For questions about the NFDI4Earth KnowledgeHub Graph: +- Email: [helpdesk@nfdi4earth.de](mailto:helpdesk@nfdi4earth.de?subject=[NFDI4Earth][KnowlegeGraph]) +- Website: [https://knowledgehub.nfdi4earth.de/](https://knowledgehub.nfdi4earth.de) \ No newline at end of file diff --git a/scripts/README.MD b/scripts/README.MD deleted file mode 100644 index bc7066b..0000000 --- a/scripts/README.MD +++ /dev/null @@ -1,44 +0,0 @@ -# KG Analysis Tool - -A command-line tool for analyzing SPARQL endpoints, specifically designed for the NFDI4Earth Knowledge Graph. - -## Features - -- Run SPARQL queries from files -- Save query results to output files -- Configurable SPARQL endpoint via environment variable - -## Installation - -```bash -# Create and activate virtual environment -python3 -m venv venv -source venv/bin/activate - -# Install in development mode -pip install -e . -``` - -## Usage - -Set the SPARQL endpoint (optional): -```bash -export KG_SPARQL_ENDPOINT="https://your-sparql-endpoint/sparql" -``` - -Run a query: -```bash -kg_analysis run-query -q path/to/query.rq -o path/to/output.txt -``` - -## Requirements - -- Python >= 3.10 -- click -- SPARQLWrapper - -## License - -This project is published under the Apache License 2.0, see file `LICENSE`. - -Contributors: Ralf Klammer diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..650e60a --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,60 @@ +# KG Analysis Tool + +A command-line tool for analyzing SPARQL endpoints, specifically designed for the NFDI4Earth Knowledge Graph and its metrics collection. + +## Features + +- Run SPARQL queries from files +- Execute predefined metric queries +- Save query results in JSON +- Configurable SPARQL endpoint, request timeout & directory of reports via environment variables +- Supports query parameters and templates + +## Installation + +```bash +# Create and activate virtual environment +python3 -m venv venv +source venv/bin/activate + +# Install in development mode +pip install -e . +``` + +## Usage + +### Environment Variables + +```bash +# Required +export SPARQL_ENDPOINT="https://sparql.knowledgehub.nfdi4earth.de" + +# Optional +export SPARQL_TIMEOUT=120 # in seconds +export REPORTS_DIR="./reports/metrics" +``` + +### CLI Commands + +Run a single query: + +```bash +kg-analysis query -q path/to/query.rq +``` + +Request/Calculate all metrics: + +```bash +kg-analysis metrics +``` + +## License + +This project is published under the Apache License 2.0, see file [`LICENSE`](LICENSE). + +## Related Projects + +- [NFDI4Earth KnowledgeHub](https://knowledgehub.nfdi4earth.de) +- [OneStop4All](https://onestop4all.nfdi4earth.de) + +Contributors: Ralf Klammer -- GitLab From 18ade219627264e81f75ac6632254aaa2cc577a0 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 21 Mar 2025 10:01:42 +0100 Subject: [PATCH 56/59] [metrics] Remove previous readme --- README.bkp.md | 173 -------------------------------------------------- 1 file changed, 173 deletions(-) delete mode 100644 README.bkp.md diff --git a/README.bkp.md b/README.bkp.md deleted file mode 100644 index 6f8681c..0000000 --- a/README.bkp.md +++ /dev/null @@ -1,173 +0,0 @@ -# KnowledgeHub - Domain Coverage - -This is collection of relevant questions and corresponding SPARQL-Queries, that answer those questions. The questions are grouped according to the different entities of interest (datasets, organizations, ...). The entities appear in alphabetical order. The first query is useful to get an overview of all entities available in the Knowledge Hub. The questions listed below form, altogether, the domain coverage of the Knowledge Hub. For details, see the [NFDI4Earth Deliverable D4.3.2](https://zenodo.org/records/7950860). - -***Overview of the types of entities*** -| ID | Question | Query/ies | -|---|---|---| -| TY001 | What are the types of entities available in the knowledge graph? | [TY001](queries/TY001.rq)| - - - - -### Aggregator - -| ID | Question | Query/ies | -|---|---|---| -| AG001 | What are all entities of type Aggregator? | [AG001](queries/AG001.rq)| -| AG001_2 | What are name and geometry of Aggregator? | [AG001_2](queries/AG001_2.rq)| -| AG002_1 | What are all attributes available for the type "Aggregator"? | [AG002_1](queries/AG002_1.rq)| -| AG002_2 | How many attributes are available for the type "Aggregator"? | [AG002_2](queries/AG002_2.rq)| - -### Article - -| ID | Question | Query/ies | -|---|---|---| -| AT001 | What are all entities of type schema:Article? | [AT001](queries/AT001.rq)| -| AT002_1 | What are all attributes available for the type "schema:Article"? | [AT002_1](queries/AT002_1.rq)| -| AT002_2 | How many attributes are available for the type "schema:Article"? | [AT002_2](queries/AT002_2.rq)| - -### Dataset - -| ID | Question | Query/ies | -|---|---|---| -| DA001 | What are all entities of type dcat:Dataset? | [DA001](queries/DA001.rq)| -| DA002_1 | What are all attributes available for the type "dcat:Dataset"? | [DA002_1](queries/DA002_1.rq)| -| DA002_2 | How many attributes are available for the type "dcat:Dataset"? | [DA002_2](queries/DA002_2.rq)| -| DA003_1 | What are the datasets having the string 'world settlement footprint' in title or description? | [DA003_1](queries/DA003_1.rq)| - -### LHBArticle - -| ID | Question | Query/ies | -|---|---|---| -| LH001 | What are all entities of type LHBArticle? | [LH001](queries/LH001.rq)| -| LH002_1 | What are all attributes available for the type "LHBArticle"? | [LH002_1](queries/LH002_1.rq)| -| LH002_2 | How many attributes are available for the type "LHBArticle"? | [LH002_2](queries/LH002_2.rq)| - -### LearningResource - -| ID | Question | Query/ies | -|---|---|---| -| LR001 | What are all entities of type LearningResource? | [LR001](queries/LR001.rq)| -| LR002_1 | What are all attributes available for the type "LearningResource"? | [LR002_1](queries/LR002_1.rq)| -| LR002_2 | How many attributes are available for the type "LearningResource"? | [LR002_2](queries/LR002_2.rq)| - -### MetadataStandard - -| ID | Question | Query/ies | -|---|---|---| -| MS001 | What are all entities of type MetadataStandard? | [MS001](queries/MS001.rq)| -| MS002_1 | What are all attributes available for the type "MetadataStandard"? | [MS002_1](queries/MS002_1.rq)| -| MS002_2 | How many attributes are available for the type "MetadataStandard"? | [MS002_2](queries/MS002_2.rq)| - -### Organization - -| ID | Question | Query/ies | -|---|---|---| -| OG001 | What are all entities of type Organization? | [OG001](queries/OG001.rq)| -| OG002_1 | What are all attributes available for the type "Organization"? | [OG002_1](queries/OG002_1.rq)| -| OG002_2 | How many attributes are available for the type "Organization"? | [OG002_2](queries/OG002_2.rq)| - -### Person - -| ID | Question | Query/ies | -|---|---|---| -| PE001 | What are all entities of type Person? | [PE001](queries/PE001.rq)| -| PE002_1 | What are all attributes available for the type "Person"? | [PE002_1](queries/PE002_1.rq)| -| PE002_2 | How many attributes are available for the type "Person"? | [PE002_2](queries/PE002_2.rq)| - -### Registry - -| ID | Question | Query/ies | -|---|---|---| -| REG001 | What are all entities of type Registry? | [REG001](queries/REG001.rq)| -| REG002_1 | What are all attributes available for the type "Registry"? | [REG002_1](queries/REG002_1.rq)| -| REG002_2 | How many attributes are available for the type "Registry"? | [REG002_2](queries/REG002_2.rq)| - -### Repository - -| ID | Question | Query/ies | -|---|---|---| -| REP001 | What are all entities of type Repository? | [REP001](queries/REP001.rq)| -| REP002_1 | What are all attributes available for the type "Repository"? | [REP002_1](queries/REP002_1.rq)| -| REP002_2 | How many attributes are available for the type "Repository"? | [REP002_2](queries/REP002_2.rq)| - -### ResearchProject - -| ID | Question | Query/ies | -|---|---|---| -| RP001 | What are all entities of type ResearchProject? | [RP001](queries/RP001.rq)| -| RP002_1 | What are all attributes available for the type "ResearchProject"? | [RP002_1](queries/RP002_1.rq)| -| RP002_2 | How many attributes are available for the type "ResearchProject"? | [RP002_2](queries/RP002_2.rq)| - - -### SoftwareSourceCode - -| ID | Question | Query/ies | -|---|---|---| -| SC001 | What are all entities of type SoftwareSourceCode? | [SC001](queries/SC001.rq)| -| SC002_1 | What are all attributes available for the type "SoftwareSourceCode"? | [SC002_1](queries/SC002_1.rq)| -| SC002_2 | How many attributes are available for the type "SoftwareSourceCode"? | [SC002_2](queries/SC002_2.rq)| - -### Graph Metrics - -| ID | Question | Query/ies | -|---|---|---| -|GM001|The number of instances in a graph|[GM001](queries/GM001.rq)| -|GM002|The number of assertions (or edges between entities)|[GM002](queries/GM002.rq)| -|||| -|||| -|||| -|||| - - -<!--- Template for a new table (including first line) - -### EntityType - -| ID | Question | Query/ies | -|---|---|---| -| XX001 | What are all entities of type EntityType? | [XX001](queries/XX001.rq)| - ---> - - -<!--- - - -### Organizations - -| ID | Question | Query/ies | -|---|---|---| -| OR001 | What is the URL of the homepage for the organization with the following name: 'Karlsruhe Institute of Technology'? | [OR001_1](queries/OR001_1.rq),[OR001_2](queries/OR001_2.rq) | -| OR002 | What is the URL of the homepage for the organization with the following ID: 'https://nfdi4earth-knowledgehub.geo.tu-dresden.de/api/objects/n4ekh/a38143be5e15bed94a20' | [OR003_1](queries/OR003_1.rq) | -| OR003 | Which organizations have not defined any homepage? | [OR003_1](queries/OR003_1.rq) | -| OR004 | Which services are published by the organization? | [OR004_1](queries/OR004_1.rq) | -| OR005 | What is the geolocation of the organization called 'TU Dresden'? | [OR005_1](queries/OR005_1.rq) | -| OR006 | What is the geolocation of all organizations, that are members of the NFDI4Earth consortium? | [OR006_1](queries/OR006_1.rq) | - -### Repositories - -| ID | Question | Query/ies | -|----|----------|-----------| -| DR1 | At which repository can I archive my [geophysical] data of [2] GB?| [OR004_1](queries/OR004_1.rq) | -| DR2 | What is the temporal coverage of a data repository?|| -| DR3 | What is the spatial coverage of a data repository?|| -| DR4 | What is the curation policy of the data repository?|| -| DR5 | Which licences are supported by the data repository?|| -| DR6 | Does the repository give identifiers for its ressources?|| -| DR7 | Which metadata harversting interface is supported by the repository?|| -| DR8 | Which type of (persistent) identifiers are used by the repository?|| -| DR9 | What is the thematic area/subject of a repository?|| -| DR10 | Limitations of data deposit at the repository?|| -| DR11 | When was the medatada for a given repository first collected/last updated?|| -| DR12 | Is the repository still available?|| -| DR13 | Which repository allows long term archiving?|| - ---> - -# Notes - -This question-based approach takes inspiration from the [GeoSPARQLBenchmark](https://github.com/OpenLinkSoftware/GeoSPARQLBenchmark). - -It is directly linked to the [Knowledge Hub landing page project](https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_landingpage) as all the questions and examples are taken to explain the basic idea and demonstrate usage of the [Knowledge Hub](https://knowledgehub.nfdi4earth.de). -- GitLab From e139a035029a04258bb4f7105a0c3b17118ca898 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 21 Mar 2025 10:16:41 +0100 Subject: [PATCH 57/59] [metrics] config site in english --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 65887ba..8f1baff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ repo_url: https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_questions theme: name: material - language: de + language: en features: - search.highlight - content.code.copy -- GitLab From 38c050193af5be81350894b1a5866b553dd5d28d Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Fri, 21 Mar 2025 10:34:18 +0100 Subject: [PATCH 58/59] [metrics] modify navbar - colors, symbol & favicon --- docs/assets/NFDI4Earth_Symbol.png | Bin 0 -> 92560 bytes docs/assets/favicon.ico | Bin 0 -> 32988 bytes mkdocs.yml | 14 +++++--------- 3 files changed, 5 insertions(+), 9 deletions(-) create mode 100644 docs/assets/NFDI4Earth_Symbol.png create mode 100644 docs/assets/favicon.ico diff --git a/docs/assets/NFDI4Earth_Symbol.png b/docs/assets/NFDI4Earth_Symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..1a357281f6c49602f4c52ce75850e81db513ae8f GIT binary patch literal 92560 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%0Ar;N$k8#g(` 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 Pd@*>s`njxgN@xNA4)oyD literal 0 HcmV?d00001 diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..83fd5498b6832c052576e708cbd4e5e5eda77d6d GIT binary patch literal 32988 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 literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 8f1baff..329d821 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,9 +5,14 @@ repo_url: https://git.rwth-aachen.de/nfdi4earth/knowledgehub/kh_questions theme: name: material language: en + logo: assets/NFDI4Earth_Symbol.png + favicon: assets/favicon.ico features: - search.highlight - content.code.copy + palette: + - primary: blue + accent: cyan plugins: - search @@ -21,12 +26,3 @@ exclude_docs: markdown_extensions: - admonition - pymdownx.details -# - pymdownx.superfences -# - pymdownx.snippets: -# base_path: ['.'] -# check_paths: true -# - pymdownx.highlight: -# anchor_linenums: true -# line_spans: __span -# pygments_lang_class: true -# - pymdownx.inlinehilite \ No newline at end of file -- GitLab From b1b4d6fc8ed4c1910be4d9cc9eac16dd53f07487 Mon Sep 17 00:00:00 2001 From: Ralf Klammer <ralf.klammer@tu-dresden.de> Date: Wed, 30 Apr 2025 14:14:41 +0200 Subject: [PATCH 59/59] [metrics] refine references on complexity measurement --- docs/metrics/index.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/metrics/index.md b/docs/metrics/index.md index 064667b..d58e1a5 100644 --- a/docs/metrics/index.md +++ b/docs/metrics/index.md @@ -79,25 +79,21 @@ This formula provides a single numerical value representing schema complexity, w ### References -> !!!Not the final references!!! +> Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" - Describes metrics like number of classes, hierarchy depth, and relations +> +> *[https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3](https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3)* -1. Structural Metrics for Ontologies: +2) OWL and Schema Complexity Measurements: -- Gómez-Pérez et al. (2004) – "Evaluation of Ontologies" -- Describes metrics like number of classes, hierarchy depth, and relations -- Source: https://link.springer.com/chapter/10.1007/978-3-540-30202-5_3 +> Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" - Develops the OntoQA model combining structural and semantic metrics +> +> *[https://ieeexplore.ieee.org/document/4338348](https://ieeexplore.ieee.org/document/4338348)* -2. OWL and Schema Complexity Measurements: +3) SPARQL Analysis and RDF Complexity: - - Tartir & Arpinar (2010) – "Ontology Evaluation and Ranking using OntoQA" - - Develops the OntoQA model combining structural and semantic metrics - - Source: https://doi.org/10.1109/ICDEW.2005.43 - -3. SPARQL Analysis and RDF Complexity: - - - Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" - - Describes hierarchical depth as key metric for RDF schema complexity - - Source: https://doi.org/10.1007/978-3-540-92673-3_10 +> Lanzenberger et al. (2008) – "Ontology Evaluation – State of the Art" - Describes hierarchical depth as key metric for RDF schema complexity +> +> *[https://doi.org/10.1007/978-3-540-92673-3_10](https://doi.org/10.1007/978-3-540-92673-3_10)* ## About the Metrics -- GitLab