国連に参加している国々を、DistanceとMDSアルゴリズムでクラスターしてみる

前回の記事では、国連の投票記録に基づく国際関係について分析してみました。 今回は、Exploratory Desktopでどのように分析を行ったのかを紹介したいと思います。

主な手順は以下の通りです。

  • データをダウンロードする
  • データのクリーンアップと準備
  • 距離を計算し、それらを二次元平面にマップする
  • 歴代大統領の任期データを結合する
  • それぞれの大統領の任期ごとに距離を計算する

それでは、ひとつひとつ見ていってみましょう。

データをダウンロードする

まず、Dataverseの国連総会投票データページからデータをダウンロードします。 ダウンロードするファイルは、投票記録の生データ、 「UNVotesPublished.tab」です。

また、「Cookbook.pdf」には、それぞれのデータについての説明があります。直接データとしては使いませんが、データを理解するためには必要不可欠ですので、こちらもダウンロードしておきます。

データのインポート

では、タブ区切りのテキストファイルである 「UNVotesPublished.tab」をインポートしてみましょう。 ファイル・データソースから「ローカル」を選択し、「テキストファイル」を選択します。

すると、データが表示されます。最初の状態ではカンマ区切りでデータを取ろうとするので、データが1列で表示されてしまいます。ここで、セパレータで「タブ」を選択し、「データの取得」ボタンをクリックします。

すると、それぞれの列がきちんと区別されて表示されます。データの確認が完了したら、「votes」と名前をつけて、「保存」ボタンをクリックしてデータをインポートします。

データの準備

データは約100万行あり、列は15列あることがわかります。

まず、ccode列に注目します。ここには国コードが入ってます。この国コードから国名を抽出したいと思います。exploratoryパッケージの提供する、countrycode関数を使うと簡単です。Cookbookによると、国コードはCOWコードというものが用いられているようなので、cownを元データ(origin)に指定し、変換先は国名(country.name)にします。列名には新たに「country_name」という列名を指定します。

mutate(country_name = countrycode(ccode, origin="cown",destination="country.name"))

ExploratoryのUIから行う場合、ccodeのカラムメニューから、「データを置換/変換」「 国」を選択します。

実行ボタンを押すと、country_name列が新たに作成され、国名が入ります。

次に、vote列に注目します。

Cookbookによると、vote列のコード「9」は、「メンバーではない」を意味します。ここでは、「はい」「いいえ」「棄権」、または「欠席」などの明確な意味のある値にのみ集中したいので、これを取り除きます。

filter(vote != 9)

ExploratoryのUIから行うには、vote列のカラムメニューをクリックし、「フィルタ」から「等しくない」を選択します。

するとダイアログが表示されるので、値に「9」を入力して実行します。

次に、投票コード値をわかりやすい文字列に置き換えてみましょう。 vote列のカラムメニューをクリックし、「データを置換/変換」から「既存を新規の値で置換」を選びます。

マッピングを定義できるダイアログが表示されるので、1, 2, 3, 8の値をそれぞれ Yes、Abstain(棄権)、No、Absent(不在) にマッピングします。

また、後で各国間の距離をよりよく計算できるように、投票値を以下のルールで重みづけをして変換します。

・「はい」の場合は: 1 ・「いいえ」の場合: -1 ・「不在」または「棄権」の場合: 0

mutate(vote_num = recode(vote, `1` =1, `2`=0, `3`=-1, `8`=0))

UIから操作する場合、先ほどと同じrecodeのダイアログを使います。

新たに作成された列の傾向を見たい場合、サマリービューを用いると便利です。Noと比較して圧倒的な数のYesがあるのが面白いですね。

長い国名を略称に変換する場合にも、recode関数は便利です。

mutate(country_name = recode(country_name, "United States"="US","Russian Federation"="Russia","United Kingdom"="UK"))

UIでは、先ほどと同様に、ダイアログから簡単に実行できます。

