カテゴリー

go

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

2021年4月27日

こんな人にオススメ

今は世界的に裕福になって、お金持ちも増えて健康になっているけど、昔ってどんな感じだったの?

昔は寿命が短いけど、人口が多い国とかもあった?

ということで、今回は以下の記事で作成した単年バブルチャートを応用して時系列で作成する。使用するデータは1800年から2040年の予想まで存在する。

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

続きを見る

もちろん1800年から1年刻みでグラフを作成してもいいが、グラフがアホほど重くなってしまったので、本記事で動かせるグラフは簡易版とする。

ptyhon環境は以下。

  • Python 3.9.6
  • numpy 1.21.1
  • matplotlib 3.4.2
  • pandas 1.3.1plotly 5.1.0
  • plotly-orca 3.4.2

運営者のメガネです。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」
  • 平均寿命:現在の死亡パターンが同じままである場合、新生児が生きる平均年数
  • 人口:単純にその国の総人口

また、グラフの最終的な完成イメージは以下。

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

今回は人口が多いほど赤系統のバブルに、少ないほど青系統のバブルになるように作成した。細かいことはの後に解説する。

なお、「https://tools-legacy.gapminder.org/tools/#$state$marker$axis_y$domainMin:null&domainMax:null&zoomedMin:null&zoomedMax:null&spaceRef:null;;;&chart-type=bubbles」にギャップマインダーのグラフがある。クオリティはめちゃくちゃ高い。めちゃくちゃ軽くてびっくりする。

下準備

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

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

まずは下準備としてのimport関連。plotly_layout_templateは自作のplotlyのテンプレートで、これを使用することで、簡単にキレイなグラフを作成することが可能となる。

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

続きを見る

csvファイルを読み込む

まずはダウンロードしたデータを読み込んで整理する作業。pandasを使用することで、表形式で扱うことができる。かなり便利。

データの読み込みと出力

# incomeは一人当たりGDPでその単位は「2011年の固定価格のinternational dollars」
# life_expectancyは「現在の死亡パターンが同じままである場合、新生児が生きる平均年数」
# populationは単純に全人口
income_name = './income_per_person_gdppercapita_ppp_inflation_adjusted.csv'
life_expectancy_name = './life_expectancy_years.csv'
population_name = './population_total.csv'

まずは収入・平均寿命・人口のファイル名を定義。予めファイル名を定義しておくことで、1行が短く済むし、どのファイルを使用しているのかが分かりやすくなる。

それぞれのファイルはpandasで読み込む。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)

それぞれのデータフレームの出力は以下。

全データが揃っている国だけ抽出

今回使用する収入・平均寿命・人口のデータは最長の年が異なっていたり、国によってはデータばない場合はある。これらのデータをそのままにしておくとエラーの原因にもなりかねない。

したがって、本記事では上記3項目全てのデータが揃っている国だけ使用する。

国名の抽出

# データ読み込み時に国名をインデックスにした
# のちに全データで存在する国だけ抽出したいのでsetを使用(setなので順番はバラバラ)
income_country = set(income.index.values)
life_expectancy_country = set(life_expectancy.index.values)
population_country = set(population.index.values)

まずは国名を抽出。シンプルにデータフレームのインデックスを取得するだけでもよいが、全データが揃っている国のみを抽出したいということでsetを使用。

setは順番を気にしない配列なので、出力するたびに国の順番が変わってしまう。しかし、今は全データが揃っている国を抽出したいだけだから気にしない。

各データでの国名は以下。

全データが揃っている国のみ抽出

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

全データが揃っている国のみ抽出するには&記号を使用する。setは集合の考え方に倣った配列だから、そのまま積集合、すなわち&をすれば1つでもかけている国は弾ける。

弾いた後もsetで国名の順番がバラバラなので、元データと同じアルファベット順にソートした。最終的に選ばれた国名は以下。

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_year(start: int, end: int, step: int):
    """データ作成の開始年と終了年、年間隔を入力することで、
    その年の収入、平均寿命、人口のデータを取得し出力

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

    Returns
    -------
    tuple
        データ作成の年の配列と
        収入、平均寿命、人口のデータを入れたdict
    """

    # グラフを作成する年
    years = np.arange(start, end + step, step)

    data = {}
    for year in years:
        data[year] = {}
        data[year]['income'] = {}
        data[year]['life_expectancy'] = {}
        data[year]['population'] = {}

        for name in common_country:
            y = f"{year}"
            data[year]['income'][name] = income[y][name]
            data[year]['life_expectancy'][name] = life_expectancy[y][name]
            data[year]['population'][name] = population[y][name]

    return years, data

今回は1800年から2040年で実に240年ほどのデータを扱う。しかし、全データを使用するとデータが重すぎるので一定の期間のデータだけ使用することにする。そのための関数がset_year関数。

