カテゴリー

go

【plotly&アニメーション】plotlyで各国の収入と平均寿命の時代変化をバブルチャート&アニメーションで描く

2021年5月1日

ubblechart_animation1820_2020_50_latest

こんな人にオススメ


日本は極端に平和でお金持ちでさらには長生きの国っていうのは知っている。けどお金持ちだと美味しいものばかり食べて栄養が偏って寿命が短いパターンもあるんじゃない?収入と平均寿命って相関があるの?
今回はアニメーションを使ってわかりやすく知りたい!

今回は以前にplotlyで作成した「世界各国の収入と平均寿命の関係性のスライダー時系列版」のグラフをアニメーションで作成する。スライダーで作成した版は以下参照。
【plotly&バブルチャート】plotlyで各国の収入と平均寿命の時代変化をバブルチャートで描く

続きを見る

また、単年版の「2017年における世界各国の収入と平均寿命の関係性」については以下参照。

bubblechart2017_pop
【plotly&バブルチャート】plotlyで各国の収入と平均寿命をバブルチャートで描く

続きを見る

スライダー版の時はファイル容量の関係で1975年から2020年までを5年ごととし合計10データだったが、今回はさらにファイル容量が重くなって1820年から2020年までを50年ごととした合計5データでしか紹介できない。

また、plotlyのバグか設定ミスか分からないが変な挙動を取る部分もあるのでそこは後ほど指摘する。

python環境は以下。

  • Python 3.9.2
  • numpy 1.20.1
  • matplotlib 3.3.4
  • plotly 4.14.3
  • plotly-orca 3.4.2
  • pandas 1.2.3

運営者のメガネです。YouTubeTwitterInstagram、自己紹介はこちら、お問い合わせはこちらから。

運営者メガネ

使用するデータと完成グラフイメージ

使用するデータ

今回使用するデータは以下の3種類。

  1. 世界各国の収入
  2. 世界各国の平均寿命
  3. 世界各国の人口

データは全て「Gapminder」からの引用で、基本的にこの組織からのデータだが一部のデータは使用していない。「Based on free material from GAPMINDER.ORG, CC-BY LICENSE」、すなわちフリーな素材ということだ。

このデータを知ったきっかけは、「FACTFULNESS」という本を読んだから。この本は執筆者が人生で一番読んで良かったと思っている本で、データや事実に基づきテレビなどでは知り得ない世界の本当の姿を知るという本だ。この本の著者がデータ引用元のギャップマインダー財団の共同創立者。

各データはGapminderの「Download the data」からダウンロードすることができる。収入、平均寿命、人口はそれぞれ以下の定義だ。

  • 収入:一人当たりGDPでその単位は「2011年の固定価格のinternational dollars」
  • 平均寿命:現在の死亡パターンが同じままである場合、新生児が生きる平均年数
  • 人口:単純にその国の総人口

完成グラフイメージ

ubblechart_animation1820_2020_50_latest

グラフの最終完成イメージは以下の通り。

  • 横軸:各国の収入
  • 縦軸:各国の平均寿命
  • バブルの大きさ:各国の人口

今回は人口が多いほど赤系統のバブルに、少ないほど青系統のバブルになるように作成した。細かいことはの後に解説する。そして「Play」「Pause」でアニメーションを起動させることができる。

なお、「https://tools-legacy.gapminder.org/tools/#$state$marker$axis_y$domainMin:null&domainMax:null&zoomedMin:null&zoomedMax:null&spaceRef:null;;;&chart-type=bubbles」にギャップマインダーのグラフがある。クオリティはめちゃくちゃ高い。データ数が多いのに滑らかに動くのですごいと思う。

グラフ作成コード

plotlyのバブルチャートの作成には特別な関数などは使わなくて、基本的にはgo.Scattermode'markers'に設定。その後、マーカーの大きさを変更する、ただそれだけ。以下に具体的な設定を書く。

  • mode='markers':プロットはマーカーのみ
  • marker=dict(size=(人口の多さで設定)):人口の多さに応じてマーカーのサイズを大きくする
  • marker=dict(line=dict(color='black', width=1)):マーカーの境目は黒にして、他のデータと混ざらないように設定

スライダー版と同様、任意の年から任意の年まで、任意の年の間隔で作成する。例えば、1800年から1900年までを2年ごとのように。また、plotlyの公式サイトの「Intro to Animations in Python」に書いている方法ではなく、それを執筆者なりに書き換えたコードを示す。もしかしたらこの方法だと変な挙動を示すのかもしれない。

