from SPARQLWrapper import SPARQLWrapper, JSONimport pandas as pdfrom IPython.display import display, Markdown, HTMLSPARQL_ENDPOINT ="https://prod-climatekg.semanticclimate.org/query/proxy/sparql"WIKIBASE_URL ="https://prod-climatekg.semanticclimate.org"PREFIXES =f"""PREFIX wd: <{WIKIBASE_URL}/entity/>PREFIX wdt: <{WIKIBASE_URL}/prop/direct/>PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>"""def run_query(query): sparql = SPARQLWrapper(SPARQL_ENDPOINT) sparql.setQuery(PREFIXES + query) sparql.setReturnFormat(JSON) results = sparql.query().convert() rows = [{k: v["value"] for k, v in b.items()} for b in results["results"]["bindings"]]return pd.DataFrame(rows)print("✓ Ready")
✓ Ready
Step 1 — Confirm the counts
First confirm the raw count vs distinct count to check for duplicate triples.
Show code
# Raw count (same as class-counts notebook)df_raw = run_query("SELECT (COUNT(?item) AS ?count) WHERE { ?item wdt:P1 wd:Q1 . }")raw_count =int(df_raw["count"].iloc[0])# Distinct count — removes duplicate ?item rows from the result setdf_distinct = run_query("SELECT (COUNT(DISTINCT ?item) AS ?count) WHERE { ?item wdt:P1 wd:Q1 . }")distinct_count =int(df_distinct["count"].iloc[0])print(f"Raw COUNT: {raw_count}")print(f"COUNT(DISTINCT): {distinct_count}")print(f"Duplicate triples: {raw_count - distinct_count}")print(f"Expected: 920")print(f"Over expected: {distinct_count -920}")
Raw COUNT: 1001
COUNT(DISTINCT): 1001
Duplicate triples: 0
Expected: 920
Over expected: 81
Step 2 — Items with duplicate P1=Q1 triples
If raw != distinct, these items have the P1=Q1 statement recorded more than once.
Show code
df_dupes = run_query("""SELECT ?item (COUNT(?item) AS ?triples) WHERE { ?item wdt:P1 wd:Q1 .}GROUP BY ?itemHAVING (COUNT(?item) > 1)ORDER BY DESC(?triples)""")if df_dupes.empty:print("✓ No duplicate triples — each item has exactly one P1=Q1 statement.")else:print(f"Found {len(df_dupes)} items with duplicate P1=Q1 triples:") df_dupes["wikibase_link"] = df_dupes["item"].apply(lambda u: f'<a href="{u}" target="_blank">{u.split("/")[-1]}</a>' ) display(HTML(df_dupes[["wikibase_link", "triples"]].to_html(escape=False, index=False)))
✓ No duplicate triples — each item has exactly one P1=Q1 statement.
Step 3 — Check if any Q1 items also have a second P1 value
Items classified as Q1 (Category) that also have another instance-of value — these might be misclassified.
Show code
df_multi = run_query("""SELECT ?item ?otherClass WHERE { ?item wdt:P1 wd:Q1 . ?item wdt:P1 ?otherClass . FILTER(?otherClass != wd:Q1)}ORDER BY ?otherClass""")if df_multi.empty:print("✓ No Q1 items have additional P1 values.")else:print(f"Found {len(df_multi)} Q1 items that also belong to another class:") df_multi["item_link"] = df_multi["item"].apply(lambda u: f'<a href="{u}" target="_blank">{u.split("/")[-1]}</a>' ) df_multi["class_link"] = df_multi["otherClass"].apply(lambda u: f'<a href="{u}" target="_blank">{u.split("/")[-1]}</a>' ) display(HTML(df_multi[["item_link", "class_link"]].to_html(escape=False, index=False)))print("\nBreakdown by other class:")print(df_multi["otherClass"].apply(lambda u: u.split("/")[-1]).value_counts().to_string())
✓ No Q1 items have additional P1 values.
Step 4 — Sample the extra items
Retrieve items with their labels to spot what the unexpected Q1 items are. We fetch all 1001 and look for patterns — items without labels, items with unusual QIDs, etc.
Total distinct Q1 items: 1001
With label: 1001
Without label: 0
QID range: Q8 — Q1175
Items without English labels
Show code
if no_label.empty:print("✓ All Q1 items have English labels.")else:print(f"{len(no_label)} Q1 items have no English label:") no_label_display = no_label.copy() no_label_display["link"] = no_label_display["item"].apply(lambda u: f'<a href="{u}" target="_blank">{u.split("/")[-1]}</a>' ) display(HTML(no_label_display[["link"]].to_html(escape=False, index=False)))
✓ All Q1 items have English labels.
High-QID items — likely recently added or imported
Items above a certain QID threshold may be from a recent import batch.
Show code
# Items with QID above 10000 — adjust threshold as neededTHRESHOLD =10000df_high = df_all[df_all["qid_num"] > THRESHOLD].copy()print(f"Q1 items with QID > Q{THRESHOLD}: {len(df_high)}")ifnot df_high.empty: df_high["link"] = df_high["item"].apply(lambda u: f'<a href="{u}" target="_blank">{u.split("/")[-1]}</a>' ) display(HTML(df_high[["link", "label"]].head(50).to_html(escape=False, index=False)))iflen(df_high) >50:print(f"... and {len(df_high) -50} more.")
Q1 items with QID > Q10000: 0
Step 5 — Summary
Summarise findings to pinpoint the source of the discrepancy.
Show code
print("="*50)print("INVESTIGATION SUMMARY")print("="*50)print(f"Raw count (P1=Q1): {raw_count}")print(f"Distinct count: {distinct_count}")print(f"Expected: 920")print(f"Excess over expected: {distinct_count -920}")print()print("Possible causes:")if raw_count != distinct_count:print(f" [!] {raw_count - distinct_count} duplicate P1=Q1 triples found — clean up recommended")else:print(" [✓] No duplicate triples")ifnot df_multi.empty:print(f" [!] {len(df_multi)} items have Q1 + another P1 class — possible misclassification")else:print(" [✓] No items with multiple P1 classes")ifnot no_label.empty:print(f" [!] {len(no_label)} items missing English label — possible test/import artefacts")else:print(" [✓] All items have English labels")ifnot df_high.empty:print(f" [!] {len(df_high)} items above Q{THRESHOLD} — check if from unexpected import batch")else:print(f" [✓] No items above Q{THRESHOLD} threshold")
==================================================
INVESTIGATION SUMMARY
==================================================
Raw count (P1=Q1): 1001
Distinct count: 1001
Expected: 920
Excess over expected: 81
Possible causes:
[✓] No duplicate triples
[✓] No items with multiple P1 classes
[✓] All items have English labels
[✓] No items above Q10000 threshold