From 605dc4f96b849ebe9206d2b06bdb73b429c50bf9 Mon Sep 17 00:00:00 2001 From: 2211275 <2211275@stud.hs-mannheim.de> Date: Fri, 1 May 2026 21:32:40 +0200 Subject: [PATCH] added pipeline and corrected typing --- ...aph_rebrickable.ipynb => lego_graph.ipynb} | 116 ++++++++++++++---- lego/paper/KGR_paper1_lego.tex | 37 +++++- lego/paper/bilder/kgr_pipeline1.drawio | 31 +++++ lego/paper/bilder/kgr_pipeline1.drawio.png | Bin 0 -> 14797 bytes 4 files changed, 153 insertions(+), 31 deletions(-) rename lego/{lego_graph_rebrickable.ipynb => lego_graph.ipynb} (77%) create mode 100644 lego/paper/bilder/kgr_pipeline1.drawio create mode 100644 lego/paper/bilder/kgr_pipeline1.drawio.png diff --git a/lego/lego_graph_rebrickable.ipynb b/lego/lego_graph.ipynb similarity index 77% rename from lego/lego_graph_rebrickable.ipynb rename to lego/lego_graph.ipynb index 60c5ce4..4ae387a 100644 --- a/lego/lego_graph_rebrickable.ipynb +++ b/lego/lego_graph.ipynb @@ -17,8 +17,7 @@ "source": [ "from rdflib import Graph, Namespace, XSD, OWL, RDF, RDFS, SKOS, URIRef, Literal\n", "import pandas as pd\n", - "from datetime import datetime\n", - "import os" + "from datetime import datetime" ] }, { @@ -88,7 +87,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "ae505704", "metadata": {}, "outputs": [], @@ -96,6 +95,7 @@ "for color in re_colors.itertuples(index=False):\n", " color_ref = thm[f\"color/{color.id}\"]\n", "\n", + " g.add((color_ref, RDF.type, THM.Color ))\n", " g.add((color_ref, RDFS.label, Literal(color.name, lang=\"en\")))\n", " g.add((color_ref, THM.color, Literal(color.rgb)))\n", " g.add((color_ref, THM.is_transparent, Literal(color.is_trans, datatype=XSD.boolean)))\n", @@ -126,6 +126,7 @@ "for part_category in re_part_categories.itertuples(index=False):\n", " part_category_ref = thm[f\"part_category/{part_category.id}\"]\n", "\n", + " g.add((part_category_ref, RDF.type, THM.PartCategory ))\n", " g.add((part_category_ref, RDFS.label, Literal(part_category_ref, lang=\"en\")))" ] }, @@ -147,6 +148,7 @@ "for part in re_parts.itertuples(index=False):\n", " part_ref = thm[f\"part/{part.part_num}\"]\n", "\n", + " g.add((part_ref, RDF.type, THM.Part))\n", " g.add((part_ref, RDFS.label, Literal(part.name, lang=\"en\")))\n", " g.add((part_ref, THM.part_category, thm[f\"part_category/{part.part_cat_id}\"]))\n", " g.add((part_ref, THM.part_material, Literal(part.part_material)))" @@ -162,7 +164,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "579b1d67", "metadata": {}, "outputs": [], @@ -171,6 +173,7 @@ " part_ref = thm[f\"part/{element.part_num}\"]\n", " color_ref = thm[f\"color/{element.color_id}\"]\n", "\n", + " g.add((part_ref, RDF.type, THM.Element))\n", " g.add((part_ref, THM.has_color, color_ref))" ] }, @@ -193,7 +196,8 @@ " part_ref_parent = thm[f\"part/{part_relationship.parent_part_num}\"]\n", " part_ref_child = thm[f\"part/{part_relationship.child_part_num}\"]\n", "\n", - " g.add((part_ref_parent, THM.has_child, part_ref_child))" + " g.add((part_ref_parent, THM.has_child, part_ref_child))\n", + " g.add((part_ref_child, THM.has_parent, part_ref_parent))" ] }, { @@ -214,6 +218,7 @@ "for theme in re_themes.itertuples(index=False):\n", " theme_ref = thm[f\"theme/{int(theme.id)}\"]\n", "\n", + " g.add((theme_ref, RDF.type, THM.Theme))\n", " g.add((theme_ref, RDFS.label, Literal(theme.name, lang=\"en\")))\n", "\n", " if not pd.isna(theme.parent_id):\n", @@ -238,8 +243,9 @@ "for lego_set in re_sets.itertuples(index=False):\n", " set_ref = thm[f\"set/lego/{lego_set.set_num}\"]\n", "\n", + " g.add((set_ref, RDF.type, THM.Set))\n", " g.add((set_ref, RDFS.label, Literal(lego_set.name, lang=\"en\")))\n", - " g.add((set_ref, THM.year, Literal(datetime(int(lego_set.year), 1, 1))))\n", + " g.add((set_ref, THM.year, Literal(int(lego_set.year), datatype=XSD.integer)))\n", " g.add((set_ref, THM.theme, thm[f\"theme/{int(lego_set.theme_id)}\"]))\n", " g.add((set_ref, THM.num_parts, Literal(int(lego_set.num_parts), datatype=XSD.integer)))\n", " g.add((set_ref, THM.brand, Literal(\"Lego\")))" @@ -263,8 +269,9 @@ "for minifig in re_minifigs.itertuples(index=False):\n", " minifig_ref = thm[f\"minifig/{minifig.fig_num}\"]\n", "\n", - " g.add((set_ref, RDFS.label, Literal(minifig.name, lang=\"en\")))\n", - " g.add((set_ref, THM.num_parts, Literal(int(minifig.num_parts), datatype=XSD.integer)))" + " g.add((minifig_ref, RDF.type, THM.Minifig))\n", + " g.add((minifig_ref, RDFS.label, Literal(minifig.name, lang=\"en\")))\n", + " g.add((minifig_ref, THM.num_parts, Literal(int(minifig.num_parts), datatype=XSD.integer)))" ] }, { @@ -277,7 +284,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "id": "0c97dc4d", "metadata": {}, "outputs": [], @@ -285,6 +292,7 @@ "for inventory in re_inventories.itertuples(index=False):\n", " inventory_ref = thm[f\"inventory/{inventory.id}\"]\n", "\n", + " g.add((inventory_ref, RDF.type, THM.Inventory))\n", " g.add((inventory_ref, THM.set, thm[f\"set/lego/{inventory.set_num}\"]))" ] }, @@ -302,21 +310,39 @@ "execution_count": 13, "id": "dc2ba03e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nfor inventory_part in re_inventory_parts.itertuples(index=False):\\n inventory_part_ref = thm[f\"inventory_part/{inventory_part.inventory_id}/{inventory_part.part_num}\"]\\n\\n inventory_ref = thm[f\"inventory/{inventory_part.inventory_id}\"]\\n part_ref = thm[f\"part/{inventory_part.part_num}\"]\\n\\n g.add((inventory_part_ref, RDF.type, THM.PartInv))\\n g.add((inventory_part_ref, RDF.type, RDF.Property))\\n\\n g.add((inventory_part_ref, RDFS.domain, THM.Inventory))\\n g.add((inventory_part_ref, RDFS.range, THM.Part))\\n\\n g.add((inventory_ref, THM.contains, inventory_part_ref))\\n g.add((part_ref, THM.belongs, inventory_part_ref))\\n\\n g.add((inventory_part_ref, THM.quantity, Literal(int(inventory_part.quantity), datatype=XSD.integer)))\\n g.add((inventory_part_ref, THM.is_spare, Literal(inventory_part.is_spare, datatype=XSD.boolean)))\\n g.add((inventory_part_ref, THM.color, thm[f\"color/{inventory_part.color_id}\"]))\\n'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ + "\"\"\"\n", "for inventory_part in re_inventory_parts.itertuples(index=False):\n", " inventory_part_ref = thm[f\"inventory_part/{inventory_part.inventory_id}/{inventory_part.part_num}\"]\n", " \n", " inventory_ref = thm[f\"inventory/{inventory_part.inventory_id}\"]\n", " part_ref = thm[f\"part/{inventory_part.part_num}\"]\n", "\n", - " g.add((inventory_part_ref, RDFS.domain, inventory_ref))\n", - " g.add((inventory_part_ref, RDFS.range, part_ref))\n", + " g.add((inventory_part_ref, RDF.type, THM.PartInv))\n", " g.add((inventory_part_ref, RDF.type, RDF.Property))\n", + "\n", + " g.add((inventory_part_ref, RDFS.domain, THM.Inventory))\n", + " g.add((inventory_part_ref, RDFS.range, THM.Part))\n", " \n", + " g.add((inventory_ref, THM.contains, inventory_part_ref))\n", + " g.add((part_ref, THM.belongs, inventory_part_ref))\n", + "\n", " g.add((inventory_part_ref, THM.quantity, Literal(int(inventory_part.quantity), datatype=XSD.integer)))\n", " g.add((inventory_part_ref, THM.is_spare, Literal(inventory_part.is_spare, datatype=XSD.boolean)))\n", - " g.add((inventory_part_ref, THM.color, thm[f\"color/{inventory_part.color_id}\"]))" + " g.add((inventory_part_ref, THM.color, thm[f\"color/{inventory_part.color_id}\"]))\n", + "\"\"\"" ] }, { @@ -332,10 +358,15 @@ " inventory_ref = thm[f\"inventory/{inventory_set.inventory_id}\"]\n", " set_ref = thm[f\"set/lego/{inventory_set.set_num}\"]\n", "\n", - " g.add((inventory_set_ref, RDFS.domain, inventory_ref))\n", - " g.add((inventory_set_ref, RDFS.range, set_ref))\n", + " g.add((inventory_set_ref, RDF.type, THM.SetInv))\n", " g.add((inventory_set_ref, RDF.type, RDF.Property))\n", "\n", + " g.add((inventory_set_ref, RDFS.domain, THM.Inventory))\n", + " g.add((inventory_set_ref, RDFS.range, THM.Set))\n", + "\n", + " g.add((inventory_ref, THM.contains, inventory_set_ref))\n", + " g.add((set_ref, THM.belongs, inventory_set_ref))\n", + " \n", " g.add((inventory_set_ref, THM.quantity, Literal(int(inventory_set.quantity), datatype=XSD.integer)))" ] }, @@ -350,12 +381,17 @@ " inventory_minifig_ref = thm[f\"inventory_minifig/{inventory_minifig.inventory_id}/{inventory_minifig.fig_num}\"]\n", "\n", " inventory_ref = thm[f\"inventory/{inventory_minifig.inventory_id}\"]\n", - " minifig_ref = thm[f\"minifig/lego/{inventory_minifig.fig_num}\"]\n", + " minifig_ref = thm[f\"minifig/{inventory_minifig.fig_num}\"]\n", "\n", - " g.add((inventory_minifig_ref, RDFS.domain, inventory_ref))\n", - " g.add((inventory_minifig_ref, RDFS.range, minifig_ref))\n", + " g.add((inventory_minifig_ref, RDF.type, THM.MinifigInv))\n", " g.add((inventory_minifig_ref, RDF.type, RDF.Property))\n", "\n", + " g.add((inventory_minifig_ref, RDFS.domain, THM.Inventory))\n", + " g.add((inventory_minifig_ref, RDFS.range, THM.Minifig))\n", + "\n", + " g.add((inventory_ref, THM.contains, inventory_minifig_ref))\n", + " g.add((minifig_ref, THM.belongs, inventory_minifig_ref))\n", + " \n", " g.add((inventory_minifig_ref, THM.quantity, Literal(int(inventory_minifig.quantity), datatype=XSD.integer)))" ] }, @@ -464,14 +500,27 @@ "execution_count": 21, "id": "ef52582e", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nfor bl_part in bl_parts.itertuples(index=False):\\n part_ref = thm[f\"part/{bl_part.part_id}\"]\\n\\n if not (part_ref, None, None) in g:\\n additional_entries += 1\\n g.add((part_ref, RDFS.label, Literal(bl_part.part_name, lang=\"en\")))\\n'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ + "\"\"\"\n", "for bl_part in bl_parts.itertuples(index=False):\n", " part_ref = thm[f\"part/{bl_part.part_id}\"]\n", "\n", " if not (part_ref, None, None) in g:\n", " additional_entries += 1\n", - " g.add((part_ref, RDFS.label, Literal(bl_part.part_name, lang=\"en\")))" + " g.add((part_ref, RDFS.label, Literal(bl_part.part_name, lang=\"en\")))\n", + "\"\"\"" ] }, { @@ -479,14 +528,27 @@ "execution_count": 22, "id": "8bf0ffeb", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "'\\nfor bl_minifig in bl_minifigs.itertuples(index=False):\\n minifig_ref = thm[f\"minfig/{bl_minifig.minifig_id}\"]\\n\\n if not (minifig_ref, None, None) in g:\\n additional_entries += 1\\n g.add((minifig_ref, RDFS.label, Literal(bl_minifig.minifig_name, lang=\"en\")))\\n'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ + "\"\"\"\n", "for bl_minifig in bl_minifigs.itertuples(index=False):\n", " minifig_ref = thm[f\"minfig/{bl_minifig.minifig_id}\"]\n", "\n", " if not (minifig_ref, None, None) in g:\n", " additional_entries += 1\n", - " g.add((minifig_ref, RDFS.label, Literal(bl_minifig.minifig_name, lang=\"en\")))" + " g.add((minifig_ref, RDFS.label, Literal(bl_minifig.minifig_name, lang=\"en\")))\n", + "\"\"\"" ] }, { @@ -499,7 +561,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Added 107748 items\n" + "Added 4131 items\n" ] } ], @@ -548,7 +610,7 @@ { "data": { "text/plain": [ - ")>" + ")>" ] }, "execution_count": 24, @@ -557,15 +619,15 @@ } ], "source": [ - "g.bind(\"thmont\", THM)\n", + "g.bind(\"thm\", THM)\n", "\n", - "g.serialize(\"lego_graph_rebrickable.ttl\", format=\"turtle\")" + "g.serialize(\"lego_graph.ttl\", format=\"turtle\")" ] } ], "metadata": { "kernelspec": { - "display_name": "venv (3.14.4)", + "display_name": "Python 3", "language": "python", "name": "python3" }, diff --git a/lego/paper/KGR_paper1_lego.tex b/lego/paper/KGR_paper1_lego.tex index 541db14..00bcbb3 100644 --- a/lego/paper/KGR_paper1_lego.tex +++ b/lego/paper/KGR_paper1_lego.tex @@ -30,7 +30,7 @@ a4paper,margin=25mm } -\title{\huge{Knowledgegraphen - Lego}} +\title{\huge{Knowledge Graph - Lego}} \date{\today} \author{ \begin{tabular}{ccc} @@ -127,7 +127,7 @@ \toprule & Brickset \\ \midrule URL & \url{https://brickset.com/}\\ - Beschaffung & Webscraping/CSV-Download \\ + Beschaffung & CSV-Download \\ Lizenz & nicht spezifiziert \\ Erhalt & 23.04.2026 \\ \bottomrule \end{tabularx} @@ -184,9 +184,11 @@ \begin{verbatim} https://thm.de/set/{brand}/{id} \end{verbatim} + Um die Dateigrösse des Graph zu reduzieren wurde \texttt{thm}, statt \texttt{th-mannheim} verwendet. \begin{figure}[H] - \includegraphics[width=\columnwidth]{bilder/example_part_number.png} + \centering + \includegraphics[width=0.8\columnwidth]{bilder/example_part_number.png} \caption{Lego Stein mit Teile-Nummer (Design-ID) 41769 \cite{cunninghamSellLEGOBricklink2018}} \label{fig:lego_example_part_number} \end{figure} @@ -199,14 +201,41 @@ \subsection{Pipeline} + Die Datensätze von \textit{Bricklink} und \textit{Merlins Steine} wurden durch Webscraping erhoben. Entstandene Fehler durch Ausnahmefälle mussten manuell bereinigt werden. Demnach ist dieser Teil nicht automatisierbar. Abbildung \ref{fig:pipeline} zeigt die Pipeline zur Erstellung des Knowledge Graph. + + \begin{figure}[H] + \includegraphics[width=\columnwidth]{./bilder/kgr_pipeline1.drawio.png} + \caption{Pipeline Erstellung Knowledge Graph} + \label{fig:pipeline} + \end{figure} + + \section{Evaluation} \subsection{Ergebnis} Das Projekt kann unter der URL: \url{https://gitty.informatik.hs-mannheim.de/2211275/kgr} betrachtet werden. - + Der resultierende Knowledge-Graph ist über 300 MB gross. \subsection{Beispiel-Queries} + Erhalten der Gesamtheit aller Lego Star Wars Minifiguren: + + \begin{verbatim} +SELECT DISTINCT ?name +WHERE { + ?set thmont:theme ?theme. + ?theme rdf:type thmont:Theme. + ?set rdf:type thmont:Set. + ?theme rdfs:label "Star Wars"@en. + ?inventory thmont:set ?set. + ?inventory rdf:type thmont:Inventory. + ?inventory thmont:contains ?minifig_inv. + ?minifig_inv rdf:type thmont:MinifigInv. + ?minifig thmont:belongs ?minifig_inv. + ?minifig rdfs:label ?name. +} + \end{verbatim} + \subsection{Abdeckung} \subsection{Konsistenz} diff --git a/lego/paper/bilder/kgr_pipeline1.drawio b/lego/paper/bilder/kgr_pipeline1.drawio new file mode 100644 index 0000000..3e24c56 --- /dev/null +++ b/lego/paper/bilder/kgr_pipeline1.drawio @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lego/paper/bilder/kgr_pipeline1.drawio.png b/lego/paper/bilder/kgr_pipeline1.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd7c7865316653ca6f9a669e760a97663feace2 GIT binary patch literal 14797 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 literal 0 HcmV?d00001