全体コード


今回作成したコードでは以前までのようにその年毎の人口順にデータを並び替えておらず、指定した年の最新の人口を基準にデータを並び替えている。上の例で言えば2020年時点での人口を基準に作成している。各年での人口順に並び替えた場合、アニメーションでは挙動がミスリーディングを引き起こす原因になる。これについては後ほどコード紹介、解説を行う。

以下よりそれぞれの項目について解説していく。

下準備

まずは必要なライブラリのimportやテンプレートの読み込み。テンプレートに関しては以下の記事参照。

【随時更新 備忘録】plotlyのグラフ即席作成コード

続きを見る

import sys
import numpy as np
import matplotlib.cm as cm
import plotly
import plotly.graph_objects as go
import plotly.io as pio
import pandas as pd

sys.path.append('../../')
import plotly_layout_template as template

使用するデータの読み込み

今回使用するデータは全てcsv形式でpyファイルと同じディレクトリに入れている。ファイルの読み込みはpandasを使用し、0列目の国名をインデックスにしてヘッダーを年にした。

# 使用するファイル名
# <https://www.gapminder.org/data/> より引用
income_name = './income_per_person_gdppercapita_ppp_inflation_adjusted.csv'
life_expectancy_name = './life_expectancy_years.csv'
population_name = './population_total.csv'

# ファイルを読み込み
income = pd.read_csv(income_name, sep=',', index_col=0)
life_expectancy = pd.read_csv(life_expectancy_name, sep=',', index_col=0)
population = pd.read_csv(population_name, sep=',', index_col=0)

incomeは以下のようなDataFrameになっている。

print(income)
#              1800  1801  1802  1803  1804  ...   2036   2037   2038   2039   2040
# country                                    ...
# Afghanistan   603   603   603   603   603  ...   2820   2880   2940   3000   3060
# Albania       667   667   667   667   667  ...  21500  21900  22300  22800  23300
# Algeria       715   716   717   718   719  ...  15800  16100  16500  16800  17100
# Andorra      1200  1200  1200  1200  1210  ...  81500  83100  84800  86500  88300
# Angola        618   620   623   626   628  ...   6750   6880   7020   7170   7310
# ...           ...   ...   ...   ...   ...  ...    ...    ...    ...    ...    ...
# Venezuela    1210  1200  1200  1190  1190  ...   9110   9300   9490   9680   9880
# Vietnam       778   778   778   778   778  ...  13300  13500  13800  14100  14400
# Yemen         877   879   882   884   887  ...   3570   3640   3720   3790   3870
# Zambia        663   665   667   668   670  ...   3860   3930   4010   4100   4180
# Zimbabwe      869   870   871   872   873  ...   2900   2960   3020   3080   3140

# [193 rows x 241 columns]

life_expectancyは以下。

print(life_expectancy)
#              1800  1801  1802  1803  1804  ...  2096  2097  2098  2099  2100
# country                                    ...
# Afghanistan  28.2  28.2  28.2  28.2  28.2  ...  77.1  77.3  77.4  77.5  77.7
# Albania      35.4  35.4  35.4  35.4  35.4  ...  87.9  88.0  88.1  88.2  88.3
# Algeria      28.8  28.8  28.8  28.8  28.8  ...  88.8  88.9  89.0  89.1  89.2
# Andorra       NaN   NaN   NaN   NaN   NaN  ...   NaN   NaN   NaN   NaN   NaN
# Angola       27.0  27.0  27.0  27.0  27.0  ...  79.4  79.5  79.7  79.8  79.9
# ...           ...   ...   ...   ...   ...  ...   ...   ...   ...   ...   ...
# Venezuela    32.2  32.2  32.2  32.2  32.2  ...  86.9  87.0  87.1  87.2  87.3
# Vietnam      32.0  32.0  32.0  32.0  32.0  ...  84.8  84.9  85.0  85.2  85.3
# Yemen        23.4  23.4  23.4  23.4  23.4  ...  77.9  78.0  78.2  78.3  78.4
# Zambia       32.6  32.6  32.6  32.6  32.6  ...  77.6  77.7  77.8  78.0  78.1
# Zimbabwe     33.7  33.7  33.7  33.7  33.7  ...  75.1  75.3  75.4  75.5  75.7

# [187 rows x 301 columns]