これで、国名(country_name)、決議ID (rcid)、投票の重み付きスコア(vote_num)の列が準備できました。次に、これらを用いて、各国間の相対的な距離を計算してみたいと思います。

各国間の距離を計算する

各国間の相対的な距離を計算するには、exploratoryパッケージの do_dist関数を使います。これは基本的にRのdist関数をtidyデータで利用できるようにしたラッパー関数です。

この関数は引数としてsubject、dimension、measureを取ります。 この場合、それぞれの決議の重み付き投票値に基づいて、各国間の距離を計算したいので、country_name列をsubjectに、 rcid列をdimensionに、 vote_num列をmeasureに指定します。コマンドは以下のようになります。

do_dist(skv = c("country_name", "rcid", "vote_num"), method = "euclidean")

Exploratory Desktopから行う場合、+ボタンをクリックし、「分析を実行」から「距離を計算」を選択します。

ダイアログが表れますので、それぞれのカラムを選択します。

実行ボタンを押すと各国間の距離が計算され、結果が3列のテーブルで返ります。

上記のスクリーンショットでは、アフガニスタンと他の国との間で計算された距離の値を見ることができます。 このデータには200の固有の国(および地域)があります。do_dist.kv関数は同じ国同士の組み合わせ(米国と米国など)を除いた全ての組み合わせを返すので、この場合、200 x 200=40,000の組み合わせから、同じ国同士の組み合わせ200を除いた39,800件の結果が返ります。スクリーンショットを見ると、たしかに39,800件のデータがあるのが確認できます。

MDSを使用して距離を2次元平面内に配置する

多次元スケーリング(MDS)アルゴリズムを使用して、距離情報を元にすべての国の位置を2次元空間内に配置することができます。まずはじめに、MDSとは何か、ということを説明したいと思います。

例えば、今、東京、大阪、京都の間のそれぞれの距離がわかっているとします。

それぞれの都市を地図上で表現すると、このようになります。

実は、このそれぞれの都市の間の距離のデータがあれば、地図を使わなくても2次元上のグラフにそれぞれの都市を相対的に配置することが可能です。例えばこの3都市の場合、3辺をつないだ3角形は必ず同じ形になります。

MDSとは、それぞれの点同士の距離データから、それぞれの点を2次元上に配置するための座標データを計算するアルゴリズムです。現在私たちは、それぞれの国同士の距離のデータがある状態ですので、このアルゴリズムを使うと、2次元上に上記の地図のように、各国の関係を配置することができます。

MDSの計算には、exploratoryパッケージの do_cmdscale関数を使います。これは、Rのcmdscale関数をtidyデータで利用できるようにしたラッパー関数です。関数の呼び出しは以下のようになります。

do_cmdscale(country_name.x, country_name.y, value)

Exploratory Desktopから操作する場合、+ボタンをクリックして「分析を実行」から「多次元スケーリング」を選択します。

ダイアログで、country_name.x列、country_name.y列、value列を割り当てます。

実行ボタンをクリックすると、それぞれの国の座標がもとめられます。次元数を変えることで、位置を表現する次元数を変えることができます。もし、3次元上に表現したい場合は3を渡すと、x、y、z軸の値がそれぞれ得られます。

各国間の距離を可視化する

各国間の位置が座標としてもとめられたので、散布図を用いてこれを可視化することができます。

それぞれの国が属する大陸ごとに色を付けたいときは、先程のcountrycode関数を利用して、国名から大陸名を導出することができます。

mutate(continent = countrycode(name, origin="country.name", destination="continent"))

大陸名の入ったcontinentという列が新たに生成されました。サマリー・ビューで確認すると、6つの欠損値があります。これは、旧東ドイツ、旧ユーゴスラビア、旧チェコスロバキアなど、現在は存在しない国々です。

これらをフィルターして取り除きます。

filter(!is.na(continent))

このcontinent列を「色で分割」に割り当てることで、それぞれの国が大陸ごとに色分けされます。

