カテゴリー

go

【plotly&add_vrect, hrect】グラフに垂直・水平の塗りつぶし

2021年8月27日

こんな人にオススメ

ploltyで垂直な塗りつぶしがしたい!

プロットとか四角の図形で塗りつぶすとプロット領域をズラした時に塗りつぶした部分がずれちゃう!

ということで、今回はplotlyで画面固定の塗りつぶしを行う。画像として保存する際には塗りつぶしはプロットの上から四角形でも貼っておけば上から下までキレイに塗りつぶせる。

しかし、plotlyの場合は保存した後からもグリグリ動かすことが簡単なので、このように四角を置いただけでは、グラフをズラせば四角もズレてしまう。

pltの場合はグラフのこの位置に固定ってのができるが、plotlyでもこれは可能。今回はこの固定について解説する。

python環境は以下。

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

なお、今回使用するadd_vrectadd_hrectplotlyのバージョンが4.12以上でないと使用できないので、この点については注意が必要。

運営者のメガネとです。YouTubeTwitterInstagramも運営中。

自己紹介はこちらから、お問い合わせはこちらからお願いいたします。

運営者メガネ

作成したコード全文

下準備

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

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

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

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

こんな人にオススメ plotlyって{ ...

続きを見る

また、使用するデータは横軸は0から29までの配列、縦軸は-10から+10までのランダム値を3種類とした。乱数はseedで固定し、コードを回すごとに値が変更されないようにした。

x = np.arange(30)

data = {}
for num in range(3):
    np.random.seed(num)
    y = np.random.randint(-10, 10, (30,))
    data[f"data{num}"] = y
print(data)
# {'data0': array([  2,   5, -10,  -7,  -7,  -3,  -1,   9,   8,  -6,  -4,   2,  -9,
#         -4,  -3,   4,   7,  -5,   3,  -2,  -1,   9,   6,   9,  -5,   5,
#          5, -10,   8,  -7]), 'data1': array([ -5,   1,   2,  -2,  -1,   1,  -5,   5, -10,   6,  -9,   2,  -3,
#          3,  -4,   8,  -5,   8,   1,   0,   4,   8,  -6,  -1,   7, -10,
#          3,  -1,  -1,  -3]), 'data2': array([-2,  5,  3, -2,  1,  8,  1, -2, -3, -8,  7,  1,  5, -5, -3, -7, -4,
#        -6,  0,  1,  9, -3, -4,  0, -9, -7, -5, -6,  4, -4])}

プロットデータ作成用の関数

def set_data(x, y, name):
    d = go.Scatter(
        mode='lines+markers',
        x=x, y=y,
        name=name,
    )
    return d

複数回、同じコードを書くのはかなり面倒なので予め関数化して使い回す。ここではプロットデータを作成するための関数を作成。今回は色とかをこだわらずにシンプルな線と点のデータとする。

関数を使用してploltyを楽に書く方法については以下参照。

【plotly&工夫】楽にグラフを描くためのplotlyの関数化

こんな人にオススメ plotlyでグ} ...

続きを見る

レイアウト作成用の関数

def set_layout():
    layout = go.Layout(
        template=template.plotly_layout(),
        xaxis=dict(range=(-1, 31),),
        yaxis=dict(range=(-11, 11),),
    )
    return layout

レイアウトも関数化して楽をする。こちらも特にこだわらずに、自作テンプレートの読み込みと横軸・縦軸の範囲指定のみ。グラフタイトルや軸ラベルもつけていない。つけてもいいけど。

グラフ保存用の関数

def save(fig, save_name, config):
    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にしている。また、保存時にもconfigを設定することで、保存後のhtmlファイルでもconfigを使用することができる。

plotlyconfigとは、グラフ上部にあるツールバーのようなもの。詳しくは以下参照。

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

こんな人にオススメ plotlyでグ} ...

続きを見る

塗りつぶし対象のグラフ


まずは塗りつぶしをしたいデータをグラフ化。今回は乱数を使用したランダム値なのでギザギザなデータとなっている。基本的には上で書いた関数を使用してグラフ化するのみ。簡単。