populationは以下のDataFrameとして作成した。

print(population)
#                 1800     1801     1802  ...       2098       2099       2100
# country                                 ...
# Afghanistan  3280000  3280000  3280000  ...   75400000   75200000   74900000
# Albania       400000   402000   404000  ...    1140000    1110000    1090000
# Algeria      2500000  2510000  2520000  ...   70700000   70700000   70700000
# Andorra         2650     2650     2650  ...      62500      62500      62400
# Angola       1570000  1570000  1570000  ...  184000000  186000000  188000000
# ...              ...      ...      ...  ...        ...        ...        ...
# Venezuela    1000000   978000   957000  ...   34500000   34400000   34200000
# Vietnam      4000000  4100000  4200000  ...   98100000   97800000   97400000
# Yemen        2590000  2590000  2590000  ...   53500000   53400000   53200000
# Zambia        747000   758000   770000  ...   80000000   80800000   81500000
# Zimbabwe     1090000  1090000  1090000  ...   31000000   31000000   31000000

# [195 rows x 301 columns]

国名の抽出

上のDataFrameを見ると分かるように、ヘッダの数や行数が異なっている。ヘッダについては2040年までか2100年までかで数が違うが、行数については一部のデータには存在しない国名もある。ここでは収入、平均寿命、人口の3種類のデータが揃っている国に絞ってグラフを作成したいので、国名を抽出した。

# 国名を抽出
income_country = set(income.index.values)
life_expectancy_country = set(life_expectancy.index.values)
population_country = set(population.index.values)

# 共通する国名だけ抽出
# 例えばincomeには「Monaco」があるが、life_expectancyにはないので全てにあるものだけ抽出
common_country = income_country & life_expectancy_country & population_country
common_country = sorted(list(common_country))

共通する国名として設定した変数common_countryは以下のようになっている。

print(common_country)
# ['Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Antigua and Barbuda', 'Argentina', 'Armenia', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bhutan', 'Bolivia', 'Bosnia and Herzegovina', 'Botswana', 'Brazil', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Canada', 'Cape Verde', 'Central African Republic', 'Chad', 'Chile', 'China', 'Colombia', 'Comoros', 'Congo, Dem. Rep.', 'Congo, Rep.', 'Costa Rica', "Cote d'Ivoire", 'Croatia', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Eritrea', 'Estonia', 'Eswatini', 'Ethiopia', 'Fiji', 'Finland', 'France', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Greece', 'Grenada', 'Guatemala', 'Guinea', 'Guinea-Bissau', 'Guyana', 'Haiti', 'Honduras', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jordan', 'Kazakhstan', 'Kenya', 'Kiribati', 'Kuwait', 'Kyrgyz Republic', 'Lao', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Lithuania', 'Luxembourg', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Marshall Islands', 'Mauritania', 'Mauritius', 'Mexico', 'Micronesia, Fed. Sts.', 'Moldova', 'Mongolia', 'Montenegro', 'Morocco', 'Mozambique', 'Myanmar', 'Namibia', 'Nepal', 'Netherlands', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'North Korea', 'North Macedonia', 'Norway', 'Oman', 'Pakistan', 'Palestine', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Qatar', 'Romania', 'Russia', 'Rwanda', 'Samoa', 'Sao Tome and Principe', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovak Republic', 'Slovenia', 'Solomon Islands', 'Somalia', 'South Africa', 'South Korea', 'South Sudan', 'Spain', 'Sri Lanka', 'St. Lucia', 'St. Vincent and the Grenadines', 'Sudan', 'Suriname', 'Sweden', 'Switzerland', 'Syria', 'Tajikistan', 'Tanzania', 'Thailand', 'Timor-Leste', 'Togo', 'Tonga', 'Trinidad and Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States', 'Uruguay', 'Uzbekistan', 'Vanuatu', 'Venezuela', 'Vietnam', 'Yemen', 'Zambia', 'Zimbabwe']

任意の年から任意の年まで任意の年間隔でデータを作成する

今回は任意のデータ取得開始年・任意のデータ取得終了年・任意のデータ取得間隔を入力することで、その条件に合ったデータを抽出することができる。