Americasには北南アメリカ大陸が含まれています。北米(米国とカナダ)とそれ以外を分けたいときは、if_else関数を使って大陸名を上書きします。

mutate(continent = countrycode(name, origin="country.name", destination="continent"), continent = if_else(name %in% c("US", "Canada"),"North America", continent))

また、アメリカとカナダ以外のAmericasを「中南米」とします。ついでに、他の大陸名も日本語に変換してみましょう。ここでも、recode関数を使います。

mutate(continent = countrycode(name, origin="country.name", destination="continent"), continent = if_else(name %in% c("US", "Canada"),"North America", continent), continent = recode(continent, Americas = "中南米", Africa = "アフリカ", Asia = "アジア", Europe = "ヨーロッパ", `North America` = "北米", Oceania = "オセアニア"))

わかりやすく色分けすることができました。

次に、国連安全保障理事会の常任理事国(アメリカ、イギリス、フランス、ロシア、中国)を目立たせるようにしてみましょう。まず、常任理事国5カ国の国名だけを残しておき、その他データには空文字が入るような列、top5_countriesを新たに作成します。

mutate(top5_countries = if_else(name %in% c("US", "UK", "France", "Russia", "China"), name, ""))

この新しいtop5_countries列をラベルに割り当て、「チャート上に表示」チェックボックスをクリックすると、常任理事国だけに文字が振られます。

次に、常任理事国だけ丸を大きくしてみます。常任理事国かどうかによって数値が異なる列を新たに作成し、常任理事国の場合に2、その他の場合には1を割り当てるようにします。

mutate(top_size = if_else(is_empty(top5_countries),1,2))

そして、この新しい列 circle_sizeをサイズに割り当てます。

これらの距離値は、1945年から2015年までのすべての決議に基づいて計算しています。前回の記事では、ここから、それぞれの米国大統領時代の距離値を計算して、時代を通じての国際関係を分析してみました。

これは、何年には誰が大統領だったか、というデータをこの投票データに結合することで可能になります。

歴代大統領の任期データとの結合

歴代の大統領の任期データは、アメリカ政府の歴代大統領のページからWebスクレイプし、それを加工することによって作ることができます。ここではあらかじめ、そのやり方で作成したpresident_dataというデータフレームをこちらにEDFとして用意してありますので、ダウンロードしてインポートすると、以下のようなデータとなります。

投票データのyear列と、この米国大統領任期データのYEAR列をキーとして、両者を結合します。

left_join(president_data, by=c("year" = "YEAR"))

投票データに、その投票時のアメリカ大統領の名前が関連付けられました。

これで、各大統領の名前でデータをフィルタすることで、その大統領が就任しているときの各国の距離関係を知ることができます。例えば、オバマ政権のときの国際関係を知りたいときには、この結合のステップの直後に以下のフィルタを追加します。

filter(PRESIDENT == "Barack Obama")

そして、右側の最後のステップをクリックします。 これにより、このステップに向かうすべてのステップが自動的に再計算され、データが生成されます。

ここで、「ピン」ボタンをクリックしてこの最後のステップにチャートを固定することができます。

一度チャートをピンしてしまえば、他のステップに移動してもチャートはピンしたステップを指し続けます。ですので、例えばオバマ政権ではなくアイゼンハワー政権の国際関係を見たいときには、この状態で先程のフィルタのステップに戻り、アイゼンハワーを選び直すだけで全ての再計算が自動的に行われ、更新されたチャートが表示されます。

今回この分析で使用したデータをexploratory.ioでこちらに共有しました。もし、Exploratory Desktopを既にお持ちならば、ダウンロードしてインポートするだけで上記のデータとステップがすべて再現できます。


再現のためのRスクリプト

# Set libPaths.
.libPaths("/Users/kei/.exploratory/R/3.4")

# Load required packages.
library(janitor)
library(lubridate)
library(hms)
library(tidyr)
library(stringr)
library(readr)
library(forcats)
library(RcppRoll)
library(dplyr)
library(tibble)
library(exploratory)

