Google Colab （Python）にて SPARQL により Wikidata からデータを抽出する方法

## 概要

Google Colab （Python）にて Wikidata に対する SPARQL の基本的な操作を実施します。 Wikidata の基本的な説明を実施後、本ノートブックにて Python で Wikidata から SPARQL によりデータの抽出を実行します。

Spark の記事ではなく、 SPARQL の記事です。SPARQL とは、RDF（Resource Description Framework）データをクエリするための問い合わせ言語です。SPARQLは、W3C（World Wide Web Consortium）によって標準化された言語で、RDFデータモデルに基づくデータベースから情報を抽出するために使用されているようです。

## Wikidata について

Wikidata とは、下記のように説明されており、取得したデータを自由に再利用にできます。

> ウィキデータは、人間とコンピュータの双方が平等に参照・編集できる無料のオープンな知識データベースサイトです。

引用元：[Wikidata](https://www.wikidata.org/w/index.php?title=Wikidata:Main_Page&uselang=ja)

Wikidata 上のデータを取得する方法として、Wikidata Query Service という SPARQL を実行できる WEB インタフェースが提供されています。操作方法については、下記のドキュメントにて詳細が記載されています。

Wikidata Query Service では実行した SPARQL を追記した URL により共有することが可能であり、下記の URL を開くと Wikidata から 人口が多い TOP 10 の国を抽出する SPARQL が記述された状態の Wikidata Query Service に接続ができる。

- [人口が多い TOP 10 の国 の SPARQL](https://query.wikidata.org/#SELECT%20DISTINCT%20%3FcountryLabel%20%3Fpopulation%0A%7B%0A%20%20%3Fcountry%20wdt%3AP31%20wd%3AQ6256%20%3B%0A%20%20%20%20%20%20%20%20%20%20%20wdt%3AP1082%20%3Fpopulation%20.%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22ja%22%20%7D%0A%7D%0AGROUP%20BY%20%3Fpopulation%20%3FcountryLabel%0AORDER%20BY%20DESC%28%3Fpopulation%29%0ALIMIT%2010)

Wikidata Query Service には、他にも下記のような機能があります。

- サンプルクエリを実行する機能
- prefix を登録する機能
- Python 等のコードに変換する機能
- データをエクスポートする機能
- グラフ化機能

SPAQL についてより学習したい場合には、下記のドキュメントが参考となります。

- [ウィキデータ:SPARQLチュートリアル](https://query.wikidata.org/)

## 事前準備

### SPARQLWrapper のインストール

```python
!pip install SPARQLWrapper -q
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f37c4411-9d12-8c70-2649-18b0bd3133a8.png)


### 利用するライブラリのインポート

```python
import requests
import pandas as pd
import pprint
import sys

from google.colab import data_table
from SPARQLWrapper import SPARQLWrapper, JSON
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f4ec45b3-0375-a660-0a56-74f9ec166197.png)

## SPARQL の基本的な実行

### Web APIを利用したSPARQLによるデータ取得

```python
# SPARQL 実行に関する変数を定義
endpoint_url = "https://query.wikidata.org/sparql"
return_format='json'
query = """
#人口が多い TOP 10 の国
SELECT DISTINCT ?countryLabel ?population
{
  ?country wdt:P31 wd:Q6256 ;
           wdt:P1082 ?population .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja" }
}
GROUP BY ?population ?countryLabel
ORDER BY DESC(?population)
LIMIT 10
"""
```

```python
# WEB API 経由でデータを取得
headers = {'Accept': f'application/sparql-results+{return_format}'}
response = requests.get(
    endpoint_url,
    headers=headers,
    params={'query': query},
)
results_01 = response.json()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f2318bed-118b-f4b6-2d63-a265d008a1a9.png)


```python
# 取得したデータを表示
pprint.pprint(results_01)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/8e9a7188-ad9e-58bc-c617-496307999c51.png)

```python
# 取得結果を Pandas Dataframe に変換
sparql_records =  [
    {k: v['value'] for k, v in item.items()}
    for item in results_01["results"]["bindings"]
]
df_1 = pd.DataFrame.from_records(sparql_records)

# Google Colab で参照しやすいように Intaractive Table に変換
data_table.DataTable(df_1)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4c97a8c0-986e-6ee4-5850-5da4e06829db.png)

### SPARQLWrapper ライブラリを利用したSPARQLによるデータ取得

```python
# SPARQL 実行に関する変数を定義
endpoint_url = "https://query.wikidata.org/sparql"
return_format='json'
query = """
#人口が多い TOP 3 の国
SELECT DISTINCT ?countryLabel ?population
{
  ?country wdt:P31 wd:Q6256 ;
           wdt:P1082 ?population .
  SERVICE wikibase:label { bd:serviceParam wikibase:language "ja" }
}
GROUP BY ?population ?countryLabel
ORDER BY DESC(?population)
LIMIT 3
"""
```

```python
# SPARQLWrapper ライブラリ経由でデータを取得
user_agent = "WDQS-example Python/%s.%s" % (sys.version_info[0], sys.version_info[1])
sparql = SPARQLWrapper(endpoint_url, agent=user_agent)
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
results_02 = sparql.query().convert()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/20edc4d8-dbdc-0d59-2a83-2e8b9b944fe4.png)