fig.show()のタイミングでもconfigを使用することで、上のグラフのようにツールバーに多くのツールが表示されるようになる。好みで減らすこともできる。

def graph(save_name):
    plot = []
    for name, y in data.items():
        d = set_data(x=x, y=y, name=name)
        plot.append(d)

    # レイアウトの作成
    layout = set_layout()
    # グラフ描画
    fig = go.Figure(data=plot, layout=layout)

    # グラフの描画
    config = template.plotly_config()
    fig.show(config=config)

    # グラフ保存
    save(fig=fig, save_name=save_name, config=config)

graph(save_name='original')

垂直・水平塗りつぶしの関数

# 垂直塗りつぶしの設定
def vrect(x0, x1, fillcolor='green', opacity=1,
          width=0, color=None, layer='below'):
    dct = dict(
        x0=x0, x1=x1,  # 塗りつぶしの開始位置と終了位置
        fillcolor=fillcolor, opacity=opacity,  # 塗りつぶしの色と透明度
        line=dict(width=width, color=color,),  # 塗りつぶしの枠線の太さと色
        layer=layer,  # プロット点の上もしくは下で塗りつぶし
    )
    return dct

ということで、本題の塗りつぶしについて。plotlyの塗りつぶしは基本は絶対的なもので、グラフを移動させると塗りつぶしも移動する。通常のプロット点のような感じ。

一応、shapesを使って、四角を相対的な位置として配置することで塗りつぶしをすることもできるが、plotlyのバージョン4.12からはもっと簡単に塗りつぶしが描けるようになる。それがvrect

上の関数はあくまでも引数をまとめたもので、後にこれを展開して使用する。引数としてはかなりシンプルで、よく使いそうなものは以下。

  • x0, x1: 塗りつぶしを開始する横軸の値、終了する横軸の値
  • fillcolor: 塗りつぶしの色
  • opacity: 塗りつぶしと枠線の透明度
  • line: 塗りつぶしの枠線の設定。今までのプロットと同じ指定の仕方
  • layar: プロット点の上に塗りつぶしを置くか、下に置くか

この塗りつぶしはプロット領域の一番下から一番上までを塗りつぶすこと前提なので、yの値は指定しなくていい。勝手に上下ピッタリに塗りつぶしてくれる。

逆に水平に塗りつぶししたいときは、hrectを使用する。

def hrect(y0, y1, fillcolor='green', opacity=1,
          width=0, color=None, layer='below'):
    dct = dict(
        y0=y0, y1=y1,  # 塗りつぶしの開始位置と終了位置
        fillcolor=fillcolor, opacity=opacity,  # 塗りつぶしの色と透明度
        line=dict(width=width, color=color,),  # 塗りつぶしの枠線の太さと色
        layer=layer,  # プロット点の上もしくは下で塗りつぶし
    )
    return dct

こちらもvrectと同じように、あくまでも引数を書いてあるだけ。後ほどグラフに要素として追加する際に展開して使用する。

垂直・水平の塗りつぶしのグラフ

ということで、目的の垂直・水平に塗りつぶすグラフを作成。普段通りのプロットに図形として塗りつぶしを追加するだけだからかなり簡単。

垂直に塗りつぶすグラフ


関数としては塗りつぶしたい領域を可変長引数argsで複数使用可能とした。今回は枠線あり+プロットの下と枠線なし+プロットの上に塗りつぶしをする2種類を使用することに。

塗りつぶしの色や透明度はそれぞれで合わせたので、対照実験的な感じで見ることができるだろう。なお、塗りつぶしは図形として入れるのでマウスオーバーは効かない。

# 垂直の塗りつぶし
def fill_vertical(*args, save_name):
    # プロットデータの作成

    plot = []
    for name, y in data.items():
        d = set_data(x=x, y=y, name=name)
        plot.append(d)

    # レイアウトの作成
    layout = set_layout()

    # グラフ描画
    fig = go.Figure(data=plot, layout=layout)

    # 垂直の塗りつぶしを追加
    for kwarg in args:
        fill = vrect(**kwarg)  # args内の各dictを展開してvrect関数の引数に
        fig.add_vrect(**fill)  # vrect関数の返り値を展開して塗りつぶしに

    # グラフの描画
    config = template.plotly_config()
    fig.show(config=config)

    # グラフの保存
    save(fig=fig, save_name=save_name, config=config)

