こんな人にオススメ
今は世界的に裕福になって、お金持ちも増えて健康になっているけど、昔ってどんな感じだったの?
昔は寿命が短いけど、人口が多い国とかもあった?
ということで、今回は以下の記事で作成した単年バブルチャートを応用して時系列で作成する。使用するデータは1800年から2040年の予想まで存在する。
-
-
【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
使用するデータ
今回使用するデータは以下の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つのバブルチャート作成
# バブルチャートの作成用関数 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
系を追加した。また、グラフの表示範囲を引数で設定できるようにした。
注意点としては横軸をlog
(log10
のこと)に設定したので、表示範囲は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
を設定することでツールバーを作成することができる。
収入・平均寿命・人口の時系列をグラフ化
ということで、最後に時系列順のバブルチャートを作成。主な流れは以下。
- 指定した年から年までの指定ステップのデータ作成
- 年毎にループを回す
- ループ中のある年で人口を多い順にソート
- その年で国ごとにバブルを作成
- スライダーで年代ごとにバブルの表示・非表示を設定
- レイアウトの作成
- グラフの表示と保存
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年だけを見てもその国は良くなっているのか否かはわからない。
しかし、時間を追うごとで世界は良くなっていることがわかる。悪いニュースをよく耳にすると思うが、世界は良くなっていることを今一度確認できた。
スライダーで作成した場合はいちいち動かすのが面倒。ということで、アニメーション版も作成したのでそちらも確認してほしい。
-
-
【plotly&アニメーション】plotlyで各国の収入と平均寿命の時代変化をバブルチャート&アニメーションで描く
続きを見る
-
-
【plotly&スライダー】plotlyのslidersにスライダーを追加
続きを見る
-
-
【plotly&Scattergl】大量のデータをplotlyで軽くグラフ化
続きを見る