```python
# 取得したデータを表示
pprint.pprint(results_02)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/43c18a5a-e528-aafb-b875-86067044b627.png)

```python
# 取得結果を Pandas Dataframe に変換
sparql_records =  [
    {k: v['value'] for k, v in item.items()}
    for item in results_02["results"]["bindings"]
]
df_02 = pd.DataFrame.from_records(sparql_records)

# Google Colab で参照しやすいように Intaractive Table に変換
data_table.DataTable(df_02)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2fbe0c5d-733b-4187-3d64-3131083e7751.png)

## 日本の自動車業界における総資産が TOP 3 の企業のデータ抽出

### SPARQL の組み立て

今回の要件を満たす SPARQL は下記のようなコードとなります。SPARQL の組み立て方法が不明であったため、とりあえずトヨタ自動車のデータの抽出から初めて、業界や国のフィルタリング条件を追加していきました。

```sparql
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
  # ?company rdfs:label "トヨタ自動車"@ja.
  
  # 自動車産業業界をフィルタリング
  ?company wdt:P452 ?industory .
  ?industory rdfs:label "自動車産業"@ja.

  # 日本をフィルタリング
  ?company wdt:P17 ?country .
  ?country rdfs:label "日本"@ja.

  # 総資産を取得
  ?company wdt:P2403 ?totalAssets .
  
  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }
  
}
ORDER BY DESC(?totalAssets)
LIMIT 3
```

下記のクエリによりトヨタ自動車のデータを取得します。`wd:Q53268`と書かれている箇所をクリックして wikidata のページに飛びます。

```sparql
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
  ?company rdfs:label "トヨタ自動車"@ja.
  
  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }
  
}
ORDER BY DESC(?totalAssets)
LIMIT 3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/a000f6ff-217e-c593-3848-553f17033ca5.png)

引用元：[Wikidata Query Service](https://query.wikidata.org/#SELECT%20%3Fcompany%20%3FcompanyLabel%20%3FindustoryLabel%20%3FcountryLabel%20%3FtotalAssets%0AWHERE%20%7B%0A%0A%20%20%23%20%E4%B8%80%E6%99%82%E7%9A%84%E3%81%AB%20%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%20%E3%82%92%E6%8A%BD%E5%87%BA%0A%20%20%3Fcompany%20rdfs%3Alabel%20%22%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%22%40ja.%0A%20%20%0A%20%20%23%20%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A8%E8%AA%AC%E6%98%8E%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%2Cja%22.%20%7D%0A%20%20%0A%7D%0AORDER%20BY%20DESC%28%3FtotalAssets%29%0ALIMIT%203)

Wikidata のページにて Industy と書かて箇所をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/dd4e8090-04b8-6f1b-c424-b9be8f21bd49.png)

Industry の識別子である P452 を取得します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/40bfc2e6-4ef2-dbe7-e481-68be47fa631f.png)

トヨタ自動車のフィルタリングを外し、industry のプロパティを追加して自動車産業のみにフィルタリングします。その結果、自動車

```spark
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
#   ?company rdfs:label "トヨタ自動車"@ja.

  # 自動車産業業界をフィルタリング
  ?company wdt:P452 ?industory .
  ?industory rdfs:label "自動車産業"@ja.
  
  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }
  
}
ORDER BY DESC(?totalAssets)
LIMIT 3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9382dc86-f89e-f98e-4917-112e21fd429c.png)

