[Python][Matplotlib] グモウスキー・ミラの写像を描く

はじめに

不思議な模様が現れることで有名な差分方程式系の一つに、グモウスキー・ミラの写像というものがある。

ja.wikipedia.org

以前、PythonのMatplotlibを使って作図したコードを発見したので貼ってみる。本当はもっときれいに描きたかったのだが、Matplotlibでの方法がよく分からなかった。

出力結果

f:id:cyanatlas:20200707223436j:plain
figure1.jpg
f:id:cyanatlas:20200707223439j:plain
figure2.jpg

figure2の方は、五枚の翼を持つ鳥に見えるということで、写像の提案者の一人であるミラは「神話の鳥 (mythic bird)」と呼んだらしい。

ソースコード

import os.path

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm


x0 = 0.1
y0 = 0.1
T = 20000
N = 1000
marker = '.'
markersize = 1
fillstyle = 'full'
DPI = 1200


def F(x, mu):
    return mu * x + 2 * (1 - mu) * x**2 / (1 + x**2)


def func(x, y, alpha, sigma, mu):
    x_next = y + alpha * (1 - sigma * y**2) * y + F(x, mu)
    y_next = - x + F(x_next, mu)
    return (x_next, y_next)


def plot_times(*, x_init, y_init, ax, N, params):
    xs, ys = [np.ones(N) for _ in range(2)]
    xs[0], ys[0] = func(x_init, y_init, **params)
    for t in np.arange(N - 1):
        xs[t + 1], ys[t + 1] = func(xs[t], ys[t], **params)
    ax.plot(
        xs, ys, fillstyle=fillstyle, markerfacecolor='C0', markeredgecolor='C0',
        marker=marker, markersize=markersize, linewidth=0
    )
    return xs[-1], ys[-1]


def plot(params, T, filename):
    x, y = (x0, y0)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(x, y)

    times_now = 0
    while True:
        if times_now >= T:
            break
        elif times_now + N < T:
            x, y = plot_times(x_init=x, y_init=y, N=N, ax=ax, params=params)
            times_now += N
        elif (times_now < T) and (times_now + N >= T):
            n = T - times_now
            x, y = plot_times(x_init=x, y_init=y, N=n, ax=ax, params=params)
            times_now += n
        else:
            raise ValueError

    fig.savefig(filename, dpi=DPI)
    return


def main():
    params = {
        'mu': -0.38,
        'alpha': 0.0083,
        'sigma': 0.1
    }
    plot(params=params, T=T, filename='figure1.jpg')
    params = {
        'mu': -0.8,
        'alpha': 0.008,
        'sigma': 0.05
    }
    plot(params=params, T=T, filename='figure2.jpg')
    return


if __name__ == '__main__':
    main()

[Python][Matplotlib] 等高線プロットcontourfで一部に色を塗らない

結論から言えば、色を塗りたくない場所にnp.nanを代入しておけばよい。

通常のcontourf

from itertools import product
import matplotlib.pyplot as plt
import numpy as np

X = np.linspace(0, 5, 20)
Y = np.linspace(0, 5, 20)
X, Y = np.meshgrid(X, Y)
Z0 = np.cos(1.5 * X) * np.cos(1.5 * Y)
fig = plt.figure()
ax = fig.add_subplot()
cs = ax.contourf(X, Y, Z0)
fig.colorbar(cs, ax=ax)
fig.savefig('test_Z0.jpg', dpi=600)
plt.close(fig)

f:id:cyanatlas:20200707013325j:plain

一部領域で色を塗らないcontourf

例えば右下( y < x)で色を塗らない場合。

from itertools import product
import matplotlib.pyplot as plt
import numpy as np

X = np.linspace(0, 5, 20)
Y = np.linspace(0, 5, 20)
Z0 = np.cos(1.5 * X) * np.cos(1.5 * Y)
Z1 = np.copy(Z0)
for yi, xi in product(range(Z1.shape[0]), range(Z1.shape[0])):
    if Y[yi] < X[xi]:
        Z1[yi][xi] = np.nan
    else:
        Z1[yi][xi] = np.cos(1.5 * X[xi]) * np.cos(1.5 * Y[yi])