fill_vertical(
    dict(x0=1, x1=8, opacity=0.5, width=5, color='blue', layer='below',),
    dict(x0=16, x1=25, opacity=0.5, width=0, color=None, layer='above',),
    save_name='vertical_fill',
)

塗りつぶし用に作成した引数のdictargsに使用して、argsforで回す。そうすると引数にしたdictが1つずつ出力されるので、これをvrectで展開して塗りつぶしの設定を行う。

あとはvrectを再度展開してfigに追加したらいい。わざわざ2回展開しなくても工夫すれば1回の展開で塗りつぶしを作成。個人的には分けた方が問題を切り分けやすいと判断。

fig.add_vrectはグラフ上に垂直な塗りつぶしを追加するという意味。プロット点やレイアウトのようにfig = go.Figureで指定することができないっぽいので、仕方なくこのように指定。

水平に塗りつぶすグラフ


水平に塗りつぶすグラフの同じように作成可能。塗りつぶしに使用する引数x0, x1y0, y1に変更になって、値を調節するだけ。塗りつぶしに使用する関数はvrectからhrectに変更。

# 水平の塗りつぶし
def fill_horizontal(*args, save_name):
    # プロットデータの作成
    plot = []
    for name, y in data.items():
        d = set_data(x=x, y=y, name=name)
        plot.append(d)

    # レイアウトの作成
    layout = set_layout()

    # グラフ描画
    fig = go.Figure(data=plot, layout=layout)

    # 垂直の塗りつぶしを追加
    for kwarg in args:
        fill = hrect(**kwarg)  # 使用する関数はvrectからhrectに変更
        fig.add_hrect(**fill)  # 塗りつぶしの図形もhrectに

    # グラフの描画
    config = template.plotly_config()
    fig.show(config=config)

    # グラフの保存
    save(fig=fig, save_name=save_name, config=config)

fill_horizontal(
    dict(y0=1, y1=8, opacity=0.5, width=5, color='blue', layer='below',),
    dict(y0=-6, y1=-1, opacity=0.5, width=0, color='blue', layer='above',),
    save_name='horizontal_fill',
)

条件に合う領域だけを塗りつぶすための関数

これまでは、ユーザー側が塗りつぶしたい部分を指定して塗りつぶしを行なってきた。しかし、あるじょうけんにあてはまる部分だけを塗りつぶしたい場合はどうすればいいだろうか。

ということで、ここではある条件下で自動で塗りつぶしの位置を判定・塗りつぶしをする関数を作成する。

条件に合う領域を判定

# データフレームに変換
df = pd.DataFrame(data)
print(df)
#     data0  data1  data2
# 0       2     -5     -2
# 1       5      1      5
# 2     -10      2      3
# 3      -7     -2     -2
# 4      -7     -1      1
# 5      -3      1      8
# 6      -1     -5      1
# 7       9      5     -2
# 8       8    -10     -3
# 9      -6      6     -8
# 10     -4     -9      7
# 11      2      2      1
# 12     -9     -3      5
# 13     -4      3     -5
# 14     -3     -4     -3
# 15      4      8     -7
# 16      7     -5     -4
# 17     -5      8     -6
# 18      3      1      0
# 19     -2      0      1
# 20     -1      4      9
# 21      9      8     -3
# 22      6     -6     -4
# 23      9     -1      0
# 24     -5      7     -9
# 25      5    -10     -7
# 26      5      3     -5
# 27    -10     -1     -6
# 28      8     -1      4
# 29     -7     -3     -4

今回は複数データを使用するということで、予めpandasのデータフレームに変換しておく。このデータフレームを使うことで、簡単に条件に合うかどうかの判定をすることができる。