この関数の引数startがデータ取得開始年、endがデータ取得終了年を示す。また、stepが何年ずつのデータとするかを指定する。例えば一例として1975年から2020年まで5年刻みなら以下。

# 一例
years, data = set_year(
    start=1975,
    end=2020,
    step=5,
)
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': }

こうすることで、一定期間だけ抽出したり、長期間を軽いデータ数で使用したりできる。

グラフ作成用の関数達

ここではグラフ作成のために作成した関数を解説。別に作成しなくてもいいけど、楽したいしコードをキレイに書きたいしということで関数を取り入れている。作成した関数は主に4つ。

  1. 1つのバブルチャート作成
  2. 時系列変更用のスライダー作成
  3. グラフレイアウト作成
  4. グラフ保存

1つのバブルチャート作成

# バブルチャートの作成用関数
def scattergl(x, y, name, color, size, pop):
    d = go.Scattergl(
        mode='markers',
        x=x, y=y, name=name,
        marker=dict(color=color, size=size, line=dict(color='black', width=1)),
        hovertemplate=f"{name}<br>"
        + 'income: %{x} <br>'
        + 'life expectancy: %{y} <br>'
        + f"population: {pop} <br>"
        + "<extra></extra>",
        visible=False,
    )

    return d

まずはバブルチャート作成用の関数。バブルチャートはそれ単体で関数などがあるわけではなく、シンプルに散布図のマーカーサイズを調節することで実現可能。

今回はデータ数が多いということなので、通常のScatterではなくWebGLを使用して軽く動作するScatterglを使用する。GLについては以下参照。

【plotly&Scattergl】大量のデータをplotlyで軽くグラフ化

続きを見る

引数markerでプロット点のマーカーの設定を行うが、このマーカーのサイズを適切に設定することでバブルっぽく見せることができる。また、バブル同士を区別するために黒線を入れている。

今回は各国の人口をバブルの大きさに設定する。人口が多いほどバブルが大きくなる。さらに、人口が多いほど青系から赤系のバブルになるように設定する。後述する。

スライダー作成関数

# スライダーの作成用関数
def set_slider(dct, years, count):

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

    # スライダーの各項目の設定
    steps = []
    for num, (year, data) in enumerate(dct.items()):
        # 一旦、全グラフを非表示に
        visible = [False] * count
        datalen = len(data)
        for d in range(datalen):
            # yearで指定した年の国のvisibleをTrueにする
            visible[num * datalen + d] = True

        title = f"income vs life expectancy: {year}"
        dct = dict(
            method="update", label=f"{year}",
            # 該当しないグラフを非表示にしつつ、グラフタイトルを変更
            args=[dict(visible=visible), dict(title=title)],
        )
        steps.append(dct)

    # スライダー全体の設定
    sliders = [
        dict(
            # currentvalueでスライダーその位置での設定が可能
            # prefixで年の前に接頭辞をつける
            active=0, currentvalue=dict(prefix="year: "),
            pad={"t": 50}, steps=steps,
        ),
    ]

    return sliders

次は本命とも言える時代変化の部分。スライダーを使用してこれを実装する。今回はある年のデータだけ表示し、それ以外の年は非表示にするようにした。

まず、forで該当する年の全ての国を非表示にする。その後、該当する年だけ表示にするようにした。ある年で国が複数個あるので、国の数だけ定数倍されるようにnum * datalen + dとした。

あとはそれらをスライダーとして作成するために、タイトルとかを設定した。

レイアウト作成の関数作成

# レイアウトの作成
def set_layout(start, legend_title, sliders, xrange):
    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(xrange[0]), np.log10(xrange[1])],
            showspikes=True, spikecolor='black',
            spikethickness=2, spikedash='dot',
        ),
        yaxis=dict(
            title='Life expectancy at birth',
            range=[0, 100],  # とりあえず0歳から100歳まで
            showspikes=True, spikecolor='black',
            spikethickness=2, spikedash='dot',
        ),
        legend=dict(title=f"country: {legend_title}", xanchor='left',),
        sliders=sliders,  # スライダーはここ
    )

    return layout

ここではグラフのレイアウトを作成した。今回はバブルをホバーした時に線を加えるようにspike系を追加した。また、グラフの表示範囲を引数で設定できるようにした。

注意点としては横軸をloglog10のこと)に設定したので、表示範囲はlog10にしなければならないということ。いちいち設定するのが面倒なので、引数の値に自動的にlogがつくようにした。

グラフ保存用の関数作成

# グラフ保存用の関数
def save(fig, config, save_name):
    pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
    pio.write_html(fig, f"{save_name}.html", config=config,)
    pio.write_image(fig, f"{save_name}.png")

最後はグラフ保存用の関数を作成。保存形式はhtmlとpng形式。htmlはconfigを設定することで、保存後のhtmlでもツールバーを追加したり削除したりすることができる。