# Steps to produce president_data
`president_data` <- exploratory::scrape_html_table("https://www.loc.gov/rr/print/list/057_chron.html", 4, "TRUE" ,encoding="English (ASCII)") %>% exploratory::clean_data_frame() %>%
  select(YEAR, PRESIDENT) %>%
  separate_rows(YEAR, sep = "\\s*\\-\\s*") %>%
  mutate(YEAR = extract_numeric(YEAR), YEAR = coalesce(YEAR, 2017)) %>%
  group_by(PRESIDENT) %>%
  expand(YEAR = min(YEAR, na.rm=TRUE):max(YEAR, na.rm=TRUE)) %>%
  filter(YEAR != last(YEAR)) %>%
  arrange(YEAR)

# Steps to produce the output
exploratory::read_delim_file("/Users/kei/Downloads/UNVotesPublished.tab" , "\t", quote = "\"", skip = 0 , col_names = TRUE , na = c("","NA") , locale=readr::locale(encoding = "UTF-8", decimal_mark = "."), trim_ws = FALSE , progress = FALSE) %>% exploratory::clean_data_frame() %>%
  filter(vote != 9) %>%
  mutate(country_name = countrycode(ccode, origin="cown",destination="country.name"), vote_name = recode(vote, `1` = "Yes", `2` = "Abstain", `3` = "No", `8` = "Absent"), vote_num = recode(vote, `1` = 1L, `2` = 0L, `3` = -1L, `8` = 0L), country_name = recode(country_name, `United States of America` = "US", `United Kingdom of Great Britain and Northern Ireland` = "UK", `Russian Federation` = "Russia")) %>%
  left_join(president_data, by = c("year" = "YEAR")) %>%
  filter(PRESIDENT == "Dwight D. Eisenhower") %>%
  do_dist(skv = c("country_name", "rcid", "vote_num"), method = "euclidean") %>%
  do_cmdscale(country_name.x, country_name.y, value) %>%
  mutate(continent = countrycode(country_name,
    origin="country.name",
    destination="continent")) %>%
  filter(!is.na(continent)) %>%
  mutate(continent = if_else(country_name %in% c("US", "Canada"),
        "North America", continent), continent = recode(continent, Americas = "中南米", Africa = "アフリカ", Asia = "アジア", Europe = "ヨーロッパ", `North America` = "北米", Oceania = "オセアニア")) %>%
  mutate(top5_countries = if_else(country_name %in% 
  c("US", "UK", "France", "Russia", "China"), 
  country_name, ""), circle_size = if_else(is_empty(top5_countries),1,2))

exploratoryパッケージはCRANではなくGithub上にあるので、以下のようにdevtoolsパッケージを使ってインストールする必要があります。

install.packages("devtools")
devtools::install_github("exploratory-io/exploratory_func")
library(exploratory)

Exploratory Desktopを既にインストールしている場合は、上記のスクリプトはライブラリパスをExploratoryのパッケージインストール場所に設定しているため、exploratoryを含む依存パッケージを個別にインストールする必要はありません。

この分析をExploratory Desktopを試してみたい方は、ぜひサインアップしてダウンロードしてみてください。最初の30日間はどなたでも無料でお使いいただけます。

データ分析をさらに学んでみたいという方へ

今年10月に、シリコンバレーのExploratory社によって行われるデータサイエンス・ブートキャンプの第3回目が東京で行われます。本格的に上記のようなデータサイエンスの手法を、プログラミングなしで学んでみたい方、そういった手法を日々のビジネスに活かしてみたい方はぜひこの機会に、参加を検討してみてはいかがでしょうか。こちらに詳しい情報がありますのでぜひご覧ください。

今回使用したRパッケージ

このブログ記事は、How to cluster countries of United Nations with Distance and MDS algorithms を翻訳、加筆修正したものです。