条件に合う部分を判定するために以下のめメソッドを使用すると簡単。

  • gt: 超過
  • ge: 以上
  • lt: 未満
  • le: 以下
  • eq: 等しい
  • ne: 等しくない

具体的な使い方は以下。pandasのシリーズに上のメソッドと数値を入れることで抽出することができる。

# 条件に合うようにデータを制限
def limit_data(conditon, value, header='data0'):

    if conditon == 'gt':  # より大きい(<)
        series_limit = df[header].gt(value)
    elif conditon == 'ge':  # 以上(≤)
        series_limit = df[header].ge(value)
    elif conditon == 'lt':  # より小さい(>)
        series_limit = df[header].lt(value)
    elif conditon == 'le':  # 以下(≥)
        series_limit = df[header].le(value)
    elif conditon == 'eq':  # 等しい(=)
        series_limit = df[header].eq(value)
    elif conditon == 'ne':  # 等しくない(≒)
        series_limit = df[header].ne(value)
    else:  # 上記以外
        raise ValueError('conditionにはgt, ge, lt, le, eq, neのいずれか')

    return series_limit

具体的な使い方は以下。ここではdata0のデータのうち、5を超過する値がTrueになる。

print(df['data0'].gt(5))
# 0     False
# 1     False
# 2     False
# 3     False
# 4     False
# 5     False
# 6     False
# 7      True
# 8      True
# 9     False
# 10    False
# 11    False
# 12    False
# 13    False
# 14    False
# 15    False
# 16     True
# 17    False
# 18    False
# 19    False
# 20    False
# 21     True
# 22     True
# 23     True
# 24    False
# 25    False
# 26    False
# 27    False
# 28     True
# 29    False
# Name: data0, dtype: bool

条件に合う連続したデータ点の始まりと終わりを取得

# 連続して条件に合うデータの最初と最後のインデックスを、各領域ごとにまとめて取得
def get_sequence(series):
    # 連続して条件に合うデータの最初と最後のインデックスを入れる
    sequences = [[]]

    # 連続の初めか終わりかを判断する用の変数
    # 0: 条件に当てはまるインデックスの開始、1: 条件に当てはまるインデックスの終了の合図
    flag = 0

    # 真偽値の差分を取るとTrueからFalse, FalseからTrueの変わり目だけTrueに
    diff = series.diff()
    # 初めの値からTrueなら差分の0番目のNaNをTrueにする
    if series[0]:
        diff[0] = True

    for num, i in enumerate(diff):
        # 差分がTrueからFalseもしくはFalseからTrueの変わり目の時
        if i and ~np.isnan(i):
            if flag:  # flag=1(条件に当てはまるインデックスが終わる)
                # flag=1インデックスの終了なので、条件に当てはまるのは1つ前
                sequences[-1].append(num - 1)
                flag = 0  # 連続数状態解除
                sequences.append([])
            else:  # flag=0(条件に当てはまるインデックスが始まる)
                sequences[-1].append(num)
                flag = 1  # 連続数状態に

    # 連続数の状態で最後のデータに来た時は強制的に最後のデータで閉じる
    if flag:
        sequences[-1].append(num)

    # 空のlistを削除
    sequences = list(filter(lambda x: x, sequences))

    return sequences

add_vrect, add_hrectはそれぞれ開始位置と終了位置を指定する必要がある。しかし、.gtなどで条件を適用した場合は開始・終了位置だけではなくその途中の値もTrueになる。

ということで、get_sequence関数では各Trueの塊の開始インデックスと終了インデックスを、塊ごとにlistに入れる。最終的な出力のイメージは以下。

[[7, 8], [16, 16], [21, 23], [28, 28]]

この場合だと、初めは7番目から8番目が条件に当てはまる。次は16番目から16番目、すなわち16番目のデータのみ条件に当てはまり、その次は21番目から23番目のデータ、そして最後が28番目のデータ。