fig = plt.figure()
ax = fig.add_subplot()
cs = ax.contourf(X, Y, Z1)
fig.colorbar(cs, ax=ax)
fig.savefig('test_Z1.jpg', dpi=600)
plt.close(fig)

f:id:cyanatlas:20200707013447j:plain

その他

corner_mask変数を使う方法もあるようだ。

matplotlib.org

[Python][Matplotlib] よく使うリンク集

Matplotlibを使うときに自分がよく開くページのリンク集。公式ドキュメントがメインだが他のページも。随時追加。

目次

Matplotlibの公式ドキュメントについて

Matplotlibについて検索すると、バージョンの異なる複数のMatplotlib公式ドキュメントがヒットする。開いている公式ドキュメントが扱っているMatplotlibのバージョンは、URLを見るか、ヘッダー画像を見れば分かる(がやや分かりにくい)。

記事執筆時点での最新バージョンは3.2.2。

matplotlib.org

古いものでは、ver2.0のドキュメントも存在する。少なくともメジャーバージョンが同じドキュメントを参照するように注意した方がよいだろう。

matplotlib.org

最新バージョンは次のページで確認できる。

matplotlib.org

ちなみに自分が使用しているバージョンは__version__変数で次で確認できる。

import matplotlib

print(matplotlib.__version__)

あまり見ることはないが、リリース情報はこのページ。

github.com



色関連

Color

色の指定の仕方とか、Colormapクラスの説明とか

matplotlib.org

名前のついている色(文字列で指定可能な色)の一覧

matplotlib.org

公式ページではないが、色の指定方法について分かりやすくまとめられている。

pythondatascience.plavox.info

Colormap

Colormapの一覧がある。

matplotlib.org

Colorpalette(seaborn)

seabornライブラリを使って色(のリスト)を作成することできる。

qiita.com


等高線プロットContour(f)関連

Axes.contour(f)関数

contourは等高線プロット。contourfは等高線プロットで色を塗ったもの。

matplotlib.org

matplotlib.org

Contourプロットで線上にラベルを付ける方法

matplotlib.org


imshow関連

Axes.imshow関数

本当はcontourfを使って作図したいが、詳細な色設定が面倒なとき、代わりにimshowを使って作図することがある。

matplotlib.org

軸と原点の設定

imshowは通常のプロットとは軸や原点が異なるので注意が必要。軸の貼り直しはextent引数で、原点位置の修正はorigin引数で行う。

matplotlib.org

[Python] CSVファイルの表をはてな記法の表に変換する

2020/07/16追記
オンラインコンバータを作ってみた。
cyanatlas.hatenablog.com

はじめに:はてな記法の表とは

はてなブログなどにははてな記法という独自の言語があり、これを使うことでHTMLファイルを編集せずにWEBページが書けます。

例えば次のように書けば表が表示されます。

|*名前|*色|*個数|
|りんご|赤|1|
|みかん|だいだい|2|
名前 個数
りんご 1
みかん だいだい 2

hatenadiary.g.hatena.ne.jp

問題点:はてな記法で表を書く面倒さ

はてな記法で表を書くのは、HTMLで表を書くより楽だが、それでもやや面倒であると感じる。理由は次の二つ。

  1. 編集中は列(縦のライン)が揃わないので、どこを編集しているのか分かりにくい
  2. WEBページの表をコピペできない

解決策としては次の二つが考えられる。

  1. はてな記法をやめてHTML編集にする(WEBページやExcelの表をコピペできるようになる)。
  2. Excelの表をプログラムではてな記法に変換する。

一つ目の方法については複数のWEBページで紹介されていたので各自検索してほしい。

一つ目の方法で解決できるとは言え、表組みのためだけに、表組以外は簡単に書けるはてな記法をやめたくないという私のような人もいるだろう。そこで、この記事は二つ目の方法を説明する。