引用元：[Wikidata Query Service](https://query.wikidata.org/#SELECT%20%3Fcompany%20%3FcompanyLabel%20%3FindustoryLabel%20%3FcountryLabel%20%3FtotalAssets%0AWHERE%20%7B%0A%0A%20%20%23%20%E4%B8%80%E6%99%82%E7%9A%84%E3%81%AB%20%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%20%E3%82%92%E6%8A%BD%E5%87%BA%0A%23%20%20%20%3Fcompany%20rdfs%3Alabel%20%22%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%22%40ja.%0A%0A%20%20%23%20%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%E6%A5%AD%E7%95%8C%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%0A%20%20%3Fcompany%20wdt%3AP452%20%3Findustory%20.%0A%20%20%3Findustory%20rdfs%3Alabel%20%22%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%22%40ja.%0A%20%20%0A%20%20%23%20%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A8%E8%AA%AC%E6%98%8E%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%2Cja%22.%20%7D%0A%20%20%0A%7D%0AORDER%20BY%20DESC%28%3FtotalAssets%29%0ALIMIT%203)

同様に county の項目値を取得して日本にフィルタリングします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/af8d233f-bd28-85ed-df3c-6b657cb4b415.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9fa852fe-4be5-b445-d44d-9782f0430f91.png)

```sparql
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
#   ?company rdfs:label "トヨタ自動車"@ja.

  # 自動車産業業界をフィルタリング
  ?company wdt:P452 ?industory .
  ?industory rdfs:label "自動車産業"@ja.
  
  # 日本をフィルタリング
  ?company wdt:P17 ?country .
  ?country rdfs:label "日本"@ja.

  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }
  
}
ORDER BY DESC(?totalAssets)
LIMIT 3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2c2d6cf4-4d88-94aa-1f58-663ca9a61515.png)

引用元：[Wikidata Query Service](https://query.wikidata.org/#SELECT%20%3Fcompany%20%3FcompanyLabel%20%3FindustoryLabel%20%3FcountryLabel%20%3FtotalAssets%0AWHERE%20%7B%0A%0A%20%20%23%20%E4%B8%80%E6%99%82%E7%9A%84%E3%81%AB%20%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%20%E3%82%92%E6%8A%BD%E5%87%BA%0A%23%20%20%20%3Fcompany%20rdfs%3Alabel%20%22%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%22%40ja.%0A%0A%20%20%23%20%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%E6%A5%AD%E7%95%8C%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%0A%20%20%3Fcompany%20wdt%3AP452%20%3Findustory%20.%0A%20%20%3Findustory%20rdfs%3Alabel%20%22%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%22%40ja.%0A%20%20%0A%20%20%23%20%E6%97%A5%E6%9C%AC%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%0A%20%20%3Fcompany%20wdt%3AP17%20%3Fcountry%20.%0A%20%20%3Fcountry%20rdfs%3Alabel%20%22%E6%97%A5%E6%9C%AC%22%40ja.%0A%0A%20%20%23%20%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A8%E8%AA%AC%E6%98%8E%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%2Cja%22.%20%7D%0A%20%20%0A%7D%0AORDER%20BY%20DESC%28%3FtotalAssets%29%0ALIMIT%203)

同様に total assets の項目値を取得します。この修正により要件を満たすクエリとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/6cd97690-3e7b-8549-af4f-402d19f62359.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/445e8aef-b632-e0a0-5f5e-8f18cc6c0391.png)

```sparql
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
#   ?company rdfs:label "トヨタ自動車"@ja.

  # 自動車産業業界をフィルタリング
  ?company wdt:P452 ?industory .
  ?industory rdfs:label "自動車産業"@ja.
  
  # 日本をフィルタリング
  ?company wdt:P17 ?country .
  ?country rdfs:label "日本"@ja.

  # 総資産を取得
  ?company wdt:P2403 ?totalAssets .

  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }
  
}
ORDER BY DESC(?totalAssets)
LIMIT 3
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/957b1231-4fc3-4ee2-05c3-be165166836d.png)