【plotly&config】グラフのツールバーを編集する

続きを見る

もちろんグラフを表示するときでもconfigを設定することでツールバーを作成することができる。

収入・平均寿命・人口の時系列をグラフ化


ということで、最後に時系列順のバブルチャートを作成。主な流れは以下。

  1. 指定した年から年までの指定ステップのデータ作成
  2. 年毎にループを回す
  3. ループ中のある年で人口を多い順にソート
  4. その年で国ごとにバブルを作成
  5. スライダーで年代ごとにバブルの表示・非表示を設定
  6. レイアウトの作成
  7. グラフの表示と保存
def graph(start, end, step, adjust, xrange, save_name):

    # 指定した期間の年配列とその年に該当するdictのデータ
    years, data = set_year(
        start=start,
        end=end,
        step=step,
    )

    plot = {}
    count = 0  # 全部で何データあったのかを勘定する用
    for year in years:
        plot[year] = []

        # ある年の各国の人口({'国名1': 人口1, '国名2': 人口, ...})
        pop = data[year]['population']
        # 人口順に並び替え
        # [('人口多い国1位': 人口1), ('人口多い国2位': 人口2), ...]の形式
        sorted_pop = sorted(pop.items(), key=lambda x: x[1], reverse=True)
        sorted_pop = dict(sorted_pop)  # 辞書に変換
        sorted_pop_name = list(sorted_pop)  # listにすることで人口順に国名を抽出

        for num, name in enumerate(sorted_pop_name):
            # 各国の人口の番数を規格化
            norm = (num - 0) / (len(sorted_pop) - 0)
            rgb = plotly.colors.convert_to_RGB_255(cm.jet_r(norm))  # RGB決める
            rgba = f"rgba{rgb + (0.7,)}"  # 透明度追加

            # プロットデータの作成
            x = (data[year]['income'][name],)
            y = (data[year]['life_expectancy'][name],)
            size = np.sqrt(pop[name]) / adjust  # マーカーのサイズ
            popt = pop[name]

            d = scattergl(
                x=x, y=y, name=name,
                color=rgba, size=size, pop=popt,
            )
            plot[year].append(d)

            # 各年の各国ごとにカウントを足していく
            count += 1

    # スライダーの作成
    sliders = set_slider(dct=plot, years=years, count=count)

    # レイアウトの設定
    layout = set_layout(
        start=start, legend_title='By pop.',
        xrange=xrange, sliders=sliders,
    )

    # グラフを表示
    # dictになっているプロットを展開して1つのlistに変換
    data = sum(list(plot.values()), [])
    # グラフ上部のツールバーを設定
    config = template.plotly_config()
    fig = go.Figure(data=data, layout=layout,)
    fig.show(config=config)

    # 作成したグラフを保存
    save(fig=fig, config=config, save_name=save_name,)

国と人口はdicrになっているので、ソートする際にはkeyで1番目、すなわち人口にフォーカスしてあげる必要がある。

また、ソートしたある年の人口を、データ数と人口順で規格化し、それをバブルの色とした。また、透明度は多少透ける0.7とした。

マーカーのサイズは記事に載せる際の値としたので、実際にブラウザで表示した時にはかなり小さく表示されるだろう。その時は引数adjustを調節してほしい。

グラフ化は以下のようにする。

graph(
    start=1880,  # 開始年
    end=2020,  # 終了年
    step=20,  # 何年ごとに取得
    adjust=5e2,  # マーカーのサイズ調節
    xrange=(100, 2e5),  # 横軸の表示範囲(linearで記述)
    save_name='pop20',
)

最大データで作成

今回のデータは1800年から2040年の予測まであるので、どうせならマックスのデータ量で作成してみようじゃないか。ということで、以下のコードを実行してみた。

保存したhtmlファイルが80 MBを超えていた。重い。ということで、画像だけにする。勘弁してください。

graph(
    start=1800,  # 開始年
    end=2040,  # 終了年
    step=1,  # 何年ごとに取得
    adjust=1e2,  # マーカーのサイズ調節
    xrange=(100, 2e5),  # 横軸の表示範囲(linearで記述)
    save_name='pop_max',
)

時間経過で関係性はより深くわかる

今回は世界の収入と平均寿命の関係性を時系列を追いながら見てきた。ある1年だけを見てもその国は良くなっているのか否かはわからない。

しかし、時間を追うごとで世界は良くなっていることがわかる。悪いニュースをよく耳にすると思うが、世界は良くなっていることを今一度確認できた。

スライダーで作成した場合はいちいち動かすのが面倒。ということで、アニメーション版も作成したのでそちらも確認してほしい。

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

続きを見る

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

続きを見る

【plotly&Scattergl】大量のデータをplotlyで軽くグラフ化

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

完全ワイヤレスイヤホン(TWS)

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-go
-, ,