解決策:CSVファイルをはてな記法に変換する(Python

  1. Excelなどを使ってCSVファイルを作成する(エンコードUTF8-BOM、コンマ区切りとすれば、後に示すPythonコードをそのまま使えるはず)。
  2. 後に示すPythonコードで、FILENAME変数を変換したいCSVファイルの名前(あるいは相対パス)に変更する。
  3. Pythonコードを実行して、はてな記法を含むtxtファイルを出力する(ファイル名はhatena_FILENAME.txt)。

コードを示す。

import os
import csv


FILENAME = 'test.csv'


def main():
    res = ''
    with open(FILENAME, mode='r', encoding='utf-8-sig') as f:
        csvreader = csv.reader(f, delimiter=',')
        head = True
        for row in csvreader:
            for cell in row:
                if head:
                    res += '|' + '*' + cell
                else:
                    res += '|' + cell
            res += '|\n'
            head = False

    outputfilename = 'hatena_' + os.path.splitext(os.path.basename(FILENAME))[0] + '.txt'
    with open(outputfilename, mode='w', encoding='utf8') as f:
        f.write(res)
    return


if __name__ == '__main__':
    main()

オンラインコンバータ

作ってみた。

cyanatlas.hatenablog.com

[Python] タプルには代入できないがタプル内リストには代入できるという話

環境:Windows10 + Python 3.7.6

リストとタプルの違い

Pythonの組み込み型には、配列を表現できるリストタプルという二つの型が用意されている。この二つには、値の変更を許すかどうかに違いがある。

リスト:要素の変更を許す
タプル:要素の変更を許さない

リストの要素を変更することはできるが、タプルの要素を変更することはできない。もし無理に変更しようとすれば、エラーとなる。

コード 1

x = [1, 2, 3]
print(x)
x[0] = 9
print(x)

出力 1

[1, 2, 3]
[9, 2, 3]


コード 2

x = (1, 2, 3)
print(x)
x[0] = 9
print(x)

出力 2

(1, 2, 3)
Traceback (most recent call last):
  File "XXX.py", line 3, in <module>
    x[0] = 9
TypeError: 'tuple' object does not support item assignment

何度も変更する予定の場合はリストを使い、変更する予定がない場合はバグを防ぐためにタプルを使う。というのが基本的な方針である。

では、タプルの中身は変更されないと考えてよいだろうか?

実はそうとは限らない。以下に紹介するタプル内リストの要素を変更する場合、エラーが出ることなくタプルの中身を変更することができてしまう。

タプル内リストの落とし穴

次のコードではタプルの第一要素であるリストを変更しようとしているが、もちろんこれはエラーが出る。

コード 3

x = ([1, 1], [2, 2], [3, 3])
print(x)
x[0] = [9, 9]
print(x)

出力 3

([1, 1], [2, 2], [3, 3])
Traceback (most recent call last):
  File "XXX.py", line 3, in <module>
    x[0] = [9, 9]
TypeError: 'tuple' object does not support item assignment

しかし、次のコードが示すように、タプル内リストの要素を変更することはできる。なぜなら、タプル側が参照しているリスト自体は変わっていないためである。タプルからすると、同じリストを参照していることは保証しているが、リストの中身が同じであることまでは保証してくれないのである。

コード 4

x = ([1, 1], [2, 2], [3, 3])
print(x)
x[0][0] = 9
print(x)

出力 4

([1, 1], [2, 2], [3, 3])
([9, 1], [2, 2], [3, 3])

解決策:タプル内リストを避ける

上で紹介したような奇妙な挙動は、バグを引き起こす原因となりやすい。そのため、プログラムの中ではタプル内リストを避けるのが良い。状況に応じてタプル内タプルかリスト内リストを使えば良いだろう。

変更予定のある変数 --> リスト内リストを使う
変更予定のない変数 --> タプル内タプルを使う

[Python][Matplotlib] 一枚の図に複数のグラフを描く

はじめに

Matplotライブラリを使って、一枚の図に複数のグラフを描きたいときがある。ここでは二つの方法を説明する。

  1. add_subplotメソッドを使う方法(簡単。単純なレイアウトで)
  2. Gridspecオブジェクトを使う方法(ちょっと面倒。複雑なレイアウトも可能)

1. add_subplot メソッドを使う

コード 1:

import numpy as np
import matplotlib.pyplot as plt


# test data
x = np.linspace(-5, 5, 100)
ys = np.ones(shape=(6, len(x))) * np.nan
ys[0] = 1
ys[1] = 1 + x
ys[2] = 1 + x + x**2 / 2
ys[3] = 1 + x + x**2 / 2 + x**3 / 6
ys[4] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24
ys[5] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24 + x**5 / 120

# plot
fig = plt.figure()
axs = [fig.add_subplot(2, 3, i + 1) for i in range(6)]
for i in range(6):
    axs[i].plot(x, ys[i], color='C0', zorder=5)
    axs[i].plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
    axs[i].set_title('plot {}'.format(i))
fig.tight_layout()

# save
fig.savefig('multiple_plot1.jpg', dpi=600)

出力 1:
f:id:cyanatlas:20191209001056j:plain

2. Gridspec オブジェクトを使う

コード 2:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


# test data
x = np.linspace(-5, 5, 100)
ys = np.ones(shape=(6, len(x))) * np.nan
ys[0] = 1
ys[1] = 1 + x
ys[2] = 1 + x + x**2 / 2
ys[3] = 1 + x + x**2 / 2 + x**3 / 6
ys[4] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24
ys[5] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24 + x**5 / 120

# plot
fig = plt.figure()
gs = gridspec.GridSpec(nrows=2, ncols=3, figure=fig)
for i in range(2):
    for j in range(3):
        ax = fig.add_subplot(gs[i, j])
        ax.plot(x, ys[3 * i + j], color='C0', zorder=5)
        ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
        ax.set_title('plot ({}, {})'.format(i, j))
fig.tight_layout()

# save
fig.savefig('multiple_plot2.jpg', dpi=600)

出力 2:
f:id:cyanatlas:20191209001114j:plain

3. Gridspecを使えば複雑なレイアウトも可能

Gridspecを使うと、add_subplotメソッドでは難しいような複雑なレイアウトも簡単に作ることができる。

コード 3:

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec


# test data
x = np.linspace(-5, 5, 100)
ys = np.ones(shape=(6, len(x))) * np.nan
ys[0] = 1
ys[1] = 1 + x
ys[2] = 1 + x + x**2 / 2
ys[3] = 1 + x + x**2 / 2 + x**3 / 6
ys[4] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24
ys[5] = 1 + x + x**2 / 2 + x**3 / 6 + x**4 / 24 + x**5 / 120

# plot
fig = plt.figure(figsize=(6.4, 6.4))
gs = gridspec.GridSpec(nrows=4, ncols=3, figure=fig)

ax = fig.add_subplot(gs[0:2, 0:2])
ax.plot(x, ys[0], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (0-1, 0-1)')

ax = fig.add_subplot(gs[0, 2])
ax.plot(x, ys[1], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (0, 2)')

ax = fig.add_subplot(gs[1, 2])
ax.plot(x, ys[2], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (1, 2)')

ax = fig.add_subplot(gs[2, 0])
ax.plot(x, ys[3], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (2, 0)')

ax = fig.add_subplot(gs[3, 0])
ax.plot(x, ys[4], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (3, 0)')

ax = fig.add_subplot(gs[2:, 1:])
ax.plot(x, ys[5], color='C0', zorder=5)
ax.plot(x, np.exp(x), color='darkgray', linestyle='dashed', zorder=4)
ax.set_title('plot (2-3, 1-2)')
fig.tight_layout()

# save
fig.savefig('multiple_plot3.jpg', dpi=600)

出力 3:
f:id:cyanatlas:20191210001458j:plain

まとめ

レイアウトの複雑さによって、add_subplotメソッドを使うシンプルな方法と、Gridspecオブジェクトを使うやや複雑な方法を使い分ければ良いと思う。

慣れてくればGridspecオブジェクトを使う方が楽ら上に拡張性も高いので、徐々にGridspecオブジェクトの方法に乗り換えていっても良いと思う。

[Python][Numpy] 組み込みのboolとNumpy.bool_は別物

Python組み込みのbool型とnumpy.bool_型は別物なので、is演算子で比較する場合は注意が必要、という話。

環境:Windows10 + Python 3.7 + Numpy 1.18.1

numpy.bool_型とPython組み込みのbool型は別物

Numpyを使って真偽値を判定すると、numpy.bool_という型になって返ってくるが、実はnumpy.bool_Python組み込みのboolとは別の型である。

次のコードを見てもらいたい。

コード1:

import numpy as np

x = np.log(10) > 1  # -> True
print(x, type(x))
print(x is True)
print(x is False)

出力1:

True <class 'numpy.bool_'>
False
False

3行目でxに、np.log(10) > 1の計算結果であるTrueが代入されるのだが、その型は組み込みのbool型ではなくnumpy.bool_というNumpyで実装されているbool型である。

通常、組み込みbool型に対しては、is演算子を使って、TrueかFalseかを判定する。もちろん==演算子を使っても判定はできるのだが、Pythonのコードスタイルではis演算子を使う方法が推奨されている。
www.flake8rules.com

しかし、numpy.bool_のTrueと組み込みbool型のTrueは別物なので、is演算子で比較すると、Falseが返ってきてしまう。つまり、「TrueでもないしFalseでもない」ように見えてしまうのである。困るのは、次のような条件分岐を書いた場合である。

コード2

import numpy as np

x = np.log(10) > 1  # -> True
if x is True:
    print('True!')
else:
    print('False!')

出力2

False!

xにはTrueが代入されているので、'True'がprintされてほしいところ、'False'がprintされてしまう。

バグではない

念のため書いておくと、これはバグではない。そういう仕様である。

github.com

バグではないとは言え、直感に反する挙動をされると困る。そこで解決策をいくつか考えてみる。

解決策1:bool関数で型変換する

Python組み込みのbool関数を使って、numpy.bool_型を組み込みのbool型に変換してやればよい。

コード3

import numpy as np

x = bool(np.log(10) > 1)  # -> True
print(x, type(x))
print(x is True)
print(x is False)

出力3

True <class 'bool'>
True
False

解決策2:==演算子で比較する

is演算子ではなく、==演算子を使って比較すれば、期待通りの結果が得られる。

コード4

import numpy as np

x = np.log(10) > 1  # -> True
print(x, type(x))
print(x == True)
print(x == False)

出力4

True <class 'numpy.bool_'>
True
False

ただし、Pythonの推奨コードスタイルから外れるので、エディタによっては注意が表示されるかもしれない*1。その場合、NOQAコメントを追加すれば、表示を一時的に消すことができる。

コード5

import numpy as np

x = np.log(10) > 1  # -> True
print(x, type(x))
print(x == True)  # NOQA
print(x == False)  # NOQA

出力5
(出力4と同じ。省略)

解決策3:if True:の形で使う

そもそもis演算子を使わずに、条件式をそのまま使えば問題は起こらない。ただし、andorを多用した複雑な条件式の場合は、is演算子を使って分かりやすく書きたくなるので悩ましいところではある。

コード6

import numpy as np

x = np.log(10) > 1  # -> True
if x:
    print('True!')
else:
    print('False!')

出力6

True!

補足:numpy.bool_()関数の挙動

最後に、numpy.bool_()関数の挙動を調べた結果をメモとして残しておく。なぜかnumpy公式ドキュメントには、numpy.bool_関数の説明ページが見当たらなかった。

import numpy as np

print(np.bool_(0))  # -> False
print(np.bool_(1))  # -> True
print(np.bool_(2))  # -> True
print(np.bool_(3))  # -> True

print(np.bool_(True))  # -> True
print(np.bool_(False))  # -> False

*1:私の環境では "comparison to False should be 'if cond is False:' or 'if not cond:'pycodestyle(E712)" という注意が表示された。