引用元：[Wikidata Query Service](https://query.wikidata.org/#SELECT%20%3Fcompany%20%3FcompanyLabel%20%3FindustoryLabel%20%3FcountryLabel%20%3FtotalAssets%0AWHERE%20%7B%0A%0A%20%20%23%20%E4%B8%80%E6%99%82%E7%9A%84%E3%81%AB%20%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%20%E3%82%92%E6%8A%BD%E5%87%BA%0A%23%20%20%20%3Fcompany%20rdfs%3Alabel%20%22%E3%83%88%E3%83%A8%E3%82%BF%E8%87%AA%E5%8B%95%E8%BB%8A%22%40ja.%0A%0A%20%20%23%20%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%E6%A5%AD%E7%95%8C%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%0A%20%20%3Fcompany%20wdt%3AP452%20%3Findustory%20.%0A%20%20%3Findustory%20rdfs%3Alabel%20%22%E8%87%AA%E5%8B%95%E8%BB%8A%E7%94%A3%E6%A5%AD%22%40ja.%0A%20%20%0A%20%20%23%20%E6%97%A5%E6%9C%AC%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%0A%20%20%3Fcompany%20wdt%3AP17%20%3Fcountry%20.%0A%20%20%3Fcountry%20rdfs%3Alabel%20%22%E6%97%A5%E6%9C%AC%22%40ja.%0A%0A%20%20%23%20%E7%B7%8F%E8%B3%87%E7%94%A3%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20%3Fcompany%20wdt%3AP2403%20%3FtotalAssets%20.%0A%0A%20%20%23%20%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A8%E8%AA%AC%E6%98%8E%E3%82%92%E5%8F%96%E5%BE%97%0A%20%20SERVICE%20wikibase%3Alabel%20%7B%20bd%3AserviceParam%20wikibase%3Alanguage%20%22%5BAUTO_LANGUAGE%5D%2Cen%2Cja%22.%20%7D%0A%20%20%0A%7D%0AORDER%20BY%20DESC%28%3FtotalAssets%29%0ALIMIT%203)

### Python でのデータを抽出から可視化までの処理

```python
# SPARQL 実行に関する変数を定義
endpoint_url = "https://query.wikidata.org/sparql"
return_format='json'
```

```python
# 日本の自動車業界における総資産が TOP 3 の企業のデータを取得する SPARQL を定義
query = """
SELECT ?company ?companyLabel ?industoryLabel ?countryLabel ?totalAssets
WHERE {

  # 一時的に トヨタ自動車 を抽出
  # ?company rdfs:label "トヨタ自動車"@ja.

  # 自動車産業業界をフィルタリング
  ?company wdt:P452 ?industory .
  ?industory rdfs:label "自動車産業"@ja.

  # 日本をフィルタリング
  ?company wdt:P17 ?country .
  ?country rdfs:label "日本"@ja.

  # 総資産を取得
  ?company wdt:P2403 ?totalAssets .

  # ラベルと説明を取得
  SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en,ja". }

}
ORDER BY DESC(?totalAssets)
LIMIT 3
"""
```

```python
# SPARQLWrapper ライブラリ経由でデータを取得
user_agent = "WDQS-example Python/%s.%s" % (sys.version_info[0], sys.version_info[1])
sparql = SPARQLWrapper(endpoint_url, agent=user_agent)
sparql.setQuery(query)
sparql.setReturnFormat(JSON)
results_03 = sparql.query().convert()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/6c0d711a-c94c-901f-d242-fb1daff32556.png)

```python
# 取得結果を Pandas Dataframe に変換
sparql_records =  [
    {k: v['value'] for k, v in item.items()}
    for item in results_03["results"]["bindings"]
]
df_03 = pd.DataFrame.from_records(sparql_records)

# Google Colab で参照しやすいように Intaractive Table に変換
data_table.DataTable(df_03)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/84f1cd2f-2291-27ca-e79b-c79910182d5a.png)

```python
import matplotlib.pyplot as plt

# totalAssets 列を数値型に変換
df_03['totalAssets'] = pd.to_numeric(df_03['totalAssets'].str.replace('^[^0-9]+', '', regex=True))

# company を軸に、totalAssets を棒グラフで表示
plt.figure(figsize=(10, 6))
plt.bar(df_03['companyLabel'], df_03['totalAssets'])
plt.xlabel('Company')
plt.ylabel('Total Assets')
plt.title('Total Assets of Top 3 Companies in Japanese Automotive Industry')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/ee3652b9-2808-2b74-8b6f-d6ebbfaf1d74.png)

## 所感

- SPARQLに関する情報源が限られているため、実装プロセスが不明瞭であったり、特定の機能の実装可能性を判断するのが難しかった。
- SQLエンジニアである私には、WHERE句の中で取得するカラムのロジックを記述することに違和感があった。
- この記事で総資産の取得を試みたが、通貨や会計年度に関する情報の取得方法を確立できなかった。複数の条件でデータを取得できるかどうかも不明だった。