def set_data_dct(
        x: float, y: float,
        name: str, rgba: str, pop: int,
        size: float, year: int, visible: bool):
    """プロット点の設定を出力

    Parameters
    ----------
    x : float
        incomeの値
    y : float
        平均寿命の値
    name : str
        国名
    rgba : str
        (R, G, B, A)の値
    pop : int
        人口の値
    size : float
        プロットマーカーのサイズ調整のために割る値
    visible : bool
        表示するか否か

    Returns
    -------
    dict
        プロット点の情報を入れたdict
    """

    data_dct = dict(
        mode='markers',
        x=(x,),
        y=(y,),
        name=name,
        marker=dict(
            symbol='circle', color=f"rgba{rgba}",
            # マーカーのサイズ調整
            size=np.sqrt(pop) / size,
            line=dict(color='black', width=1),
        ),
        hoverlabel=dict(
            font=dict(
                family='Times New Roman', size=15
            )
        ),
        hovertemplate=f"{name}: {year}
"
        + 'income: %{x}
'
        + 'life expectancy: %{y}
'
        + f"population: {pop}
"
        + "",
        visible=visible,
    )

    return data_dct

この関数のそれぞれの引数に条件を入れることで、その条件に合ったデータを出力することができる。

  • start:データ作成開始年
  • end:データ作成終了年
  • step:データ作成の年間隔

例えば、1975年から2020年までを5年ごとでデータを作成したい場合は以下のように設定する。

years, data = set_year(
        start=1975,
        end=2020,
        step=5,
    )

この設定だと以下の様な出力を得ることができる。dataについては長すぎるので途中省略しているが、イメージは年、3項目、国名のネスト。

print(years)
# [1975 1980 1985 1990 1995 2000 2005 2010 2015 2020]

print(data)
# {1975: {'income': {'Afghanistan': 2150, 'Albania': 4080, ...}, 'life_expectancy': {'Afghanistan': 47.1, 'Albania': 69.8, ...}, 'population': {'Afghanistan': 12700000, 'Albania': 2410000, ...}}, 1980: {'income': }

グラフ化

最後にグラフ化。全体は長いけど部分ずつ説明していく。まずは作成するバブルチャートのプロット設定を行うdefから。今回はアニメーション用で別にプロット設定が必要となるので予め共通設定としてdefで関数を設定した。出力は辞書型。

def set_data_dct(
        x: float, y: float,
        name: str, rgba: str, pop: int,
        size: float, year: int, visible: bool):
    """プロット点の設定を出力

    Parameters
    ----------
    x : float
        incomeの値
    y : float
        平均寿命の値
    name : str
        国名
    rgba : str
        (R, G, B, A)の値
    pop : int
        人口の値
    size : float
        プロットマーカーのサイズ調整のために割る値
    visible : bool
        表示するか否か

    Returns
    -------
    dict
        プロット点の情報を入れたdict
    """

    data_dct = dict(
        mode='markers',
        x=(x,),
        y=(y,),
        name=name,
        marker=dict(
            symbol='circle', color=f"rgba{rgba}",
            # マーカーのサイズ調整
            size=np.sqrt(pop) / size,
            line=dict(color='black', width=1),
        ),
        hoverlabel=dict(
            font=dict(
                family='Times New Roman', size=15
            )
        ),
        hovertemplate=f"{name}: {year}
"
        + 'income: %{x}
'
        + 'life expectancy: %{y}'
        + f"population: {pop}
"
        + "",
        visible=visible,
    )

    return data_dct

指定した年とそれに合ったデータを定義。これは前節と同じ。

yearw, data = set_year(
    start=start,
    end=end,
    step=step,
)

今回は指定した最終年での人口を降順でデータプロットの順番とするので、yearsのループ内では人口の降順の配列は作成しない。

# データ最後の年を基準にする
pop_latest = data[max(years)]['population']
# 人口を降順にソートし、国名を抽出
sorted_pop = list(
    dict(sorted(pop_latest.items(), key=lambda x: x[1], reverse=True))
)
# 降順の人口
sorted_pop_val = sorted(list(pop_latest.values()))