get_sequence関数の処理手順は以下。

  1. 最終結果(上の[[7, 8]で始まる配列)になる配列sequenceを定義
  2. 条件に当てはまる領域の開始位置か終了位置かを判定する変数flagを定義
  3. 配列の差分を取る
    • 同じ真偽値の差分ならFalseに、異なる真偽値の差分ならTrue
  4. 0番目が条件に当てはまっていたら、差分の0番目をTrue
    • 差分の0番目はNaNとなる
    • これは0番目と1つ前のデータ(存在しない)の差分だから
  5. 差分(真偽値)をforで回し、Trueの時のみ次の処理を行う
    • Trueということは条件に当てはまる・らないの切り替えの時
    • この切り替えが当てはまるの開始か終了かを判定するのがflag
  6. 条件に当てはまる開始インデックスの時はflag=0なのでelse
  7. 開始位置のインデックスを変数sequence格納し、flag=1に変更
  8. 次にif iに入るのは条件の当てはまりが終わる時
  9. その時はflag=1なので、if flagに入る
  10. if flagに入るループの1つ前が条件に当てはまるループの最後
  11. その時のループnumから1引いたnum - 1sequenceに格納
  12. 上記を繰り返す
  13. もし、条件に当てはまる状態で差分のループが終われば、条件に当てはまる状態で閉める
    • 最後のインデックスnumsequence-1番目に追加すればいい
  14. 最後のnumでピッタリ終わった場合は空のlistが残るので、最後に削除
    • 空のlistifFalseになるので、filter関数で弾ける

という具合。かなりややこしいので、各人で色々と試してみてほしい。多分、エラーは起きないと思うが、エラーが起きたら申し訳ない。

試しでgtdiffを使ったコードを出力してみる。確かに差分の初めはNaNになるしgtの条件の切り替え部分では差分がTrueになる。

limited = df['data0'].gt(5)
limited_zip = zip(limited, limited.diff())
print("num, original, diff")
for num, (original, diff) in enumerate(limited_zip):
    print(f"{num}, {original}, {diff}")
# num, original, diff
# 0, False, nan
# 1, False, False
# 2, False, False
# 3, False, False
# 4, False, False
# 5, False, False
# 6, False, False
# 7, True, True
# 8, True, False
# 9, False, True
# 10, False, False
# 11, False, False
# 12, False, False
# 13, False, False
# 14, False, False
# 15, False, False
# 16, True, True
# 17, False, True
# 18, False, False
# 19, False, False
# 20, False, False
# 21, True, True
# 22, True, False
# 23, True, False
# 24, False, True
# 25, False, False
# 26, False, False
# 27, False, False
# 28, True, True
# 29, False, True

条件に合う領域だけを塗りつぶし

# 条件に合うデータ位置のみ塗りつぶしをしたグラフを作成
def select_fill(conditon, value, save_name, header='data0'):
    # 条件に合うデータをTrueに、合わないデータをFalseにする
    series_limit = limit_data(conditon=conditon, value=value, header=header,)

    # 連続して条件に合うデータの最初と最後のインデックスをlistに
    sequences = get_sequence(series_limit)

    # 塗りつぶすデータのインデックスから塗りつぶしの設定を作成
    ans = []
    for x0, x1 in sequences:
        fill = dict(
            x0=x0, x1=x1, opacity=0.5,
            width=5, color='blue', layer='below',
        )
        ans.append(fill)

    # 条件に合う領域だけ垂直塗りつぶし
    fill_vertical(*ans, save_name=save_name,)

ということで、最後にこれまでの関数を組み合わせ、条件に当てはまる領域のみを垂直に塗りつぶしできる関数を作成する。コードは上のもので、予め関数化しているからかなり簡単に書ける。

data0の中で5を超過する範囲だけ塗りつぶし


まずはdata0の中で5を超過する範囲だけを塗りつぶす。条件に合うデータが1点しかなかった場合は面ではなく線として塗りつぶされる。

# data0で5より大きい値を持つ領域だけ塗りつぶし
select_fill(conditon='gt', value=5, header='data0', save_name='gt0_5')

data0の中で5以上の範囲だけ塗りつぶし


条件を超過から以上に変更したので、data0の値が5のデータも塗りつぶされる。

# data1で-1未満の値を持つ領域だけ塗りつぶし
select_fill(conditon='ge', value=5, header='data0', save_name='ge0_5')

data1の中で-1を超過する範囲だけ塗りつぶし


お次はdata1の中で-1を超過する領域のみを塗りつぶすというもの。-1より大きいのでかなりあるのではと思いきや、以外にもギザギザしすぎていて1点ものが多数。

# data1で-1未満の値を持つ領域だけ塗りつぶし
select_fill(conditon='gt', value=-1, header='data1', save_name='gt1_-1')

data13未満の領域だけ塗りつぶし


最後はdata13未満の値を持つ領域だけ塗りつぶし。逆に3未満とすると結構小さい値が多いから当てはまる領域が多い印象。ランダム値だけど、これだけデータ数が少ないと偏りが目立ちやすいんだろう。

発展途上なplotly

今回はplotlyで垂直・水平な塗りつぶしをする方法について解説した。塗りつぶし自体は簡単にできるが、条件に合わせて塗りつぶす際には結構面倒だった。まあ、もしかしたら簡単にできるかもしれないが。

add_vrect, add_hrectはバージョン4.12から登場した機能であるが、このバージョンのリリースは2020年でかなり最近のアップデートでできるようになった。

matplotlibでは当たり前にできることだが、plotlyでは最近までできなかった。このように、もっと便利になることがこれから増えていくのが楽しみだ。

関連記事

【plt vs plotly】matplotlibとgoでグラフの比較

こんな人にオススメ 今までpyt ...

続きを見る

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

こんな人にオススメplotlyを使Ӗ ...

続きを見る

plotlyでグラフを静止画として保存する際に出るポップアップ
【plotly&orca】plotlyで静止画保存(orca)

こんな人にオススメplotlyで画ࠔ ...

続きを見る

【plotly&工夫】楽にグラフを描くためのplotlyの関数化

こんな人にオススメ plotlyでグ} ...