print(sorted_pop)
# ['China', 'India', 'United States', 'Indonesia', 'Pakistan', 'Brazil', 'Nigeria', 'Bangladesh', 'Russia', 'Mexico', 'Japan', 'Ethiopia', 'Philippines', 'Egypt', 'Vietnam', 'Congo, Dem. Rep.', 'Turkey', 'Iran', 'Germany', 'Thailand', 'United Kingdom', 'France', 'Italy', 'Tanzania', 'South Africa', 'Myanmar', 'Kenya', 'South Korea', 'Colombia', 'Spain', 'Uganda', 'Argentina', 'Algeria', 'Sudan', 'Ukraine', 'Iraq', 'Afghanistan', 'Poland', 'Canada', 'Morocco', 'Saudi Arabia', 'Uzbekistan', 'Peru', 'Angola', 'Malaysia', 'Mozambique', 'Ghana', 'Yemen', 'Nepal', 'Venezuela', 'Madagascar', 'Cameroon', "Cote d'Ivoire", 'North Korea', 'Australia', 'Niger', 'Sri Lanka', 'Burkina Faso', 'Mali', 'Romania', 'Chile', 'Malawi', 'Kazakhstan', 'Zambia', 'Guatemala', 'Ecuador', 'Syria', 'Netherlands', 'Cambodia', 'Senegal', 'Chad', 'Somalia', 'Zimbabwe', 'Guinea', 'Rwanda', 'Benin', 'Burundi', 'Tunisia', 'Bolivia', 'Belgium', 'Haiti', 'Cuba', 'South Sudan', 'Dominican Republic', 'Czech Republic', 'Greece', 'Jordan', 'Portugal', 'Azerbaijan', 'Sweden', 'Honduras', 'United Arab Emirates', 'Hungary', 'Tajikistan', 'Belarus', 'Austria', 'Papua New Guinea', 'Serbia', 'Israel', 'Switzerland', 'Togo', 'Sierra Leone', 'Lao', 'Paraguay', 'Bulgaria', 'Libya', 'Lebanon', 'Nicaragua', 'Kyrgyz Republic', 'El Salvador', 'Turkmenistan', 'Singapore', 'Denmark', 'Finland', 'Congo, Rep.', 'Slovak Republic', 'Norway', 'Oman', 'Palestine', 'Costa Rica', 'Liberia', 'Ireland', 'Central African Republic', 'New Zealand', 'Mauritania', 'Panama', 'Kuwait', 'Croatia', 'Moldova', 'Georgia', 'Eritrea', 'Uruguay', 'Bosnia and Herzegovina', 'Mongolia', 'Armenia', 'Jamaica', 'Albania', 'Qatar', 'Lithuania', 'Namibia', 'Gambia', 'Botswana', 'Gabon', 'Lesotho', 'North Macedonia', 'Slovenia', 'Guinea-Bissau', 'Latvia', 'Bahrain', 'Equatorial Guinea', 'Trinidad and Tobago', 'Estonia', 'Timor-Leste', 'Mauritius', 'Cyprus', 'Eswatini', 'Djibouti', 'Fiji', 'Comoros', 'Guyana', 'Bhutan', 'Solomon Islands', 'Montenegro', 'Luxembourg', 'Suriname', 'Cape Verde', 'Maldives', 'Malta', 'Brunei', 'Belize', 'Bahamas', 'Iceland', 'Vanuatu', 'Barbados', 'Sao Tome and Principe', 'Samoa', 'St. Lucia', 'Kiribati', 'Micronesia, Fed. Sts.', 'Grenada', 'St. Vincent and the Grenadines', 'Tonga', 'Seychelles', 'Antigua and Barbuda', 'Andorra', 'Dominica', 'Marshall Islands']
print(sorted_pop_val)
# [1440000000, 1380000000, 331000000, 274000000, 221000000, 213000000, 206000000, 165000000, 146000000, 129000000, 126000000, 115000000, 110000000, 102000000, 97300000, 89600000, 84300000, 84000000, 83800000, 69800000, 67900000, 65300000, 60500000, 59700000, 59300000, 54400000, 53800000, 51300000, 50900000, 46800000, 45700000, 45200000, 43900000, 43800000, 43700000, 40200000, 38900000, 37800000, 37700000, 36900000, 34800000, 33500000, 33000000, 32900000, 32400000, 31300000, 31100000, 29800000, 29100000, 28400000, 27700000, 26500000, 26400000, 25800000, 25500000, 24200000, 21400000, 20900000, 20300000, 19200000, 19100000, 19100000, 18800000, 18400000, 17900000, 17600000, 17500000, 17100000, 16700000, 16700000, 16400000, 15900000, 14900000, 13100000, 13000000, 12100000, 11900000, 11800000, 11700000, 11600000, 11400000, 11300000, 11200000, 10800000, 10700000, 10400000, 10200000, 10200000, 10100000, 10100000, 9900000, 9890000, 9660000, 9540000, 9450000, 9010000, 8950000, 8740000, 8660000, 8650000, 8280000, 7980000, 7280000, 7130000, 6950000, 6870000, 6830000, 6620000, 6520000, 6490000, 6030000, 5850000, 5790000, 5540000, 5520000, 5460000, 5420000, 5110000, 5100000, 5090000, 5060000, 4940000, 4830000, 4820000, 4650000, 4310000, 4270000, 4110000, 4030000, 3990000, 3550000, 3470000, 3280000, 3280000, 2960000, 2960000, 2880000, 2880000, 2720000, 2540000, 2420000, 2350000, 2230000, 2140000, 2080000, 2080000, 1970000, 1890000, 1700000, 1400000, 1400000, 1330000, 1320000, 1270000, 1210000, 1160000, 988000, 896000, 870000, 787000, 772000, 687000, 628000, 626000, 587000, 556000, 541000, 442000, 437000, 398000, 393000, 341000, 307000, 287000, 219000, 198000, 184000, 119000, 115000, 113000, 111000, 106000, 98300, 97900, 77300, 72000, 59200]