続きを見る

スイッチボット

2022/11/28

【SwitchBotロックレビュー】これからのスタンダードになりうるスマートロック

こんな人にオススメ SwitchBotからスマートロック「SwitchBotロック」が発売された ...

生活に役立つ

2022/11/28

【メガネ厳選】クソ便利に使っているサービスやアイテム達

このページでは執筆者「メガネ」が実際に使って便利だと感じているサ ...

マウス

2022/9/11

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

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

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

2022/11/21

【ながら聴きイヤホン比較】SONY LinkBuds、ambie、BoCoはどれがおすすめ?

こんな人におすすめ 耳を塞がない開放型のイヤホンに完全ワイヤレスӟ ...

macOSアプリケーション

2022/10/15

【M1 Mac】MacBook Proに入れている便利でニッチなアプリを21個紹介する

こんな人におすすめ MacBookを購入してLINEとか必要最低限のアプリは入れた。 ...

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

2022/10/23

【SENNHEISER MOMENTUM True Wireless 3レビュー】高レベルでバランス型の高音質イヤホン

こんな人におすすめ SENNHEISER MOMENTUM True Wireless 3って実際のところどうなの? 評判は良い ...

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

2022/11/21

【SONY WF-1000XM4レビュー】神とゴミのハーフ&ハーフ

こんな人におすすめ SONYのフラグシップモデル「SONY WF-1000XM4」ってどれくらい性 ...

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

2022/8/19

【Nothing ear (1)レビュー】ライトな完成度、アップデートに期待

こんな人にオススメ 完全ワイヤレスイヤホン(TWS)でスケルトンボディ ...

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

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

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

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

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

  • この記事を書いた人

メガネ

ベンチャー企業のWebエンジニア駆け出し。独学のPythonで天文学系の大学院を修了→新卒を1.5年で辞める→転職→今に至る。
常時金欠のガジェット好きでM1 MacBook Pro x Galaxy S22 Ultraの狂人。
人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂う→人生が楽しい

ガジェットのレビューとPythonコードを記事にしています。ぜひ楽しんでください🦊
自己紹介と半生→変わって楽しいの繰り返し

-go
-, ,