次はいよいよプロットデータの作成。プロットデータの下準備を先に説明する。年毎にframe_dctを作成し、その中にデータと名称、そしてレイアウトの設定として年を識別出来るタイトル情報を入れている。次にその年において国名をループさせていき、色を決定。色は指定した年の最新の人口を基準に作成。

plot = {}
frames = []
count = 0
for year in years:
    frame_dct = dict(
        data=[],
        name=f"{year}",
        layout=dict(title_text=f"income vs life expectancy: {year}"),
    )

    plot[year] = []
    for name in sorted_pop:

        pop = data[year]['population'][name]

        element = pop_latest[name]
        # 各国の人口は何番目か
        index = sorted_pop_val.index(element)
        # 各国の人口の番数を規格化
        norm = (index - 0) / (len(sorted_pop_val) - 0)

        # RGB決める
        rgba = tuple(X * 255 for X in cm.jet_r(norm)[:-1])
        # 透明度追加
        rgba += (0.7,)

        size = 1e2 if not blog else 4e2

frame_dctdataとプロットするデータにそれぞれset_data_dctでの辞書を格納する。この時、visibleframe_dctではTrue、プロットするデータではFalseにしないとバグる。最後に全データとして何データあるのかをカウントする用のcount+1し、framesframe_dctを入れる。

    # frame作成
    frame_dct['data'].append(
        set_data_dct(
            x=data[year]['income'][name],
            y=data[year]['life_expectancy'][name],
            name=name,
            rgba=rgba,
            pop=pop,
            size=size,
            year=year,
            visible=True,
        )
    )

    # plotデータ作成
    d = go.Scattergl(
        set_data_dct(
            x=data[year]['income'][name],
            y=data[year]['life_expectancy'][name],
            name=name,
            rgba=rgba,
            pop=pop,
            size=size,
            year=year,
            visible=False,
        )
    )
    plot[year].append(d)

    count += 1
frames.append(frame_dct)

初期グラフを非表示から表示にしたいので、一番古い年のvisibleTrueに変更。

# 初期表示時のグラフを非表示から表示にする
for p in plot[min(years)]:
    p['visible'] = True

アニメーションの再生・停止ボタンを作成する。ボタンはupdatemenusで作成する。再生は「Play」、停止は「Pause」とし、それぞれでフレーム間の間隔(duration)を調整したり、アニメーションの動き方(easing)を設定したりできる。また、redraw=Trueにすることで各年でのタイトルを変更することができる。しかし、この設定をするとアニメーションがカクカクになる。

ボタンについては以下参照。

【plotly&ボタン】plotlyのupdatemenusに2回押し対応のbuttonsを追加

続きを見る

play = dict(
    label="Play",
    method="animate",
    args=[
        None,
        dict(
            # durationは各フレーム間の時間(小さくすると高速で動く)
            frame=dict(duration=1000, redraw=True),
            fromcurrent=True,
            transition={
                # plot点の移動の遅延(数字が小さくなるとキビキビになる)
                "duration": 1000,
                "easing": "cubic-in-out",
            },
        )
    ],
)
pause = dict(
    label="Pause",
    method="animate",
    args=[
        None,
        dict(
            frame=dict(duration=0, redraw=True),
            mode="immediate",
            transition=dict(duration=0),
        ),
    ],
)
updatemenus = [
    dict(
        buttons=[play, pause],
        direction="left",
        showactive=False,
        type='buttons',
        x=0, xanchor="left",
        y=-0.1, yanchor="top",
    )
]

スライダーを追加することで、ユーザーが任意のタイミングで任意の年の情報を得ることができる。また、スライダーとボタンのタイミングを一致させないと、プロットは2020年なのにその瞬間のスライダーは1970年のままということになりかねない。スライダーでのredraw=Trueとしても特に何も変わらなかった。

スライダーについては以下参照。

【plotly&スライダー】plotlyのslidersにスライダーを追加

続きを見る

sliders = dict(
    active=0,
    currentvalue=dict(
        font=dict(size=20),
        prefix="Year: ",
        visible=True,
    ),
    transition=dict(duration=1000, easing='linear'),
    len=0.9,
    x=0.1, xanchor='left',
    y=-0.05, yanchor='top',
    steps=[],
)
for year in years:
    slider_step = dict(
        label=f"{year}",
        method="animate",
        args=[
            [year],
            dict(
                frame=dict(duration=1000, redraw=False),
                mode="immediate",
                transition=dict(duration=1000),
                title=f"income vs life expectancy: {year}",
            ),
        ],
    )
    sliders["steps"].append(slider_step)
sliders = [sliders]

レイアウトを設定。この部分に関しては今まで通り。上に書いた記事などを参照していただきたい。

layout = go.Layout(
    template=template.plotly_layout(),
    title=dict(
        text=f"income vs life expectancy: {start}",
    ),
    xaxis=dict(
        title='income [GDP per capita, constant PPP dollars]',
        type='log',
        range=[np.log10(200), np.log10(2e5)],
        showspikes=True,
        spikecolor='black',
        spikethickness=2,
        spikedash='dot',
    ),
    yaxis=dict(
        title='Life expectancy at birth',
        range=[0, 100],
        showspikes=True,
        spikecolor='black',
        spikethickness=2,
        spikedash='dot',
    ),
    legend=dict(
        title=f"country: {save_name}",
        xanchor='left',
    ),
    sliders=sliders,
    updatemenus=updatemenus,
)

グラフの作成について、今回はdata, layoutだけではなくframesも存在しているのでそれも追加。保存はいつも通り。グラフのconfigeditableはスマホでの凡例のスクロール時に凡例が移動するのを防ぐためにOFFにしている。

    # 作成したデータの入った配列と、レイアウトの配列を図にする
    fig = go.Figure(
        data=sum(list(plot.values()), []),
        layout=layout,
        frames=frames
    )

    # 作成したグラフを保存
    plotly.offline.iplot(fig, config=template.plotly_config(tf=False),)
    pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
    pio.write_html(
        fig,
        f"plotly_bubblechart_animation{start}_{end}_{step}_{save_name}.html",
        auto_open=False,
        config=template.plotly_config(tf=False),
    )
    pio.write_image(
        fig,
        f"plotly_bubblechart_animation{start}_{end}_{step}_{save_name}.png"
    )

実際のグラフ

前述の通り、ファイルサイズが大きいので今回は1820年から2020年までを50年おきに作成した。
ここまで順番に読んでくださった方は気づかなかったかもしれないが、実はグラフ表示時に勝手にアニメーションが動き始めるという現象が起きている。
バグか何かだとは思うが、上述のplotlyの公式サイトのグラフではこの現象は起きないのでグラフ作成コードの書き方に問題があるのかもしれない。一応plotlyのcommunityの質問場?で同じような現象が起きている人がいるのでバグかもしれない。


もちろん開始・終了年、年間隔は調整することができる。以下の例では1800年から2020年までを10年おきに作成したもの。このレベルになるとファイルが重いせいか、グラフが滑らかに動かない。

bubblechart_animation1800_2020_10_latest

各年で人口順に並び替える

最後に以前までのコードで使用した、各年で人口順にデータを並び替えるということについて解説する。全体コードは下に書いた通り。変更点は以下のコードの位置がyearのループ内にあるということ。すなわち、基準となる人口は各年で変わっていくということ。

pop_year = data[year]['population']
sorted_pop = list(
    dict(sorted(pop_year.items(), key=lambda x: x[1], reverse=True))
)
sorted_pop_val = sorted(list(pop_year.values()), reverse=True)

このコードで完成するグラフは以下のもの。ぱっと見では何も変更点がないように見えるが、実際に動かすとバブルが行ったり来たりしていることがわかる。